fix(voice): preserve microphone channels during screen sharing (#3070)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-30 20:47:30 +02:00
1 parent dfdfffe5de
commit 5e0806f479
8 files changed
+274 -14

No files matched your search

@@ -294,7 +294,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
value: backend,
label: getNoiseSuppressionChoiceLabel(i18n, backend),
}));
const stereoMicrophoneAvailable = isStereoMicrophoneChoiceAvailable();
const stereoMicrophoneAvailable = isStereoMicrophoneChoiceAvailable(activeInputLabel);
const setPushToTalkEnabled = (enabled: boolean) => {
const mode = enabled ? 'voice_push_to_talk' : 'voice_activity';
if (enabled && !isNativeDesktop) {
@@ -0,0 +1,63 @@
import type {VoiceProcessingMode} from '@app/features/voice/utils/VoiceProcessingProfile';
import {beforeEach, describe, expect, it, vi} from 'vitest';
const {deviceModes, voiceSettings} = vi.hoisted(() => ({
deviceModes: new Map<string, VoiceProcessingMode>(),
voiceSettings: {
voiceProcessingMode: 'voice' as VoiceProcessingMode,
echoCancellation: true,
autoGainControl: false,
getNoiseSuppressionBackend: () => 'none',
getStereoMicrophone: () => false,
getVoiceProcessingModeForDeviceLabel: vi.fn<(label: string | null | undefined) => VoiceProcessingMode>(),
},
}));
vi.mock('@app/features/voice/state/VoiceSettings', () => ({default: voiceSettings}));
vi.mock('@app/features/voice/commands/VoiceSettingsCommands', () => ({update: vi.fn()}));
vi.mock('@app/features/voice/engine/VoiceDevicePermissionState', () => ({
default: {getState: () => ({inputDevices: []})},
}));
vi.mock('@app/features/voice/utils/VoiceDeviceManager', () => ({resolveEffectiveDeviceId: () => null}));
import {isStereoMicrophoneChoiceAvailable} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionChoices';
describe('stereo microphone choice for the selected device', () => {
beforeEach(() => {
deviceModes.clear();
voiceSettings.voiceProcessingMode = 'voice';
voiceSettings.echoCancellation = true;
voiceSettings.autoGainControl = false;
voiceSettings.getVoiceProcessingModeForDeviceLabel.mockReset();
voiceSettings.getVoiceProcessingModeForDeviceLabel.mockImplementation(
(label) => (label ? deviceModes.get(label) : undefined) ?? voiceSettings.voiceProcessingMode,
);
});
it('shows the switch for a device using direct input when the global mode is focused voice', () => {
deviceModes.set('USB audio interface', 'studio');
expect(isStereoMicrophoneChoiceAvailable('USB audio interface')).toBe(true);
expect(voiceSettings.getVoiceProcessingModeForDeviceLabel).toHaveBeenCalledWith('USB audio interface');
});
it('hides the switch for a device with custom processing when the global mode is direct input', () => {
voiceSettings.voiceProcessingMode = 'studio';
deviceModes.set('Headset microphone', 'custom');
expect(isStereoMicrophoneChoiceAvailable('Headset microphone')).toBe(false);
});
it('shows the switch for a custom device profile with processing disabled', () => {
deviceModes.set('USB audio interface', 'custom');
voiceSettings.echoCancellation = false;
expect(isStereoMicrophoneChoiceAvailable('USB audio interface')).toBe(true);
});
it.each([
{mode: 'voice' as const, available: false},
{mode: 'studio' as const, available: true},
])('uses the global $mode profile when the device has no override', ({mode, available}) => {
voiceSettings.voiceProcessingMode = mode;
expect(isStereoMicrophoneChoiceAvailable('Unconfigured microphone')).toBe(available);
expect(isStereoMicrophoneChoiceAvailable(null)).toBe(available);
});
});
@@ -12,7 +12,7 @@ import {
readNoiseSuppressionRuntimeCapabilities,
supportsStereoCapture,
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
import {resolveVoiceProcessingFromState} from '@app/features/voice/utils/VoiceProcessingProfile';
import {resolveVoiceProcessingFromStateForDeviceLabel} from '@app/features/voice/utils/VoiceProcessingProfile';
export function getNoiseSuppressionChoiceValues(): ReadonlyArray<VoiceNoiseSuppressionBackend> {
const capabilities = readNoiseSuppressionRuntimeCapabilities();
@@ -29,8 +29,8 @@ export function setNoiseSuppressionChoice(backend: VoiceNoiseSuppressionBackend)
VoiceSettingsCommands.update({noiseSuppressionBackend: backend});
}
export function isStereoMicrophoneChoiceAvailable(): boolean {
return supportsStereoCapture(resolveVoiceProcessingFromState(VoiceSettings));
export function isStereoMicrophoneChoiceAvailable(deviceLabel: string | null): boolean {
return supportsStereoCapture(resolveVoiceProcessingFromStateForDeviceLabel(VoiceSettings, deviceLabel));
}
export function isStereoMicrophoneEnabled(): boolean {