Skip to main content

rust_audio_api/
graph.rs

1use crate::nodes::NodeType;
2use crate::types::{AudioUnit, empty_audio_unit};
3use crossbeam_channel::Receiver;
4use std::collections::HashMap;
5use uuid::Uuid;
6
7/// A unique identifier for a node in the audio graph.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct NodeId(pub Uuid);
10
11impl Default for NodeId {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl NodeId {
18    pub fn new() -> Self {
19        Self(Uuid::new_v4())
20    }
21}
22
23/// Generic parameter, supporting dynamic updates of node properties.
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum NodeParameter {
26    /// Gain/Volume adjustment.
27    Gain(f32),
28    /// Frequency in Hz.
29    Frequency(f32),
30    /// Boolean switch (on/off).
31    Switch(bool),
32    /// Delay time in units (blocks).
33    DelayUnits(usize),
34    /// Filter cutoff frequency.
35    Cutoff(f32),
36    /// Filter Q factor (Resonance).
37    Q(f32),
38}
39
40/// Commands sent to the audio thread to update node parameters.
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum ControlMessage {
43    /// Sets a parameter on a specific node.
44    SetParameter(NodeId, NodeParameter),
45}
46
47/// Builder for constructing the audio processing graph.
48///
49/// `GraphBuilder` allows you to add nodes and define the connections (edges)
50/// between them. It supports both standard forward connections and feedback loops.
51pub struct GraphBuilder {
52    nodes: Vec<NodeType>,
53    // edges: [source_node_index] -> [destination_node_index]
54    edges: Vec<Vec<usize>>,
55    // Feedback edges: (source_node_index, destination_node_index)
56    feedback_edges: Vec<(usize, usize)>,
57    id_to_index: HashMap<NodeId, usize>,
58}
59
60impl Default for GraphBuilder {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl GraphBuilder {
67    pub fn new() -> Self {
68        Self {
69            nodes: Vec::new(),
70            edges: Vec::new(),
71            feedback_edges: Vec::new(),
72            id_to_index: HashMap::new(),
73        }
74    }
75
76    /// Adds a node to the graph and returns its unique [`NodeId`].
77    pub fn add_node(&mut self, node: NodeType) -> NodeId {
78        let index = self.nodes.len();
79        self.nodes.push(node);
80        self.edges.push(Vec::new()); // Initialize an empty output edge list for each node
81        let id = NodeId::new();
82        self.id_to_index.insert(id, index);
83        id
84    }
85
86    /// Connects the output of the source node to the input of the destination node.
87    pub fn connect(&mut self, source: NodeId, destination: NodeId) {
88        if let (Some(&src_idx), Some(&dest_idx)) = (
89            self.id_to_index.get(&source),
90            self.id_to_index.get(&destination),
91        ) {
92            self.edges[src_idx].push(dest_idx);
93        }
94    }
95
96    /// Establishes a feedback connection (back-edge) between nodes.
97    ///
98    /// Feedback edges are excluded from topological sorting and introduce a 1-block delay.
99    /// Use this for feedback loops (e.g., in delays or recursive filters).
100    pub fn connect_feedback(&mut self, source: NodeId, destination: NodeId) {
101        if let (Some(&src_idx), Some(&dest_idx)) = (
102            self.id_to_index.get(&source),
103            self.id_to_index.get(&destination),
104        ) {
105            self.feedback_edges.push((src_idx, dest_idx));
106        }
107    }
108
109    /// Topological sorting and generation of high-performance StaticGraph with buffer reuse optimization
110    pub fn build(
111        self,
112        destination_id: NodeId,
113        msg_receiver: Receiver<ControlMessage>,
114    ) -> StaticGraph {
115        // 1. Create petgraph for topological sorting (only normal edges, no feedback edges)
116        let mut pet_graph = petgraph::graph::DiGraph::<(), ()>::new();
117        let mut pet_indices = Vec::with_capacity(self.nodes.len());
118
119        for _ in 0..self.nodes.len() {
120            pet_indices.push(pet_graph.add_node(()));
121        }
122
123        for (src, targets) in self.edges.iter().enumerate() {
124            for &dest in targets {
125                pet_graph.add_edge(pet_indices[src], pet_indices[dest], ());
126            }
127        }
128        // Feedback edges are not added to petgraph, preventing toposort failure from cycles
129
130        let sorted_pet_indices = petgraph::algo::toposort(&pet_graph, None)
131            .expect("Audio graph contains a cycle! Use connect_feedback() for feedback loops.");
132
133        let sorted_indices: Vec<usize> = sorted_pet_indices
134            .into_iter()
135            .map(|idx| idx.index())
136            .collect();
137        let final_dest_idx = self.id_to_index[&destination_id];
138
139        // 2. Buffer allocation optimization: identify the last usage of each node as an input
140        //    Note: source buffers for feedback edges must be preserved until the end (for use in the next frame)
141        let mut last_usage = vec![0; self.nodes.len()];
142        for (exec_idx, &node_idx) in sorted_indices.iter().enumerate() {
143            let mut last_used_at = exec_idx;
144            for &dest_idx in &self.edges[node_idx] {
145                let dest_exec_idx = sorted_indices.iter().position(|&x| x == dest_idx).unwrap();
146                last_used_at = last_used_at.max(dest_exec_idx);
147            }
148            if node_idx == final_dest_idx {
149                last_used_at = usize::MAX; // The buffer for the final destination must be kept until return
150            }
151            // If this node is a source for any feedback edge, its buffer cannot be reused
152            for &(fb_src, _) in &self.feedback_edges {
153                if fb_src == node_idx {
154                    last_used_at = usize::MAX;
155                }
156            }
157            last_usage[node_idx] = last_used_at;
158        }
159
160        let mut buffer_assignment = vec![0; self.nodes.len()];
161        let mut buffer_free_list = Vec::new();
162        let mut next_buffer_id = 0;
163        let mut active_nodes = Vec::new();
164
165        // Simulating execution and buffer allocation
166        for (exec_idx, &node_idx) in sorted_indices.iter().enumerate() {
167            // Acquire from Free List or allocate a new buffer
168            let assigned_buffer = if let Some(buf_id) = buffer_free_list.pop() {
169                buf_id
170            } else {
171                let id = next_buffer_id;
172                next_buffer_id += 1;
173                id
174            };
175            buffer_assignment[node_idx] = assigned_buffer;
176            active_nodes.push(node_idx);
177
178            // Check which nodes are no longer needed, releasing their buffers for reuse
179            active_nodes.retain(|&active_node| {
180                if last_usage[active_node] == exec_idx {
181                    buffer_free_list.push(buffer_assignment[active_node]);
182                    false
183                } else {
184                    true
185                }
186            });
187        }
188
189        let buffers_count = next_buffer_id;
190        let buffers = vec![empty_audio_unit(); buffers_count];
191
192        // 3. Reverse edge relationships to: [destination_node] -> Vec<[source_node]>
193        let mut inputs_map = vec![Vec::new(); self.nodes.len()];
194        for (src_idx, targets) in self.edges.iter().enumerate() {
195            for &dest_idx in targets {
196                inputs_map[dest_idx].push(src_idx);
197            }
198        }
199
200        // 4. Create reverse mapping for feedback edges: [destination_node] -> Vec<[source_node]>
201        let mut feedback_inputs_map = vec![Vec::new(); self.nodes.len()];
202        for &(src_idx, dest_idx) in &self.feedback_edges {
203            feedback_inputs_map[dest_idx].push(src_idx);
204        }
205
206        // 5. Configure "previous frame" backup buffers for feedback edge sources
207        //    prev_frame_buffers: node_idx -> Option<AudioUnit>
208        //    Only nodes marked as feedback sources need this
209        let mut feedback_source_set = vec![false; self.nodes.len()];
210        for &(src_idx, _) in &self.feedback_edges {
211            feedback_source_set[src_idx] = true;
212        }
213        let prev_frame_buffers: Vec<Option<AudioUnit>> = feedback_source_set
214            .iter()
215            .map(|&is_fb| {
216                if is_fb {
217                    Some(empty_audio_unit())
218                } else {
219                    None
220                }
221            })
222            .collect();
223
224        StaticGraph {
225            nodes: self.nodes,
226            node_buffers: buffers,
227            buffer_assignment,
228            inputs_map,
229            feedback_inputs_map,
230            prev_frame_buffers,
231            final_destination_index: final_dest_idx,
232            msg_receiver,
233            id_to_index: self.id_to_index,
234            execution_order: sorted_indices,
235        }
236    }
237}
238
239/// Fully static, zero-allocation audio runtime core
240pub struct StaticGraph {
241    nodes: Vec<NodeType>,
242    /// Shared and optimized AudioUnit buffers
243    node_buffers: Vec<AudioUnit>,
244    /// Maps node_idx to its corresponding buffer ID
245    buffer_assignment: Vec<usize>,
246    /// For each node i, `inputs_map[i]` records its inputs (normal edges)
247    inputs_map: Vec<Vec<usize>>,
248    /// For each node i, `feedback_inputs_map[i]` records its feedback inputs
249    feedback_inputs_map: Vec<Vec<usize>>,
250    /// Previous frame backup buffer for feedback source nodes
251    prev_frame_buffers: Vec<Option<AudioUnit>>,
252    final_destination_index: usize,
253    msg_receiver: Receiver<ControlMessage>,
254    id_to_index: HashMap<NodeId, usize>,
255    /// Correct processing order (determined by topological sort)
256    execution_order: Vec<usize>,
257}
258
259impl StaticGraph {
260    /// Called when CPAL or outer loop requests the next 64-frame chunk
261    #[inline(always)]
262    pub fn pull_next_unit(&mut self) -> &AudioUnit {
263        // 1. Process all non-blocking control messages (e.g., volume changes)
264        while let Ok(msg) = self.msg_receiver.try_recv() {
265            self.handle_message(msg);
266        }
267
268        // 2. Compute nodes following the topological sort order
269        // Avoids recursive calls, using a flat for-loop (Cache friendly)
270        for &i in &self.execution_order {
271            // Combine all input buffers (normal + feedback)
272            let mut combined_input = empty_audio_unit();
273            let sources = &self.inputs_map[i];
274            let feedback_sources = &self.feedback_inputs_map[i];
275
276            let has_input = if sources.is_empty() && feedback_sources.is_empty() {
277                false
278            } else {
279                // Inputs from normal edges
280                for &src_idx in sources {
281                    let src_buf_idx = self.buffer_assignment[src_idx];
282                    let src_buf = &self.node_buffers[src_buf_idx];
283                    dasp::slice::add_in_place(&mut combined_input[..], &src_buf[..]);
284                }
285                // Inputs from feedback edges (reading previous frame's buffer)
286                for &src_idx in feedback_sources {
287                    if let Some(ref prev_buf) = self.prev_frame_buffers[src_idx] {
288                        dasp::slice::add_in_place(&mut combined_input[..], &prev_buf[..]);
289                    }
290                }
291                true
292            };
293
294            // Execute node logic and write to its assigned output buffer
295            let input_ref = if has_input {
296                Some(&combined_input)
297            } else {
298                None
299            };
300            let output_buf_idx = self.buffer_assignment[i];
301            let output_ref = &mut self.node_buffers[output_buf_idx];
302
303            self.nodes[i].process(input_ref, output_ref);
304        }
305
306        // 3. Backup output of all feedback source nodes before returning
307        for (node_idx, prev_buf) in self.prev_frame_buffers.iter_mut().enumerate() {
308            if let Some(buf) = prev_buf {
309                let src_buf_idx = self.buffer_assignment[node_idx];
310                buf.copy_from_slice(&self.node_buffers[src_buf_idx]);
311            }
312        }
313
314        // 4. Return result from the destination node
315        let final_buf_idx = self.buffer_assignment[self.final_destination_index];
316        &self.node_buffers[final_buf_idx]
317    }
318
319    fn handle_message(&mut self, msg: ControlMessage) {
320        match msg {
321            ControlMessage::SetParameter(node_id, parameter) => {
322                if let Some(&index) = self.id_to_index.get(&node_id)
323                    && let Some(node) = self.nodes.get_mut(index)
324                {
325                    // Enum dispatching (static dispatch)
326                    match (node, parameter) {
327                        (NodeType::Gain(g), NodeParameter::Gain(val)) => g.set_gain(val),
328                        (NodeType::Oscillator(o), NodeParameter::Gain(val)) => o.set_gain(val),
329                        (NodeType::Mixer(m), NodeParameter::Gain(val)) => m.set_gain(val),
330                        (NodeType::Delay(d), NodeParameter::DelayUnits(val)) => {
331                            d.set_delay_units(val)
332                        }
333                        (NodeType::Filter(f), NodeParameter::Cutoff(val)) => f.set_cutoff(val),
334                        (NodeType::Filter(f), NodeParameter::Q(val)) => f.set_q(val),
335                        // Dynamic updates for Convolver are not supported (requires IR FFT recalculation)
336                        // Implement other property updates here if expanded later
337                        // (NodeType::Oscillator(o), NodeParameter::Frequency(val)) => o.set_frequency(val),
338                        _ => {}
339                    }
340                }
341            }
342        }
343    }
344}