Skip to main content

rill_core/
io.rs

1//! # Signal I/O — generic multi-channel real-time I/O abstraction
2
3use std::sync::atomic::AtomicBool;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use crate::queues::{SpscQueue, TelemetryBlock};
8use crate::time::ClockTick;
9
10/// Result alias for signal I/O operations.
11pub type IoResult<T> = Result<T, String>;
12
13/// Control interface for backends that accept operational data
14/// separate from the signal stream (e.g. chip register writes).
15pub trait IoControl {
16    /// Write control data. Interpretation is device-specific.
17    fn write_data(&self, data: &[u8]) -> usize;
18}
19
20// ============================================================================
21// IoDriver — drives the graph
22// ============================================================================
23
24/// A backend that can be the **clock driver** for the signal graph.
25///
26/// The driver owns the timing loop: it registers a process callback and
27/// fires it on every I/O tick. Only one driver is active per rack.
28///
29/// A single backend struct may implement `IoDriver` together with
30/// [`IoCapture`] and/or [`IoPlayback`] — capturing and playing are
31/// orthogonal capabilities on top of the driver role.
32pub trait IoDriver: Send + Sync {
33    /// Register the process callback that the driver calls each tick.
34    ///
35    /// The callback receives a [`ClockTick`] with timing metadata
36    /// (sample position, rate, speed_ratio, etc.).
37    fn set_callback(&self, cb: Box<dyn FnMut(&ClockTick)>);
38
39    /// Enter the I/O lifecycle.
40    ///
41    /// Blocks until the driver is stopped (via [`stop`](IoDriver::stop) or
42    /// the `running` flag becomes `false`). The process callback set via
43    /// [`set_callback`](IoDriver::set_callback) fires inside this call.
44    fn run(&self, running: Arc<AtomicBool>) -> IoResult<()>;
45
46    /// Signal the driver to shut down. Called from the control thread.
47    /// After this returns the driver must be safe to drop.
48    fn stop(&self) -> IoResult<()>;
49
50    /// Returns a control interface if this driver supports runtime
51    /// register/data writes. Returns `None` by default.
52    fn as_control(&self) -> Option<&dyn IoControl> {
53        None
54    }
55}
56
57// ============================================================================
58// IoCapture — reads input samples
59// ============================================================================
60
61/// A backend that **captures** (reads) signal data from hardware.
62///
63/// Nodes of type `rill/input` hold an `Arc<dyn IoCapture>` and call
64/// [`read_input`](IoCapture::read_input) directly from `generate()`.
65///
66/// A capture backend may or may not also be the driver. When it is not
67/// the driver, the driver's callback ensures that fresh capture data is
68/// available before the graph runs (e.g. PipeWire processes all streams
69/// in the same cycle).
70pub trait IoCapture: Send + Sync {
71    /// Read captured samples for one channel into `dst`.
72    ///
73    /// Returns the number of samples actually read (may be less than
74    /// `dst.len()` if insufficient data is available).
75    fn read_input(&self, channel: usize, dst: &mut [f32]) -> usize;
76
77    /// Number of capture channels.
78    fn num_input_channels(&self) -> usize;
79}
80
81// ============================================================================
82// IoPlayback — writes output samples
83// ============================================================================
84
85/// A backend that **plays** (writes) signal data to hardware.
86///
87/// Nodes of type `rill/output` hold an `Arc<dyn IoPlayback>` and call
88/// [`write_output`](IoPlayback::write_output) directly from `consume()`.
89pub trait IoPlayback: Send + Sync {
90    /// Write output samples for one channel from `src`.
91    ///
92    /// Returns the number of samples actually written (may be less than
93    /// `src.len()` if insufficient space is available).
94    fn write_output(&self, channel: usize, src: &[f32]) -> usize;
95
96    /// Number of playback channels.
97    fn num_output_channels(&self) -> usize;
98}
99
100/// A passive I/O backend that writes to nowhere and reads zeros.
101///
102/// Implements both [`IoCapture`] and [`IoPlayback`] as no-ops.
103/// Useful as a placeholder for the unused direction in input-only
104/// or output-only scenarios.
105pub struct NullBackend {
106    channels: usize,
107}
108
109impl NullBackend {
110    /// Create a null backend with the given number of channels.
111    pub fn new(channels: usize) -> Self {
112        Self { channels }
113    }
114}
115
116impl IoCapture for NullBackend {
117    fn read_input(&self, _channel: usize, dst: &mut [f32]) -> usize {
118        dst.fill(0.0);
119        dst.len()
120    }
121
122    fn num_input_channels(&self) -> usize {
123        self.channels
124    }
125}
126
127impl IoPlayback for NullBackend {
128    fn write_output(&self, _channel: usize, _src: &[f32]) -> usize {
129        _src.len()
130    }
131
132    fn num_output_channels(&self) -> usize {
133        self.channels
134    }
135}
136
137/// An `IoPlayback` that pushes signal blocks into a lock-free SPSC queue.
138///
139/// Each `write_output` call wraps the signal data into a [`TelemetryBlock`]
140/// and pushes it into a [`SpscQueue`]. No allocations, no locks — safe to
141/// call from the RT signal path. A non-RT collector drains the queue.
142pub struct SpmcPlayback<T: crate::math::Transcendental, const BUF: usize, const CAP: usize> {
143    queue: Arc<SpscQueue<TelemetryBlock<T, BUF>, CAP>>,
144    channels: usize,
145    sample_rate: f32,
146    sample_pos: AtomicU64,
147}
148
149impl<T: crate::math::Transcendental, const BUF: usize, const CAP: usize> SpmcPlayback<T, BUF, CAP> {
150    /// Create an SPSC-based playback that writes to `queue`.
151    pub fn new(
152        queue: Arc<SpscQueue<TelemetryBlock<T, BUF>, CAP>>,
153        channels: usize,
154        sample_rate: f32,
155    ) -> Self {
156        Self {
157            queue,
158            channels,
159            sample_rate,
160            sample_pos: AtomicU64::new(0),
161        }
162    }
163
164    /// Return the shared queue for draining on the non-RT side.
165    pub fn queue(&self) -> &Arc<SpscQueue<TelemetryBlock<T, BUF>, CAP>> {
166        &self.queue
167    }
168}
169
170impl IoPlayback for SpmcPlayback<f32, 256, 64> {
171    fn write_output(&self, channel: usize, src: &[f32]) -> usize {
172        let n = src.len();
173        if n == 0 {
174            return 0;
175        }
176        let pos = self.sample_pos.fetch_add(n as u64, Ordering::Relaxed);
177        let mut block = TelemetryBlock::default();
178        let limit = n.min(256);
179        block.data[..limit].copy_from_slice(&src[..limit]);
180        block.channel = channel as u32;
181        block.sample_rate = self.sample_rate;
182        block.block_index = pos;
183        block.timestamp = pos;
184        block.compute_metrics();
185        let _ = self.queue.push(block);
186        limit
187    }
188
189    fn num_output_channels(&self) -> usize {
190        self.channels
191    }
192}
193
194// ============================================================================
195// Backward-compatible alias
196// ============================================================================
197
198/// Backward-compatible alias for code that only needs a driver.
199pub trait IoBackend: IoDriver {}
200
201impl<T: IoDriver> IoBackend for T {}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
207
208    struct TestBackend {
209        reg: AtomicU8,
210    }
211
212    impl IoDriver for TestBackend {
213        fn set_callback(&self, _cb: Box<dyn FnMut(&ClockTick)>) {}
214
215        fn run(&self, _: Arc<AtomicBool>) -> IoResult<()> {
216            Ok(())
217        }
218
219        fn stop(&self) -> IoResult<()> {
220            Ok(())
221        }
222
223        fn as_control(&self) -> Option<&dyn IoControl> {
224            Some(self)
225        }
226    }
227
228    impl IoControl for TestBackend {
229        fn write_data(&self, data: &[u8]) -> usize {
230            if let Some(&v) = data.first() {
231                self.reg.store(v, Ordering::Relaxed);
232            }
233            1
234        }
235    }
236
237    #[test]
238    fn test_iocontrol_write_data() {
239        let b = TestBackend {
240            reg: AtomicU8::new(0),
241        };
242        let ctrl = b.as_control().unwrap();
243        ctrl.write_data(&[42]);
244        assert_eq!(b.reg.load(Ordering::Relaxed), 42);
245    }
246
247    #[test]
248    fn test_iocontrol_default_returns_none() {
249        struct NoControl;
250        impl IoDriver for NoControl {
251            fn set_callback(&self, _cb: Box<dyn FnMut(&ClockTick)>) {}
252            fn run(&self, _: Arc<AtomicBool>) -> IoResult<()> {
253                Ok(())
254            }
255            fn stop(&self) -> IoResult<()> {
256                Ok(())
257            }
258        }
259        let b = NoControl;
260        assert!(b.as_control().is_none());
261    }
262}