Skip to main content

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::{Event, 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    diagnostics: crate::CaptureDiagnostics,
20}
21
22impl RawSink {
23    /// Create a raw-frame sink with the backend native format.
24    pub fn new(producer: RawProducer, native_rate: u32, native_channels: u16) -> Self {
25        Self {
26            producer,
27            diagnostics: crate::CaptureDiagnostics::new(native_rate, native_channels),
28            native_rate,
29            native_channels,
30        }
31    }
32
33    /// Send raw interleaved f32 frames without blocking.
34    ///
35    /// `pts_ns` is a device-provided presentation timestamp. Never block;
36    /// increment the internal overflow counter and drop data when full.
37    ///
38    /// PTS normalization and 20 ms chunking belong to the ingest thread. This sends only raw frames;
39    /// the raw ring carries samples only, so the wiring layer handles `pts_ns` separately
40    /// (backends that can provide it should do so for future frame association).
41    ///
42    /// `push` is intended for backend RT callbacks. Custom backends must not allocate, lock, block,
43    /// or make system calls along the path that calls push.
44    pub fn push(&mut self, interleaved: &[f32], pts_ns: i64) -> usize {
45        let _ = pts_ns;
46        self.producer.push_slice(interleaved)
47    }
48
49    /// Clone the rejection handle before entering a realtime callback.
50    pub fn diagnostics(&self) -> crate::CaptureDiagnostics {
51        self.diagnostics.clone()
52    }
53
54    /// Backend native sample rate (Hz).
55    pub fn native_rate(&self) -> u32 {
56        self.native_rate
57    }
58
59    /// Backend native channel count.
60    pub fn native_channels(&self) -> u16 {
61        self.native_channels
62    }
63
64    /// Cumulative samples dropped because the buffer was full.
65    pub fn overflow_count(&self) -> u64 {
66        self.producer.overflow_count()
67    }
68}
69
70/// Trait implemented by OS-specific capture backends.
71///
72/// The facade reads the native format via [`native_format`](Self::native_format), configures a
73/// [`Normalizer`](crate::normalizer), then passes a [`RawSink`] to [`start`](Self::start) to begin
74/// capture. The backend calls `sink.push(...)` from its RT callback.
75///
76/// Pass a custom backend as `Box<dyn CaptureBackend>` to `Stream::open`.
77///
78/// Requirements for custom backends:
79/// - Never panic on the capture thread / RT callback (a panic can silently stop capture).
80///
81/// - Call [`RawSink::push`] from RT callbacks in an RT-safe way (no allocation, locks, blocking,
82///   or system calls; see [`RawSink::push`] for details).
83/// - Make `start` / `stop` idempotent: calling `start` while running returns `Ok(())` without
84///   doing anything; calling `stop` before startup does nothing.
85pub trait CaptureBackend: Send {
86    /// Backend native format `(sample_rate, channels)`.
87    fn native_format(&self) -> (u32, u16);
88
89    /// Start sending raw frames to the given sink.
90    fn start(&mut self, sink: RawSink) -> Result<()>;
91
92    /// Stop capture.
93    fn stop(&mut self);
94
95    /// Checked stop. External backends retain compatibility, but must override this
96    /// to expose hidden owner failures.
97    fn stop_checked(&mut self) -> Result<()> {
98        self.stop();
99        Ok(())
100    }
101
102    /// Poll a backend notification on the control thread. Confirmed permission
103    /// denial and [`Event::TerminalError`] are terminal; the stream stops capture
104    /// and suppresses recovery.
105    /// Return promptly and return `None` when empty. After `stop`, final owner
106    /// notifications must remain pollable until drained; do not discard denial
107    /// when stopping or replacing a capture generation.
108    /// Typed error messages must be safe library-authored explanations; OS error text is
109    /// allowed, but device names, private environment diagnostics and internal call labels
110    /// are not. Legacy [`Event::Error`] strings are conservatively terminal and replaced
111    /// with a library-authored explanation at the facade boundary.
112    fn poll_event(&mut self) -> Option<Event> {
113        None
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::raw_ring::raw_ring;
121
122    /// Tiny backend for tests. Pushes one block from `start`.
123    struct DummyBackend {
124        sink: Option<RawSink>,
125    }
126
127    impl CaptureBackend for DummyBackend {
128        fn native_format(&self) -> (u32, u16) {
129            (44_100, 2)
130        }
131        fn start(&mut self, mut sink: RawSink) -> Result<()> {
132            assert_eq!(sink.native_rate(), 44_100);
133            assert_eq!(sink.native_channels(), 2);
134            sink.push(&[0.1, 0.2, 0.3, 0.4], 0);
135            self.sink = Some(sink);
136            Ok(())
137        }
138        fn stop(&mut self) {
139            self.sink = None;
140        }
141    }
142
143    #[test]
144    fn backend_pushes_into_raw_ring() {
145        let (prod, mut cons) = raw_ring(16);
146        let sink = RawSink::new(prod, 44_100, 2);
147        let mut be = DummyBackend { sink: None };
148        assert_eq!(be.native_format(), (44_100, 2));
149        be.start(sink).unwrap();
150
151        let mut out = [0.0f32; 4];
152        let got = cons.pop_slice(&mut out);
153        assert_eq!(got, 4);
154        assert_eq!(out, [0.1, 0.2, 0.3, 0.4]);
155        be.stop();
156    }
157}