diff --git a/src/node/ringrtc/Service.ts b/src/node/ringrtc/Service.ts index 1cf0249a..b0bfca3f 100644 --- a/src/node/ringrtc/Service.ts +++ b/src/node/ringrtc/Service.ts @@ -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; setAudioOutput(index: number): void; + setVoiceProcessingEnabled(enabled: boolean): void; } export interface CallManagerCallbacks { diff --git a/src/rust/src/electron.rs b/src/rust/src/electron.rs index 62fc8a7c..01f423fe 100644 --- a/src/rust/src/electron.rs +++ b/src/rust/src/electron.rs @@ -2309,6 +2309,23 @@ fn setAudioOutput(mut cx: FunctionContext) -> JsResult { Ok(cx.undefined().upcast()) } +#[allow(non_snake_case)] +fn setVoiceProcessingEnabled(mut cx: FunctionContext) -> JsResult { + let enabled = cx.argument::(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 { let client_id = cx.argument::(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(()) diff --git a/src/rust/src/webrtc/audio_device_module.rs b/src/rust/src/webrtc/audio_device_module.rs index 9e97f96f..27b6f919 100644 --- a/src/rust/src/webrtc/audio_device_module.rs +++ b/src/rust/src/webrtc/audio_device_module.rs @@ -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>, input_stream: Option>, + 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 { 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)] diff --git a/src/rust/src/webrtc/peer_connection_factory.rs b/src/rust/src/webrtc/peer_connection_factory.rs index fdb8d4ed..1187d80d 100644 --- a/src/rust/src/webrtc/peer_connection_factory.rs +++ b/src/rust/src/webrtc/peer_connection_factory.rs @@ -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> { let devices = self