fix(voice): stop forcing automatic gain control off on every capture profile (#1781)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-20 17:41:44 +02:00
1 parent 5331c0216a
commit c020eded31
6 files changed
+87 -22

No files matched your search

@@ -169,8 +169,8 @@ const resolvePushToTalkReleaseDelayInput = (
).value; ).value;
}; };
const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({ const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({
message: 'Evens out your mic volume. Off when enhanced suppression is on.', message: 'Evens out your mic volume so you are not too quiet.',
comment: 'Description for the automatic gain control toggle in the custom voice processing settings.', comment: 'Description for the automatic gain control toggle in the voice processing settings.',
}); });
const INPUT_AND_OUTPUT_DESCRIPTOR = msg({ const INPUT_AND_OUTPUT_DESCRIPTOR = msg({
message: 'Input and output', message: 'Input and output',
@@ -293,7 +293,6 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
}, },
]; ];
const noiseSuppressionMethod = resolveNoiseSuppressionMethod(deepFilterNoiseSuppression, noiseSuppression); const noiseSuppressionMethod = resolveNoiseSuppressionMethod(deepFilterNoiseSuppression, noiseSuppression);
const effectiveAutoGainControl = !deepFilterNoiseSuppression && autoGainControl;
const noiseSuppressionOptions: Array<ComboboxOption<NoiseSuppressionMethod>> = [ const noiseSuppressionOptions: Array<ComboboxOption<NoiseSuppressionMethod>> = [
{value: 'enhanced', label: i18n._(NOISE_SUPPRESSION_ENHANCED_DESCRIPTOR)}, {value: 'enhanced', label: i18n._(NOISE_SUPPRESSION_ENHANCED_DESCRIPTOR)},
{value: 'standard', label: i18n._(NOISE_SUPPRESSION_STANDARD_DESCRIPTOR)}, {value: 'standard', label: i18n._(NOISE_SUPPRESSION_STANDARD_DESCRIPTOR)},
@@ -449,6 +448,16 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
)} )}
</> </>
); );
const renderAutoGainControlSwitch = () => (
<Switch
label={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)}
description={i18n._(AUTO_GAIN_DESCRIPTION_DESCRIPTOR)}
value={autoGainControl}
onChange={(value) => 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 = () => ( const renderCustomProfile = () => (
<div className={styles.profileSubSection} data-flx="user.voice-tab.render-custom-profile.profile-sub-section"> <div className={styles.profileSubSection} data-flx="user.voice-tab.render-custom-profile.profile-sub-section">
{renderPttControls()} {renderPttControls()}
@@ -507,15 +516,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
ariaLabel={i18n._(VOICE_ECHO_CANCELLATION_DESCRIPTOR)} ariaLabel={i18n._(VOICE_ECHO_CANCELLATION_DESCRIPTOR)}
data-flx="user.voice-tab.render-custom-profile.switch.update--2" data-flx="user.voice-tab.render-custom-profile.switch.update--2"
/> />
<Switch {renderAutoGainControlSwitch()}
label={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)}
description={i18n._(AUTO_GAIN_DESCRIPTION_DESCRIPTOR)}
value={effectiveAutoGainControl}
disabled={deepFilterNoiseSuppression}
onChange={(value) => VoiceSettingsCommands.update({autoGainControl: value})}
ariaLabel={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)}
data-flx="user.voice-tab.render-custom-profile.switch.update-auto-gain-control"
/>
</div> </div>
); );
return ( return (
@@ -662,6 +663,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
{voiceProcessingMode === 'voice' && ( {voiceProcessingMode === 'voice' && (
<div className={styles.profileSubSection} data-flx="user.voice-tab.profile-sub-section"> <div className={styles.profileSubSection} data-flx="user.voice-tab.profile-sub-section">
{renderPttControls()} {renderPttControls()}
{renderAutoGainControlSwitch()}
</div> </div>
)} )}
{voiceProcessingMode === 'studio' && pttCombo?.key && isPushToTalk && ( {voiceProcessingMode === 'studio' && pttCombo?.key && isPushToTalk && (
@@ -682,7 +684,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
outputVolume, outputVolume,
echoCancellation, echoCancellation,
noiseSuppression, noiseSuppression,
autoGainControl: effectiveAutoGainControl, autoGainControl,
deepFilterNoiseSuppression, deepFilterNoiseSuppression,
deepFilterNoiseSuppressionLevel, deepFilterNoiseSuppressionLevel,
voiceProcessingMode, voiceProcessingMode,
@@ -359,8 +359,7 @@ export const VoiceAudioSettingsMenu: React.FC<VoiceAudioSettingsMenuProps> = obs
data-flx="voice.voice-settings-menus.voice-audio-settings-menu.icon--2" data-flx="voice.voice-settings-menus.voice-audio-settings-menu.icon--2"
/> />
} }
checked={voiceSettings.autoGainControl && !deepFilterEnabled} checked={voiceSettings.autoGainControl}
disabled={deepFilterEnabled}
onCheckedChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})} onCheckedChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})}
data-flx="voice.voice-settings-menus.voice-audio-settings-menu.checkbox-item--2" data-flx="voice.voice-settings-menus.voice-audio-settings-menu.checkbox-item--2"
> >
@@ -281,7 +281,6 @@ export const VoiceAudioSettingsBottomSheet: React.FC<VoiceAudioSettingsBottomShe
/> />
), ),
label: i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR), label: i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR),
disabled: deepFilterEnabled,
onClick: () => { onClick: () => {
VoiceSettingsCommands.update({autoGainControl: !voiceSettings.autoGainControl}); VoiceSettingsCommands.update({autoGainControl: !voiceSettings.autoGainControl});
}, },
@@ -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.', comment: 'Description for the echo cancellation toggle in the custom voice processing settings.',
}); });
const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({ const AUTO_GAIN_DESCRIPTION_DESCRIPTOR = msg({
message: 'Evens out your mic volume. Off when enhanced suppression is on.', message: 'Evens out your mic volume so you are not too quiet.',
comment: 'Description for the automatic gain control toggle in the custom voice processing settings.', comment: 'Description for the automatic gain control toggle in the voice processing settings.',
}); });
const DEEP_FILTER_STRENGTH_DESCRIPTOR = msg({ const DEEP_FILTER_STRENGTH_DESCRIPTOR = msg({
message: 'Suppression strength', message: 'Suppression strength',
@@ -183,7 +183,6 @@ export const AudioProcessingModal = observer(() => {
label={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)} label={i18n._(VOICE_AUTOMATIC_GAIN_CONTROL_DESCRIPTOR)}
description={i18n._(AUTO_GAIN_DESCRIPTION_DESCRIPTOR)} description={i18n._(AUTO_GAIN_DESCRIPTION_DESCRIPTOR)}
value={VoiceSettings.autoGainControl} value={VoiceSettings.autoGainControl}
disabled={deepFilterEnabled}
onChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})} onChange={(checked) => VoiceSettingsCommands.update({autoGainControl: checked})}
compact compact
data-flx="voice.voice-connection-status.audio-processing-modal.switch.update--2" data-flx="voice.voice-connection-status.audio-processing-modal.switch.update--2"
@@ -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> = {}): 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');
});
});
@@ -50,13 +50,12 @@ export function resolveVoiceProcessing(settings: VoiceProcessingSettingsLike): R
contentHint: 'music', contentHint: 'music',
}; };
case 'custom': { case 'custom': {
const autoGain = settings.deepFilterNoiseSuppression ? false : settings.autoGainControl;
const browserNs = settings.noiseSuppression && !settings.deepFilterNoiseSuppression; const browserNs = settings.noiseSuppression && !settings.deepFilterNoiseSuppression;
return { return {
mode: 'custom', mode: 'custom',
echoCancellation: settings.echoCancellation, echoCancellation: settings.echoCancellation,
browserNoiseSuppression: browserNs, browserNoiseSuppression: browserNs,
autoGainControl: autoGain, autoGainControl: settings.autoGainControl,
deepFilter: settings.deepFilterNoiseSuppression, deepFilter: settings.deepFilterNoiseSuppression,
deepFilterNoiseReductionLevel: settings.deepFilterNoiseSuppression deepFilterNoiseReductionLevel: settings.deepFilterNoiseSuppression
? clampDeepFilterNoiseReductionLevel(settings.deepFilterNoiseSuppressionLevel) ? clampDeepFilterNoiseReductionLevel(settings.deepFilterNoiseSuppressionLevel)
@@ -69,7 +68,7 @@ export function resolveVoiceProcessing(settings: VoiceProcessingSettingsLike): R
mode: 'voice', mode: 'voice',
echoCancellation: true, echoCancellation: true,
browserNoiseSuppression: false, browserNoiseSuppression: false,
autoGainControl: false, autoGainControl: settings.autoGainControl,
deepFilter: true, deepFilter: true,
deepFilterNoiseReductionLevel: FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL, deepFilterNoiseReductionLevel: FOCUSED_VOICE_DEEP_FILTER_NOISE_REDUCTION_LEVEL,
contentHint: 'speech', contentHint: 'speech',