fix(voice): make stereo microphones work in studio and custom (#2852)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-20 00:19:53 +02:00
1 parent a3454e8245
commit 487febac8e
52 files changed
+105 -138

No files matched your search

@@ -5,7 +5,7 @@ import type {TrackPublishOptions} from 'livekit-client';
export const OPUS_MAX_AUDIO_BITRATE_BPS = 510000;
export const VOICE_CHANNEL_MIN_AUDIO_BITRATE_BPS = 8000;
export const STEREO_VOICE_MIN_AUDIO_BITRATE_BPS = 64000;
export const STEREO_VOICE_MIN_AUDIO_BITRATE_BPS = 128000;
export function normaliseAudioBitrateBps(value: number | null | undefined): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined;
@@ -27,7 +27,7 @@ export function buildMicrophonePublishOptions(
},
dtx: false,
red: true,
forceStereo: stereoCapture && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS,
forceStereo: stereoCapture && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS ? undefined : false,
};
}
@@ -4,14 +4,17 @@ import * as VoiceSettingsCommands from '@app/features/voice/commands/VoiceSettin
import VoiceNoiseSuppressionRollout from '@app/features/voice/state/VoiceNoiseSuppressionRollout';
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
import {
getNoiseSuppressionBackendDescriptor,
isNoiseSuppressionBackendSupported,
isVoiceNoiseSuppressionBackend,
VOICE_NOISE_SUPPRESSION_BACKENDS,
type VoiceNoiseSuppressionBackend,
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionBackends';
import {readEffectiveNoiseSuppression} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionRuntime';
import {readNoiseSuppressionRuntimeCapabilities} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
import {
applyNoiseSuppressionOverride,
readNoiseSuppressionRuntimeCapabilities,
supportsStereoCapture,
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
import {
legacyNoiseSuppressionBackend,
resolveVoiceProcessingFromState,
@@ -67,13 +70,11 @@ export function setNoiseSuppressionChoice(backend: VoiceNoiseSuppressionBackend)
}
export function isStereoMicrophoneChoiceAvailable(): boolean {
const assignment = VoiceNoiseSuppressionRollout.assignment;
if (!assignment.enabled || !assignment.stereo_enabled) return false;
const effective = readEffectiveNoiseSuppression(NOISE_SUPPRESSION_UI_SAMPLE_RATE);
const backend = effective.backend ?? resolveVoiceProcessingFromState(VoiceSettings).noiseSuppressionBackend;
return getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
const profile = applyNoiseSuppressionOverride(resolveVoiceProcessingFromState(VoiceSettings), effective);
return supportsStereoCapture(profile);
}
export function isStereoMicrophoneEnabled(): boolean {
return VoiceSettings.getStereoMicrophone() !== false;
return VoiceSettings.getStereoMicrophone() === true;
}
@@ -47,7 +47,7 @@ const STEREO_MICROPHONE_DESCRIPTOR = msg({
comment: 'Switch label in the voice settings that sends both channels of a stereo microphone. Keep it concise.',
});
const STEREO_MICROPHONE_DESCRIPTION_DESCRIPTOR = msg({
message: 'Sends both channels of a stereo microphone. Works only with no suppression or standard suppression.',
message: 'Sends both channels of a stereo microphone. Needs all processing off and a channel at 128 kbps or higher.',
comment: 'Description for the stereo microphone switch in the voice settings.',
});
@@ -21,7 +21,7 @@ export interface EffectiveNoiseSuppression {
requestedBackend: VoiceNoiseSuppressionBackend | null;
source: VoiceNoiseSuppressionResolutionSource | null;
suppressionStrength: number;
stereoEnabled: boolean;
stereoPreferred: boolean;
configVersion: number;
}
@@ -31,7 +31,7 @@ export const INERT_EFFECTIVE_NOISE_SUPPRESSION: EffectiveNoiseSuppression = {
requestedBackend: null,
source: null,
suppressionStrength: 80,
stereoEnabled: false,
stereoPreferred: false,
configVersion: 0,
};
@@ -44,8 +44,8 @@ export function resolveEffectiveNoiseSuppression(
): EffectiveNoiseSuppression {
const preference = isVoiceNoiseSuppressionBackend(userPreference) ? userPreference : null;
const resolution = resolveVoiceNoiseSuppressionForCall(assignment, guildId, preference);
const stereoAllowed = assignment.enabled && assignment.stereo_enabled && stereoPreference !== false;
if (resolution == null) return {...INERT_EFFECTIVE_NOISE_SUPPRESSION, stereoEnabled: stereoAllowed};
const stereoAllowed = stereoPreference === true;
if (resolution == null) return {...INERT_EFFECTIVE_NOISE_SUPPRESSION, stereoPreferred: stereoAllowed};
const backend = selectUsableNoiseSuppressionBackend(resolution.backend, capabilities);
return {
rolloutApplied: true,
@@ -53,8 +53,7 @@ export function resolveEffectiveNoiseSuppression(
requestedBackend: resolution.backend,
source: resolution.source,
suppressionStrength: resolution.suppressionStrength,
stereoEnabled:
stereoAllowed && resolution.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels,
stereoPreferred: stereoAllowed,
configVersion: resolution.configVersion,
};
}
@@ -77,11 +76,17 @@ export function getNoiseSuppressionScopeGuildId(): string | null {
return activeScopeGuildId;
}
export function resolveStereoCapture(
effective: EffectiveNoiseSuppression,
backend: VoiceNoiseSuppressionBackend,
): boolean {
return effective.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
export function supportsStereoCapture(profile: ResolvedVoiceProcessing): boolean {
if (profile.echoCancellation || profile.autoGainControl) return false;
if (profile.browserNoiseSuppression || profile.deepFilter) return false;
return getNoiseSuppressionBackendDescriptor(profile.noiseSuppressionBackend).preservesInputChannels;
}
export function resolveStereoCapture(effective: EffectiveNoiseSuppression, profile: ResolvedVoiceProcessing): boolean {
if (!supportsStereoCapture(profile)) return false;
if (profile.mode === 'studio') return true;
if (profile.mode === 'custom') return effective.stereoPreferred;
return false;
}
export function applyNoiseSuppressionOverride(
@@ -89,16 +94,17 @@ export function applyNoiseSuppressionOverride(
effective: EffectiveNoiseSuppression,
): ResolvedVoiceProcessing {
if (!effective.rolloutApplied || effective.backend == null || profile.mode === 'studio') {
return {...profile, stereoCapture: resolveStereoCapture(effective, profile.noiseSuppressionBackend)};
return {...profile, stereoCapture: resolveStereoCapture(effective, profile)};
}
const backend = effective.backend;
const descriptor = getNoiseSuppressionBackendDescriptor(backend);
return {
const next: ResolvedVoiceProcessing = {
...profile,
browserNoiseSuppression: descriptor.browserNoiseSuppression,
deepFilter: backend === 'deep_filter',
deepFilterNoiseReductionLevel: backend === 'deep_filter' ? effective.suppressionStrength : 0,
noiseSuppressionBackend: backend,
stereoCapture: resolveStereoCapture(effective, backend),
stereoCapture: false,
};
return {...next, stereoCapture: resolveStereoCapture(effective, next)};
}