Skip to main content

forge_audio/
graph.rs

1//! Audio processing DAG — directed acyclic graph with topological sort.
2//!
3//! Nodes are audio processors (decks, FX, groups, master).
4//! Edges define signal flow. Graph is evaluated in topological order.
5//! Mutation via RCU: clone → modify → atomic swap (arc-swap).
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// Node identity
11// ---------------------------------------------------------------------------
12
13/// Unique identifier for a node in the audio graph.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct NodeId(pub u32);
16
17/// What kind of node this is (for display / routing logic).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum NodeKind {
20    Deck,
21    Effect,
22    Group,
23    Master,
24    Cue,
25}
26
27// ---------------------------------------------------------------------------
28// AudioNode trait
29// ---------------------------------------------------------------------------
30
31/// Trait for any audio-processing node in the graph.
32///
33/// All processing is f64 internally. Conversion to f32 happens only at
34/// the hardware output boundary.
35pub trait AudioNode: Send + Sync {
36    /// Process `frames` of audio.
37    /// `inputs` contains one buffer per incoming edge (may be empty for source nodes).
38    /// Write output into `output`. Both are interleaved stereo f64.
39    fn process(&mut self, inputs: &[&[f64]], output: &mut [f64], frames: usize);
40
41    /// The node's unique ID.
42    fn node_id(&self) -> NodeId;
43
44    /// What kind of node this is.
45    fn kind(&self) -> NodeKind;
46
47    /// Number of output channels (default: 2 for stereo).
48    fn channels(&self) -> usize { 2 }
49}
50
51// ---------------------------------------------------------------------------
52// Graph edge
53// ---------------------------------------------------------------------------
54
55/// A directed edge: audio flows from `from` to `to`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct Edge {
58    pub from: NodeId,
59    pub to: NodeId,
60}
61
62// ---------------------------------------------------------------------------
63// AudioGraph
64// ---------------------------------------------------------------------------
65
66/// The audio processing graph. Immutable once built — mutate via clone + swap.
67pub struct AudioGraph {
68    /// Nodes keyed by ID. Order doesn't matter — eval_order defines execution.
69    nodes: HashMap<NodeId, Box<dyn AudioNode>>,
70    /// All edges in the graph.
71    edges: Vec<Edge>,
72    /// Topologically sorted execution order. Computed once on build.
73    eval_order: Vec<NodeId>,
74    /// Pre-allocated output buffers per node (frames * channels * f64).
75    buffers: HashMap<NodeId, Vec<f64>>,
76    /// Max frames this graph was allocated for.
77    max_frames: usize,
78}
79
80impl AudioGraph {
81    /// Build a new graph from nodes and edges. Runs topological sort.
82    ///
83    /// Returns Err if the graph contains a cycle that can't be broken.
84    pub fn build(
85        nodes: Vec<Box<dyn AudioNode>>,
86        edges: Vec<Edge>,
87        max_frames: usize,
88    ) -> Result<Self, GraphError> {
89        let node_map: HashMap<NodeId, Box<dyn AudioNode>> = nodes
90            .into_iter()
91            .map(|n| (n.node_id(), n))
92            .collect();
93
94        let eval_order = topological_sort(&node_map, &edges)?;
95
96        // Pre-allocate output buffers for every node.
97        let mut buffers = HashMap::new();
98        for (id, node) in &node_map {
99            buffers.insert(*id, vec![0.0f64; max_frames * node.channels()]);
100        }
101
102        Ok(Self { nodes: node_map, edges, eval_order, buffers, max_frames })
103    }
104
105    /// Process one block of audio through the entire graph.
106    ///
107    /// REAL-TIME SAFE: no allocations, no locks, no I/O.
108    /// Caller must ensure `frames <= max_frames`.
109    pub fn process(&mut self, frames: usize) {
110        debug_assert!(frames <= self.max_frames);
111
112        // Clear all buffers.
113        for buf in self.buffers.values_mut() {
114            for s in buf.iter_mut() { *s = 0.0; }
115        }
116
117        // Process nodes in topological order.
118        for &node_id in &self.eval_order {
119            // Gather input buffers from upstream nodes.
120            let input_ids: Vec<NodeId> = self.edges.iter()
121                .filter(|e| e.to == node_id)
122                .map(|e| e.from)
123                .collect();
124
125            // SAFETY: We need to borrow input buffers immutably and the
126            // current node's buffer mutably. Since they're different keys
127            // in the HashMap, we use raw pointers to avoid borrow conflicts.
128            let input_ptrs: Vec<*const [f64]> = input_ids.iter()
129                .filter_map(|id| self.buffers.get(id).map(|b| b.as_slice() as *const [f64]))
130                .collect();
131
132            let inputs: Vec<&[f64]> = unsafe {
133                input_ptrs.iter().map(|p| &**p).collect()
134            };
135
136            let output_buf = self.buffers.get_mut(&node_id).unwrap();
137            let node = self.nodes.get_mut(&node_id).unwrap();
138            node.process(&inputs, output_buf, frames);
139        }
140    }
141
142    /// Get the output buffer of a specific node (e.g., Master for final output).
143    pub fn output(&self, node_id: NodeId) -> Option<&[f64]> {
144        self.buffers.get(&node_id).map(|b| b.as_slice())
145    }
146
147    /// List all node IDs in evaluation order.
148    pub fn eval_order(&self) -> &[NodeId] {
149        &self.eval_order
150    }
151
152    /// Get the number of nodes.
153    pub fn node_count(&self) -> usize {
154        self.nodes.len()
155    }
156
157    /// Get the number of edges.
158    pub fn edge_count(&self) -> usize {
159        self.edges.len()
160    }
161
162    /// Get a reference to a node by ID.
163    pub fn node(&self, id: NodeId) -> Option<&dyn AudioNode> {
164        self.nodes.get(&id).map(|n| n.as_ref())
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Topological sort — Kahn's algorithm
170// ---------------------------------------------------------------------------
171
172/// Errors that can occur during graph construction.
173#[derive(Debug, Clone)]
174pub enum GraphError {
175    /// The graph contains a cycle that couldn't be auto-broken.
176    Cycle(Vec<NodeId>),
177    /// An edge references a node that doesn't exist.
178    MissingNode(NodeId),
179}
180
181impl std::fmt::Display for GraphError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            GraphError::Cycle(ids) => write!(f, "Graph cycle detected involving {:?}", ids),
185            GraphError::MissingNode(id) => write!(f, "Edge references missing node {:?}", id),
186        }
187    }
188}
189
190impl std::error::Error for GraphError {}
191
192/// Kahn's algorithm for topological sorting.
193///
194/// Returns the node IDs in an order such that for every edge U→V,
195/// U appears before V.
196fn topological_sort(
197    nodes: &HashMap<NodeId, Box<dyn AudioNode>>,
198    edges: &[Edge],
199) -> Result<Vec<NodeId>, GraphError> {
200    // Validate edges reference existing nodes.
201    for edge in edges {
202        if !nodes.contains_key(&edge.from) {
203            return Err(GraphError::MissingNode(edge.from));
204        }
205        if !nodes.contains_key(&edge.to) {
206            return Err(GraphError::MissingNode(edge.to));
207        }
208    }
209
210    // Build in-degree map and adjacency list.
211    let mut in_degree: HashMap<NodeId, usize> = nodes.keys().map(|&id| (id, 0)).collect();
212    let mut adjacency: HashMap<NodeId, Vec<NodeId>> = nodes.keys().map(|&id| (id, Vec::new())).collect();
213
214    for edge in edges {
215        *in_degree.entry(edge.to).or_insert(0) += 1;
216        adjacency.entry(edge.from).or_default().push(edge.to);
217    }
218
219    // Seed the queue with zero-in-degree nodes (sources).
220    let mut queue: VecDeque<NodeId> = in_degree.iter()
221        .filter(|(_, &deg)| deg == 0)
222        .map(|(&id, _)| id)
223        .collect();
224
225    let mut sorted = Vec::with_capacity(nodes.len());
226
227    while let Some(node_id) = queue.pop_front() {
228        sorted.push(node_id);
229        if let Some(neighbors) = adjacency.get(&node_id) {
230            for &neighbor in neighbors {
231                if let Some(deg) = in_degree.get_mut(&neighbor) {
232                    *deg -= 1;
233                    if *deg == 0 {
234                        queue.push_back(neighbor);
235                    }
236                }
237            }
238        }
239    }
240
241    if sorted.len() != nodes.len() {
242        // Cycle detected — collect remaining nodes.
243        let remaining: Vec<NodeId> = nodes.keys()
244            .filter(|id| !sorted.contains(id))
245            .copied()
246            .collect();
247        return Err(GraphError::Cycle(remaining));
248    }
249
250    Ok(sorted)
251}
252
253// ---------------------------------------------------------------------------
254// Tests
255// ---------------------------------------------------------------------------
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    /// Minimal test node that passes input through to output.
262    struct PassthroughNode {
263        id: NodeId,
264        kind: NodeKind,
265    }
266
267    impl AudioNode for PassthroughNode {
268        fn process(&mut self, inputs: &[&[f64]], output: &mut [f64], frames: usize) {
269            // Sum all inputs.
270            let len = frames * self.channels();
271            for input in inputs {
272                for i in 0..len.min(input.len()).min(output.len()) {
273                    output[i] += input[i];
274                }
275            }
276        }
277        fn node_id(&self) -> NodeId { self.id }
278        fn kind(&self) -> NodeKind { self.kind }
279    }
280
281    /// Source node that generates a constant value.
282    struct ConstNode {
283        id: NodeId,
284        value: f64,
285    }
286
287    impl AudioNode for ConstNode {
288        fn process(&mut self, _inputs: &[&[f64]], output: &mut [f64], frames: usize) {
289            let len = frames * self.channels();
290            for i in 0..len.min(output.len()) {
291                output[i] = self.value;
292            }
293        }
294        fn node_id(&self) -> NodeId { self.id }
295        fn kind(&self) -> NodeKind { NodeKind::Deck }
296    }
297
298    fn make_node(id: u32, kind: NodeKind) -> Box<dyn AudioNode> {
299        Box::new(PassthroughNode { id: NodeId(id), kind })
300    }
301
302    fn make_const(id: u32, value: f64) -> Box<dyn AudioNode> {
303        Box::new(ConstNode { id: NodeId(id), value })
304    }
305
306    #[test]
307    fn empty_graph() {
308        let g = AudioGraph::build(vec![], vec![], 64).unwrap();
309        assert_eq!(g.node_count(), 0);
310        assert_eq!(g.edge_count(), 0);
311    }
312
313    #[test]
314    fn single_node() {
315        let nodes: Vec<Box<dyn AudioNode>> = vec![make_const(1, 0.5)];
316        let mut g = AudioGraph::build(nodes, vec![], 64).unwrap();
317        g.process(64);
318        let out = g.output(NodeId(1)).unwrap();
319        assert!((out[0] - 0.5).abs() < 1e-10);
320    }
321
322    #[test]
323    fn linear_chain() {
324        // Deck(1) → FX(2) → Master(3)
325        let nodes: Vec<Box<dyn AudioNode>> = vec![
326            make_const(1, 1.0),
327            make_node(2, NodeKind::Effect),
328            make_node(3, NodeKind::Master),
329        ];
330        let edges = vec![
331            Edge { from: NodeId(1), to: NodeId(2) },
332            Edge { from: NodeId(2), to: NodeId(3) },
333        ];
334        let mut g = AudioGraph::build(nodes, edges, 64).unwrap();
335        assert_eq!(g.eval_order().len(), 3);
336
337        // Deck must come before FX, FX before Master.
338        let order = g.eval_order();
339        let pos = |id: u32| order.iter().position(|n| n.0 == id).unwrap();
340        assert!(pos(1) < pos(2));
341        assert!(pos(2) < pos(3));
342
343        g.process(64);
344        let out = g.output(NodeId(3)).unwrap();
345        // Const(1.0) passes through passthrough nodes unchanged.
346        assert!((out[0] - 1.0).abs() < 1e-10);
347    }
348
349    #[test]
350    fn diamond_graph() {
351        // Deck(1) → FX(2) → Master(4)
352        //         ↘ FX(3) ↗
353        let nodes: Vec<Box<dyn AudioNode>> = vec![
354            make_const(1, 0.5),
355            make_node(2, NodeKind::Effect),
356            make_node(3, NodeKind::Effect),
357            make_node(4, NodeKind::Master),
358        ];
359        let edges = vec![
360            Edge { from: NodeId(1), to: NodeId(2) },
361            Edge { from: NodeId(1), to: NodeId(3) },
362            Edge { from: NodeId(2), to: NodeId(4) },
363            Edge { from: NodeId(3), to: NodeId(4) },
364        ];
365        let mut g = AudioGraph::build(nodes, edges, 64).unwrap();
366        g.process(64);
367        let out = g.output(NodeId(4)).unwrap();
368        // Master receives 0.5 from each FX path = 1.0.
369        assert!((out[0] - 1.0).abs() < 1e-10);
370    }
371
372    #[test]
373    fn four_decks_to_master() {
374        // Deck A(1), B(2), C(3), D(4) → Group L(5) / Group R(6) → Master(7)
375        let nodes: Vec<Box<dyn AudioNode>> = vec![
376            make_const(1, 0.25), // Deck A
377            make_const(2, 0.25), // Deck B
378            make_const(3, 0.25), // Deck C
379            make_const(4, 0.25), // Deck D
380            make_node(5, NodeKind::Group),  // Group L
381            make_node(6, NodeKind::Group),  // Group R
382            make_node(7, NodeKind::Master), // Master
383        ];
384        let edges = vec![
385            Edge { from: NodeId(1), to: NodeId(5) }, // A → L
386            Edge { from: NodeId(3), to: NodeId(5) }, // C → L
387            Edge { from: NodeId(2), to: NodeId(6) }, // B → R
388            Edge { from: NodeId(4), to: NodeId(6) }, // D → R
389            Edge { from: NodeId(5), to: NodeId(7) }, // L → Master
390            Edge { from: NodeId(6), to: NodeId(7) }, // R → Master
391        ];
392        let mut g = AudioGraph::build(nodes, edges, 64).unwrap();
393        g.process(64);
394        let out = g.output(NodeId(7)).unwrap();
395        // All 4 decks at 0.25 sum to 1.0 at master.
396        assert!((out[0] - 1.0).abs() < 1e-10);
397    }
398
399    #[test]
400    fn cycle_detected() {
401        // A → B → A (cycle)
402        let nodes: Vec<Box<dyn AudioNode>> = vec![
403            make_node(1, NodeKind::Effect),
404            make_node(2, NodeKind::Effect),
405        ];
406        let edges = vec![
407            Edge { from: NodeId(1), to: NodeId(2) },
408            Edge { from: NodeId(2), to: NodeId(1) },
409        ];
410        let result = AudioGraph::build(nodes, edges, 64);
411        assert!(result.is_err());
412        if let Err(GraphError::Cycle(ids)) = result {
413            assert_eq!(ids.len(), 2);
414        }
415    }
416
417    #[test]
418    fn missing_node_in_edge() {
419        let nodes: Vec<Box<dyn AudioNode>> = vec![make_node(1, NodeKind::Deck)];
420        let edges = vec![Edge { from: NodeId(1), to: NodeId(99) }];
421        let result = AudioGraph::build(nodes, edges, 64);
422        assert!(matches!(result, Err(GraphError::MissingNode(NodeId(99)))));
423    }
424
425    #[test]
426    fn eval_order_respects_topology() {
427        // 1 → 3, 2 → 3, 3 → 4
428        let nodes: Vec<Box<dyn AudioNode>> = vec![
429            make_node(1, NodeKind::Deck),
430            make_node(2, NodeKind::Deck),
431            make_node(3, NodeKind::Group),
432            make_node(4, NodeKind::Master),
433        ];
434        let edges = vec![
435            Edge { from: NodeId(1), to: NodeId(3) },
436            Edge { from: NodeId(2), to: NodeId(3) },
437            Edge { from: NodeId(3), to: NodeId(4) },
438        ];
439        let g = AudioGraph::build(nodes, edges, 64).unwrap();
440        let order = g.eval_order();
441        let pos = |id: u32| order.iter().position(|n| n.0 == id).unwrap();
442        assert!(pos(1) < pos(3));
443        assert!(pos(2) < pos(3));
444        assert!(pos(3) < pos(4));
445    }
446}