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::Arc;
5
6use crate::time::ClockTick;
7
8/// Result alias for signal I/O operations.
9pub type IoResult<T> = Result<T, String>;
10
11/// Control interface for backends that accept operational data
12/// separate from the signal stream (e.g. chip register writes).
13pub trait IoControl {
14    /// Write control data. Interpretation is device-specific.
15    fn write_data(&self, data: &[u8]) -> usize;
16}
17
18// ============================================================================
19// IoDriver — drives the graph
20// ============================================================================
21
22/// A backend that can be the **clock driver** for the signal graph.
23///
24/// The driver owns the timing loop: it registers a process callback and
25/// fires it on every I/O tick. Only one driver is active per rack.
26///
27/// A single backend struct may implement `IoDriver` together with
28/// [`IoCapture`] and/or [`IoPlayback`] — capturing and playing are
29/// orthogonal capabilities on top of the driver role.
30pub trait IoDriver: Send + Sync {
31    /// Register the process callback that the driver calls each tick.
32    ///
33    /// The callback receives a [`ClockTick`] with timing metadata
34    /// (sample position, rate, speed_ratio, etc.).
35    fn set_process_callback(&self, cb: Box<dyn FnMut(&ClockTick)>);
36
37    /// Register an optional input-stream callback for split-chain backends
38    /// (e.g. PipeWire full-duplex).
39    ///
40    /// When both input and output streams exist, the recording chain
41    /// (Source → WriteHead) is driven from this callback, while the
42    /// playback chain (ReadHeads → Sink) is driven from
43    /// [`set_process_callback`]. Default no-op for single-stream backends.
44    fn set_input_process_callback(&self, _cb: Box<dyn FnMut(&ClockTick)>) {}
45
46    /// Enter the I/O lifecycle. Called on the pre-created signal thread.
47    ///
48    /// For poll-driven backends (ALSA, PipeWire) this blocks inside the
49    /// I/O loop and returns only after `running` becomes false.
50    /// For callback-driven backends (JACK, PortAudio) it sets up the
51    /// stream and returns immediately — the process callback fires on
52    /// the I/O API's own thread.
53    fn run(&self, running: Arc<AtomicBool>) -> IoResult<()>;
54
55    /// Signal the driver to shut down. Called from the control thread.
56    /// After this returns the driver must be safe to drop.
57    fn stop(&self) -> IoResult<()>;
58
59    /// Returns a control interface if this driver supports runtime
60    /// register/data writes. Returns `None` by default.
61    fn as_control(&self) -> Option<&dyn IoControl> {
62        None
63    }
64}
65
66// ============================================================================
67// IoCapture — reads input samples
68// ============================================================================
69
70/// A backend that **captures** (reads) signal data from hardware.
71///
72/// Nodes of type `rill/input` hold an `Arc<dyn IoCapture>` and call
73/// [`read_input`](IoCapture::read_input) directly from `generate()`.
74///
75/// A capture backend may or may not also be the driver. When it is not
76/// the driver, the driver's callback ensures that fresh capture data is
77/// available before the graph runs (e.g. PipeWire processes all streams
78/// in the same cycle).
79pub trait IoCapture: Send + Sync {
80    /// Read captured samples for one channel into `dst`.
81    ///
82    /// Returns the number of samples actually read (may be less than
83    /// `dst.len()` if insufficient data is available).
84    fn read_input(&self, channel: usize, dst: &mut [f32]) -> usize;
85
86    /// Number of capture channels.
87    fn num_input_channels(&self) -> usize;
88}
89
90// ============================================================================
91// IoPlayback — writes output samples
92// ============================================================================
93
94/// A backend that **plays** (writes) signal data to hardware.
95///
96/// Nodes of type `rill/output` hold an `Arc<dyn IoPlayback>` and call
97/// [`write_output`](IoPlayback::write_output) directly from `consume()`.
98pub trait IoPlayback: Send + Sync {
99    /// Write output samples for one channel from `src`.
100    ///
101    /// Returns the number of samples actually written (may be less than
102    /// `src.len()` if insufficient space is available).
103    fn write_output(&self, channel: usize, src: &[f32]) -> usize;
104
105    /// Number of playback channels.
106    fn num_output_channels(&self) -> usize;
107}
108
109// ============================================================================
110// Backward-compatible alias
111// ============================================================================
112
113/// Backward-compatible alias for code that only needs a driver.
114pub trait IoBackend: IoDriver {}
115
116impl<T: IoDriver> IoBackend for T {}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
122
123    struct TestBackend {
124        reg: AtomicU8,
125    }
126
127    impl IoDriver for TestBackend {
128        fn set_process_callback(&self, _cb: Box<dyn FnMut(&ClockTick)>) {}
129
130        fn run(&self, _: Arc<AtomicBool>) -> IoResult<()> {
131            Ok(())
132        }
133
134        fn stop(&self) -> IoResult<()> {
135            Ok(())
136        }
137
138        fn as_control(&self) -> Option<&dyn IoControl> {
139            Some(self)
140        }
141    }
142
143    impl IoControl for TestBackend {
144        fn write_data(&self, data: &[u8]) -> usize {
145            if let Some(&v) = data.first() {
146                self.reg.store(v, Ordering::Relaxed);
147            }
148            1
149        }
150    }
151
152    #[test]
153    fn test_iocontrol_write_data() {
154        let b = TestBackend {
155            reg: AtomicU8::new(0),
156        };
157        let ctrl = b.as_control().unwrap();
158        ctrl.write_data(&[42]);
159        assert_eq!(b.reg.load(Ordering::Relaxed), 42);
160    }
161
162    #[test]
163    fn test_iocontrol_default_returns_none() {
164        struct NoControl;
165        impl IoDriver for NoControl {
166            fn set_process_callback(&self, _cb: Box<dyn FnMut(&ClockTick)>) {}
167            fn run(&self, _: Arc<AtomicBool>) -> IoResult<()> {
168                Ok(())
169            }
170            fn stop(&self) -> IoResult<()> {
171                Ok(())
172            }
173        }
174        let b = NoControl;
175        assert!(b.as_control().is_none());
176    }
177}