Skip to main content

rill_graph/
graph.rs

1use rill_core::buffer::Buffer;
2use rill_core::math::Transcendental;
3use rill_core::time::{ClockSource, ClockTick, SystemClock};
4use rill_core::traits::{AudioNode, NodeVariant, PortId};
5use std::collections::VecDeque;
6
7// ============================================================================
8// Internal routing metadata
9// ============================================================================
10
11/// Describes how an audio input port routes to an audio output port within a node.
12#[derive(Debug, Clone)]
13#[allow(dead_code)]
14pub struct InternalRoute {
15    pub from: PortId,
16    pub to: PortId,
17}
18
19// ============================================================================
20// Connection classification (auto-detected by the builder)
21// ============================================================================
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ConnectionKind {
25    Direct,
26    FanOut,
27    FanIn,
28}
29
30// ============================================================================
31// Build Errors
32// ============================================================================
33
34#[derive(Debug, Clone)]
35pub enum BuildError {
36    CycleDetected,
37}
38
39// ============================================================================
40// Graph Builder
41// ============================================================================
42
43#[derive(Debug, Clone, Copy, Default)]
44pub struct GraphStats {
45    pub blocks_processed: u64,
46    pub max_process_time_ns: u64,
47    pub avg_process_time_ns: f64,
48}
49
50// ============================================================================
51// Node Storage
52// ============================================================================
53
54struct NodeEntry<T: Transcendental, const BUF_SIZE: usize> {
55    node: NodeVariant<T, BUF_SIZE>,
56}
57
58// ============================================================================
59// GraphBuilder (Mutable Construction)
60// ============================================================================
61
62/// Mutable builder for an immutable audio graph.
63pub struct GraphBuilder<T: Transcendental, const BUF_SIZE: usize> {
64    nodes: Vec<NodeEntry<T, BUF_SIZE>>,
65    audio_edges: Vec<(usize, usize, usize, usize)>,
66    feedback_edges: Vec<(usize, usize, usize, usize)>,
67}
68
69impl<T: Transcendental, const BUF_SIZE: usize> Default for GraphBuilder<T, BUF_SIZE> {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl<T: Transcendental, const BUF_SIZE: usize> GraphBuilder<T, BUF_SIZE> {
76    pub fn new() -> Self {
77        Self {
78            nodes: Vec::new(),
79            audio_edges: Vec::new(),
80            feedback_edges: Vec::new(),
81        }
82    }
83
84    pub fn add_source(&mut self, source: Box<dyn rill_core::traits::Source<T, BUF_SIZE>>) -> usize {
85        let idx = self.nodes.len();
86        self.nodes.push(NodeEntry {
87            node: NodeVariant::Source(source),
88        });
89        idx
90    }
91
92    pub fn add_processor(
93        &mut self,
94        processor: Box<dyn rill_core::traits::Processor<T, BUF_SIZE>>,
95    ) -> usize {
96        let idx = self.nodes.len();
97        self.nodes.push(NodeEntry {
98            node: NodeVariant::Processor(processor),
99        });
100        idx
101    }
102
103    pub fn add_sink(&mut self, sink: Box<dyn rill_core::traits::Sink<T, BUF_SIZE>>) -> usize {
104        let idx = self.nodes.len();
105        self.nodes.push(NodeEntry {
106            node: NodeVariant::Sink(sink),
107        });
108        idx
109    }
110
111    /// Connect audio output port `from_port` of node `from_node`
112    /// to audio input port `to_port` of node `to_node`.
113    pub fn connect_audio(
114        &mut self,
115        from_node: usize,
116        from_port: usize,
117        to_node: usize,
118        to_port: usize,
119    ) {
120        self.audio_edges
121            .push((from_node, from_port, to_node, to_port));
122    }
123
124    /// Connect a feedback output to a feedback input.
125    /// This creates a feedback path (previous output → current input).
126    pub fn connect_feedback(
127        &mut self,
128        from_node: usize,
129        from_port: usize,
130        to_node: usize,
131        to_port: usize,
132    ) {
133        self.feedback_edges
134            .push((from_node, from_port, to_node, to_port));
135    }
136
137    /// Build the immutable AudioGraph.
138    pub fn build(
139        mut self,
140        clock_source: Box<dyn ClockSource>,
141    ) -> Result<AudioGraph<T, BUF_SIZE>, BuildError> {
142        let num_nodes = self.nodes.len();
143
144        // --- adjacency for Kahn (audio edges only; feedback is not a DAG edge) ---
145        let mut in_degree = vec![0usize; num_nodes];
146        let mut out_edges: Vec<Vec<(usize, usize, usize)>> = vec![Vec::new(); num_nodes];
147
148        for &(from_n, from_p, to_n, to_p) in &self.audio_edges {
149            in_degree[to_n] += 1;
150            out_edges[from_n].push((from_p, to_n, to_p));
151        }
152
153        // --- Kahn's algorithm ---
154        let mut queue: VecDeque<usize> = in_degree
155            .iter()
156            .enumerate()
157            .filter(|(_, &d)| d == 0)
158            .map(|(i, _)| i)
159            .collect();
160
161        let mut topo = Vec::with_capacity(num_nodes);
162        let mut indeg = in_degree;
163        while let Some(idx) = queue.pop_front() {
164            topo.push(idx);
165            for &(_, to_n, _) in &out_edges[idx] {
166                indeg[to_n] -= 1;
167                if indeg[to_n] == 0 {
168                    queue.push_back(to_n);
169                }
170            }
171        }
172
173        if topo.len() != num_nodes {
174            return Err(BuildError::CycleDetected);
175        }
176
177        // --- populate Port::downstream from audio edges ---
178        for &(from_n, from_p, to_n, to_p) in &self.audio_edges {
179            if let Some(port) = self.nodes[from_n].node.output_port_mut(from_p) {
180                port.downstream.push((to_n, to_p));
181            }
182        }
183
184        // --- enable feedback buffers and populate Port::feedback_downstream ---
185        for &(from_n, from_p, to_n, to_p) in &self.feedback_edges {
186            // mark the source output port as a feedback provider
187            if let Some(port) = self.nodes[from_n].node.output_port_mut(from_p) {
188                port.feedback_buffer = Some(Buffer::new());
189                port.feedback_downstream.push((to_n, to_p));
190            }
191        }
192
193        let sample_rate = clock_source.sample_rate();
194
195        Ok(AudioGraph {
196            nodes: self.nodes,
197            topo_order: topo,
198            clock_source,
199            current_tick: ClockTick::new(0, BUF_SIZE as u32, sample_rate),
200        })
201    }
202}
203
204// ============================================================================
205// AudioGraph (Static DAG)
206// ============================================================================
207
208/// Immutable audio graph with static DAG topology.
209///
210/// Once built the graph cannot be modified. The graph owns no processing
211/// logic — it is a pure topology description. Processing is driven by
212/// port-level methods (`pre_process`, `snapshot_feedback`, `propagate`)
213/// called from external code (e.g. a real-time audio callback or an
214/// offline renderer).
215pub struct AudioGraph<T: Transcendental, const BUF_SIZE: usize> {
216    nodes: Vec<NodeEntry<T, BUF_SIZE>>,
217    topo_order: Vec<usize>,
218    #[allow(dead_code)]
219    clock_source: Box<dyn ClockSource>,
220    current_tick: ClockTick,
221}
222
223impl<T: Transcendental, const BUF_SIZE: usize> AudioGraph<T, BUF_SIZE> {
224    /// Create an empty graph with the given clock source.
225    pub fn new(clock_source: Box<dyn ClockSource>) -> Self {
226        let sample_rate = clock_source.sample_rate();
227        Self {
228            nodes: Vec::new(),
229            topo_order: Vec::new(),
230            clock_source,
231            current_tick: ClockTick::new(0, BUF_SIZE as u32, sample_rate),
232        }
233    }
234
235    /// Create an empty graph with a system clock at the given sample rate.
236    pub fn with_sample_rate(sample_rate: f32) -> Self {
237        Self::new(Box::new(SystemClock::with_sample_rate(sample_rate)))
238    }
239
240    /// Borrow an output port buffer (for inspection in tests).
241    pub fn output_buffer(&self, node_idx: usize, port_idx: usize) -> Option<&[T; BUF_SIZE]> {
242        self.nodes
243            .get(node_idx)?
244            .node
245            .output_port(port_idx)
246            .map(|p| p.buffer.as_array())
247    }
248
249    // ========================================================================
250    // Accessors
251    // ========================================================================
252
253    pub fn current_tick(&self) -> ClockTick {
254        self.current_tick
255    }
256
257    pub fn node_count(&self) -> usize {
258        self.nodes.len()
259    }
260
261    pub fn topo_order(&self) -> &[usize] {
262        &self.topo_order
263    }
264
265    /// Dispatch `SetParameter` commands to their target nodes.
266    ///
267    /// Each command is routed to the node identified by `cmd.port.node_id()`
268    /// via that node's `apply_set_parameter` method.
269    pub fn dispatch_set_parameters(
270        &mut self,
271        commands: &[rill_core::queues::signal::SetParameter],
272    ) {
273        for cmd in commands {
274            let target = cmd.port.node_id();
275            for entry in self.nodes.iter_mut() {
276                if entry.node.id() == target {
277                    let _ = entry.node.apply_set_parameter(cmd);
278                    break;
279                }
280            }
281        }
282    }
283}
284
285// ============================================================================
286// Tests
287// ============================================================================
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use rill_core::math::Transcendental;
293    use rill_core::time::ClockTick;
294    use rill_core::traits::{
295        AudioNode, NodeCategory, NodeId, NodeMetadata, NodeState, ParamValue, ParameterId, Port,
296        PortDirection, PortId, ProcessResult, Processor, Sink, Source,
297    };
298
299    // ------------------------------------------------------------------------
300    // Mock: ConstantSource — fills output with a constant value
301    // ------------------------------------------------------------------------
302    struct ConstantSource<T: Transcendental, const BUF_SIZE: usize> {
303        value: T,
304        state: NodeState<T, BUF_SIZE>,
305        outputs: Vec<Port<T, BUF_SIZE>>,
306    }
307
308    impl<T: Transcendental, const BUF_SIZE: usize> ConstantSource<T, BUF_SIZE> {
309        fn new(value: T, sample_rate: f32) -> Self {
310            let mut outputs = Vec::with_capacity(1);
311            outputs.push(Port {
312                id: PortId::audio_out(NodeId(0), 0),
313                name: "output".into(),
314                direction: PortDirection::Output,
315                action: None,
316                pending_command: None,
317                buffer: Default::default(),
318                feedback_buffer: None,
319                downstream: Vec::new(),
320                feedback_downstream: Vec::new(),
321            });
322            Self {
323                value,
324                state: NodeState::new(sample_rate),
325                outputs,
326            }
327        }
328    }
329
330    impl<T: Transcendental, const BUF_SIZE: usize> AudioNode<T, BUF_SIZE> for ConstantSource<T, BUF_SIZE> {
331        fn metadata(&self) -> NodeMetadata {
332            NodeMetadata {
333                name: "ConstantSource".into(),
334                category: NodeCategory::Source,
335                description: String::new(),
336                author: String::new(),
337                version: "1.0".into(),
338                audio_inputs: 0,
339                audio_outputs: 1,
340                control_inputs: 0,
341                control_outputs: 0,
342                clock_inputs: 0,
343                clock_outputs: 0,
344                feedback_ports: 0,
345                parameters: vec![],
346            }
347        }
348        fn init(&mut self, _sample_rate: f32) {}
349        fn reset(&mut self) {}
350        fn get_parameter(&self, _id: &ParameterId) -> Option<ParamValue> {
351            None
352        }
353        fn set_parameter(&mut self, _id: &ParameterId, _value: ParamValue) -> ProcessResult<()> {
354            Ok(())
355        }
356        fn id(&self) -> NodeId {
357            NodeId(0)
358        }
359        fn set_id(&mut self, _id: NodeId) {}
360        fn input_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
361            None
362        }
363        fn input_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
364            None
365        }
366        fn output_port(&self, index: usize) -> Option<&Port<T, BUF_SIZE>> {
367            self.outputs.get(index)
368        }
369        fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
370            self.outputs.get_mut(index)
371        }
372        fn control_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
373            None
374        }
375        fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
376            None
377        }
378        fn state(&self) -> &NodeState<T, BUF_SIZE> {
379            &self.state
380        }
381        fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
382            &mut self.state
383        }
384    }
385
386    impl<T: Transcendental, const BUF_SIZE: usize> Source<T, BUF_SIZE> for ConstantSource<T, BUF_SIZE> {
387        fn generate(
388            &mut self,
389            _clock: &ClockTick,
390            _control_inputs: &[T],
391            _clock_inputs: &[ClockTick],
392        ) -> ProcessResult<()> {
393            let out = self.outputs[0].buffer.as_mut_array();
394            for sample in out.iter_mut() {
395                *sample = self.value;
396            }
397            Ok(())
398        }
399        fn num_audio_outputs(&self) -> usize {
400            1
401        }
402    }
403
404    // ------------------------------------------------------------------------
405    // Mock: NoopProcessor — minimal processor for topology tests
406    // ------------------------------------------------------------------------
407    struct NoopProcessor<T: Transcendental, const BUF_SIZE: usize> {
408        state: NodeState<T, BUF_SIZE>,
409    }
410
411    impl<T: Transcendental, const BUF_SIZE: usize> NoopProcessor<T, BUF_SIZE> {
412        fn new(sample_rate: f32) -> Self {
413            Self {
414                state: NodeState::new(sample_rate),
415            }
416        }
417    }
418
419    impl<T: Transcendental, const BUF_SIZE: usize> AudioNode<T, BUF_SIZE> for NoopProcessor<T, BUF_SIZE> {
420        fn metadata(&self) -> NodeMetadata {
421            NodeMetadata {
422                name: "NoopProcessor".into(),
423                category: NodeCategory::Processor,
424                description: String::new(),
425                author: String::new(),
426                version: "1.0".into(),
427                audio_inputs: 0,
428                audio_outputs: 0,
429                control_inputs: 0,
430                control_outputs: 0,
431                clock_inputs: 0,
432                clock_outputs: 0,
433                feedback_ports: 0,
434                parameters: vec![],
435            }
436        }
437        fn init(&mut self, _sample_rate: f32) {}
438        fn reset(&mut self) {}
439        fn get_parameter(&self, _id: &ParameterId) -> Option<ParamValue> {
440            None
441        }
442        fn set_parameter(&mut self, _id: &ParameterId, _value: ParamValue) -> ProcessResult<()> {
443            Ok(())
444        }
445        fn id(&self) -> NodeId {
446            NodeId(1)
447        }
448        fn set_id(&mut self, _id: NodeId) {}
449        fn input_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
450            None
451        }
452        fn input_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
453            None
454        }
455        fn output_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
456            None
457        }
458        fn output_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
459            None
460        }
461        fn control_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
462            None
463        }
464        fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
465            None
466        }
467        fn state(&self) -> &NodeState<T, BUF_SIZE> {
468            &self.state
469        }
470        fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
471            &mut self.state
472        }
473    }
474
475    impl<T: Transcendental, const BUF_SIZE: usize> Processor<T, BUF_SIZE> for NoopProcessor<T, BUF_SIZE> {
476        fn process(
477            &mut self,
478            _clock: &ClockTick,
479            _audio_inputs: &[&[T; BUF_SIZE]],
480            _control_inputs: &[T],
481            _clock_inputs: &[ClockTick],
482            _feedback_inputs: &[&[T; BUF_SIZE]],
483        ) -> ProcessResult<()> {
484            Ok(())
485        }
486    }
487
488    // ------------------------------------------------------------------------
489    // Mock: NoopSink — minimal sink for topology tests
490    // ------------------------------------------------------------------------
491    struct NoopSink<T: Transcendental, const BUF_SIZE: usize> {
492        state: NodeState<T, BUF_SIZE>,
493    }
494
495    impl<T: Transcendental, const BUF_SIZE: usize> NoopSink<T, BUF_SIZE> {
496        fn new(sample_rate: f32) -> Self {
497            Self {
498                state: NodeState::new(sample_rate),
499            }
500        }
501    }
502
503    impl<T: Transcendental, const BUF_SIZE: usize> AudioNode<T, BUF_SIZE> for NoopSink<T, BUF_SIZE> {
504        fn metadata(&self) -> NodeMetadata {
505            NodeMetadata {
506                name: "NoopSink".into(),
507                category: NodeCategory::Sink,
508                description: String::new(),
509                author: String::new(),
510                version: "1.0".into(),
511                audio_inputs: 0,
512                audio_outputs: 0,
513                control_inputs: 0,
514                control_outputs: 0,
515                clock_inputs: 0,
516                clock_outputs: 0,
517                feedback_ports: 0,
518                parameters: vec![],
519            }
520        }
521        fn init(&mut self, _sample_rate: f32) {}
522        fn reset(&mut self) {}
523        fn get_parameter(&self, _id: &ParameterId) -> Option<ParamValue> {
524            None
525        }
526        fn set_parameter(&mut self, _id: &ParameterId, _value: ParamValue) -> ProcessResult<()> {
527            Ok(())
528        }
529        fn id(&self) -> NodeId {
530            NodeId(2)
531        }
532        fn set_id(&mut self, _id: NodeId) {}
533        fn input_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
534            None
535        }
536        fn input_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
537            None
538        }
539        fn output_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
540            None
541        }
542        fn output_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
543            None
544        }
545        fn control_port(&self, _index: usize) -> Option<&Port<T, BUF_SIZE>> {
546            None
547        }
548        fn control_port_mut(&mut self, _index: usize) -> Option<&mut Port<T, BUF_SIZE>> {
549            None
550        }
551        fn state(&self) -> &NodeState<T, BUF_SIZE> {
552            &self.state
553        }
554        fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
555            &mut self.state
556        }
557    }
558
559    impl<T: Transcendental, const BUF_SIZE: usize> Sink<T, BUF_SIZE> for NoopSink<T, BUF_SIZE> {
560        fn consume(
561            &mut self,
562            _clock: &ClockTick,
563            _audio_inputs: &[&[T; BUF_SIZE]],
564            _control_inputs: &[T],
565            _clock_inputs: &[ClockTick],
566            _feedback_inputs: &[&[T; BUF_SIZE]],
567        ) -> ProcessResult<()> {
568            Ok(())
569        }
570    }
571
572    // ========================================================================
573    // Tests
574    // ========================================================================
575
576    #[test]
577    fn test_graph_creation() {
578        let graph = AudioGraph::<f32, 64>::with_sample_rate(44100.0);
579        assert_eq!(graph.node_count(), 0);
580    }
581
582    #[test]
583    fn test_topo_order_correct() {
584        const BUF: usize = 64;
585        let mut builder = GraphBuilder::<f32, BUF>::new();
586
587        let src = builder.add_source(Box::new(ConstantSource::new(1.0, 44100.0)));
588        let proc = builder.add_processor(Box::new(NoopProcessor::new(44100.0)));
589        let sink = builder.add_sink(Box::new(NoopSink::new(44100.0)));
590
591        builder.connect_audio(src, 0, proc, 0);
592        builder.connect_audio(proc, 0, sink, 0);
593
594        let graph = builder
595            .build(Box::new(SystemClock::with_sample_rate(44100.0)))
596            .expect("build failed");
597
598        let order = graph.topo_order();
599        let src_pos = order.iter().position(|&i| i == src).unwrap();
600        let proc_pos = order.iter().position(|&i| i == proc).unwrap();
601        let sink_pos = order.iter().position(|&i| i == sink).unwrap();
602        assert!(src_pos < proc_pos);
603        assert!(proc_pos < sink_pos);
604    }
605
606    #[test]
607    fn test_cycle_detection() {
608        const BUF: usize = 64;
609        let mut builder = GraphBuilder::<f32, BUF>::new();
610
611        let a = builder.add_processor(Box::new(NoopProcessor::new(44100.0)));
612        let b = builder.add_processor(Box::new(NoopProcessor::new(44100.0)));
613
614        builder.connect_audio(a, 0, b, 0);
615        builder.connect_audio(b, 0, a, 0);
616
617        let result = builder.build(Box::new(SystemClock::with_sample_rate(44100.0)));
618        assert!(matches!(result, Err(BuildError::CycleDetected)));
619    }
620
621    #[test]
622    fn test_source_node_create() {
623        const BUF: usize = 64;
624        let mut builder = GraphBuilder::<f32, BUF>::new();
625        let idx = builder.add_source(Box::new(ConstantSource::new(0.5, 44100.0)));
626        let graph = builder
627            .build(Box::new(SystemClock::with_sample_rate(44100.0)))
628            .expect("build failed");
629        assert_eq!(graph.node_count(), 1);
630        assert_eq!(graph.topo_order(), &[idx]);
631    }
632}