Skip to main content

aether_core/
node.rs

1//! Node abstraction for the DSP graph.
2//!
3//! Each node is a self-contained DSP unit. The `DspNode` trait is the only
4//! interface the graph scheduler calls — keeping the hot path minimal.
5
6use crate::{
7    arena::NodeId, buffer_pool::BufferId, param::ParamBlock, state::StateBlob, BUFFER_SIZE,
8    MAX_INPUTS,
9};
10
11/// The core DSP processing trait.
12///
13/// Implement this trait to create custom audio processors (oscillators, filters,
14/// effects, etc.). The scheduler calls `process()` once per audio block in
15/// topological order.
16///
17/// # Real-Time Safety Requirements
18///
19/// Implementations **MUST** be real-time safe:
20/// - ✅ No allocation (pre-allocate in `new()`)
21/// - ✅ No locks (use lock-free data structures)
22/// - ✅ No I/O (no file/network operations)
23/// - ✅ Bounded execution time (no unbounded loops)
24///
25/// # Example
26///
27/// ```
28/// use aether_core::node::DspNode;
29/// use aether_core::param::ParamBlock;
30/// use aether_core::{BUFFER_SIZE, MAX_INPUTS};
31///
32/// /// Simple gain node
33/// struct Gain {
34///     gain: f32,
35/// }
36///
37/// impl Gain {
38///     fn new(gain: f32) -> Self {
39///         Self { gain }
40///     }
41/// }
42///
43/// impl DspNode for Gain {
44///     fn process(
45///         &mut self,
46///         inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
47///         output: &mut [f32; BUFFER_SIZE],
48///         _params: &mut ParamBlock,
49///         _sample_rate: f32,
50///     ) {
51///         // Get first input (if connected)
52///         if let Some(input) = inputs[0] {
53///             // Apply gain to each sample
54///             for (i, out) in output.iter_mut().enumerate() {
55///                 *out = input[i] * self.gain;
56///             }
57///         } else {
58///             // No input connected, output silence
59///             output.fill(0.0);
60///         }
61///     }
62///
63///     fn type_name(&self) -> &'static str {
64///         "Gain"
65///     }
66/// }
67/// ```
68///
69/// # Performance Tips
70///
71/// - Pre-allocate buffers in `new()`, not in `process()`
72/// - Use SIMD when possible (see `std::simd`)
73/// - Avoid branching in inner loops
74/// - Use `#[inline]` for hot functions
75///
76/// # See Also
77///
78/// * [`NodeRecord`] - Graph-level node storage
79/// * [`Scheduler::process_block`](crate::scheduler::Scheduler::process_block) - Calls this trait
80pub trait DspNode: Send {
81    /// Process one buffer of audio.
82    ///
83    /// Called once per audio block by the scheduler. This is the hot path -
84    /// optimize carefully and maintain real-time safety.
85    ///
86    /// # Arguments
87    ///
88    /// * `inputs` - Array of optional input buffers. `None` means no connection (silence).
89    ///   Index corresponds to input slot (0 to MAX_INPUTS-1).
90    /// * `output` - Output buffer to fill with processed audio (64 samples).
91    /// * `params` - Parameter block for this node (smoothed parameters).
92    /// * `sample_rate` - Current sample rate in Hz (e.g., 48000.0).
93    ///
94    /// # Real-Time Safety
95    ///
96    /// This function is called from the audio thread. It **MUST**:
97    /// - Complete within the buffer duration (1.33ms @ 48kHz)
98    /// - Not allocate memory
99    /// - Not acquire locks
100    /// - Not perform I/O
101    ///
102    /// # Example
103    ///
104    /// ```
105    /// use aether_core::node::DspNode;
106    /// use aether_core::param::ParamBlock;
107    /// use aether_core::{BUFFER_SIZE, MAX_INPUTS};
108    ///
109    /// struct Oscillator {
110    ///     frequency: f32,
111    ///     phase: f32,
112    /// }
113    ///
114    /// impl DspNode for Oscillator {
115    ///     fn process(
116    ///         &mut self,
117    ///         _inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
118    ///         output: &mut [f32; BUFFER_SIZE],
119    ///         _params: &mut ParamBlock,
120    ///         sample_rate: f32,
121    ///     ) {
122    ///         let phase_inc = self.frequency / sample_rate;
123    ///         
124    ///         for sample in output.iter_mut() {
125    ///             *sample = (self.phase * std::f32::consts::TAU).sin() * 0.3;
126    ///             self.phase = (self.phase + phase_inc).fract();
127    ///         }
128    ///     }
129    ///
130    ///     fn type_name(&self) -> &'static str {
131    ///         "Oscillator"
132    ///     }
133    /// }
134    /// ```
135    fn process(
136        &mut self,
137        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
138        output: &mut [f32; BUFFER_SIZE],
139        params: &mut ParamBlock,
140        sample_rate: f32,
141    );
142
143    /// Capture internal state for continuity transfer.
144    ///
145    /// Called when the graph structure changes (add/remove nodes, reconnect).
146    /// Return any internal state that should be preserved across the mutation.
147    ///
148    /// # Returns
149    ///
150    /// A `StateBlob` containing serialized state, or `StateBlob::EMPTY` if
151    /// the node has no state to preserve.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use aether_core::node::DspNode;
157    /// use aether_core::state::StateBlob;
158    /// use aether_core::param::ParamBlock;
159    /// use aether_core::{BUFFER_SIZE, MAX_INPUTS};
160    ///
161    /// struct Oscillator {
162    ///     phase: f32,
163    /// }
164    ///
165    /// impl DspNode for Oscillator {
166    ///     fn process(&mut self, _: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
167    ///                output: &mut [f32; BUFFER_SIZE], _: &mut ParamBlock, _: f32) {
168    ///         output.fill(0.0);
169    ///     }
170    ///
171    ///     fn capture_state(&self) -> StateBlob {
172    ///         // Preserve phase to avoid clicks
173    ///         StateBlob::from_value(&self.phase)
174    ///     }
175    ///
176    ///     fn restore_state(&mut self, state: StateBlob) {
177    ///         if state.len == std::mem::size_of::<f32>() {
178    ///             self.phase = state.to_value::<f32>();
179    ///         }
180    ///     }
181    ///
182    ///     fn type_name(&self) -> &'static str {
183    ///         "Oscillator"
184    ///     }
185    /// }
186    /// ```
187    ///
188    /// # See Also
189    ///
190    /// * [`restore_state`](Self::restore_state) - Restore captured state
191    fn capture_state(&self) -> StateBlob {
192        StateBlob::EMPTY
193    }
194
195    /// Restore internal state after a graph mutation.
196    ///
197    /// Called after the graph structure changes. Restore any state that was
198    /// captured by `capture_state()` to maintain audio continuity.
199    ///
200    /// # Arguments
201    ///
202    /// * `state` - State blob from `capture_state()`
203    ///
204    /// # Example
205    ///
206    /// See [`capture_state`](Self::capture_state) for a complete example.
207    fn restore_state(&mut self, _state: StateBlob) {}
208
209    /// Human-readable node type name (for serialization/UI).
210    ///
211    /// Returns a static string identifying the node type. Used for:
212    /// - Debugging and logging
213    /// - Serialization/deserialization
214    /// - UI display
215    /// - Node registry lookups
216    ///
217    /// # Returns
218    ///
219    /// A static string slice with the node type name.
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use aether_core::node::DspNode;
225    /// use aether_core::param::ParamBlock;
226    /// use aether_core::{BUFFER_SIZE, MAX_INPUTS};
227    ///
228    /// struct Reverb;
229    ///
230    /// impl DspNode for Reverb {
231    ///     fn process(&mut self, _: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
232    ///                output: &mut [f32; BUFFER_SIZE], _: &mut ParamBlock, _: f32) {
233    ///         output.fill(0.0);
234    ///     }
235    ///
236    ///     fn type_name(&self) -> &'static str {
237    ///         "Reverb" // Used for identification
238    ///     }
239    /// }
240    ///
241    /// let reverb = Reverb;
242    /// assert_eq!(reverb.type_name(), "Reverb");
243    /// ```
244    fn type_name(&self) -> &'static str;
245}
246
247/// Graph-level node record. Stored in the arena.
248///
249/// Each node in the DSP graph is represented by a `NodeRecord` stored in the
250/// arena. The record contains the DSP processor, input connections, output
251/// buffer, and parameters.
252///
253/// # Structure
254///
255/// - **processor:** The DSP implementation (boxed trait object)
256/// - **inputs:** Array of input connections (NodeId or None)
257/// - **output_buffer:** Buffer ID where this node writes output
258/// - **params:** Parameter block for smoothed parameter automation
259///
260/// # Memory Layout
261///
262/// The processor is heap-allocated (Box) at node creation time, not during
263/// audio processing. All other fields are inline in the arena slot.
264///
265/// # Example
266///
267/// ```
268/// use aether_core::node::{NodeRecord, DspNode};
269/// use aether_core::buffer_pool::{BufferPool, BufferId};
270/// use aether_core::param::ParamBlock;
271/// use aether_core::{BUFFER_SIZE, MAX_INPUTS};
272///
273/// // Custom node implementation
274/// struct Gain { gain: f32 }
275///
276/// impl DspNode for Gain {
277///     fn process(
278///         &mut self,
279///         inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
280///         output: &mut [f32; BUFFER_SIZE],
281///         _params: &mut ParamBlock,
282///         _sample_rate: f32,
283///     ) {
284///         if let Some(input) = inputs[0] {
285///             for (i, out) in output.iter_mut().enumerate() {
286///                 *out = input[i] * self.gain;
287///             }
288///         }
289///     }
290///
291///     fn type_name(&self) -> &'static str {
292///         "Gain"
293///     }
294/// }
295///
296/// // Create node record
297/// let mut pool = BufferPool::new(10);
298/// let buffer = pool.acquire().unwrap();
299/// let processor = Box::new(Gain { gain: 0.5 });
300/// let record = NodeRecord::new(processor, buffer);
301/// ```
302///
303/// # Lifecycle
304///
305/// 1. **Creation:** Allocated in arena when `AddNode` command is processed
306/// 2. **Processing:** `processor.process()` called each audio block
307/// 3. **Removal:** Dropped when `RemoveNode` command is processed
308///
309/// # See Also
310///
311/// * [`DspNode`] - The processing trait
312/// * [`Arena`](crate::arena::Arena) - Storage for node records
313/// * [`Scheduler`](crate::scheduler::Scheduler) - Processes nodes in order
314pub struct NodeRecord {
315    /// The DSP implementation (boxed, allocated at node creation time — not in RT).
316    pub processor: Box<dyn DspNode>,
317    /// Input connections: each slot holds the NodeId of the upstream node.
318    pub inputs: [Option<NodeId>; MAX_INPUTS],
319    /// The buffer this node writes its output into.
320    pub output_buffer: BufferId,
321    /// Parameter block for this node.
322    pub params: ParamBlock,
323}
324
325impl NodeRecord {
326    pub fn new(processor: Box<dyn DspNode>, output_buffer: BufferId) -> Self {
327        Self {
328            processor,
329            inputs: [None; MAX_INPUTS],
330            output_buffer,
331            params: ParamBlock::new(),
332        }
333    }
334}