fix: address sender-side voice ducking with voice processing controls

Disable AGC by default to prevent Chrome from crushing mic sensitivity
when stream audio is playing. Add user-facing toggles for echo
cancellation, noise suppression, and auto gain control. Decouple voice
and stream audio by routing through ctx.destination instead of shared
compressor. Track mic stream generation to re-publish when settings change.
This commit is contained in:
Jannis Braun
2026-02-20 15:00:08 +01:00
parent 5c38f076d3
commit 25f72aef3b
8 changed files with 189 additions and 26 deletions
+11 -1
View File
@@ -131,7 +131,11 @@ export const useVoiceStore = create()(persist((set, get) => ({
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
setVideoQuality: (quality) => set({ videoQuality: quality }),
noiseSuppression: true,
echoCancellation: true,
autoGainControl: false,
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
setEchoCancellation: (enabled) => set({ echoCancellation: enabled }),
setAutoGainControl: (enabled) => set({ autoGainControl: enabled }),
deafenedUserIds: new Set(),
setUserDeafened: (userId, deafened) => {
set((state) => {
@@ -203,11 +207,15 @@ export const useVoiceStore = create()(persist((set, get) => ({
}),
}), {
name: 'opencord-voice-settings',
version: 1,
version: 2,
migrate: (persistedState, version) => {
if (version === 0) {
persistedState.streamAttenuationEnabled = false;
}
if (version < 2) {
persistedState.echoCancellation = true;
persistedState.autoGainControl = false;
}
return persistedState;
},
storage: createJSONStorage(() => localStorage),
@@ -222,6 +230,8 @@ export const useVoiceStore = create()(persist((set, get) => ({
outputDeviceId: state.outputDeviceId,
videoQuality: state.videoQuality,
noiseSuppression: state.noiseSuppression,
echoCancellation: state.echoCancellation,
autoGainControl: state.autoGainControl,
streamAttenuationEnabled: state.streamAttenuationEnabled,
streamAttenuationStrength: state.streamAttenuationStrength,
}),
+15 -1
View File
@@ -62,7 +62,11 @@ interface VoiceState {
setFocusedParticipant: (id: string | null) => void;
setVideoQuality: (quality: '1080p' | '1080p60' | '720p' | '720p60' | '540p' | '360p') => void;
noiseSuppression: boolean;
echoCancellation: boolean;
autoGainControl: boolean;
toggleNoiseSuppression: () => void;
setEchoCancellation: (enabled: boolean) => void;
setAutoGainControl: (enabled: boolean) => void;
deafenedUserIds: Set<string>;
setUserDeafened: (userId: string, deafened: boolean) => void;
// WebSocket-based voice user status (visible without joining LiveKit)
@@ -221,7 +225,11 @@ export const useVoiceStore = create<VoiceState>()(
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
setVideoQuality: (quality) => set({ videoQuality: quality }),
noiseSuppression: true,
echoCancellation: true,
autoGainControl: false,
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
setEchoCancellation: (enabled) => set({ echoCancellation: enabled }),
setAutoGainControl: (enabled) => set({ autoGainControl: enabled }),
deafenedUserIds: new Set(),
setUserDeafened: (userId, deafened) => {
set((state) => {
@@ -296,11 +304,15 @@ export const useVoiceStore = create<VoiceState>()(
}),
{
name: 'opencord-voice-settings',
version: 1,
version: 2,
migrate: (persistedState: any, version: number) => {
if (version === 0) {
persistedState.streamAttenuationEnabled = false;
}
if (version < 2) {
persistedState.echoCancellation = true;
persistedState.autoGainControl = false;
}
return persistedState;
},
storage: createJSONStorage(() => localStorage),
@@ -315,6 +327,8 @@ export const useVoiceStore = create<VoiceState>()(
outputDeviceId: state.outputDeviceId,
videoQuality: state.videoQuality,
noiseSuppression: state.noiseSuppression,
echoCancellation: state.echoCancellation,
autoGainControl: state.autoGainControl,
streamAttenuationEnabled: state.streamAttenuationEnabled,
streamAttenuationStrength: state.streamAttenuationStrength,
}),