Merge remote-tracking branch 'origin/main'

This commit is contained in:
Weblate
2026-09-24 12:51:09 +00:00
4 changed files with 42 additions and 24 deletions
@@ -445,6 +445,19 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
data-flx="user.voice-tab.render-auto-gain-control-switch.switch.update-auto-gain-control" data-flx="user.voice-tab.render-auto-gain-control-switch.switch.update-auto-gain-control"
/> />
); );
const renderStereoMicrophoneSwitch = (dataFlx: string) =>
stereoMicrophoneAvailable && (
<Switch
label={i18n._(STEREO_MICROPHONE_DESCRIPTOR)}
description={i18n._(STEREO_MICROPHONE_DESCRIPTION_DESCRIPTOR)}
value={isStereoMicrophoneEnabled()}
onChange={(value) => {
VoiceSettings.stereoMicrophone = value;
}}
ariaLabel={i18n._(STEREO_MICROPHONE_DESCRIPTOR)}
data-flx={dataFlx}
/>
);
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()}
@@ -496,18 +509,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
dataFlx="user.voice-tab.render-custom-profile.select.set-noise-suppression-method" dataFlx="user.voice-tab.render-custom-profile.select.set-noise-suppression-method"
data-flx="user.user-voice-tab.render-custom-profile.compact-combobox-row.set-noise-suppression-method" data-flx="user.user-voice-tab.render-custom-profile.compact-combobox-row.set-noise-suppression-method"
/> />
{stereoMicrophoneAvailable && ( {renderStereoMicrophoneSwitch('user.voice-tab.render-custom-profile.switch.set-stereo-microphone')}
<Switch
label={i18n._(STEREO_MICROPHONE_DESCRIPTOR)}
description={i18n._(STEREO_MICROPHONE_DESCRIPTION_DESCRIPTOR)}
value={isStereoMicrophoneEnabled()}
onChange={(value) => {
VoiceSettings.stereoMicrophone = value;
}}
ariaLabel={i18n._(STEREO_MICROPHONE_DESCRIPTOR)}
data-flx="user.voice-tab.render-custom-profile.switch.set-stereo-microphone"
/>
)}
<Switch <Switch
label={i18n._(VOICE_ECHO_CANCELLATION_DESCRIPTOR)} label={i18n._(VOICE_ECHO_CANCELLATION_DESCRIPTOR)}
value={echoCancellation} value={echoCancellation}
@@ -669,6 +671,11 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
{renderAutoGainControlSwitch()} {renderAutoGainControlSwitch()}
</div> </div>
)} )}
{voiceProcessingMode === 'studio' && stereoMicrophoneAvailable && (
<div className={styles.profileSubSection} data-flx="user.voice-tab.studio-profile-sub-section">
{renderStereoMicrophoneSwitch('user.voice-tab.studio-profile.switch.set-stereo-microphone')}
</div>
)}
{voiceProcessingMode === 'studio' && pttCombo?.key && isPushToTalk && ( {voiceProcessingMode === 'studio' && pttCombo?.key && isPushToTalk && (
<WarningAlert data-flx="user.voice-tab.warning-alert--2"> <WarningAlert data-flx="user.voice-tab.warning-alert--2">
<Trans>Push-to-talk is ignored in direct input. Switch to focused voice or custom to use it.</Trans> <Trans>Push-to-talk is ignored in direct input. Switch to focused voice or custom to use it.</Trans>
@@ -98,7 +98,7 @@ import EntranceSoundLibrary from '@app/features/voice/state/EntranceSoundLibrary
import LocalVoiceState from '@app/features/voice/state/LocalVoiceState'; import LocalVoiceState from '@app/features/voice/state/LocalVoiceState';
import ParticipantVolume from '@app/features/voice/state/ParticipantVolume'; import ParticipantVolume from '@app/features/voice/state/ParticipantVolume';
import VoiceSettings from '@app/features/voice/state/VoiceSettings'; import VoiceSettings from '@app/features/voice/state/VoiceSettings';
import {buildMicrophonePublishOptions} from '@app/features/voice/utils/AudioPublishOptions'; import {buildMicrophonePublishOptions, sendsStereoMicrophone} from '@app/features/voice/utils/AudioPublishOptions';
import { import {
buildCameraPublishOptions, buildCameraPublishOptions,
findVideoPublishCodecPolicyViolation, findVideoPublishCodecPolicyViolation,
@@ -605,24 +605,31 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
return applyNoiseSuppressionOverride(profile, readEffectiveNoiseSuppression(MICROPHONE_CAPTURE_SAMPLE_RATE)); return applyNoiseSuppressionOverride(profile, readEffectiveNoiseSuppression(MICROPHONE_CAPTURE_SAMPLE_RATE));
} }
private getMicrophoneCaptureOptions(options: VoiceEngineV2MicrophoneOptions = {}): AudioCaptureOptions { private resolveMicrophoneChannelBitrate(channelId: string | null): number {
const channel = channelId ? Channels.getChannel(channelId) : null;
const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null;
return resolveVoiceChannelBitrate(channel?.bitrate, guild?.features);
}
private getMicrophoneCaptureOptions(
options: VoiceEngineV2MicrophoneOptions = {},
channelId: string | null = this.getActiveChannelId(),
): AudioCaptureOptions {
const profile = this.resolveActiveMicrophoneProfile(); const profile = this.resolveActiveMicrophoneProfile();
const stereo = sendsStereoMicrophone(this.resolveMicrophoneChannelBitrate(channelId), profile.stereoCapture);
return { return {
deviceId: options.deviceId ?? this.resolveInputDeviceId(), deviceId: options.deviceId ?? this.resolveInputDeviceId(),
echoCancellation: options.echoCancellation ?? profile.echoCancellation, echoCancellation: options.echoCancellation ?? profile.echoCancellation,
noiseSuppression: options.noiseSuppression ?? profile.browserNoiseSuppression, noiseSuppression: options.noiseSuppression ?? profile.browserNoiseSuppression,
autoGainControl: options.autoGainControl ?? profile.autoGainControl, autoGainControl: options.autoGainControl ?? profile.autoGainControl,
voiceIsolation: false, voiceIsolation: false,
...(profile.stereoCapture ? {channelCount: {ideal: 2}} : {}), ...(stereo ? {channelCount: {ideal: 2}} : {}),
}; };
} }
private getMicrophonePublishOptions(channelId: string | null): TrackPublishOptions | undefined { private getMicrophonePublishOptions(channelId: string | null): TrackPublishOptions | undefined {
const channel = channelId ? Channels.getChannel(channelId) : null;
const guild = channel?.guildId ? Guilds.getGuild(channel.guildId) : null;
const channelBitrate = resolveVoiceChannelBitrate(channel?.bitrate, guild?.features);
const profile = this.resolveActiveMicrophoneProfile(); const profile = this.resolveActiveMicrophoneProfile();
return buildMicrophonePublishOptions(channelBitrate, profile.stereoCapture); return buildMicrophonePublishOptions(this.resolveMicrophoneChannelBitrate(channelId), profile.stereoCapture);
} }
async refreshMicrophonePublishSettings(room: Room | null, channelId: string | null): Promise<void> { async refreshMicrophonePublishSettings(room: Room | null, channelId: string | null): Promise<void> {
@@ -902,7 +909,7 @@ export class VoiceEngineV2AppMediaExecutionAdapter extends Store {
assertObjectLike<MicrophoneEnableState>(state, 'publishMicrophoneAudioTrack.state'); assertObjectLike<MicrophoneEnableState>(state, 'publishMicrophoneAudioTrack.state');
await ctx.room.localParticipant.setMicrophoneEnabled( await ctx.room.localParticipant.setMicrophoneEnabled(
true, true,
this.getMicrophoneCaptureOptions(ctx.options), this.getMicrophoneCaptureOptions(ctx.options, ctx.channelId),
this.getMicrophonePublishOptions(ctx.channelId), this.getMicrophonePublishOptions(ctx.channelId),
); );
state.microphoneWasPublished = true; state.microphoneWasPublished = true;
@@ -16,6 +16,11 @@ export function normaliseAudioBitrateBps(value: number | null | undefined): numb
return Math.min(Math.max(bitsPerSecond, VOICE_CHANNEL_MIN_AUDIO_BITRATE_BPS), OPUS_MAX_AUDIO_BITRATE_BPS); return Math.min(Math.max(bitsPerSecond, VOICE_CHANNEL_MIN_AUDIO_BITRATE_BPS), OPUS_MAX_AUDIO_BITRATE_BPS);
} }
export function sendsStereoMicrophone(channelBitrate: number | null | undefined, stereoCapture: boolean): boolean {
const maxBitrate = normaliseAudioBitrateBps(channelBitrate);
return stereoCapture && maxBitrate !== undefined && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS;
}
export function buildMicrophonePublishOptions( export function buildMicrophonePublishOptions(
channelBitrate: number | null | undefined, channelBitrate: number | null | undefined,
stereoCapture = false, stereoCapture = false,
@@ -29,7 +34,7 @@ export function buildMicrophonePublishOptions(
}, },
dtx: false, dtx: false,
red: true, red: true,
forceStereo: stereoCapture && maxBitrate >= STEREO_VOICE_MIN_AUDIO_BITRATE_BPS ? undefined : false, forceStereo: sendsStereoMicrophone(maxBitrate, stereoCapture) ? undefined : false,
}; };
} }
@@ -84,9 +84,8 @@ export function supportsStereoCapture(profile: ResolvedVoiceProcessing): boolean
export function resolveStereoCapture(effective: EffectiveNoiseSuppression, profile: ResolvedVoiceProcessing): boolean { export function resolveStereoCapture(effective: EffectiveNoiseSuppression, profile: ResolvedVoiceProcessing): boolean {
if (!supportsStereoCapture(profile)) return false; if (!supportsStereoCapture(profile)) return false;
if (profile.mode === 'studio') return true; if (profile.mode === 'voice') return false;
if (profile.mode === 'custom') return effective.stereoPreferred; return effective.stereoPreferred;
return false;
} }
export function applyNoiseSuppressionOverride( export function applyNoiseSuppressionOverride(