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}