flexaudio_core/backend.rs
1//! The [`CaptureBackend`] trait implemented by OS backends, and the [`RawSink`] handle used
2//! by backends to pass raw frames to the core.
3//!
4//! The facade layer wires the path (backend → [`RawRing`](mod@crate::raw_ring) → [`Normalizer`](crate::normalizer)
5//! → [`ChunkRing`](mod@crate::chunk_ring)). This module only defines backend contract types.
6
7use crate::raw_ring::RawProducer;
8use crate::types::Result;
9
10/// Sink for passing raw interleaved f32 frames from a backend to the core.
11///
12/// Holds a [`RawProducer`] internally. [`push`](Self::push) writes without blocking and is RT-safe
13/// (drops data when full). This is the SPSC producer side; call `push` only from the backend's
14/// RT callback thread.
15pub struct RawSink {
16 producer: RawProducer,
17 native_rate: u32,
18 native_channels: u16,
19}
20
21impl RawSink {
22 /// Create a raw-frame sink with the backend native format.
23 pub fn new(producer: RawProducer, native_rate: u32, native_channels: u16) -> Self {
24 Self {
25 producer,
26 native_rate,
27 native_channels,
28 }
29 }
30
31 /// Send raw interleaved f32 frames without blocking.
32 ///
33 /// `pts_ns` is a device-provided presentation timestamp. Never block;
34 /// increment the internal overflow counter and drop data when full.
35 ///
36 /// PTS normalization and 20 ms chunking belong to the ingest thread. This sends only raw frames;
37 /// the raw ring carries samples only, so the wiring layer handles `pts_ns` separately
38 /// (backends that can provide it should do so for future frame association).
39 ///
40 /// `push` is intended for backend RT callbacks. Custom backends must not allocate, lock, block,
41 /// or make system calls along the path that calls push.
42 pub fn push(&mut self, interleaved: &[f32], pts_ns: i64) -> usize {
43 let _ = pts_ns;
44 self.producer.push_slice(interleaved)
45 }
46
47 /// Backend native sample rate (Hz).
48 pub fn native_rate(&self) -> u32 {
49 self.native_rate
50 }
51
52 /// Backend native channel count.
53 pub fn native_channels(&self) -> u16 {
54 self.native_channels
55 }
56
57 /// Cumulative samples dropped because the buffer was full.
58 pub fn overflow_count(&self) -> u64 {
59 self.producer.overflow_count()
60 }
61}
62
63/// Trait implemented by OS-specific capture backends.
64///
65/// The facade reads the native format via [`native_format`](Self::native_format), configures a
66/// [`Normalizer`](crate::normalizer), then passes a [`RawSink`] to [`start`](Self::start) to begin
67/// capture. The backend calls `sink.push(...)` from its RT callback.
68///
69/// Pass a custom backend as `Box<dyn CaptureBackend>` to `Stream::open`.
70///
71/// Requirements for custom backends:
72/// - Never panic on the capture thread / RT callback (a panic can silently stop capture).
73///
74/// - Call [`RawSink::push`] from RT callbacks in an RT-safe way (no allocation, locks, blocking,
75/// or system calls; see [`RawSink::push`] for details).
76/// - Make `start` / `stop` idempotent: calling `start` while running returns `Ok(())` without
77/// doing anything; calling `stop` before startup does nothing.
78pub trait CaptureBackend: Send {
79 /// Backend native format `(sample_rate, channels)`.
80 fn native_format(&self) -> (u32, u16);
81
82 /// Start sending raw frames to the given sink.
83 fn start(&mut self, sink: RawSink) -> Result<()>;
84
85 /// Stop capture.
86 fn stop(&mut self);
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::raw_ring::raw_ring;
93
94 /// Tiny backend for tests. Pushes one block from `start`.
95 struct DummyBackend {
96 sink: Option<RawSink>,
97 }
98
99 impl CaptureBackend for DummyBackend {
100 fn native_format(&self) -> (u32, u16) {
101 (44_100, 2)
102 }
103 fn start(&mut self, mut sink: RawSink) -> Result<()> {
104 assert_eq!(sink.native_rate(), 44_100);
105 assert_eq!(sink.native_channels(), 2);
106 sink.push(&[0.1, 0.2, 0.3, 0.4], 0);
107 self.sink = Some(sink);
108 Ok(())
109 }
110 fn stop(&mut self) {
111 self.sink = None;
112 }
113 }
114
115 #[test]
116 fn backend_pushes_into_raw_ring() {
117 let (prod, mut cons) = raw_ring(16);
118 let sink = RawSink::new(prod, 44_100, 2);
119 let mut be = DummyBackend { sink: None };
120 assert_eq!(be.native_format(), (44_100, 2));
121 be.start(sink).unwrap();
122
123 let mut out = [0.0f32; 4];
124 let got = cons.pop_slice(&mut out);
125 assert_eq!(got, 4);
126 assert_eq!(out, [0.1, 0.2, 0.3, 0.4]);
127 be.stop();
128 }
129}