Skip to main content

rill_graph/
graph.rs

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