From c020eded311e3dc5af255b3c107bfb3094151f49 Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 20 Aug 2026 17:41:44 +0200 Subject: [PATCH] fix(voice): stop forcing automatic gain control off on every capture profile (#1781) --- .../components/modals/tabs/UserVoiceTab.tsx | 28 ++++---- .../voice/components/VoiceSettingsMenus.tsx | 3 +- .../VoiceSettingsBottomSheets.tsx | 1 - .../AudioProcessingModal.tsx | 5 +- .../utils/VoiceProcessingProfile.test.ts | 67 +++++++++++++++++++ .../voice/utils/VoiceProcessingProfile.ts | 5 +- 6 files changed, 87 insertions(+), 22 deletions(-) create mode 100644 fluxer_app/src/features/voice/utils/VoiceProcessingProfile.test.ts diff --git a/fluxer_app/src/features/user/components/modals/tabs/UserVoiceTab.tsx b/fluxer_app/src/features/user/components/modals/tabs/UserVoiceTab.tsx index 2a115b29f..707a9f05f 100644 --- a/fluxer_app/src/features/user/components/modals/tabs/UserVoiceTab.tsx +++ b/fluxer_app/src/features/user/components/modals/tabs/UserVoiceTab.tsx @@ -169,8 +169,8 @@ const resolvePushToTalkReleaseDelayInput = ( ).value; }; const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({ - message: 'Evens out your mic volume. Off when enhanced suppression is on.', - comment: 'Description for the automatic gain control toggle in the custom voice processing settings.', + message: 'Evens out your mic volume so you are not too quiet.', + comment: 'Description for the automatic gain control toggle in the voice processing settings.', }); const INPUT_AND_OUTPUT_DESCRIPTOR = msg({ message: 'Input and output', @@ -293,7 +293,6 @@ export const VoiceTab: React.FC = observer(({voiceSettings, autoR }, ]; const noiseSuppressionMethod = resolveNoiseSuppressionMethod(deepFilterNoiseSuppression, noiseSuppression); - const effectiveAutoGainControl = !deepFilterNoiseSuppression && autoGainControl; const noiseSuppressionOptions: Array> = [ {value: 'enhanced', label: i18n._(NOISE_SUPPRESSION_ENHANCED_DESCRIPTOR)}, {value: 'standard', label: i18n._(NOISE_SUPPRESSION_STANDARD_DESCRIPTOR)}, @@ -449,6 +448,16 @@ export const VoiceTab: React.FC = observer(({voiceSettings, autoR )} ); + const renderAutoGainControlSwitch = () => ( + VoiceSettingsCommands.update({autoGainControl: value})} + ariaLabel={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)} + data-flx="user.voice-tab.render-auto-gain-control-switch.switch.update-auto-gain-control" + /> + ); const renderCustomProfile = () => (
{renderPttControls()} @@ -507,15 +516,7 @@ export const VoiceTab: React.FC = observer(({voiceSettings, autoR ariaLabel={i18n._(VOICE_ECHO_CANCELLATION_DESCRIPTOR)} data-flx="user.voice-tab.render-custom-profile.switch.update--2" /> - VoiceSettingsCommands.update({autoGainControl: value})} - ariaLabel={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)} - data-flx="user.voice-tab.render-custom-profile.switch.update-auto-gain-control" - /> + {renderAutoGainControlSwitch()}
); return ( @@ -662,6 +663,7 @@ export const VoiceTab: React.FC = observer(({voiceSettings, autoR {voiceProcessingMode === 'voice' && (
{renderPttControls()} + {renderAutoGainControlSwitch()}
)} {voiceProcessingMode === 'studio' && pttCombo?.key && isPushToTalk && ( @@ -682,7 +684,7 @@ export const VoiceTab: React.FC = observer(({voiceSettings, autoR outputVolume, echoCancellation, noiseSuppression, - autoGainControl: effectiveAutoGainControl, + autoGainControl, deepFilterNoiseSuppression, deepFilterNoiseSuppressionLevel, voiceProcessingMode, diff --git a/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx b/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx index 8c3c77c30..56de89f5f 100644 --- a/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx +++ b/fluxer_app/src/features/voice/components/VoiceSettingsMenus.tsx @@ -359,8 +359,7 @@ export const VoiceAudioSettingsMenu: React.FC = obs data-flx="voice.voice-settings-menus.voice-audio-settings-menu.icon--2" /> } - checked={voiceSettings.autoGainControl && !deepFilterEnabled} - disabled={deepFilterEnabled} + checked={voiceSettings.autoGainControl} onCheckedChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})} data-flx="voice.voice-settings-menus.voice-audio-settings-menu.checkbox-item--2" > diff --git a/fluxer_app/src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx b/fluxer_app/src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx index 90de832ee..248bc69bc 100644 --- a/fluxer_app/src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx +++ b/fluxer_app/src/features/voice/components/bottomsheets/VoiceSettingsBottomSheets.tsx @@ -281,7 +281,6 @@ export const VoiceAudioSettingsBottomSheet: React.FC ), label: i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR), - disabled: deepFilterEnabled, onClick: () => { VoiceSettingsCommands.update({autoGainControl: !voiceSettings.autoGainControl}); }, diff --git a/fluxer_app/src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx b/fluxer_app/src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx index fc28ae47a..bedee6b47 100644 --- a/fluxer_app/src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx +++ b/fluxer_app/src/features/voice/components/voice_connection_status/AudioProcessingModal.tsx @@ -70,8 +70,8 @@ const STOPS_YOUR_SPEAKERS_FROM_LOOPING_BACK_INTO_YOUR_DESCRIPTOR = msg({ comment: 'Description for the echo cancellation toggle in the custom voice processing settings.', }); const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({ - message: 'Evens out your mic volume. Off when enhanced suppression is on.', - comment: 'Description for the automatic gain control toggle in the custom voice processing settings.', + message: 'Evens out your mic volume so you are not too quiet.', + comment: 'Description for the automatic gain control toggle in the voice processing settings.', }); const DEEP_FILTER_STRENGTH_DESCRIPTOR = msg({ message: 'Suppression strength', @@ -183,7 +183,6 @@ export const AudioProcessingModal = observer(() => { label={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)} description={i18n._(AUTO_GAIN_DESCRIPTION_DESCRIPTOR)} value={VoiceSettings.autoGainControl} - disabled={deepFilterEnabled} onChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})} compact data-flx="voice.voice-connection-status.audio-processing-modal.switch.update--2" diff --git a/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.test.ts b/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.test.ts new file mode 100644 index 000000000..7d40035b9 --- /dev/null +++ b/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.test.ts @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { + FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL, + resolveVoiceProcessing, + type VoiceProcessingSettingsLike, +} from '@app/features/voice/utils/VoiceProcessingProfile'; +import {describe, expect, it, vi} from 'vitest'; + +vi.mock('@app/features/voice/engine/VoiceDevicePermissionState', () => ({ + default: {getState: () => ({inputDevices: []})}, +})); + +vi.mock('@app/features/voice/utils/VoiceDeviceManager', () => ({ + resolveEffectiveDeviceId: () => null, +})); + +function createSettings(overrides: Partial = {}): VoiceProcessingSettingsLike { + return { + voiceProcessingMode: 'voice', + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + deepFilterNoiseSuppression: true, + deepFilterNoiseSuppressionLevel: 80, + ...overrides, + }; +} + +describe('resolveVoiceProcessing', () => { + it('keeps automatic gain control on for focused voice at factory defaults', () => { + const resolved = resolveVoiceProcessing(createSettings()); + expect(resolved.mode).toBe('voice'); + expect(resolved.autoGainControl).toBe(true); + expect(resolved.deepFilter).toBe(true); + expect(resolved.deepFilterNoiseReductionLevel).toBe(FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL); + }); + + it('honours an explicit opt out of automatic gain control in focused voice', () => { + const resolved = resolveVoiceProcessing(createSettings({autoGainControl: false})); + expect(resolved.autoGainControl).toBe(false); + }); + + it('keeps automatic gain control on in custom mode while deep filter is enabled', () => { + const resolved = resolveVoiceProcessing( + createSettings({voiceProcessingMode: 'custom', deepFilterNoiseSuppression: true}), + ); + expect(resolved.autoGainControl).toBe(true); + expect(resolved.deepFilter).toBe(true); + }); + + it('still suppresses browser noise suppression when deep filter is enabled in custom mode', () => { + const resolved = resolveVoiceProcessing( + createSettings({voiceProcessingMode: 'custom', noiseSuppression: true, deepFilterNoiseSuppression: true}), + ); + expect(resolved.browserNoiseSuppression).toBe(false); + }); + + it('leaves studio mode as the untouched bypass', () => { + const resolved = resolveVoiceProcessing(createSettings({voiceProcessingMode: 'studio'})); + expect(resolved.autoGainControl).toBe(false); + expect(resolved.echoCancellation).toBe(false); + expect(resolved.browserNoiseSuppression).toBe(false); + expect(resolved.deepFilter).toBe(false); + expect(resolved.contentHint).toBe('music'); + }); +}); diff --git a/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.ts b/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.ts index a9dedc830..b3b258c82 100644 --- a/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.ts +++ b/fluxer_app/src/features/voice/utils/VoiceProcessingProfile.ts @@ -50,13 +50,12 @@ export function resolveVoiceProcessing(settings: VoiceProcessingSettingsLike): R contentHint: 'music', }; case 'custom': { - const autoGain = settings.deepFilterNoiseSuppression ? false : settings.autoGainControl; const browserNs = settings.noiseSuppression && !settings.deepFilterNoiseSuppression; return { mode: 'custom', echoCancellation: settings.echoCancellation, browserNoiseSuppression: browserNs, - autoGainControl: autoGain, + autoGainControl: settings.autoGainControl, deepFilter: settings.deepFilterNoiseSuppression, deepFilterNoiseReductionLevel: settings.deepFilterNoiseSuppression ? clampDeepFilterNoiseReductionLevel(settings.deepFilterNoiseSuppressionLevel) @@ -69,7 +68,7 @@ export function resolveVoiceProcessing(settings: VoiceProcessingSettingsLike): R mode: 'voice', echoCancellation: true, browserNoiseSuppression: false, - autoGainControl: false, + autoGainControl: settings.autoGainControl, deepFilter: true, deepFilterNoiseReductionLevel: FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL, contentHint: 'speech',