Skip to main content

ringo_core/baresip/
ausrc.rs

1//! Custom `ringo` baresip audio-source module.
2//!
3//! Why this exists: baresip's public `audio_set_source()` is *transient* — a
4//! media re-negotiation (re-INVITE: hold/resume, transfer, line switch) rebuilds
5//! the audio stream and resets the source to the account default. And the only
6//! persistent knob (`account.ausrc_mod` / `config.audio.src_mod`) is either a
7//! private struct field with no public setter, or process-global (all UAs in
8//! this single process would collide — the old backend got away with a global
9//! because each UA was its own process).
10//!
11//! So instead of fighting baresip's source management, ringo registers its OWN
12//! source module via `ausrc_register()` (baresip's official extension point) and
13//! points each UA's account at `audio_source=ringo,<key>`. baresip then re-allocs
14//! *our* source on every stream rebuild, and we render whatever the per-key
15//! [`REGISTRY`] currently says — race-free, persistent across re-INVITEs, and
16//! per-UA isolated. Changing a UA's audio (tone/file/silence) is just a registry
17//! update; the running render thread picks it up on the next frame.
18
19use std::collections::{HashMap, VecDeque};
20use std::os::raw::{c_char, c_void};
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex, OnceLock};
23use std::time::{Duration, Instant};
24
25use super::bindings::*;
26
27const AMPLITUDE: f64 = 0.25;
28
29/// What a UA's source should emit. Cheap to clone (file PCM is shared via `Arc`).
30#[derive(Clone)]
31enum GenSpec {
32    Silence,
33    /// Sine wave at the given frequency in Hz.
34    Tone(u32),
35    /// Mono S16 samples at the given sample rate, looped.
36    File(Arc<Vec<i16>>, u32),
37    /// Live-streamed mono S16 at the given sample rate: each frame is pulled from
38    /// the UA's [`STREAM_IN`] queue (fed via [`push_audio`]); underrun → silence.
39    Stream(u32),
40}
41
42/// A registry entry: the desired spec plus a version that bumps on every change,
43/// so a render thread can cheaply detect "my spec changed, reset my phase".
44struct Entry {
45    version: u64,
46    spec: GenSpec,
47}
48
49/// Per-UA desired source, keyed by the device string (the `<key>` in
50/// `audio_source=ringo,<key>`, which ringo sets to the account username).
51static REGISTRY: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new();
52
53fn registry() -> &'static Mutex<HashMap<String, Entry>> {
54    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
55}
56
57/// Registered `struct ausrc *` (kept alive for the process lifetime).
58static AUSRC: OnceLock<usize> = OnceLock::new();
59
60/// A mono PCM (s16) audio frame plus its sample rate — the unit fed into a call
61/// via [`push_audio`] and handed out of one via [`subscribe_received_audio`].
62#[derive(Debug, Clone)]
63pub struct AudioFrame {
64    /// Mono 16-bit PCM samples.
65    pub samples: Vec<i16>,
66    /// Sample rate in Hz.
67    pub rate: u32,
68}
69
70/// A single UA's streamed-in PCM queue. Each UA gets its OWN lock so the
71/// real-time render thread only ever contends with that UA's own [`push_audio`]
72/// — never with other UAs' render/feed (which a single global lock would force).
73type StreamQueue = Arc<Mutex<VecDeque<i16>>>;
74
75/// Per-UA stream-in queues, keyed by username. The outer lock is held only for
76/// the O(1) lookup/clone of the per-UA [`StreamQueue`]; all rendering/feeding
77/// happens under the inner per-UA lock, off the global path.
78static STREAM_IN: OnceLock<Mutex<HashMap<String, StreamQueue>>> = OnceLock::new();
79
80fn stream_in() -> &'static Mutex<HashMap<String, StreamQueue>> {
81    STREAM_IN.get_or_init(|| Mutex::new(HashMap::new()))
82}
83
84/// The per-UA stream-in queue for `key`, if streaming was started for it.
85fn stream_queue(key: &str) -> Option<StreamQueue> {
86    stream_in()
87        .lock()
88        .unwrap_or_else(|e| e.into_inner())
89        .get(key)
90        .map(Arc::clone)
91}
92
93/// Per-UA live subscriber for received (decoded) audio frames. One per key; a
94/// new subscription replaces the old.
95static RX_TAPS: OnceLock<Mutex<HashMap<String, std::sync::mpsc::Sender<AudioFrame>>>> =
96    OnceLock::new();
97
98fn rx_taps() -> &'static Mutex<HashMap<String, std::sync::mpsc::Sender<AudioFrame>>> {
99    RX_TAPS.get_or_init(|| Mutex::new(HashMap::new()))
100}
101
102/// Cap on buffered streamed-in audio (samples ≈ 1 s at 48 kHz). Past this the
103/// oldest samples are dropped, bounding added latency / memory under overrun.
104const STREAM_IN_CAP_SAMPLES: usize = 48_000;
105
106/// Translate a baresip-style source spec into a [`GenSpec`] and store it for
107/// `key`, bumping the version so the render thread reloads it. Accepts the same
108/// specs the engine already produces: `ausine,<freq>`, `aufile,<path>`, and
109/// anything else (e.g. `aubridge,...`) → silence.
110pub(super) fn set_generator(key: &str, spec: &str) {
111    let parsed = parse_spec(spec);
112    let mut map = registry().lock().unwrap_or_else(|e| e.into_inner());
113    let version = map.get(key).map(|e| e.version + 1).unwrap_or(0);
114    map.insert(
115        key.to_string(),
116        Entry {
117            version,
118            spec: parsed,
119        },
120    );
121}
122
123/// Switch the UA's audio source to live-streamed mono s16 PCM at `rate` Hz, fed
124/// via [`push_audio`]. Until samples arrive (or on underrun) the source renders
125/// silence; queued audio is emitted in real time by the render thread.
126pub(super) fn start_audio_stream(key: &str, rate: u32) {
127    stream_in()
128        .lock()
129        .unwrap_or_else(|e| e.into_inner())
130        .entry(key.to_string())
131        .or_default();
132    let mut map = registry().lock().unwrap_or_else(|e| e.into_inner());
133    let version = map.get(key).map(|e| e.version + 1).unwrap_or(0);
134    map.insert(
135        key.to_string(),
136        Entry {
137            version,
138            spec: GenSpec::Stream(rate.max(1)),
139        },
140    );
141}
142
143/// Append mono s16 `samples` (at the rate given to [`start_audio_stream`]) to the
144/// UA's stream-in queue. No-op if the queue was never started.
145pub(super) fn push_audio(key: &str, samples: &[i16]) {
146    let Some(q) = stream_queue(key) else {
147        return;
148    };
149    let mut q = q.lock().unwrap_or_else(|e| e.into_inner());
150    q.extend(samples.iter().copied());
151    while q.len() > STREAM_IN_CAP_SAMPLES {
152        q.pop_front();
153    }
154}
155
156/// Subscribe to the UA's received (decoded) audio: returns a receiver of mono
157/// s16 [`AudioFrame`]s as they arrive. Replaces any previous subscription.
158pub(super) fn subscribe_received_audio(key: &str) -> std::sync::mpsc::Receiver<AudioFrame> {
159    let (tx, rx) = std::sync::mpsc::channel();
160    rx_taps()
161        .lock()
162        .unwrap_or_else(|e| e.into_inner())
163        .insert(key.to_string(), tx);
164    rx
165}
166
167/// Pre-seed a UA's source with silence (called at session setup) so the source
168/// has a defined default before the first `set_generator`.
169pub(super) fn init_generator(key: &str) {
170    let mut map = registry().lock().unwrap_or_else(|e| e.into_inner());
171    map.entry(key.to_string()).or_insert(Entry {
172        version: 0,
173        spec: GenSpec::Silence,
174    });
175}
176
177/// Drop a UA's audio state (source generator + received-audio buffer), called
178/// when the session is dropped.
179pub(super) fn remove_generator(key: &str) {
180    registry()
181        .lock()
182        .unwrap_or_else(|e| e.into_inner())
183        .remove(key);
184    rx_buffers()
185        .lock()
186        .unwrap_or_else(|e| e.into_inner())
187        .remove(key);
188    tx_buffers()
189        .lock()
190        .unwrap_or_else(|e| e.into_inner())
191        .remove(key);
192    stream_in()
193        .lock()
194        .unwrap_or_else(|e| e.into_inner())
195        .remove(key);
196    rx_taps()
197        .lock()
198        .unwrap_or_else(|e| e.into_inner())
199        .remove(key);
200}
201
202fn spec_name(spec: &GenSpec) -> String {
203    match spec {
204        GenSpec::Silence => "silence".into(),
205        GenSpec::Tone(f) => format!("tone({f}Hz)"),
206        GenSpec::File(s, sr) => format!("file({} samples @{sr}Hz)", s.len()),
207        GenSpec::Stream(sr) => format!("stream(@{sr}Hz)"),
208    }
209}
210
211fn parse_spec(spec: &str) -> GenSpec {
212    let (driver, device) = spec.split_once(',').unwrap_or((spec, ""));
213    match driver {
214        "ausine" => match device.split(',').next().unwrap_or("").parse::<u32>() {
215            Ok(f) if (10..=20000).contains(&f) => GenSpec::Tone(f),
216            _ => {
217                crate::rlog!(
218                    Warn,
219                    "ringo ausrc: invalid tone freq in '{spec}', using silence"
220                );
221                GenSpec::Silence
222            }
223        },
224        "aufile" => match load_wav_mono(device) {
225            Some((samples, srate)) => GenSpec::File(Arc::new(samples), srate),
226            None => {
227                crate::rlog!(
228                    Warn,
229                    "ringo ausrc: failed to load '{device}', using silence"
230                );
231                GenSpec::Silence
232            }
233        },
234        _ => GenSpec::Silence,
235    }
236}
237
238/// Load a WAV file as mono S16 samples + its sample rate. Stereo is downmixed.
239fn load_wav_mono(path: &str) -> Option<(Vec<i16>, u32)> {
240    let data = std::fs::read(path).ok()?;
241    let pcm = super::sounds::parse_wav(&data)?;
242    // `samples` is S16LE bytes. Convert to i16, downmixing stereo to mono.
243    let i16s: Vec<i16> = pcm
244        .samples
245        .chunks_exact(2)
246        .map(|b| i16::from_le_bytes([b[0], b[1]]))
247        .collect();
248    let mono = if pcm.channels >= 2 {
249        i16s.chunks_exact(pcm.channels as usize)
250            .map(|frame| {
251                let sum: i32 = frame.iter().take(2).map(|&s| s as i32).sum();
252                (sum / 2) as i16
253            })
254            .collect()
255    } else {
256        i16s
257    };
258    if mono.is_empty() {
259        return None;
260    }
261    Some((mono, pcm.srate))
262}
263
264// ─── baresip ausrc module ──────────────────────────────────────────────────
265
266/// Rust-owned state for one active source, referenced from the mem_zalloc'd
267/// `ausrc_st` cell. Its `Drop` stops and joins the render thread.
268struct SrcState {
269    key: String,
270    run: Arc<AtomicBool>,
271    thread: Option<std::thread::JoinHandle<()>>,
272}
273
274impl Drop for SrcState {
275    fn drop(&mut self) {
276        crate::rlog!(Info, "ringo ausrc: FREE key={}", self.key);
277        self.run.store(false, Ordering::Release);
278        if let Some(t) = self.thread.take() {
279            let _ = t.join();
280        }
281    }
282}
283
284/// Pointers handed to the render thread. `rh`/`arg` come from baresip and are
285/// only ever used to call back into baresip from this one thread (the same
286/// pattern baresip's own `ausine` uses).
287struct ReadCb {
288    rh: ausrc_read_h,
289    arg: usize,
290}
291// SAFETY: `arg` is a baresip pointer used solely to call `rh` from the render
292// thread; baresip's audio path is built to have the ausrc read handler invoked
293// from the source's own thread. No use-after-free: baresip frees the source via
294// `stop_tx()` (src/audio.c), which mem_derefs `tx->ausrc` (→ our destructor →
295// thread join) BEFORE freeing `tx->aubuf`/`tx->mtx` (the `arg`) — "audio source
296// must be stopped first". So `rh` can never run against a freed `arg`.
297unsafe impl Send for ReadCb {}
298
299/// Called by baresip to free the `ausrc_st` (on stream teardown / re-INVITE).
300unsafe extern "C" fn destructor(arg: *mut c_void) {
301    // `arg` points at the mem_zalloc'd cell holding our `*mut SrcState`.
302    let cell = arg as *mut *mut SrcState;
303    let state = unsafe { *cell };
304    if !state.is_null() {
305        // Reclaim and drop → SrcState::Drop stops + joins the render thread.
306        drop(unsafe { Box::from_raw(state) });
307        unsafe { *cell = std::ptr::null_mut() };
308    }
309}
310
311unsafe extern "C" fn alloc_handler(
312    stp: *mut *mut ausrc_st,
313    _as: *const ausrc,
314    prm: *mut ausrc_prm,
315    dev: *const c_char,
316    rh: ausrc_read_h,
317    _errh: ausrc_error_h,
318    arg: *mut c_void,
319) -> i32 {
320    const EINVAL: i32 = 22;
321    const ENOMEM: i32 = 12;
322    const ENOTSUP: i32 = 95;
323
324    if stp.is_null() || prm.is_null() || dev.is_null() || rh.is_none() {
325        return EINVAL;
326    }
327
328    let prm = unsafe { &*prm };
329    let key = unsafe { std::ffi::CStr::from_ptr(dev) }
330        .to_string_lossy()
331        .into_owned();
332
333    let srate = prm.srate;
334    let ptime = if prm.ptime == 0 { 20 } else { prm.ptime };
335    let fmt = prm.fmt;
336
337    // Reject degenerate params rather than masking them (e.g. `ch.max(1)`): a
338    // channel/rate that doesn't match what the TX path negotiated would
339    // mis-interleave or divide-by-zero.
340    if srate == 0 || prm.ch == 0 {
341        crate::rlog!(Warn, "ringo ausrc: invalid prm srate={srate} ch={}", prm.ch);
342        return EINVAL;
343    }
344    let ch = prm.ch;
345
346    // The audio TX drops any frame whose fmt != tx->src_fmt, so we must render
347    // in exactly the requested format. Only S16LE and FLOAT are implemented
348    // (matching ausine); telephony configs use S16LE.
349    if fmt != aufmt::AUFMT_S16LE as i32 && fmt != aufmt::AUFMT_FLOAT as i32 {
350        crate::rlog!(Warn, "ringo ausrc: unsupported sample format {fmt}");
351        return ENOTSUP;
352    }
353
354    // Allocate the cell baresip will mem_deref (running our destructor).
355    let cell = unsafe { mem_zalloc(std::mem::size_of::<*mut SrcState>(), Some(destructor)) };
356    if cell.is_null() {
357        return ENOMEM;
358    }
359
360    crate::rlog!(
361        Info,
362        "ringo ausrc: ALLOC key={key} srate={srate} ch={ch} ptime={ptime} fmt={fmt}"
363    );
364
365    let run = Arc::new(AtomicBool::new(true));
366    let run_thread = run.clone();
367    let cb = ReadCb {
368        rh,
369        arg: arg as usize,
370    };
371
372    // Arm the sent-audio buffer for this stream (keeps samples when full
373    // capture is on — see alloc_buffer).
374    alloc_buffer(tx_buffers(), &key, srate);
375
376    let key_thread = key.clone();
377    let thread = match std::thread::Builder::new()
378        .name("ringo-ausrc".into())
379        .spawn(move || render_loop(key_thread, srate, ch, ptime, fmt, run_thread, cb))
380    {
381        Ok(t) => t,
382        Err(e) => {
383            // Don't hand baresip a source that never produces frames; free the
384            // cell (destructor no-ops on the still-null state) and fail.
385            crate::rlog!(Error, "ringo ausrc: spawn render thread failed: {e}");
386            unsafe { mem_deref(cell) };
387            return ENOMEM;
388        }
389    };
390
391    let state = Box::new(SrcState {
392        key,
393        run,
394        thread: Some(thread),
395    });
396    unsafe {
397        *(cell as *mut *mut SrcState) = Box::into_raw(state);
398        *stp = cell as *mut ausrc_st;
399    }
400    0
401}
402
403/// Fill `mono` from the front of the stream-in queue `q`, nearest-sample
404/// resampling input→output by `step` (= in_rate / out_rate). The input read is
405/// consumed (dropped from the queue front); `pos` carries the fractional read
406/// position across calls. Underrun (empty/short queue) renders silence, and an
407/// emptied queue resets `pos` so newly-fed audio starts cleanly.
408fn render_stream(q: Option<&mut VecDeque<i16>>, pos: &mut f64, step: f64, mono: &mut [i16]) {
409    let Some(q) = q else {
410        mono.iter_mut().for_each(|s| *s = 0);
411        return;
412    };
413    for s in mono.iter_mut() {
414        *s = q.get(*pos as usize).copied().unwrap_or(0);
415        *pos += step;
416    }
417    // Consume the read input by popping from the front (O(consumed), no memmove
418    // of the tail — this runs under the per-UA lock on the real-time thread).
419    let consumed = (*pos as usize).min(q.len());
420    for _ in 0..consumed {
421        q.pop_front();
422    }
423    *pos -= consumed as f64;
424    if q.is_empty() {
425        *pos = 0.0; // underran: restart cleanly when audio resumes
426    }
427}
428
429/// The render thread: every `ptime` ms, generate one frame from the current
430/// registry spec for `key` and hand it to baresip via `rh`.
431fn render_loop(
432    key: String,
433    srate: u32,
434    ch: u8,
435    ptime: u32,
436    fmt: i32,
437    run: Arc<AtomicBool>,
438    cb: ReadCb,
439) {
440    let is_float = fmt == aufmt::AUFMT_FLOAT as i32;
441    let sample_size = if is_float { 4 } else { 2 };
442    let frames = (srate as usize * ptime as usize / 1000).max(1);
443    let sampc = frames * ch as usize;
444
445    let mut sampv = vec![0u8; sampc * sample_size];
446    let mut mono = vec![0i16; frames];
447
448    // Render state (reset when the spec version changes).
449    let mut cur_version = u64::MAX;
450    let mut cur_spec = GenSpec::Silence;
451    let mut phase = 0.0f64; // tone phase accumulator
452    let mut file_pos = 0.0f64; // fractional read position into the file
453    let mut stream_pos = 0.0f64; // fractional read position into the stream-in queue
454
455    let mut start = Instant::now();
456    let mut frame_idx: u64 = 0;
457
458    while run.load(Ordering::Acquire) {
459        // Pace to real time: wake at start + frame_idx * ptime.
460        let target = start + Duration::from_millis(frame_idx * ptime as u64);
461        let now = Instant::now();
462        if target > now {
463            std::thread::sleep(target - now);
464        } else if now - target > Duration::from_millis(ptime as u64 * 4) {
465            // Fell far behind (suspend / scheduling stall): rebase the clock so
466            // we don't fire a long burst of catch-up frames (which the TX aubuf
467            // would just drop as overruns anyway).
468            start = now;
469            frame_idx = 0;
470        }
471        if !run.load(Ordering::Acquire) {
472            break;
473        }
474
475        // Reload spec if it changed.
476        let present = {
477            let map = registry().lock().unwrap_or_else(|e| e.into_inner());
478            map.get(&key).map(|e| (e.version, e.spec.clone()))
479        };
480        match &present {
481            Some((version, spec)) => {
482                if *version != cur_version {
483                    cur_version = *version;
484                    cur_spec = spec.clone();
485                    phase = 0.0;
486                    file_pos = 0.0;
487                    stream_pos = 0.0;
488                    crate::rlog!(
489                        Info,
490                        "ringo ausrc: key={key} spec={} (v{version})",
491                        spec_name(spec)
492                    );
493                }
494            }
495            None => {
496                // Registry entry gone (shouldn't happen while a source is live):
497                // render silence rather than a stale spec.
498                if cur_version != u64::MAX {
499                    crate::rlog!(Warn, "ringo ausrc: key={key} no registry entry, silence");
500                    cur_version = u64::MAX;
501                    cur_spec = GenSpec::Silence;
502                }
503            }
504        }
505
506        // Render `frames` mono samples.
507        match &cur_spec {
508            GenSpec::Silence => mono.iter_mut().for_each(|s| *s = 0),
509            GenSpec::Tone(freq) => {
510                let step = std::f64::consts::TAU * (*freq as f64) / (srate as f64);
511                for s in mono.iter_mut() {
512                    *s = (phase.sin() * AMPLITUDE * 32767.0) as i16;
513                    phase += step;
514                    if phase >= std::f64::consts::TAU {
515                        phase -= std::f64::consts::TAU;
516                    }
517                }
518            }
519            GenSpec::File(samples, file_srate) => {
520                let step = *file_srate as f64 / srate as f64;
521                let len = samples.len() as f64;
522                for s in mono.iter_mut() {
523                    // `while`, not `if`: a single step may overshoot `len` for a
524                    // tiny file (len < step), so wrap fully to keep looping.
525                    while file_pos >= len {
526                        file_pos -= len;
527                    }
528                    let i = file_pos as usize;
529                    *s = samples.get(i).copied().unwrap_or(0);
530                    file_pos += step;
531                }
532            }
533            GenSpec::Stream(stream_srate) => {
534                let step = (*stream_srate as f64 / srate as f64).max(f64::MIN_POSITIVE);
535                // Resolve the per-UA queue (brief global lock for the O(1) clone),
536                // then render under that UA's own lock — no cross-UA contention.
537                match stream_queue(&key) {
538                    Some(q) => {
539                        let mut q = q.lock().unwrap_or_else(|e| e.into_inner());
540                        render_stream(Some(&mut q), &mut stream_pos, step, &mut mono);
541                    }
542                    None => render_stream(None, &mut stream_pos, step, &mut mono),
543                }
544            }
545        }
546
547        // Capture the sent audio for --save-audio (full capture only).
548        if FULL_CAPTURE.load(Ordering::Acquire) {
549            capture_mono(tx_buffers(), &key, &mono);
550        }
551
552        // Pack mono → interleaved sampv in the requested format.
553        if is_float {
554            let out =
555                unsafe { std::slice::from_raw_parts_mut(sampv.as_mut_ptr() as *mut f32, sampc) };
556            for (f, &m) in out.chunks_mut(ch as usize).zip(mono.iter()) {
557                let v = m as f32 / 32768.0;
558                f.iter_mut().for_each(|x| *x = v);
559            }
560        } else {
561            let out =
562                unsafe { std::slice::from_raw_parts_mut(sampv.as_mut_ptr() as *mut i16, sampc) };
563            for (f, &m) in out.chunks_mut(ch as usize).zip(mono.iter()) {
564                f.iter_mut().for_each(|x| *x = m);
565            }
566        }
567
568        // Build the auframe and push it to baresip.
569        let mut af: auframe = unsafe { std::mem::zeroed() };
570        unsafe {
571            auframe_init(
572                &mut af,
573                if is_float {
574                    aufmt::AUFMT_FLOAT
575                } else {
576                    aufmt::AUFMT_S16LE
577                },
578                sampv.as_mut_ptr() as *mut c_void,
579                sampc,
580                srate,
581                ch,
582            );
583        }
584        af.timestamp = frame_idx * ptime as u64 * 1000; // AUDIO_TIMEBASE = microseconds
585
586        if let Some(rh) = cb.rh {
587            unsafe { rh(&mut af, cb.arg as *mut c_void) };
588        }
589
590        frame_idx += 1;
591    }
592}
593
594// ─── ringo auplay (self-clocked RX sink) ───────────────────────────────────
595//
596// Why we also need a player: the headless RX path (decode → aubuf → player) is
597// *pull-driven* — something must call the player's write handler at real-time
598// pace, or the decoder never advances and the received audio is silent.
599// aubridge's player is clocked by its device thread, which only runs when BOTH
600// an aubridge source AND player share a device (modules/aubridge/device.c) — but
601// ringo replaced the source, so that thread would never start. So ringo provides
602// its own self-clocked player: a timer thread that pulls `wh()` every ptime,
603// which both clocks the decode pipeline and yields the received frames we
604// capture for verify-audio / --save-audio.
605
606/// Registered `struct auplay *` (kept alive for the process lifetime).
607static AUPLAY: OnceLock<usize> = OnceLock::new();
608
609struct PlayState {
610    run: Arc<AtomicBool>,
611    thread: Option<std::thread::JoinHandle<()>>,
612}
613
614impl Drop for PlayState {
615    fn drop(&mut self) {
616        self.run.store(false, Ordering::Release);
617        if let Some(t) = self.thread.take() {
618            let _ = t.join();
619        }
620    }
621}
622
623struct WriteCb {
624    wh: auplay_write_h,
625    arg: usize,
626}
627// SAFETY: same contract as ReadCb — `arg`/`wh` are baresip pointers used only to
628// call back into baresip from this player's own thread. baresip frees the player
629// via `aurecv_stop()` (src/aureceiver.c), which mem_derefs `ar->auplay` (→ our
630// destructor → thread join) before freeing the receiver `arg`, so `wh` never
631// runs against a freed `arg`.
632unsafe impl Send for WriteCb {}
633
634/// Per-UA mono audio buffer (one for received, one for sent). Lets ringo-flow
635/// verify a received tone in-process (Goertzel on these samples) and save
636/// recordings — instead of baresip's sndfile WAV dumps. No shared-dir race, no
637/// disk round-trip, per-UA isolated by construction.
638struct AudioBuf {
639    srate: u32,
640    samples: VecDeque<i16>,
641    /// Holds a finished recording (capture was disarmed): no more samples are
642    /// appended, so the 3 s verify cap can't trim it before it is saved.
643    /// Cleared by the next arm or stream (re)alloc.
644    frozen: bool,
645}
646
647/// Seconds of audio retained per UA for verification (the verify window tail).
648const VERIFY_RETAIN_SECS: usize = 3;
649/// Cap on a full-call recording: only the newest [`RECORDING_MAX_SECS`] seconds
650/// are retained per UA (older audio is dropped; a save flags it truncated).
651/// Public via `ringo_core::RECORDING_MAX_SECS` so callers can flag truncation
652/// against the same cap.
653pub const RECORDING_MAX_SECS: u64 = 600;
654/// Seconds retained when full capture is on (`--save-audio`); bounds a runaway
655/// call. Test calls are far shorter.
656const FULL_RETAIN_SECS: usize = RECORDING_MAX_SECS as usize;
657
658/// Whether to retain the whole call (for `--save-audio`) vs. just the verify
659/// window. Set once per process from `BackendOptions.record_audio`.
660static FULL_CAPTURE: AtomicBool = AtomicBool::new(false);
661
662static RX_BUFFERS: OnceLock<Mutex<HashMap<String, AudioBuf>>> = OnceLock::new();
663static TX_BUFFERS: OnceLock<Mutex<HashMap<String, AudioBuf>>> = OnceLock::new();
664
665fn rx_buffers() -> &'static Mutex<HashMap<String, AudioBuf>> {
666    RX_BUFFERS.get_or_init(|| Mutex::new(HashMap::new()))
667}
668fn tx_buffers() -> &'static Mutex<HashMap<String, AudioBuf>> {
669    TX_BUFFERS.get_or_init(|| Mutex::new(HashMap::new()))
670}
671
672/// Enable/disable full-call capture (for `--save-audio`). When off, only the
673/// rolling verify window is retained and the sent buffer isn't captured.
674pub(super) fn set_full_capture(on: bool) {
675    FULL_CAPTURE.store(on, Ordering::Release);
676}
677
678/// Arm or disarm full-call capture for `key`. Arming starts a fresh recording
679/// (the UA's buffered samples are cleared, the srate stays); disarming returns
680/// to the 3 s verify window (sent side no longer captured) but KEEPS the
681/// buffers until the next arm/alloc, so a save after hangup still works.
682pub(super) fn arm_capture(key: &str, full: bool) {
683    let was_full = FULL_CAPTURE.swap(full, Ordering::AcqRel);
684    for buffers in [tx_buffers(), rx_buffers()] {
685        if let Some(buf) = buffers
686            .lock()
687            .unwrap_or_else(|e| e.into_inner())
688            .get_mut(key)
689        {
690            if full {
691                buf.samples.clear();
692                buf.frozen = false;
693            } else if was_full {
694                buf.frozen = true;
695            }
696        }
697    }
698}
699
700fn retain_secs() -> usize {
701    if FULL_CAPTURE.load(Ordering::Acquire) {
702        FULL_RETAIN_SECS
703    } else {
704        VERIFY_RETAIN_SECS
705    }
706}
707
708/// Reset a UA's buffer in `buffers` to empty at `srate` (called when a stream
709/// (re)alloc must start fresh — see [`alloc_buffer`]).
710fn reset_buffer(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, srate: u32) {
711    buffers.lock().unwrap_or_else(|e| e.into_inner()).insert(
712        key.to_string(),
713        AudioBuf {
714            srate,
715            samples: VecDeque::new(),
716            frozen: false,
717        },
718    );
719}
720
721/// Prepare a UA's buffer for a (re)allocated stream: create it at the negotiated
722/// `srate` if absent, reset it if the rate changed (mixing rates would corrupt
723/// the WAV), otherwise keep the samples.
724fn prepare_buffer(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, srate: u32) {
725    let mut map = buffers.lock().unwrap_or_else(|e| e.into_inner());
726    match map.get_mut(key) {
727        None => {
728            map.insert(
729                key.to_string(),
730                AudioBuf {
731                    srate,
732                    samples: VecDeque::new(),
733                    frozen: false,
734                },
735            );
736        }
737        Some(buf) if buf.srate != srate => {
738            crate::rlog!(
739                Info,
740                "ringo ausrc: key={key} srate {} -> {srate}, resetting buffer",
741                buf.srate
742            );
743            buf.srate = srate;
744            buf.samples.clear();
745            buf.frozen = false;
746        }
747        _ => {}
748    }
749}
750
751/// Buffer policy on stream (re)alloc: with full capture on, samples survive
752/// (a re-INVITE hold/resume must not wipe a running recording); with it off,
753/// reset — the verify window reflects only the current stream, and a disarmed
754/// recording is bounded by the next call.
755fn alloc_buffer(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, srate: u32) {
756    if FULL_CAPTURE.load(Ordering::Acquire) {
757        prepare_buffer(buffers, key, srate);
758    } else {
759        reset_buffer(buffers, key, srate);
760    }
761}
762
763/// The retained received mono samples for `key` and their sample rate.
764pub(super) fn received_window(key: &str) -> Option<(Vec<i16>, u32)> {
765    buffer_window(rx_buffers(), key)
766}
767
768/// The retained sent mono samples for `key` and their sample rate (full capture
769/// only; empty otherwise).
770pub(super) fn sent_window(key: &str) -> Option<(Vec<i16>, u32)> {
771    buffer_window(tx_buffers(), key)
772}
773
774fn buffer_window(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str) -> Option<(Vec<i16>, u32)> {
775    let map = buffers.lock().unwrap_or_else(|e| e.into_inner());
776    let buf = map.get(key)?;
777    Some((buf.samples.iter().copied().collect(), buf.srate))
778}
779
780/// Append already-mono `samples` to the UA's buffer in `buffers`, capped.
781fn capture_mono(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, samples: &[i16]) {
782    let mut map = buffers.lock().unwrap_or_else(|e| e.into_inner());
783    let Some(buf) = map.get_mut(key) else {
784        return;
785    };
786    if buf.frozen {
787        return;
788    }
789    buf.samples.extend(samples.iter().copied());
790    let cap = (buf.srate as usize * retain_secs()).max(1);
791    while buf.samples.len() > cap {
792        buf.samples.pop_front();
793    }
794}
795
796/// Append the channel-0 mono samples from an interleaved player frame to the
797/// UA's received buffer. `wh` always fills the full `sampc` (aurecv_read pads an
798/// underrun with silence), so the whole `sampv` is valid decoded audio.
799fn capture_rx(key: &str, sampv: &[u8], is_float: bool, ch: usize, srate: u32) {
800    let ch = ch.max(1);
801    let mono: Vec<i16> = if is_float {
802        let f =
803            unsafe { std::slice::from_raw_parts(sampv.as_ptr() as *const f32, sampv.len() / 4) };
804        f.chunks_exact(ch)
805            .map(|fr| (fr[0] * 32767.0) as i16)
806            .collect()
807    } else {
808        let s =
809            unsafe { std::slice::from_raw_parts(sampv.as_ptr() as *const i16, sampv.len() / 2) };
810        s.chunks_exact(ch).map(|fr| fr[0]).collect()
811    };
812    capture_mono(rx_buffers(), key, &mono);
813    // Live tap: hand the frame to a subscriber (e.g. streaming RX out for STT).
814    // Drop the subscription if the receiver is gone.
815    let mut taps = rx_taps().lock().unwrap_or_else(|e| e.into_inner());
816    if let Some(tx) = taps.get(key) {
817        let frame = AudioFrame {
818            samples: mono,
819            rate: srate,
820        };
821        if tx.send(frame).is_err() {
822            taps.remove(key);
823        }
824    }
825}
826
827unsafe extern "C" fn play_destructor(arg: *mut c_void) {
828    let cell = arg as *mut *mut PlayState;
829    let state = unsafe { *cell };
830    if !state.is_null() {
831        drop(unsafe { Box::from_raw(state) });
832        unsafe { *cell = std::ptr::null_mut() };
833    }
834}
835
836unsafe extern "C" fn play_alloc_handler(
837    stp: *mut *mut auplay_st,
838    _ap: *const auplay,
839    prm: *mut auplay_prm,
840    dev: *const c_char,
841    wh: auplay_write_h,
842    arg: *mut c_void,
843) -> i32 {
844    const EINVAL: i32 = 22;
845    const ENOMEM: i32 = 12;
846
847    if stp.is_null() || prm.is_null() || wh.is_none() {
848        return EINVAL;
849    }
850
851    let prm = unsafe { &*prm };
852    let srate = prm.srate;
853    let ptime = if prm.ptime == 0 { 20 } else { prm.ptime };
854    let fmt = prm.fmt;
855
856    if srate == 0 || prm.ch == 0 {
857        crate::rlog!(
858            Warn,
859            "ringo auplay: invalid prm srate={srate} ch={}",
860            prm.ch
861        );
862        return EINVAL;
863    }
864    let ch = prm.ch;
865
866    // The device key (= account username) identifies this UA's RX buffer.
867    // Arm it for this stream (reset while full capture is off, keep samples
868    // while it is on — see alloc_buffer).
869    let key = if dev.is_null() {
870        String::new()
871    } else {
872        unsafe { std::ffi::CStr::from_ptr(dev) }
873            .to_string_lossy()
874            .into_owned()
875    };
876    if !key.is_empty() {
877        alloc_buffer(rx_buffers(), &key, srate);
878    }
879
880    let cell = unsafe { mem_zalloc(std::mem::size_of::<*mut PlayState>(), Some(play_destructor)) };
881    if cell.is_null() {
882        return ENOMEM;
883    }
884
885    let run = Arc::new(AtomicBool::new(true));
886    let run_thread = run.clone();
887    let cb = WriteCb {
888        wh,
889        arg: arg as usize,
890    };
891
892    let thread = match std::thread::Builder::new()
893        .name("ringo-auplay".into())
894        .spawn(move || play_loop(key, srate, ch, ptime, fmt, run_thread, cb))
895    {
896        Ok(t) => t,
897        Err(e) => {
898            // A player that never pulls would stall the decode pipeline; free the
899            // cell (destructor no-ops on the null state) and fail.
900            crate::rlog!(Error, "ringo auplay: spawn play thread failed: {e}");
901            unsafe { mem_deref(cell) };
902            return ENOMEM;
903        }
904    };
905
906    let state = Box::new(PlayState {
907        run,
908        thread: Some(thread),
909    });
910    unsafe {
911        *(cell as *mut *mut PlayState) = Box::into_raw(state);
912        *stp = cell as *mut auplay_st;
913    }
914    0
915}
916
917/// Self-clocked player: every `ptime` ms, pull one frame from baresip via `wh`
918/// (which drives the decode pipeline) and capture it into the UA's RX buffer for
919/// in-process tone verification.
920fn play_loop(
921    key: String,
922    srate: u32,
923    ch: u8,
924    ptime: u32,
925    fmt: i32,
926    run: Arc<AtomicBool>,
927    cb: WriteCb,
928) {
929    let is_float = fmt == aufmt::AUFMT_FLOAT as i32;
930    let sample_size = if is_float { 4 } else { 2 };
931    let frames = (srate as usize * ptime as usize / 1000).max(1);
932    let sampc = frames * ch as usize;
933    let mut sampv = vec![0u8; sampc * sample_size];
934
935    let af_fmt = if is_float {
936        aufmt::AUFMT_FLOAT
937    } else {
938        aufmt::AUFMT_S16LE
939    };
940
941    let mut start = Instant::now();
942    let mut frame_idx: u64 = 0;
943
944    while run.load(Ordering::Acquire) {
945        let target = start + Duration::from_millis(frame_idx * ptime as u64);
946        let now = Instant::now();
947        if target > now {
948            std::thread::sleep(target - now);
949        } else if now - target > Duration::from_millis(ptime as u64 * 4) {
950            start = now;
951            frame_idx = 0;
952        }
953        if !run.load(Ordering::Acquire) {
954            break;
955        }
956
957        let mut af: auframe = unsafe { std::mem::zeroed() };
958        unsafe {
959            auframe_init(
960                &mut af,
961                af_fmt,
962                sampv.as_mut_ptr() as *mut c_void,
963                sampc,
964                srate,
965                ch,
966            );
967        }
968        af.timestamp = frame_idx * ptime as u64 * 1000;
969
970        if let Some(wh) = cb.wh {
971            // wh fills sampv with the decoded received audio (aurecv_read →
972            // aubuf_read_auframe), then we capture it for in-process verify.
973            unsafe { wh(&mut af, cb.arg as *mut c_void) };
974            if !key.is_empty() {
975                capture_rx(&key, &sampv, is_float, ch as usize, srate);
976            }
977        }
978
979        frame_idx += 1;
980    }
981}
982
983/// Register the `ringo` audio source + player modules with baresip. Must run
984/// once after `baresip_init`/`ua_init` (so `baresip_ausrcl()`/`baresip_auplayl()`
985/// exist) on the RE thread. Returns `Err` if registration fails — the caller
986/// must abort, since the agents would otherwise start with no working audio.
987/// The registered `ausrc`/`auplay` are intentionally never `mem_deref`'d (kept
988/// for the process lifetime); we stash the pointers for documentation.
989pub(super) fn register_module() -> Result<(), String> {
990    let mut asp: *mut ausrc = std::ptr::null_mut();
991    let rc = unsafe {
992        ausrc_register(
993            &mut asp,
994            baresip_ausrcl(),
995            c"ringo".as_ptr(),
996            Some(alloc_handler),
997        )
998    };
999    if rc != 0 {
1000        return Err(format!("ausrc_register(ringo) failed (rc={rc})"));
1001    }
1002    let _ = AUSRC.set(asp as usize);
1003
1004    let mut pp: *mut auplay = std::ptr::null_mut();
1005    let rc = unsafe {
1006        auplay_register(
1007            &mut pp,
1008            baresip_auplayl(),
1009            c"ringo".as_ptr(),
1010            Some(play_alloc_handler),
1011        )
1012    };
1013    if rc != 0 {
1014        return Err(format!("auplay_register(ringo) failed (rc={rc})"));
1015    }
1016    let _ = AUPLAY.set(pp as usize);
1017    Ok(())
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023
1024    #[test]
1025    fn render_stream_same_rate_drains_in_order() {
1026        let mut q: VecDeque<i16> = (1..=6).collect();
1027        let mut pos = 0.0;
1028        let mut out = [0i16; 4];
1029        render_stream(Some(&mut q), &mut pos, 1.0, &mut out);
1030        assert_eq!(out, [1, 2, 3, 4]);
1031        assert_eq!(q.iter().copied().collect::<Vec<_>>(), vec![5, 6]); // 4 consumed
1032        // Next frame continues where we left off.
1033        let mut out2 = [0i16; 2];
1034        render_stream(Some(&mut q), &mut pos, 1.0, &mut out2);
1035        assert_eq!(out2, [5, 6]);
1036        assert!(q.is_empty());
1037    }
1038
1039    #[test]
1040    fn render_stream_underrun_is_silence() {
1041        let mut q: VecDeque<i16> = VecDeque::from(vec![7, 8]);
1042        let mut pos = 0.0;
1043        let mut out = [0i16; 4];
1044        render_stream(Some(&mut q), &mut pos, 1.0, &mut out);
1045        assert_eq!(out, [7, 8, 0, 0]); // ran out → silence
1046        assert!(q.is_empty());
1047        assert_eq!(pos, 0.0); // reset on underrun
1048        // No subscription / no queue → all silence.
1049        let mut out2 = [9i16; 3];
1050        render_stream(None, &mut pos, 1.0, &mut out2);
1051        assert_eq!(out2, [0, 0, 0]);
1052    }
1053
1054    #[test]
1055    fn render_stream_downsamples_2to1() {
1056        // step = in/out = 2.0 → take every other input sample.
1057        let mut q: VecDeque<i16> = (0..8).collect();
1058        let mut pos = 0.0;
1059        let mut out = [0i16; 4];
1060        render_stream(Some(&mut q), &mut pos, 2.0, &mut out);
1061        assert_eq!(out, [0, 2, 4, 6]);
1062        assert!(q.is_empty()); // consumed all 8
1063    }
1064
1065    #[test]
1066    fn push_audio_is_noop_before_start_then_caps() {
1067        let key = "ringo-test-stream-cap"; // unique key, isolated from other tests
1068        // Before start_audio_stream: no queue exists, push is a no-op.
1069        push_audio(key, &[1, 2, 3]);
1070        assert!(stream_queue(key).is_none());
1071
1072        // After start: samples are buffered, bounded by STREAM_IN_CAP_SAMPLES.
1073        start_audio_stream(key, 8000);
1074        push_audio(key, &vec![7i16; STREAM_IN_CAP_SAMPLES + 100]);
1075        let q = stream_queue(key).expect("queue exists after start");
1076        assert_eq!(
1077            q.lock().unwrap_or_else(|e| e.into_inner()).len(),
1078            STREAM_IN_CAP_SAMPLES
1079        );
1080
1081        // Teardown drops the per-UA queue.
1082        remove_generator(key);
1083        assert!(stream_queue(key).is_none());
1084    }
1085
1086    #[test]
1087    fn prepare_buffer_creates_keeps_and_resets_on_rate_change() {
1088        let key = "ringo-test-prepare";
1089        // Missing → created empty at the negotiated rate.
1090        prepare_buffer(rx_buffers(), key, 8000);
1091        assert_eq!(buffer_window(rx_buffers(), key), Some((vec![], 8000)));
1092        capture_mono(rx_buffers(), key, &[1, 2, 3]);
1093        // Same rate → samples survive (a hold/resume re-alloc must not wipe).
1094        prepare_buffer(rx_buffers(), key, 8000);
1095        assert_eq!(
1096            buffer_window(rx_buffers(), key),
1097            Some((vec![1, 2, 3], 8000))
1098        );
1099        // Different rate → reset (mixing rates would corrupt the WAV).
1100        prepare_buffer(rx_buffers(), key, 16000);
1101        assert_eq!(buffer_window(rx_buffers(), key), Some((vec![], 16000)));
1102        remove_generator(key);
1103    }
1104
1105    // FULL_CAPTURE is a process-global static shared by parallel tests, so
1106    // every test that toggles it lives in this ONE test fn.
1107    #[test]
1108    fn arm_capture_scopes_retention_and_allocs() {
1109        let key = "ringo-test-arm";
1110        let srate = 8000u32;
1111        let sec = srate as usize;
1112
1113        // Armed: a re-arm clears the previous recording (fresh start), the
1114        // buffers keep their srate, and retention is capped at 600 s.
1115        arm_capture(key, true);
1116        prepare_buffer(tx_buffers(), key, srate);
1117        prepare_buffer(rx_buffers(), key, srate);
1118        capture_mono(tx_buffers(), key, &vec![7i16; sec * 601]);
1119        assert_eq!(
1120            buffer_window(tx_buffers(), key).unwrap().0.len(),
1121            sec * FULL_RETAIN_SECS
1122        );
1123        arm_capture(key, true);
1124        assert_eq!(
1125            (
1126                buffer_window(tx_buffers(), key),
1127                buffer_window(rx_buffers(), key)
1128            ),
1129            (Some((vec![], srate)), Some((vec![], srate)))
1130        );
1131
1132        // With capture on, a (re)alloc keeps samples (hold/resume survives).
1133        capture_mono(rx_buffers(), key, &[3]);
1134        alloc_buffer(rx_buffers(), key, srate);
1135        assert_eq!(buffer_window(rx_buffers(), key), Some((vec![3], srate)));
1136
1137        // Disarmed mid-call: the recording is kept as is — audio still
1138        // arriving must neither append to it nor trim it to the 3 s window…
1139        let long = vec![3i16; sec * 10];
1140        capture_mono(rx_buffers(), key, &long[1..]);
1141        arm_capture(key, false);
1142        capture_mono(rx_buffers(), key, &vec![5i16; sec * 4]);
1143        assert_eq!(
1144            buffer_window(rx_buffers(), key),
1145            Some((long.clone(), srate))
1146        );
1147        // …and a second disarm doesn't change that.
1148        arm_capture(key, false);
1149        assert_eq!(
1150            buffer_window(rx_buffers(), key).unwrap().0.len(),
1151            long.len()
1152        );
1153        // The next stream (re)alloc starts fresh with the 3 s verify window.
1154        alloc_buffer(rx_buffers(), key, srate);
1155        assert_eq!(buffer_window(rx_buffers(), key), Some((vec![], srate)));
1156        capture_mono(rx_buffers(), key, &vec![5i16; sec * 4]);
1157        assert_eq!(
1158            buffer_window(rx_buffers(), key).unwrap().0.len(),
1159            sec * VERIFY_RETAIN_SECS
1160        );
1161        // A new arm unfreezes and clears.
1162        arm_capture(key, false);
1163        arm_capture(key, true);
1164        assert_eq!(buffer_window(rx_buffers(), key), Some((vec![], srate)));
1165
1166        // Leave the process-global flag off for the other tests.
1167        arm_capture(key, false);
1168        remove_generator(key);
1169    }
1170}