#[allow(dead_code)]
pub const APM_FRAME_SIZE: usize = 160;
#[allow(dead_code)]
pub const APM_SAMPLE_RATE: i32 = 16_000;
#[allow(dead_code)]
pub const APM_PLAYBACK_DELAY_MS: i32 = 150;
#[cfg(target_os = "windows")]
mod imp {
use super::{APM_FRAME_SIZE, APM_PLAYBACK_DELAY_MS, APM_SAMPLE_RATE};
use libloading::{Library, Symbol};
use parking_lot::Mutex;
use std::env;
use std::ffi::c_void;
use std::path::PathBuf;
use std::sync::OnceLock;
type ApmCreate = unsafe extern "C" fn() -> *mut c_void;
type ApmDestroy = unsafe extern "C" fn(*mut c_void);
type ApmInitialize = unsafe extern "C" fn(*mut c_void) -> i32;
type ApmApplyConfig = unsafe extern "C" fn(*mut c_void, *mut c_void) -> i32;
type ApmProcessStream = unsafe extern "C" fn(
apm: *mut c_void,
src: *const *const f32,
input_cfg: *mut c_void,
output_cfg: *mut c_void,
dest: *const *mut f32,
) -> i32;
type ApmProcessReverseStream = ApmProcessStream;
type ApmSetStreamDelay = unsafe extern "C" fn(*mut c_void, i32);
type ApmStreamConfigCreate = unsafe extern "C" fn(i32, usize) -> *mut c_void;
type ApmStreamConfigDestroy = unsafe extern "C" fn(*mut c_void);
type ApmConfigCreate = unsafe extern "C" fn() -> *mut c_void;
type ApmConfigDestroy = unsafe extern "C" fn(*mut c_void);
type ApmConfigSetEchoCanceller = unsafe extern "C" fn(*mut c_void, i32, i32);
type ApmConfigSetNoiseSuppression = unsafe extern "C" fn(*mut c_void, i32, i32);
type ApmConfigSetHighPassFilter = unsafe extern "C" fn(*mut c_void, i32);
type ApmConfigSetGainController1 = unsafe extern "C" fn(*mut c_void, i32, i32, i32, i32, i32);
type ApmConfigSetGainController2 = unsafe extern "C" fn(*mut c_void, i32);
type ApmConfigSetPipeline = unsafe extern "C" fn(*mut c_void, i32, i32, i32, i32);
pub struct Apm {
_lib: Library,
handle: *mut c_void,
process_stream: ApmProcessStream,
process_reverse: ApmProcessReverseStream,
set_delay: ApmSetStreamDelay,
destroy: ApmDestroy,
cfg_destroy: ApmStreamConfigDestroy,
stream_cfg: *mut c_void,
scratch_out: Mutex<Vec<f32>>,
}
unsafe impl Send for Apm {}
unsafe impl Sync for Apm {}
static LIB_PATH: OnceLock<Mutex<Option<String>>> = OnceLock::new();
pub fn set_lib_path(p: String) {
let cell = LIB_PATH.get_or_init(|| Mutex::new(None));
*cell.lock() = Some(p);
}
fn candidate_paths() -> Vec<String> {
let mut out = Vec::new();
if let Some(cell) = LIB_PATH.get() {
if let Some(explicit) = cell.lock().clone() {
out.push(explicit);
}
}
out.push("webrtc-apm.dll".to_string());
if let Ok(exe) = env::current_exe() {
if let Some(dir) = exe.parent() {
let exe_dir = PathBuf::from(dir);
out.push(
exe_dir
.join("webrtc-apm.dll")
.to_string_lossy()
.into_owned(),
);
out.push(
exe_dir
.join("resources")
.join("webrtc-apm.dll")
.to_string_lossy()
.into_owned(),
);
}
}
out
}
pub fn open() -> anyhow::Result<Apm> {
let candidates = candidate_paths();
let mut last_err: Option<String> = None;
let lib = candidates
.iter()
.find_map(|p| match unsafe { Library::new(p) } {
Ok(lib) => Some(lib),
Err(e) => {
last_err = Some(format!("{p}: {e}"));
None
}
})
.ok_or_else(|| {
anyhow::anyhow!(
"webrtc-apm.dll not found (tried {} candidates) — last error: {}",
candidates.len(),
last_err.unwrap_or_else(|| "<none>".to_string())
)
})?;
unsafe {
let create: Symbol<ApmCreate> = lib.get(b"webrtc_apm_create")?;
let init: Symbol<ApmInitialize> = lib.get(b"webrtc_apm_initialize")?;
let apply: Symbol<ApmApplyConfig> = lib.get(b"webrtc_apm_apply_config")?;
let cfg_create: Symbol<ApmStreamConfigCreate> =
lib.get(b"webrtc_apm_stream_config_create")?;
let cfg_destroy: Symbol<ApmStreamConfigDestroy> =
lib.get(b"webrtc_apm_stream_config_destroy")?;
let process_stream: Symbol<ApmProcessStream> = lib.get(b"webrtc_apm_process_stream")?;
let process_reverse: Symbol<ApmProcessReverseStream> =
lib.get(b"webrtc_apm_process_reverse_stream")?;
let set_delay: Symbol<ApmSetStreamDelay> =
lib.get(b"webrtc_apm_set_stream_delay_ms")?;
let destroy: Symbol<ApmDestroy> = lib.get(b"webrtc_apm_destroy")?;
let apm_cfg_create: Symbol<ApmConfigCreate> = lib.get(b"webrtc_apm_config_create")?;
let apm_cfg_destroy: Symbol<ApmConfigDestroy> =
lib.get(b"webrtc_apm_config_destroy")?;
let cfg_set_aec: Symbol<ApmConfigSetEchoCanceller> =
lib.get(b"webrtc_apm_config_set_echo_canceller")?;
let cfg_set_ns: Symbol<ApmConfigSetNoiseSuppression> =
lib.get(b"webrtc_apm_config_set_noise_suppression")?;
let cfg_set_hpf: Symbol<ApmConfigSetHighPassFilter> =
lib.get(b"webrtc_apm_config_set_high_pass_filter")?;
let cfg_set_agc1: Symbol<ApmConfigSetGainController1> =
lib.get(b"webrtc_apm_config_set_gain_controller1")?;
let cfg_set_agc2: Symbol<ApmConfigSetGainController2> =
lib.get(b"webrtc_apm_config_set_gain_controller2")?;
let cfg_set_pipeline: Symbol<ApmConfigSetPipeline> =
lib.get(b"webrtc_apm_config_set_pipeline")?;
let handle = create();
if handle.is_null() {
anyhow::bail!("webrtc_apm_create returned null");
}
let cfg = apm_cfg_create();
if cfg.is_null() {
(destroy)(handle);
anyhow::bail!("webrtc_apm_config_create returned null");
}
cfg_set_aec(cfg, 1, 0);
cfg_set_ns(cfg, 1, 0);
cfg_set_hpf(cfg, 0);
cfg_set_agc1(cfg, 0, 1, 3, 9, 1);
cfg_set_agc2(cfg, 0);
cfg_set_pipeline(cfg, APM_SAMPLE_RATE, 0, 0, 0);
let apply_err = apply(handle, cfg);
apm_cfg_destroy(cfg);
if apply_err != 0 {
(destroy)(handle);
anyhow::bail!("webrtc_apm_apply_config returned {apply_err}");
}
let init_err = init(handle);
if init_err != 0 {
(destroy)(handle);
anyhow::bail!("webrtc_apm_initialize returned {init_err}");
}
let stream_cfg = cfg_create(APM_SAMPLE_RATE, 1);
if stream_cfg.is_null() {
(destroy)(handle);
anyhow::bail!("webrtc_apm_stream_config_create returned null");
}
set_delay(handle, APM_PLAYBACK_DELAY_MS);
Ok(Apm {
process_stream: *process_stream,
process_reverse: *process_reverse,
set_delay: *set_delay,
destroy: *destroy,
cfg_destroy: *cfg_destroy,
_lib: lib,
handle,
stream_cfg,
scratch_out: Mutex::new(vec![0.0f32; APM_FRAME_SIZE]),
})
}
}
impl Apm {
pub fn process_near(&self, frame: &mut [f32]) -> i32 {
let mut last: i32 = 0;
let mut buf_out = self.scratch_out.lock();
let mut i = 0;
while i + APM_FRAME_SIZE <= frame.len() {
let src_ptrs: [*const f32; 1] = [frame[i..].as_ptr()];
let dst_ptrs: [*mut f32; 1] = [buf_out.as_mut_ptr()];
unsafe {
last = (self.process_stream)(
self.handle,
src_ptrs.as_ptr(),
self.stream_cfg,
self.stream_cfg,
dst_ptrs.as_ptr(),
);
}
frame[i..i + APM_FRAME_SIZE].copy_from_slice(&buf_out[..APM_FRAME_SIZE]);
i += APM_FRAME_SIZE;
}
last
}
pub fn process_far(&self, frame: &[f32]) -> i32 {
let mut last: i32 = 0;
let mut buf_out = self.scratch_out.lock();
let mut i = 0;
while i + APM_FRAME_SIZE <= frame.len() {
let src_ptrs: [*const f32; 1] = [frame[i..].as_ptr()];
let dst_ptrs: [*mut f32; 1] = [buf_out.as_mut_ptr()];
unsafe {
last = (self.process_reverse)(
self.handle,
src_ptrs.as_ptr(),
self.stream_cfg,
self.stream_cfg,
dst_ptrs.as_ptr(),
);
}
i += APM_FRAME_SIZE;
}
last
}
pub fn set_stream_delay_ms(&self, ms: i32) {
unsafe { (self.set_delay)(self.handle, ms) }
}
}
impl Drop for Apm {
fn drop(&mut self) {
unsafe {
(self.cfg_destroy)(self.stream_cfg);
(self.destroy)(self.handle);
}
}
}
}
#[cfg(not(target_os = "windows"))]
mod imp {
pub struct Apm;
pub fn open() -> anyhow::Result<Apm> {
anyhow::bail!("APM not used on this platform (Apple VPIO handles AEC + NS on macOS)")
}
impl Apm {
pub fn process_near(&self, _frame: &mut [f32]) -> i32 {
0
}
pub fn process_far(&self, _frame: &[f32]) -> i32 {
0
}
pub fn set_stream_delay_ms(&self, _ms: i32) {}
}
#[allow(dead_code)]
pub fn set_lib_path(_p: String) {}
}
#[allow(unused_imports)]
pub use imp::{open, set_lib_path, Apm};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apm_open_returns_either_ok_or_err() {
let res = open();
match res {
Ok(_apm) => {
}
Err(e) => {
let msg = e.to_string();
assert!(!msg.is_empty(), "error message must not be empty");
}
}
}
#[test]
fn apm_frame_size_is_10ms_at_16khz() {
assert_eq!(APM_FRAME_SIZE, 160);
assert_eq!(APM_SAMPLE_RATE, 16_000);
let ms = (APM_FRAME_SIZE as f32 / APM_SAMPLE_RATE as f32) * 1000.0;
assert!((ms - 10.0).abs() < 0.001, "frame must be 10ms, got {ms}ms");
}
#[cfg(not(target_os = "windows"))]
#[test]
fn apm_stub_is_passthrough() {
let apm = Apm;
let mut frame = vec![0.25f32; APM_FRAME_SIZE];
let original = frame.clone();
let rc = apm.process_near(&mut frame);
assert_eq!(rc, 0);
assert_eq!(frame, original, "stub must not modify near-end frame");
}
}