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,
dtx: false,
red: true,
forceStereo: false,
simulcast: true,
screenShareEncoding: ScreenSharePresets.original.encoding,
stopMicTrackOnMute: false,
@@ -622,7 +622,7 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null;
const channelBitrate = resolveVoiceChannelBitrate(channel?.bitrate, guild?.features);
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> {
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
import type {VoiceProcessingMode} from '@app/features/voice/utils/VoiceProcessingProfile';
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 = 128000;
export const STEREO_VOICE_MIN_AUDIO_BITRATE_BPS = 64000;
export function normaliseAudioBitrateBps(value: number | null | undefined): number | 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(
channelBitrate: number | null | undefined,
processingMode: VoiceProcessingMode,
stereoCapture = false,
): TrackPublishOptions | undefined {
const maxBitrate = normaliseAudioBitrateBps(channelBitrate);
if (!maxBitrate) return undefined;
const studioMode = processingMode === 'studio';
const bitrateAllowsStereo = maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS;
return {
audioPreset: {
maxBitrate,
priority: 'high',
},
red: true,
...(studioMode
? {
dtx: false,
forceStereo: bitrateAllowsStereo,
}
: stereoCapture && !bitrateAllowsStereo
? {forceStereo: false}
: {}),
red: true,
forceStereo: stereoCapture && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS,
};
}
@@ -12,7 +12,10 @@ import {
} 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 {legacyNoiseSuppressionBackend} from '@app/features/voice/utils/VoiceProcessingProfile';
import {
legacyNoiseSuppressionBackend,
resolveVoiceProcessingFromState,
} from '@app/features/voice/utils/VoiceProcessingProfile';
export const NOISE_SUPPRESSION_UI_SAMPLE_RATE = 48000;
@@ -67,8 +70,8 @@ export function isStereoMicrophoneChoiceAvailable(): boolean {
const assignment = VoiceNoiseSuppressionRollout.assignment;
if (!assignment.enabled || !assignment.stereo_enabled) return false;
const effective = readEffectiveNoiseSuppression(NOISE_SUPPRESSION_UI_SAMPLE_RATE);
if (!effective.rolloutApplied || effective.backend == null) return false;
return getNoiseSuppressionBackendDescriptor(effective.backend).preservesInputChannels;
const backend = effective.backend ?? resolveVoiceProcessingFromState(VoiceSettings).noiseSuppressionBackend;
return getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
}
export function isStereoMicrophoneEnabled(): boolean {
@@ -44,7 +44,8 @@ export function resolveEffectiveNoiseSuppression(
): EffectiveNoiseSuppression {
const preference = isVoiceNoiseSuppressionBackend(userPreference) ? userPreference : null;
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);
return {
rolloutApplied: true,
@@ -53,9 +54,7 @@ export function resolveEffectiveNoiseSuppression(
source: resolution.source,
suppressionStrength: resolution.suppressionStrength,
stereoEnabled:
resolution.stereoEnabled &&
stereoPreference !== false &&
getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels,
stereoAllowed && resolution.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels,
configVersion: resolution.configVersion,
};
}
@@ -78,12 +77,20 @@ export function getNoiseSuppressionScopeGuildId(): string | null {
return activeScopeGuildId;
}
export function resolveStereoCapture(
effective: EffectiveNoiseSuppression,
backend: VoiceNoiseSuppressionBackend,
): boolean {
return effective.stereoEnabled && getNoiseSuppressionBackendDescriptor(backend).preservesInputChannels;
}
export function applyNoiseSuppressionOverride(
profile: ResolvedVoiceProcessing,
effective: EffectiveNoiseSuppression,
): ResolvedVoiceProcessing {
if (!effective.rolloutApplied || effective.backend == null) return profile;
if (profile.mode === 'studio') return profile;
if (!effective.rolloutApplied || effective.backend == null || profile.mode === 'studio') {
return {...profile, stereoCapture: resolveStereoCapture(effective, profile.noiseSuppressionBackend)};
}
const backend = effective.backend;
const descriptor = getNoiseSuppressionBackendDescriptor(backend);
return {
@@ -92,6 +99,6 @@ export function applyNoiseSuppressionOverride(
deepFilter: backend === 'deep_filter',
deepFilterNoiseReductionLevel: backend === 'deep_filter' ? effective.suppressionStrength : 0,
noiseSuppressionBackend: backend,
stereoCapture: effective.stereoEnabled,
stereoCapture: resolveStereoCapture(effective, backend),
};
}