Add support for toggling AGC/AEC/NS in desktop

Co-authored-by: Adel Lahlou <adel@signal.org>
This commit is contained in:
Miriam Zimmerman 2026-05-04 17:40:26 -04:00 committed by GitHub
parent 15125f8c01
commit e36bee834d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 4 deletions

View File

@ -162,6 +162,8 @@ class NativeCallManager {
(NativeCallManager.prototype as any).getAudioOutputs =
Native.cm_getAudioOutputs;
(NativeCallManager.prototype as any).setAudioOutput = Native.cm_setAudioOutput;
(NativeCallManager.prototype as any).setVoiceProcessingEnabled =
Native.cm_setVoiceProcessingEnabled;
(NativeCallManager.prototype as any).processEvents = Native.cm_processEvents;
(NativeCallManager.prototype as any).setRtcStatsInterval =
Native.cm_setRtcStatsInterval;
@ -1955,6 +1957,20 @@ export class RingRTCType {
setAudioOutput(index: number): void {
this.callManager.setAudioOutput(index);
}
/**
* Enables or disables different voice processing techniques including:
* - Acoustic Echo Cancellation (AEC)
* - Noise Supression (NS)
* - Automatic Gain Control (AGC)
*
* This request is idempotent
*
* @param enabled - whether to enable voice processing
*/
setVoiceProcessingEnabled(enabled: boolean): void {
this.callManager.setVoiceProcessingEnabled(enabled);
}
}
export interface CallSettings {
@ -3176,6 +3192,7 @@ export interface CallManager {
setAudioInput(index: number): void;
getAudioOutputs(): Array<AudioDevice>;
setAudioOutput(index: number): void;
setVoiceProcessingEnabled(enabled: boolean): void;
}
export interface CallManagerCallbacks {

View File

@ -2309,6 +2309,23 @@ fn setAudioOutput(mut cx: FunctionContext) -> JsResult<JsValue> {
Ok(cx.undefined().upcast())
}
#[allow(non_snake_case)]
fn setVoiceProcessingEnabled(mut cx: FunctionContext) -> JsResult<JsValue> {
let enabled = cx.argument::<JsBoolean>(0)?.value(&mut cx);
info!("setVoiceProcessingEnabled(): {:?}", enabled);
match with_call_endpoint(&mut cx, |endpoint| {
endpoint
.peer_connection_factory
.set_input_voice_processing_enabled(enabled)
}) {
Ok(_) => (),
Err(err) => error!("setVoiceProcessingEnabled failed: {}", err),
};
Ok(cx.undefined().upcast())
}
#[allow(non_snake_case)]
fn setRtcStatsInterval(mut cx: FunctionContext) -> JsResult<JsValue> {
let client_id = cx.argument::<JsNumber>(0)?.value(&mut cx) as group_call::ClientId;
@ -3235,6 +3252,7 @@ fn register(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("cm_setAudioInput", setAudioInput)?;
cx.export_function("cm_getAudioOutputs", getAudioOutputs)?;
cx.export_function("cm_setAudioOutput", setAudioOutput)?;
cx.export_function("cm_setVoiceProcessingEnabled", setVoiceProcessingEnabled)?;
cx.export_function("cm_setRtcStatsInterval", setRtcStatsInterval)?;
cx.export_function("cm_processEvents", processEvents)?;
Ok(())

View File

@ -19,7 +19,7 @@ use std::{
use anyhow::{Context as AnyhowContext, anyhow, bail};
use cubeb::{Context, DeviceId, DeviceType, MonoFrame, StereoFrame, Stream, StreamPrefs};
use cubeb_core::{LogLevel, log_enabled, set_logging};
use cubeb_core::{InputProcessingParams, LogLevel, log_enabled, set_logging};
use lazy_static::lazy_static;
use regex::Regex;
#[cfg(target_os = "windows")]
@ -83,6 +83,7 @@ enum Event {
InitRecording,
StartRecording,
WarmupRecording,
SetInputProcessing(bool),
StopRecording,
PlayoutDelay,
Terminate,
@ -107,6 +108,7 @@ struct Worker {
// Note that the streams must not outlive the ctx.
output_stream: Option<Stream<OutFrame>>,
input_stream: Option<Stream<Frame>>,
voice_processing_enabled: bool,
// Note that the caches must not outlive the ctx.
input_device_cache: DeviceCollectionWrapper,
output_device_cache: DeviceCollectionWrapper,
@ -440,9 +442,7 @@ impl Worker {
.rate(SAMPLE_FREQUENCY)
.channels(NUM_CHANNELS)
.layout(cubeb::ChannelLayout::MONO);
// On Mac, the AEC pipeline runs at 24kHz (FB15839727 tracks this). For now,
// disable it.
let params = if cfg!(not(target_os = "macos")) {
let params = if cfg!(not(target_os = "macos")) || self.voice_processing_enabled {
builder.prefs(StreamPrefs::VOICE)
} else {
builder
@ -513,6 +513,41 @@ impl Worker {
});
match builder.init(&self.ctx) {
Ok(stream) => {
if cfg!(target_os = "macos") && self.voice_processing_enabled {
// Note: On Mac, the AEC pipeline runs at 24kHz (FB15839727 tracks this).
// This results in a slightly thin sound because of the Nyquist theorem.
// See https://en.wikipedia.org/wiki/Nyquist_frequency
match self.ctx.supported_input_processing_params() {
Ok(params) => {
// With cubeb-coreaudio-rs, the VPIO input is inaudible without these settings.
// See https://github.com/mozilla/cubeb-coreaudio-rs/issues/239#issuecomment-2430361990
info!("Available input processing params: {:?}", params);
let mut desired_params = InputProcessingParams::empty();
if params.contains(InputProcessingParams::AUTOMATIC_GAIN_CONTROL)
&& self.voice_processing_enabled
{
desired_params |= InputProcessingParams::AUTOMATIC_GAIN_CONTROL;
}
// With the coreaudio-rust backend, these settings must be set together.
if params.contains(
InputProcessingParams::ECHO_CANCELLATION
| InputProcessingParams::NOISE_SUPPRESSION,
) && self.voice_processing_enabled
{
desired_params |= InputProcessingParams::ECHO_CANCELLATION
| InputProcessingParams::NOISE_SUPPRESSION;
}
if let Err(e) = stream.set_input_processing_params(desired_params) {
error!("couldn't set input params: {:?}", e);
}
}
Err(e) => warn!(
"Failed to get supported input processing parameters; proceeding without: {}",
e
),
}
}
self.input_stream = Some(stream);
Ok(())
}
@ -574,6 +609,18 @@ impl Worker {
Ok(())
}
fn set_input_processing_enabled(&mut self, enabled: bool) -> anyhow::Result<()> {
if enabled == self.voice_processing_enabled {
return Ok(());
}
self.voice_processing_enabled = enabled;
if let Some(device) = self.recording_device {
self.update_recording_device(device)
} else {
Ok(())
}
}
// Get the playout delay, in ms.
fn playout_delay(&mut self) -> anyhow::Result<u16> {
match &self.output_stream {
@ -654,6 +701,9 @@ impl Worker {
Event::InitRecording => self.init_recording(),
Event::StartRecording => self.start_recording(true),
Event::WarmupRecording => self.start_recording(false),
Event::SetInputProcessing(voice_processing_enabled) => {
self.set_input_processing_enabled(voice_processing_enabled)
}
Event::StopRecording => self.stop_recording(),
Event::PlayoutDelay => {
if let Err(e) = playout_delay_sender.send(self.playout_delay()) {
@ -808,6 +858,7 @@ impl Worker {
recording_device: None,
output_stream: None,
input_stream: None,
voice_processing_enabled: false,
input_device_cache: Default::default(),
output_device_cache: Default::default(),
input_device_names,
@ -1647,6 +1698,13 @@ impl AudioDeviceModule {
}
out
}
pub fn set_input_processing_enabled(&mut self, enabled: bool) -> anyhow::Result<()> {
if let Err(e) = self.mpsc_sender.send(Event::SetInputProcessing(enabled)) {
return Err(anyhow!("Failed to request SetInputProcessing: {}", e));
}
Ok(())
}
}
#[cfg(test)]

View File

@ -528,6 +528,16 @@ impl PeerConnectionFactory {
)
}
#[cfg(all(not(feature = "sim"), feature = "native"))]
pub fn set_input_voice_processing_enabled(&mut self, enabled: bool) -> Result<()> {
self.adm
.as_ref()
.and_then(|adm| adm.lock().ok())
.map_or(Err(anyhow!("couldn't access ADM")), |mut adm| {
adm.set_input_processing_enabled(enabled)
})
}
#[cfg(feature = "native")]
pub fn get_audio_recording_devices(&mut self) -> Result<Vec<AudioDevice>> {
let devices = self