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    // Reset the sent-audio buffer so a save sees only this call.
373    reset_buffer(tx_buffers(), &key, srate);
374
375    let key_thread = key.clone();
376    let thread = match std::thread::Builder::new()
377        .name("ringo-ausrc".into())
378        .spawn(move || render_loop(key_thread, srate, ch, ptime, fmt, run_thread, cb))
379    {
380        Ok(t) => t,
381        Err(e) => {
382            // Don't hand baresip a source that never produces frames; free the
383            // cell (destructor no-ops on the still-null state) and fail.
384            crate::rlog!(Error, "ringo ausrc: spawn render thread failed: {e}");
385            unsafe { mem_deref(cell) };
386            return ENOMEM;
387        }
388    };
389
390    let state = Box::new(SrcState {
391        key,
392        run,
393        thread: Some(thread),
394    });
395    unsafe {
396        *(cell as *mut *mut SrcState) = Box::into_raw(state);
397        *stp = cell as *mut ausrc_st;
398    }
399    0
400}
401
402/// Fill `mono` from the front of the stream-in queue `q`, nearest-sample
403/// resampling input→output by `step` (= in_rate / out_rate). The input read is
404/// consumed (dropped from the queue front); `pos` carries the fractional read
405/// position across calls. Underrun (empty/short queue) renders silence, and an
406/// emptied queue resets `pos` so newly-fed audio starts cleanly.
407fn render_stream(q: Option<&mut VecDeque<i16>>, pos: &mut f64, step: f64, mono: &mut [i16]) {
408    let Some(q) = q else {
409        mono.iter_mut().for_each(|s| *s = 0);
410        return;
411    };
412    for s in mono.iter_mut() {
413        *s = q.get(*pos as usize).copied().unwrap_or(0);
414        *pos += step;
415    }
416    // Consume the read input by popping from the front (O(consumed), no memmove
417    // of the tail — this runs under the per-UA lock on the real-time thread).
418    let consumed = (*pos as usize).min(q.len());
419    for _ in 0..consumed {
420        q.pop_front();
421    }
422    *pos -= consumed as f64;
423    if q.is_empty() {
424        *pos = 0.0; // underran: restart cleanly when audio resumes
425    }
426}
427
428/// The render thread: every `ptime` ms, generate one frame from the current
429/// registry spec for `key` and hand it to baresip via `rh`.
430fn render_loop(
431    key: String,
432    srate: u32,
433    ch: u8,
434    ptime: u32,
435    fmt: i32,
436    run: Arc<AtomicBool>,
437    cb: ReadCb,
438) {
439    let is_float = fmt == aufmt::AUFMT_FLOAT as i32;
440    let sample_size = if is_float { 4 } else { 2 };
441    let frames = (srate as usize * ptime as usize / 1000).max(1);
442    let sampc = frames * ch as usize;
443
444    let mut sampv = vec![0u8; sampc * sample_size];
445    let mut mono = vec![0i16; frames];
446
447    // Render state (reset when the spec version changes).
448    let mut cur_version = u64::MAX;
449    let mut cur_spec = GenSpec::Silence;
450    let mut phase = 0.0f64; // tone phase accumulator
451    let mut file_pos = 0.0f64; // fractional read position into the file
452    let mut stream_pos = 0.0f64; // fractional read position into the stream-in queue
453
454    let mut start = Instant::now();
455    let mut frame_idx: u64 = 0;
456
457    while run.load(Ordering::Acquire) {
458        // Pace to real time: wake at start + frame_idx * ptime.
459        let target = start + Duration::from_millis(frame_idx * ptime as u64);
460        let now = Instant::now();
461        if target > now {
462            std::thread::sleep(target - now);
463        } else if now - target > Duration::from_millis(ptime as u64 * 4) {
464            // Fell far behind (suspend / scheduling stall): rebase the clock so
465            // we don't fire a long burst of catch-up frames (which the TX aubuf
466            // would just drop as overruns anyway).
467            start = now;
468            frame_idx = 0;
469        }
470        if !run.load(Ordering::Acquire) {
471            break;
472        }
473
474        // Reload spec if it changed.
475        let present = {
476            let map = registry().lock().unwrap_or_else(|e| e.into_inner());
477            map.get(&key).map(|e| (e.version, e.spec.clone()))
478        };
479        match &present {
480            Some((version, spec)) => {
481                if *version != cur_version {
482                    cur_version = *version;
483                    cur_spec = spec.clone();
484                    phase = 0.0;
485                    file_pos = 0.0;
486                    stream_pos = 0.0;
487                    crate::rlog!(
488                        Info,
489                        "ringo ausrc: key={key} spec={} (v{version})",
490                        spec_name(spec)
491                    );
492                }
493            }
494            None => {
495                // Registry entry gone (shouldn't happen while a source is live):
496                // render silence rather than a stale spec.
497                if cur_version != u64::MAX {
498                    crate::rlog!(Warn, "ringo ausrc: key={key} no registry entry, silence");
499                    cur_version = u64::MAX;
500                    cur_spec = GenSpec::Silence;
501                }
502            }
503        }
504
505        // Render `frames` mono samples.
506        match &cur_spec {
507            GenSpec::Silence => mono.iter_mut().for_each(|s| *s = 0),
508            GenSpec::Tone(freq) => {
509                let step = std::f64::consts::TAU * (*freq as f64) / (srate as f64);
510                for s in mono.iter_mut() {
511                    *s = (phase.sin() * AMPLITUDE * 32767.0) as i16;
512                    phase += step;
513                    if phase >= std::f64::consts::TAU {
514                        phase -= std::f64::consts::TAU;
515                    }
516                }
517            }
518            GenSpec::File(samples, file_srate) => {
519                let step = *file_srate as f64 / srate as f64;
520                let len = samples.len() as f64;
521                for s in mono.iter_mut() {
522                    // `while`, not `if`: a single step may overshoot `len` for a
523                    // tiny file (len < step), so wrap fully to keep looping.
524                    while file_pos >= len {
525                        file_pos -= len;
526                    }
527                    let i = file_pos as usize;
528                    *s = samples.get(i).copied().unwrap_or(0);
529                    file_pos += step;
530                }
531            }
532            GenSpec::Stream(stream_srate) => {
533                let step = (*stream_srate as f64 / srate as f64).max(f64::MIN_POSITIVE);
534                // Resolve the per-UA queue (brief global lock for the O(1) clone),
535                // then render under that UA's own lock — no cross-UA contention.
536                match stream_queue(&key) {
537                    Some(q) => {
538                        let mut q = q.lock().unwrap_or_else(|e| e.into_inner());
539                        render_stream(Some(&mut q), &mut stream_pos, step, &mut mono);
540                    }
541                    None => render_stream(None, &mut stream_pos, step, &mut mono),
542                }
543            }
544        }
545
546        // Capture the sent audio for --save-audio (full capture only).
547        if FULL_CAPTURE.load(Ordering::Acquire) {
548            capture_mono(tx_buffers(), &key, &mono);
549        }
550
551        // Pack mono → interleaved sampv in the requested format.
552        if is_float {
553            let out =
554                unsafe { std::slice::from_raw_parts_mut(sampv.as_mut_ptr() as *mut f32, sampc) };
555            for (f, &m) in out.chunks_mut(ch as usize).zip(mono.iter()) {
556                let v = m as f32 / 32768.0;
557                f.iter_mut().for_each(|x| *x = v);
558            }
559        } else {
560            let out =
561                unsafe { std::slice::from_raw_parts_mut(sampv.as_mut_ptr() as *mut i16, sampc) };
562            for (f, &m) in out.chunks_mut(ch as usize).zip(mono.iter()) {
563                f.iter_mut().for_each(|x| *x = m);
564            }
565        }
566
567        // Build the auframe and push it to baresip.
568        let mut af: auframe = unsafe { std::mem::zeroed() };
569        unsafe {
570            auframe_init(
571                &mut af,
572                if is_float {
573                    aufmt::AUFMT_FLOAT
574                } else {
575                    aufmt::AUFMT_S16LE
576                },
577                sampv.as_mut_ptr() as *mut c_void,
578                sampc,
579                srate,
580                ch,
581            );
582        }
583        af.timestamp = frame_idx * ptime as u64 * 1000; // AUDIO_TIMEBASE = microseconds
584
585        if let Some(rh) = cb.rh {
586            unsafe { rh(&mut af, cb.arg as *mut c_void) };
587        }
588
589        frame_idx += 1;
590    }
591}
592
593// ─── ringo auplay (self-clocked RX sink) ───────────────────────────────────
594//
595// Why we also need a player: the headless RX path (decode → aubuf → player) is
596// *pull-driven* — something must call the player's write handler at real-time
597// pace, or the decoder never advances and the received audio is silent.
598// aubridge's player is clocked by its device thread, which only runs when BOTH
599// an aubridge source AND player share a device (modules/aubridge/device.c) — but
600// ringo replaced the source, so that thread would never start. So ringo provides
601// its own self-clocked player: a timer thread that pulls `wh()` every ptime,
602// which both clocks the decode pipeline and yields the received frames we
603// capture for verify-audio / --save-audio.
604
605/// Registered `struct auplay *` (kept alive for the process lifetime).
606static AUPLAY: OnceLock<usize> = OnceLock::new();
607
608struct PlayState {
609    run: Arc<AtomicBool>,
610    thread: Option<std::thread::JoinHandle<()>>,
611}
612
613impl Drop for PlayState {
614    fn drop(&mut self) {
615        self.run.store(false, Ordering::Release);
616        if let Some(t) = self.thread.take() {
617            let _ = t.join();
618        }
619    }
620}
621
622struct WriteCb {
623    wh: auplay_write_h,
624    arg: usize,
625}
626// SAFETY: same contract as ReadCb — `arg`/`wh` are baresip pointers used only to
627// call back into baresip from this player's own thread. baresip frees the player
628// via `aurecv_stop()` (src/aureceiver.c), which mem_derefs `ar->auplay` (→ our
629// destructor → thread join) before freeing the receiver `arg`, so `wh` never
630// runs against a freed `arg`.
631unsafe impl Send for WriteCb {}
632
633/// Per-UA mono audio buffer (one for received, one for sent). Lets ringo-flow
634/// verify a received tone in-process (Goertzel on these samples) and save
635/// recordings — instead of baresip's sndfile WAV dumps. No shared-dir race, no
636/// disk round-trip, per-UA isolated by construction.
637struct AudioBuf {
638    srate: u32,
639    samples: VecDeque<i16>,
640}
641
642/// Seconds of audio retained per UA for verification (the verify window tail).
643const VERIFY_RETAIN_SECS: usize = 3;
644/// Seconds retained when full capture is on (`--save-audio`); bounds a runaway
645/// call. Test calls are far shorter.
646const FULL_RETAIN_SECS: usize = 600;
647
648/// Whether to retain the whole call (for `--save-audio`) vs. just the verify
649/// window. Set once per process from `BackendOptions.record_audio`.
650static FULL_CAPTURE: AtomicBool = AtomicBool::new(false);
651
652static RX_BUFFERS: OnceLock<Mutex<HashMap<String, AudioBuf>>> = OnceLock::new();
653static TX_BUFFERS: OnceLock<Mutex<HashMap<String, AudioBuf>>> = OnceLock::new();
654
655fn rx_buffers() -> &'static Mutex<HashMap<String, AudioBuf>> {
656    RX_BUFFERS.get_or_init(|| Mutex::new(HashMap::new()))
657}
658fn tx_buffers() -> &'static Mutex<HashMap<String, AudioBuf>> {
659    TX_BUFFERS.get_or_init(|| Mutex::new(HashMap::new()))
660}
661
662/// Enable/disable full-call capture (for `--save-audio`). When off, only the
663/// rolling verify window is retained and the sent buffer isn't captured.
664pub(super) fn set_full_capture(on: bool) {
665    FULL_CAPTURE.store(on, Ordering::Release);
666}
667
668fn retain_secs() -> usize {
669    if FULL_CAPTURE.load(Ordering::Acquire) {
670        FULL_RETAIN_SECS
671    } else {
672        VERIFY_RETAIN_SECS
673    }
674}
675
676/// Reset a UA's buffer in `buffers` to empty at `srate` (called on (re)alloc so
677/// a verify/save sees only the current call).
678fn reset_buffer(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, srate: u32) {
679    buffers.lock().unwrap_or_else(|e| e.into_inner()).insert(
680        key.to_string(),
681        AudioBuf {
682            srate,
683            samples: VecDeque::new(),
684        },
685    );
686}
687
688/// The retained received mono samples for `key` and their sample rate.
689pub(super) fn received_window(key: &str) -> Option<(Vec<i16>, u32)> {
690    buffer_window(rx_buffers(), key)
691}
692
693/// The retained sent mono samples for `key` and their sample rate (full capture
694/// only; empty otherwise).
695pub(super) fn sent_window(key: &str) -> Option<(Vec<i16>, u32)> {
696    buffer_window(tx_buffers(), key)
697}
698
699fn buffer_window(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str) -> Option<(Vec<i16>, u32)> {
700    let map = buffers.lock().unwrap_or_else(|e| e.into_inner());
701    let buf = map.get(key)?;
702    Some((buf.samples.iter().copied().collect(), buf.srate))
703}
704
705/// Append already-mono `samples` to the UA's buffer in `buffers`, capped.
706fn capture_mono(buffers: &Mutex<HashMap<String, AudioBuf>>, key: &str, samples: &[i16]) {
707    let mut map = buffers.lock().unwrap_or_else(|e| e.into_inner());
708    let Some(buf) = map.get_mut(key) else {
709        return;
710    };
711    buf.samples.extend(samples.iter().copied());
712    let cap = (buf.srate as usize * retain_secs()).max(1);
713    while buf.samples.len() > cap {
714        buf.samples.pop_front();
715    }
716}
717
718/// Append the channel-0 mono samples from an interleaved player frame to the
719/// UA's received buffer. `wh` always fills the full `sampc` (aurecv_read pads an
720/// underrun with silence), so the whole `sampv` is valid decoded audio.
721fn capture_rx(key: &str, sampv: &[u8], is_float: bool, ch: usize, srate: u32) {
722    let ch = ch.max(1);
723    let mono: Vec<i16> = if is_float {
724        let f =
725            unsafe { std::slice::from_raw_parts(sampv.as_ptr() as *const f32, sampv.len() / 4) };
726        f.chunks_exact(ch)
727            .map(|fr| (fr[0] * 32767.0) as i16)
728            .collect()
729    } else {
730        let s =
731            unsafe { std::slice::from_raw_parts(sampv.as_ptr() as *const i16, sampv.len() / 2) };
732        s.chunks_exact(ch).map(|fr| fr[0]).collect()
733    };
734    capture_mono(rx_buffers(), key, &mono);
735    // Live tap: hand the frame to a subscriber (e.g. streaming RX out for STT).
736    // Drop the subscription if the receiver is gone.
737    let mut taps = rx_taps().lock().unwrap_or_else(|e| e.into_inner());
738    if let Some(tx) = taps.get(key) {
739        let frame = AudioFrame {
740            samples: mono,
741            rate: srate,
742        };
743        if tx.send(frame).is_err() {
744            taps.remove(key);
745        }
746    }
747}
748
749unsafe extern "C" fn play_destructor(arg: *mut c_void) {
750    let cell = arg as *mut *mut PlayState;
751    let state = unsafe { *cell };
752    if !state.is_null() {
753        drop(unsafe { Box::from_raw(state) });
754        unsafe { *cell = std::ptr::null_mut() };
755    }
756}
757
758unsafe extern "C" fn play_alloc_handler(
759    stp: *mut *mut auplay_st,
760    _ap: *const auplay,
761    prm: *mut auplay_prm,
762    dev: *const c_char,
763    wh: auplay_write_h,
764    arg: *mut c_void,
765) -> i32 {
766    const EINVAL: i32 = 22;
767    const ENOMEM: i32 = 12;
768
769    if stp.is_null() || prm.is_null() || wh.is_none() {
770        return EINVAL;
771    }
772
773    let prm = unsafe { &*prm };
774    let srate = prm.srate;
775    let ptime = if prm.ptime == 0 { 20 } else { prm.ptime };
776    let fmt = prm.fmt;
777
778    if srate == 0 || prm.ch == 0 {
779        crate::rlog!(
780            Warn,
781            "ringo auplay: invalid prm srate={srate} ch={}",
782            prm.ch
783        );
784        return EINVAL;
785    }
786    let ch = prm.ch;
787
788    // The device key (= account username) identifies this UA's RX buffer. Reset
789    // it on (re)alloc so a verify reads only the current call's received audio.
790    let key = if dev.is_null() {
791        String::new()
792    } else {
793        unsafe { std::ffi::CStr::from_ptr(dev) }
794            .to_string_lossy()
795            .into_owned()
796    };
797    if !key.is_empty() {
798        reset_buffer(rx_buffers(), &key, srate);
799    }
800
801    let cell = unsafe { mem_zalloc(std::mem::size_of::<*mut PlayState>(), Some(play_destructor)) };
802    if cell.is_null() {
803        return ENOMEM;
804    }
805
806    let run = Arc::new(AtomicBool::new(true));
807    let run_thread = run.clone();
808    let cb = WriteCb {
809        wh,
810        arg: arg as usize,
811    };
812
813    let thread = match std::thread::Builder::new()
814        .name("ringo-auplay".into())
815        .spawn(move || play_loop(key, srate, ch, ptime, fmt, run_thread, cb))
816    {
817        Ok(t) => t,
818        Err(e) => {
819            // A player that never pulls would stall the decode pipeline; free the
820            // cell (destructor no-ops on the null state) and fail.
821            crate::rlog!(Error, "ringo auplay: spawn play thread failed: {e}");
822            unsafe { mem_deref(cell) };
823            return ENOMEM;
824        }
825    };
826
827    let state = Box::new(PlayState {
828        run,
829        thread: Some(thread),
830    });
831    unsafe {
832        *(cell as *mut *mut PlayState) = Box::into_raw(state);
833        *stp = cell as *mut auplay_st;
834    }
835    0
836}
837
838/// Self-clocked player: every `ptime` ms, pull one frame from baresip via `wh`
839/// (which drives the decode pipeline) and capture it into the UA's RX buffer for
840/// in-process tone verification.
841fn play_loop(
842    key: String,
843    srate: u32,
844    ch: u8,
845    ptime: u32,
846    fmt: i32,
847    run: Arc<AtomicBool>,
848    cb: WriteCb,
849) {
850    let is_float = fmt == aufmt::AUFMT_FLOAT as i32;
851    let sample_size = if is_float { 4 } else { 2 };
852    let frames = (srate as usize * ptime as usize / 1000).max(1);
853    let sampc = frames * ch as usize;
854    let mut sampv = vec![0u8; sampc * sample_size];
855
856    let af_fmt = if is_float {
857        aufmt::AUFMT_FLOAT
858    } else {
859        aufmt::AUFMT_S16LE
860    };
861
862    let mut start = Instant::now();
863    let mut frame_idx: u64 = 0;
864
865    while run.load(Ordering::Acquire) {
866        let target = start + Duration::from_millis(frame_idx * ptime as u64);
867        let now = Instant::now();
868        if target > now {
869            std::thread::sleep(target - now);
870        } else if now - target > Duration::from_millis(ptime as u64 * 4) {
871            start = now;
872            frame_idx = 0;
873        }
874        if !run.load(Ordering::Acquire) {
875            break;
876        }
877
878        let mut af: auframe = unsafe { std::mem::zeroed() };
879        unsafe {
880            auframe_init(
881                &mut af,
882                af_fmt,
883                sampv.as_mut_ptr() as *mut c_void,
884                sampc,
885                srate,
886                ch,
887            );
888        }
889        af.timestamp = frame_idx * ptime as u64 * 1000;
890
891        if let Some(wh) = cb.wh {
892            // wh fills sampv with the decoded received audio (aurecv_read →
893            // aubuf_read_auframe), then we capture it for in-process verify.
894            unsafe { wh(&mut af, cb.arg as *mut c_void) };
895            if !key.is_empty() {
896                capture_rx(&key, &sampv, is_float, ch as usize, srate);
897            }
898        }
899
900        frame_idx += 1;
901    }
902}
903
904/// Register the `ringo` audio source + player modules with baresip. Must run
905/// once after `baresip_init`/`ua_init` (so `baresip_ausrcl()`/`baresip_auplayl()`
906/// exist) on the RE thread. Returns `Err` if registration fails — the caller
907/// must abort, since the agents would otherwise start with no working audio.
908/// The registered `ausrc`/`auplay` are intentionally never `mem_deref`'d (kept
909/// for the process lifetime); we stash the pointers for documentation.
910pub(super) fn register_module() -> Result<(), String> {
911    let mut asp: *mut ausrc = std::ptr::null_mut();
912    let rc = unsafe {
913        ausrc_register(
914            &mut asp,
915            baresip_ausrcl(),
916            c"ringo".as_ptr(),
917            Some(alloc_handler),
918        )
919    };
920    if rc != 0 {
921        return Err(format!("ausrc_register(ringo) failed (rc={rc})"));
922    }
923    let _ = AUSRC.set(asp as usize);
924
925    let mut pp: *mut auplay = std::ptr::null_mut();
926    let rc = unsafe {
927        auplay_register(
928            &mut pp,
929            baresip_auplayl(),
930            c"ringo".as_ptr(),
931            Some(play_alloc_handler),
932        )
933    };
934    if rc != 0 {
935        return Err(format!("auplay_register(ringo) failed (rc={rc})"));
936    }
937    let _ = AUPLAY.set(pp as usize);
938    Ok(())
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944
945    #[test]
946    fn render_stream_same_rate_drains_in_order() {
947        let mut q: VecDeque<i16> = (1..=6).collect();
948        let mut pos = 0.0;
949        let mut out = [0i16; 4];
950        render_stream(Some(&mut q), &mut pos, 1.0, &mut out);
951        assert_eq!(out, [1, 2, 3, 4]);
952        assert_eq!(q.iter().copied().collect::<Vec<_>>(), vec![5, 6]); // 4 consumed
953        // Next frame continues where we left off.
954        let mut out2 = [0i16; 2];
955        render_stream(Some(&mut q), &mut pos, 1.0, &mut out2);
956        assert_eq!(out2, [5, 6]);
957        assert!(q.is_empty());
958    }
959
960    #[test]
961    fn render_stream_underrun_is_silence() {
962        let mut q: VecDeque<i16> = VecDeque::from(vec![7, 8]);
963        let mut pos = 0.0;
964        let mut out = [0i16; 4];
965        render_stream(Some(&mut q), &mut pos, 1.0, &mut out);
966        assert_eq!(out, [7, 8, 0, 0]); // ran out → silence
967        assert!(q.is_empty());
968        assert_eq!(pos, 0.0); // reset on underrun
969        // No subscription / no queue → all silence.
970        let mut out2 = [9i16; 3];
971        render_stream(None, &mut pos, 1.0, &mut out2);
972        assert_eq!(out2, [0, 0, 0]);
973    }
974
975    #[test]
976    fn render_stream_downsamples_2to1() {
977        // step = in/out = 2.0 → take every other input sample.
978        let mut q: VecDeque<i16> = (0..8).collect();
979        let mut pos = 0.0;
980        let mut out = [0i16; 4];
981        render_stream(Some(&mut q), &mut pos, 2.0, &mut out);
982        assert_eq!(out, [0, 2, 4, 6]);
983        assert!(q.is_empty()); // consumed all 8
984    }
985
986    #[test]
987    fn push_audio_is_noop_before_start_then_caps() {
988        let key = "ringo-test-stream-cap"; // unique key, isolated from other tests
989        // Before start_audio_stream: no queue exists, push is a no-op.
990        push_audio(key, &[1, 2, 3]);
991        assert!(stream_queue(key).is_none());
992
993        // After start: samples are buffered, bounded by STREAM_IN_CAP_SAMPLES.
994        start_audio_stream(key, 8000);
995        push_audio(key, &vec![7i16; STREAM_IN_CAP_SAMPLES + 100]);
996        let q = stream_queue(key).expect("queue exists after start");
997        assert_eq!(
998            q.lock().unwrap_or_else(|e| e.into_inner()).len(),
999            STREAM_IN_CAP_SAMPLES
1000        );
1001
1002        // Teardown drops the per-UA queue.
1003        remove_generator(key);
1004        assert!(stream_queue(key).is_none());
1005    }
1006}