tauri_plugin_system_audio/
apm.rs1#[allow(dead_code)]
38pub const APM_FRAME_SIZE: usize = 160;
39#[allow(dead_code)]
40pub const APM_SAMPLE_RATE: i32 = 16_000;
41
42#[allow(dead_code)]
49pub const APM_PLAYBACK_DELAY_MS: i32 = 150;
50
51#[cfg(target_os = "windows")]
52mod imp {
53 use super::{APM_FRAME_SIZE, APM_PLAYBACK_DELAY_MS, APM_SAMPLE_RATE};
54 use libloading::{Library, Symbol};
55 use parking_lot::Mutex;
56 use std::env;
57 use std::ffi::c_void;
58 use std::path::PathBuf;
59 use std::sync::OnceLock;
60
61 type ApmCreate = unsafe extern "C" fn() -> *mut c_void;
72 type ApmDestroy = unsafe extern "C" fn(*mut c_void);
73 type ApmInitialize = unsafe extern "C" fn(*mut c_void) -> i32;
74 type ApmApplyConfig = unsafe extern "C" fn(*mut c_void, *mut c_void) -> i32;
75
76 type ApmProcessStream = unsafe extern "C" fn(
78 apm: *mut c_void,
79 src: *const *const f32,
80 input_cfg: *mut c_void,
81 output_cfg: *mut c_void,
82 dest: *const *mut f32,
83 ) -> i32;
84 type ApmProcessReverseStream = ApmProcessStream;
85
86 type ApmSetStreamDelay = unsafe extern "C" fn(*mut c_void, i32);
87
88 type ApmStreamConfigCreate = unsafe extern "C" fn(i32, usize) -> *mut c_void;
90 type ApmStreamConfigDestroy = unsafe extern "C" fn(*mut c_void);
91
92 type ApmConfigCreate = unsafe extern "C" fn() -> *mut c_void;
93 type ApmConfigDestroy = unsafe extern "C" fn(*mut c_void);
94
95 type ApmConfigSetEchoCanceller = unsafe extern "C" fn(*mut c_void, i32, i32);
96 type ApmConfigSetNoiseSuppression = unsafe extern "C" fn(*mut c_void, i32, i32);
97 type ApmConfigSetHighPassFilter = unsafe extern "C" fn(*mut c_void, i32);
98 type ApmConfigSetGainController1 = unsafe extern "C" fn(*mut c_void, i32, i32, i32, i32, i32);
99 type ApmConfigSetGainController2 = unsafe extern "C" fn(*mut c_void, i32);
100 type ApmConfigSetPipeline = unsafe extern "C" fn(*mut c_void, i32, i32, i32, i32);
101
102 pub struct Apm {
103 _lib: Library,
104 handle: *mut c_void,
105 process_stream: ApmProcessStream,
106 process_reverse: ApmProcessReverseStream,
107 set_delay: ApmSetStreamDelay,
108 destroy: ApmDestroy,
109 cfg_destroy: ApmStreamConfigDestroy,
110 stream_cfg: *mut c_void,
111 scratch_out: Mutex<Vec<f32>>,
118 }
119 unsafe impl Send for Apm {}
120 unsafe impl Sync for Apm {}
121
122 static LIB_PATH: OnceLock<Mutex<Option<String>>> = OnceLock::new();
123
124 pub fn set_lib_path(p: String) {
127 let cell = LIB_PATH.get_or_init(|| Mutex::new(None));
128 *cell.lock() = Some(p);
129 }
130
131 fn candidate_paths() -> Vec<String> {
132 let mut out = Vec::new();
133 if let Some(cell) = LIB_PATH.get() {
134 if let Some(explicit) = cell.lock().clone() {
135 out.push(explicit);
136 }
137 }
138 out.push("webrtc-apm.dll".to_string());
139 if let Ok(exe) = env::current_exe() {
140 if let Some(dir) = exe.parent() {
141 let exe_dir = PathBuf::from(dir);
142 out.push(
143 exe_dir
144 .join("webrtc-apm.dll")
145 .to_string_lossy()
146 .into_owned(),
147 );
148 out.push(
149 exe_dir
150 .join("resources")
151 .join("webrtc-apm.dll")
152 .to_string_lossy()
153 .into_owned(),
154 );
155 }
156 }
157 out
158 }
159
160 pub fn open() -> anyhow::Result<Apm> {
161 let candidates = candidate_paths();
162 let mut last_err: Option<String> = None;
163 let lib = candidates
164 .iter()
165 .find_map(|p| match unsafe { Library::new(p) } {
166 Ok(lib) => Some(lib),
167 Err(e) => {
168 last_err = Some(format!("{p}: {e}"));
169 None
170 }
171 })
172 .ok_or_else(|| {
173 anyhow::anyhow!(
174 "webrtc-apm.dll not found (tried {} candidates) — last error: {}",
175 candidates.len(),
176 last_err.unwrap_or_else(|| "<none>".to_string())
177 )
178 })?;
179
180 unsafe {
181 let create: Symbol<ApmCreate> = lib.get(b"webrtc_apm_create")?;
182 let init: Symbol<ApmInitialize> = lib.get(b"webrtc_apm_initialize")?;
183 let apply: Symbol<ApmApplyConfig> = lib.get(b"webrtc_apm_apply_config")?;
184 let cfg_create: Symbol<ApmStreamConfigCreate> =
185 lib.get(b"webrtc_apm_stream_config_create")?;
186 let cfg_destroy: Symbol<ApmStreamConfigDestroy> =
187 lib.get(b"webrtc_apm_stream_config_destroy")?;
188 let process_stream: Symbol<ApmProcessStream> = lib.get(b"webrtc_apm_process_stream")?;
189 let process_reverse: Symbol<ApmProcessReverseStream> =
190 lib.get(b"webrtc_apm_process_reverse_stream")?;
191 let set_delay: Symbol<ApmSetStreamDelay> =
192 lib.get(b"webrtc_apm_set_stream_delay_ms")?;
193 let destroy: Symbol<ApmDestroy> = lib.get(b"webrtc_apm_destroy")?;
194
195 let apm_cfg_create: Symbol<ApmConfigCreate> = lib.get(b"webrtc_apm_config_create")?;
200 let apm_cfg_destroy: Symbol<ApmConfigDestroy> =
201 lib.get(b"webrtc_apm_config_destroy")?;
202 let cfg_set_aec: Symbol<ApmConfigSetEchoCanceller> =
203 lib.get(b"webrtc_apm_config_set_echo_canceller")?;
204 let cfg_set_ns: Symbol<ApmConfigSetNoiseSuppression> =
205 lib.get(b"webrtc_apm_config_set_noise_suppression")?;
206 let cfg_set_hpf: Symbol<ApmConfigSetHighPassFilter> =
207 lib.get(b"webrtc_apm_config_set_high_pass_filter")?;
208 let cfg_set_agc1: Symbol<ApmConfigSetGainController1> =
209 lib.get(b"webrtc_apm_config_set_gain_controller1")?;
210 let cfg_set_agc2: Symbol<ApmConfigSetGainController2> =
211 lib.get(b"webrtc_apm_config_set_gain_controller2")?;
212 let cfg_set_pipeline: Symbol<ApmConfigSetPipeline> =
213 lib.get(b"webrtc_apm_config_set_pipeline")?;
214
215 let handle = create();
216 if handle.is_null() {
217 anyhow::bail!("webrtc_apm_create returned null");
218 }
219
220 let cfg = apm_cfg_create();
221 if cfg.is_null() {
222 (destroy)(handle);
223 anyhow::bail!("webrtc_apm_config_create returned null");
224 }
225 cfg_set_aec(cfg, 1, 0);
232 cfg_set_ns(cfg, 1, 0);
233 cfg_set_hpf(cfg, 0);
234 cfg_set_agc1(cfg, 0, 1, 3, 9, 1);
235 cfg_set_agc2(cfg, 0);
236 cfg_set_pipeline(cfg, APM_SAMPLE_RATE, 0, 0, 0);
237 let apply_err = apply(handle, cfg);
238 apm_cfg_destroy(cfg);
239 if apply_err != 0 {
240 (destroy)(handle);
241 anyhow::bail!("webrtc_apm_apply_config returned {apply_err}");
242 }
243 let init_err = init(handle);
244 if init_err != 0 {
245 (destroy)(handle);
246 anyhow::bail!("webrtc_apm_initialize returned {init_err}");
247 }
248
249 let stream_cfg = cfg_create(APM_SAMPLE_RATE, 1);
252 if stream_cfg.is_null() {
253 (destroy)(handle);
254 anyhow::bail!("webrtc_apm_stream_config_create returned null");
255 }
256 set_delay(handle, APM_PLAYBACK_DELAY_MS);
259
260 Ok(Apm {
261 process_stream: *process_stream,
262 process_reverse: *process_reverse,
263 set_delay: *set_delay,
264 destroy: *destroy,
265 cfg_destroy: *cfg_destroy,
266 _lib: lib,
267 handle,
268 stream_cfg,
269 scratch_out: Mutex::new(vec![0.0f32; APM_FRAME_SIZE]),
270 })
271 }
272 }
273
274 impl Apm {
275 pub fn process_near(&self, frame: &mut [f32]) -> i32 {
282 let mut last: i32 = 0;
283 let mut buf_out = self.scratch_out.lock();
284 let mut i = 0;
285 while i + APM_FRAME_SIZE <= frame.len() {
286 let src_ptrs: [*const f32; 1] = [frame[i..].as_ptr()];
287 let dst_ptrs: [*mut f32; 1] = [buf_out.as_mut_ptr()];
288 unsafe {
289 last = (self.process_stream)(
290 self.handle,
291 src_ptrs.as_ptr(),
292 self.stream_cfg,
293 self.stream_cfg,
294 dst_ptrs.as_ptr(),
295 );
296 }
297 frame[i..i + APM_FRAME_SIZE].copy_from_slice(&buf_out[..APM_FRAME_SIZE]);
298 i += APM_FRAME_SIZE;
299 }
300 last
301 }
302
303 pub fn process_far(&self, frame: &[f32]) -> i32 {
309 let mut last: i32 = 0;
310 let mut buf_out = self.scratch_out.lock();
311 let mut i = 0;
312 while i + APM_FRAME_SIZE <= frame.len() {
313 let src_ptrs: [*const f32; 1] = [frame[i..].as_ptr()];
314 let dst_ptrs: [*mut f32; 1] = [buf_out.as_mut_ptr()];
315 unsafe {
316 last = (self.process_reverse)(
317 self.handle,
318 src_ptrs.as_ptr(),
319 self.stream_cfg,
320 self.stream_cfg,
321 dst_ptrs.as_ptr(),
322 );
323 }
324 i += APM_FRAME_SIZE;
325 }
326 last
327 }
328
329 pub fn set_stream_delay_ms(&self, ms: i32) {
330 unsafe { (self.set_delay)(self.handle, ms) }
331 }
332 }
333
334 impl Drop for Apm {
335 fn drop(&mut self) {
336 unsafe {
337 (self.cfg_destroy)(self.stream_cfg);
338 (self.destroy)(self.handle);
339 }
340 }
341 }
342}
343
344#[cfg(not(target_os = "windows"))]
345mod imp {
346 pub struct Apm;
347 pub fn open() -> anyhow::Result<Apm> {
348 anyhow::bail!("APM not used on this platform (Apple VPIO handles AEC + NS on macOS)")
349 }
350 impl Apm {
351 pub fn process_near(&self, _frame: &mut [f32]) -> i32 {
352 0
353 }
354 pub fn process_far(&self, _frame: &[f32]) -> i32 {
355 0
356 }
357 pub fn set_stream_delay_ms(&self, _ms: i32) {}
358 }
359 #[allow(dead_code)]
360 pub fn set_lib_path(_p: String) {}
361}
362
363#[allow(unused_imports)]
367pub use imp::{open, set_lib_path, Apm};
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
379 fn apm_open_returns_either_ok_or_err() {
380 let res = open();
381 match res {
382 Ok(_apm) => {
383 }
387 Err(e) => {
388 let msg = e.to_string();
389 assert!(!msg.is_empty(), "error message must not be empty");
390 }
391 }
392 }
393
394 #[test]
398 fn apm_frame_size_is_10ms_at_16khz() {
399 assert_eq!(APM_FRAME_SIZE, 160);
400 assert_eq!(APM_SAMPLE_RATE, 16_000);
401 let ms = (APM_FRAME_SIZE as f32 / APM_SAMPLE_RATE as f32) * 1000.0;
403 assert!((ms - 10.0).abs() < 0.001, "frame must be 10ms, got {ms}ms");
404 }
405
406 #[cfg(not(target_os = "windows"))]
409 #[test]
410 fn apm_stub_is_passthrough() {
411 let apm = Apm;
412 let mut frame = vec![0.25f32; APM_FRAME_SIZE];
413 let original = frame.clone();
414 let rc = apm.process_near(&mut frame);
415 assert_eq!(rc, 0);
416 assert_eq!(frame, original, "stub must not modify near-end frame");
417 }
418}