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}