fix(voice): stop stereo microphones publishing as mono (#2846)

This commit is contained in:
Hampus
2026-09-19 21:54:38 +02:00
committed by GitHub
parent d93604afa2
commit 9005139dc8
5 changed files with 24 additions and 25 deletions
@@ -12,7 +12,6 @@ export const publishDefaults: TrackPublishDefaults = {
audioPreset: AudioPresets.music, audioPreset: AudioPresets.music,
dtx: false, dtx: false,
red: true, red: true,
forceStereo: false,
simulcast: true, simulcast: true,
screenShareEncoding: ScreenSharePresets.original.encoding, screenShareEncoding: ScreenSharePresets.original.encoding,
stopMicTrackOnMute: false, stopMicTrackOnMute: false,
@@ -622,7 +622,7 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null; const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null;
const channelBitrate = resolveVoiceChannelBitrate(channel?.bitrate, guild?.features); const channelBitrate = resolveVoiceChannelBitrate(channel?.bitrate, guild?.features);
const profile = this.resolveActiveMicrophoneProfile(); const profile = this.resolveActiveMicrophoneProfile();
return buildMicrophonePublishOptions(channelBitrate, profile.mode, profile.stereoCapture); return buildMicrophonePublishOptions(channelBitrate, profile.stereoCapture);
} }
async refreshMicrophonePublishSettings(room: Room | null, channelId: string | null): Promise<void> { async refreshMicrophonePublishSettings(room: Room | null, channelId: string | null): Promise<void> {
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource'; import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
import type {VoiceProcessingMode} from '@app/features/voice/utils/VoiceProcessingProfile';
import type {TrackPublishOptions} from 'livekit-client'; import type {TrackPublishOptions} from 'livekit-client';
export const OPUS_MAX_AUDIO_BITRATE_BPS = 510000; export const OPUS_MAX_AUDIO_BITRATE_BPS = 510000;
export const VOICE_CHANNEL_MIN_AUDIO_BITRATE_BPS = 8000; export const VOICE_CHANNEL_MIN_AUDIO_BITRATE_BPS = 8000;
export const STEREO_VOICE_MIN_AUDIO_BITRATE_BPS = 128000; export const STEREO_VOICE_MIN_AUDIO_BITRATE_BPS = 64000;
export function normaliseAudioBitrateBps(value: number | null | undefined): number | undefined { export function normaliseAudioBitrateBps(value: number | null | undefined): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined; if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined;
@@ -17,27 +16,18 @@ export function normaliseAudioBitrateBps(value: number | null | undefined): numb
export function buildMicrophonePublishOptions( export function buildMicrophonePublishOptions(
channelBitrate: number | null | undefined, channelBitrate: number | null | undefined,
processingMode: VoiceProcessingMode,
stereoCapture = false, stereoCapture = false,
): TrackPublishOptions | undefined { ): TrackPublishOptions | undefined {
const maxBitrate = normaliseAudioBitrateBps(channelBitrate); const maxBitrate = normaliseAudioBitrateBps(channelBitrate);
if (!maxBitrate) return undefined; if (!maxBitrate) return undefined;
const studioMode = processingMode === 'studio';
const bitrateAllowsStereo = maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS;
return { return {
audioPreset: { audioPreset: {
maxBitrate, maxBitrate,
priority: 'high', priority: 'high',
}, },
dtx: false,
red: true, red: true,
...(studioMode forceStereo: stereoCapture && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS,
? {
dtx: false,
forceStereo: bitrateAllowsStereo,
}
: stereoCapture && !bitrateAllowsStereo
? {forceStereo: false}
: {}),
}; };
} }
@@ -12,7 +12,10 @@ import {
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionBackends'; } from '@app/features/voice/utils/noise_suppression/NoiseSuppressionBackends';
import {readEffectiveNoiseSuppression} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionRuntime'; import {readEffectiveNoiseSuppression} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionRuntime';
import {readNoiseSuppressionRuntimeCapabilities} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection'; import {readNoiseSuppressionRuntimeCapabilities} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
import {legacyNoiseSuppressionBackend} from '@app/features/voice/utils/VoiceProcessingProfile'; import {
legacyNoiseSuppressionBackend,
resolveVoiceProcessingFromState,
} from '@app/features/voice/utils/VoiceProcessingProfile';
export const NOISE_SUPPRESSION_UI_SAMPLE_RATE = 48000; export const NOISE_SUPPRESSION_UI_SAMPLE_RATE = 48000;
@@ -67,8 +70,8 @@ export function isStereoMicrophoneChoiceAvailable(): boolean {
const assignment = VoiceNoiseSuppressionRollout.assignment; const assignment = VoiceNoiseSuppressionRollout.assignment;
if (!assignment.enabled || !assignment.stereo_enabled) return false; if (!assignment.enabled || !assignment.stereo_enabled) return false;
const effective = readEffectiveNoiseSuppression(NOISE_SUPPRESSION_UI_SAMPLE_RATE); const effective = readEffectiveNoiseSuppression(NOISE_SUPPRESSION_UI_SAMPLE_RATE);
if (!effective.rolloutApplied || effective.backend == null) return false; const backend = effective.backend ?? resolveVoiceProcessingFromState(VoiceSettings).noiseSuppressionBackend;
return getNoiseSuppressionBackendDescriptor(effective.backend).preservesInputChannels; return getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
} }
export function isStereoMicrophoneEnabled(): boolean { export function isStereoMicrophoneEnabled(): boolean {
@@ -44,7 +44,8 @@ export function resolveEffectiveNoiseSuppression(
): EffectiveNoiseSuppression { ): EffectiveNoiseSuppression {
const preference = isVoiceNoiseSuppressionBackend(userPreference) ? userPreference : null; const preference = isVoiceNoiseSuppressionBackend(userPreference) ? userPreference : null;
const resolution = resolveVoiceNoiseSuppressionForCall(assignment, guildId, preference); const resolution = resolveVoiceNoiseSuppressionForCall(assignment, guildId, preference);
if (resolution == null) return INERT_EFFECTIVE_NOISE_SUPPRESSION; const stereoAllowed = assignment.enabled && assignment.stereo_enabled && stereoPreference !== false;
if (resolution == null) return {...INERT_EFFECTIVE_NOISE_SUPPRESSION, stereoEnabled: stereoAllowed};
const backend = selectUsableNoiseSuppressionBackend(resolution.backend, capabilities); const backend = selectUsableNoiseSuppressionBackend(resolution.backend, capabilities);
return { return {
rolloutApplied: true, rolloutApplied: true,
@@ -53,9 +54,7 @@ export function resolveEffectiveNoiseSuppression(
source: resolution.source, source: resolution.source,
suppressionStrength: resolution.suppressionStrength, suppressionStrength: resolution.suppressionStrength,
stereoEnabled: stereoEnabled:
resolution.stereoEnabled && stereoAllowed && resolution.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels,
stereoPreference !== false &&
getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels,
configVersion: resolution.configVersion, configVersion: resolution.configVersion,
}; };
} }
@@ -78,12 +77,20 @@ export function getNoiseSuppressionScopeGuildId(): string | null {
return activeScopeGuildId; return activeScopeGuildId;
} }
export function resolveStereoCapture(
effective: EffectiveNoiseSuppression,
backend: VoiceNoiseSuppressionBackend,
): boolean {
return effective.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
}
export function applyNoiseSuppressionOverride( export function applyNoiseSuppressionOverride(
profile: ResolvedVoiceProcessing, profile: ResolvedVoiceProcessing,
effective: EffectiveNoiseSuppression, effective: EffectiveNoiseSuppression,
): ResolvedVoiceProcessing { ): ResolvedVoiceProcessing {
if (!effective.rolloutApplied || effective.backend == null) return profile; if (!effective.rolloutApplied || effective.backend == null || profile.mode === 'studio') {
if (profile.mode === 'studio') return profile; return {...profile, stereoCapture: resolveStereoCapture(effective, profile.noiseSuppressionBackend)};
}
const backend = effective.backend; const backend = effective.backend;
const descriptor = getNoiseSuppressionBackendDescriptor(backend); const descriptor = getNoiseSuppressionBackendDescriptor(backend);
return { return {
@@ -92,6 +99,6 @@ export function applyNoiseSuppressionOverride(
deepFilter: backend === 'deep_filter', deepFilter: backend === 'deep_filter',
deepFilterNoiseReductionLevel: backend === 'deep_filter' ? effective.suppressionStrength : 0, deepFilterNoiseReductionLevel: backend === 'deep_filter' ? effective.suppressionStrength : 0,
noiseSuppressionBackend: backend, noiseSuppressionBackend: backend,
stereoCapture: effective.stereoEnabled, stereoCapture: resolveStereoCapture(effective, backend),
}; };
} }