Files
backspace/packages/web/src/components/voice/SoundController.tsx
T
Jannis Braun 770e0490dd feat: increase perceived audio loudness — +3dB master boost, configurable SFX volume
Three changes to bring output volume closer to native apps:
- Insert masterBoost GainNode (+3dB) before the compressor/limiter
- Raise default system sound volume from 0.5 to 0.8
- Add configurable Sound Effects Volume slider (0–200%) in Voice settings
2026-03-12 23:42:11 +01:00

161 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react';
import { useVoiceStore } from '../../stores/voiceStore';
import { useChatStore } from '../../stores/chatStore';
import { useAuthStore } from '../../stores/authStore';
import { AudioManager } from '../../audio/AudioManager';
/** Compute the effective sound effect gain: base volume (0.8) scaled by the user's SFX slider (0200). */
function getSfxVolume(): number {
return 0.8 * (useVoiceStore.getState().soundEffectVolume / 100);
}
export function SoundController() {
const audioManager = AudioManager.getInstance();
const currentUser = useAuthStore((s) => s.user);
// Refs to track previous states
const isInitialMount = useRef(true);
const prevIsMuted = useRef<boolean>(useVoiceStore.getState().isMuted);
const prevIsDeafened = useRef<boolean>(useVoiceStore.getState().isDeafened);
const prevIsCameraOn = useRef<boolean>(useVoiceStore.getState().isCameraOn);
const prevIsScreenSharing = useRef<boolean>(useVoiceStore.getState().isScreenSharing);
const prevIsConnected = useRef<boolean>(useVoiceStore.getState().isLiveKitConnected);
const prevParticipantIds = useRef<Set<string>>(new Set(useVoiceStore.getState().participants.map(p => p.userId)));
const prevScreenShareUserIds = useRef<Set<string>>(new Set(useVoiceStore.getState().participants.filter(p => p.isScreenSharing).map(p => p.userId)));
const incomingCallLoop = useRef<AudioBufferSourceNode | null>(null);
const outgoingCallLoop = useRef<AudioBufferSourceNode | null>(null);
useEffect(() => {
// Set initial mount flag to false after first run
const timer = setTimeout(() => {
isInitialMount.current = false;
}, 1000);
// 1. Listen to Voice State Changes
const unsubscribeVoice = useVoiceStore.subscribe((state) => {
if (isInitialMount.current) return;
const sfxOpts = { volume: getSfxVolume() };
// Mute/Unmute
if (state.isMuted !== prevIsMuted.current) {
audioManager.playSound(state.isMuted ? 'mute' : 'unmute', sfxOpts);
prevIsMuted.current = state.isMuted;
}
// Deafen/Undeafen
if (state.isDeafened !== prevIsDeafened.current) {
audioManager.playSound(state.isDeafened ? 'deafen' : 'undeafen', sfxOpts);
prevIsDeafened.current = state.isDeafened;
}
// Camera Toggle
if (state.isCameraOn !== prevIsCameraOn.current) {
audioManager.playSound(state.isCameraOn ? 'camera_on' : 'camera_off', sfxOpts);
prevIsCameraOn.current = state.isCameraOn;
}
// Screen Share Toggle (Self)
if (state.isScreenSharing !== prevIsScreenSharing.current) {
audioManager.playSound(state.isScreenSharing ? 'stream_started' : 'stream_ended', sfxOpts);
prevIsScreenSharing.current = state.isScreenSharing;
}
// Disconnect (Self)
if (prevIsConnected.current && !state.isLiveKitConnected) {
audioManager.playSound('disconnect', sfxOpts);
}
// Connect (Self)
if (!prevIsConnected.current && state.isLiveKitConnected) {
audioManager.playSound('user_join', sfxOpts);
}
prevIsConnected.current = state.isLiveKitConnected;
// Participant Joins/Leaves & Screen Sharing
const currentParticipantIds = new Set(state.participants.map(p => p.userId));
const currentScreenShareUserIds = new Set(state.participants.filter(p => p.isScreenSharing).map(p => p.userId));
if (state.isLiveKitConnected) {
// Someone joined voice (Others only)
state.participants.forEach(p => {
if (!prevParticipantIds.current.has(p.userId) && p.userId !== currentUser?.id) {
audioManager.playSound('user_join', sfxOpts);
}
});
// Someone left voice (Others only)
prevParticipantIds.current.forEach(userId => {
if (!currentParticipantIds.has(userId) && userId !== currentUser?.id) {
audioManager.playSound('user_leave', sfxOpts);
}
});
// Someone started screen sharing (Others only)
state.participants.forEach(p => {
if (p.isScreenSharing && !prevScreenShareUserIds.current.has(p.userId) && p.userId !== currentUser?.id) {
audioManager.playSound('stream_user_joined', sfxOpts);
}
});
// Someone stopped screen sharing (Others only)
prevScreenShareUserIds.current.forEach(userId => {
if (!currentScreenShareUserIds.has(userId) && userId !== currentUser?.id) {
audioManager.playSound('stream_user_left', sfxOpts);
}
});
}
prevParticipantIds.current = currentParticipantIds;
prevScreenShareUserIds.current = currentScreenShareUserIds;
// Incoming Call (Ringing)
if (state.incomingCall && !incomingCallLoop.current) {
audioManager.playSound('call_ringing', { loop: true, volume: getSfxVolume() }).then(source => {
incomingCallLoop.current = source;
});
} else if (!state.incomingCall && incomingCallLoop.current) {
incomingCallLoop.current.stop();
incomingCallLoop.current = null;
}
// Outgoing Call (Calling)
if (state.outgoingCall && !outgoingCallLoop.current) {
audioManager.playSound('call_calling', { loop: true, volume: getSfxVolume() }).then(source => {
outgoingCallLoop.current = source;
});
} else if (!state.outgoingCall && outgoingCallLoop.current) {
outgoingCallLoop.current.stop();
outgoingCallLoop.current = null;
}
});
// 2. Listen to Chat State Changes (New Real-Time Messages from any channel)
const unsubscribeChat = useChatStore.subscribe((state, prevState) => {
if (isInitialMount.current) return;
// Only trigger on NEW realtimeMessageEvents entries (not API loads)
if (state.realtimeMessageEvents.length > prevState.realtimeMessageEvents.length) {
const newEvents = state.realtimeMessageEvents.slice(prevState.realtimeMessageEvents.length);
for (const { message } of newEvents) {
if (message.userId !== currentUser?.id) {
audioManager.playSound('message', { volume: getSfxVolume() });
break; // one sound per batch
}
}
}
});
return () => {
clearTimeout(timer);
unsubscribeVoice();
unsubscribeChat();
if (incomingCallLoop.current) incomingCallLoop.current.stop();
if (outgoingCallLoop.current) outgoingCallLoop.current.stop();
};
}, [audioManager, currentUser?.id]);
return null;
}