Skip to main content

audiograph/
lib.rs

1//! Audiograph - a realtime audio processing graph library for Rust.
2//!
3//! Audiograph provides abstractions for audio processors, audio buffers and channel routing,
4//! and enables the construction and management of directed signal processing graphs.
5//!
6//! # Building blocks
7//!
8//! [`DspGraph`] represents the main graph structure. It contains audio processing nodes and typed edges
9//! that describe the signal flow between nodes. The major building blocks for constructing and using
10//! a [`DspGraph`] are:
11//!
12//! - [`Processor`]: Trait representing an audio processing unit. Processors are the core of a graph node.
13//!   Conceptually, a graph node consists of a processor and its associated output buffer.
14//! - [`AudioBuffer`]: Trait representing a buffer of audio samples organized by channels. Multiple
15//!   implementations are provided, including [`MultiChannelBuffer`] (owning) and
16//!   [`MultiChannelBufferView`] (non-owning).
17//! - [`ChannelSelection`]: Struct describing the active channels of a connection between nodes. Graph edges
18//!   can carry an optional channel selection to indicate which channels of a node's output buffer are
19//!   processed by connected successor nodes.
20//! - [`ProcessingContext`]: Struct providing context for audio processing, including input and output buffer
21//!   references, the channel selection, and the number of frames to process.
22//!
23//! # Graph structure
24//!
25//! A [`DspGraph`] is a directed graph where nodes represent audio processors and edges represent
26//! the signal flow between processors. It is possible for a node to have multiple incoming edges,
27//! in which case the inputs are summed before being passed to the processor. Similarly, a node can have
28//! multiple outgoing edges, allowing its output to be routed to multiple successor nodes.
29//!
30//! Edges of the graph are typed edges carrying additional information about the connection between nodes,
31//! allowing complex routing scenarios such as channel selection and remapping.
32//!
33//! A graph can be designed and modified using the provided API, most notably using the following construction
34//! methods:
35//! - [`DspGraph::add_processor`]: Adds a new processor node to the graph along with its associated output buffer.
36//! - [`DspGraph::connect`]: Connects two nodes in the graph with an edge, optionally specifying a channel selection.
37//!
38//! Additional methods are provided for more advanced operations, such as enabling and
39//! disabling edges and removing connections.
40//!
41//! For channel rewiring support, see [`RewireDspGraph`], which extends the base graph with
42//! the ability to remap channels on existing connections.
43//!
44//! Graph nodes and edges are identified using the `NodeIndex` and `EdgeIndex` types from the `petgraph` crate,
45//! which provides the underlying directed graph implementation.
46//!
47//! Input and output nodes: These are special nodes that serve as the entry and exit points of the graph.
48//! Input and output nodes do not process any audio data and do not have dedicated buffers within the graph
49//! structure. The corresponding buffers are passed to the [`DspGraph::process`] method instead. See also [`GraphNode`]
50//! and [`DspGraph::connect`].
51//!
52//! # Realtime safety
53//!
54//! Realtime safety is guaranteed for all [`BasicDspGraph`] operations, including processing and modifying
55//! the graph structure. [`RewireDspGraph`] additionally supports channel rewiring, which is not
56//! realtime-safe and is documented as such.
57//!
58//! Adding a node to the graph requires the node's audio buffer to be allocated. It is the caller's
59//! responsibility to ensure this allocation is performed safely and not on the high-priority audio thread.
60//!
61//! Graph-internal memory allocations are performed upfront during graph initialization. Adding nodes
62//! or edges within the specified graph capacity limits does not allocate.
63//!
64
65pub mod buffer;
66pub mod channel;
67pub mod processor;
68pub mod sample;
69
70pub use crate::buffer::*;
71pub use crate::channel::*;
72pub use crate::processor::*;
73pub use crate::sample::*;
74
75#[doc(no_inline)]
76pub use petgraph::graph::{EdgeIndex, NodeIndex};
77
78use petgraph::Direction;
79use petgraph::stable_graph::StableDiGraph;
80use petgraph::visit::{DfsPostOrder, EdgeRef, Reversed};
81
82use std::collections::HashMap;
83
84pub type AudioGraphError = &'static str;
85
86/// Identifier for a graph node
87///
88/// Input and Output nodes are special nodes representing the entry and exit points of the graph.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum GraphNode {
91    Input,
92    Output,
93    Node(NodeIndex),
94}
95
96impl From<NodeIndex> for GraphNode {
97    fn from(value: NodeIndex) -> Self {
98        GraphNode::Node(value)
99    }
100}
101
102/// Represents an audio processing node
103struct ProcessorNode<T: Sample> {
104    processor: Box<dyn Processor<T> + Send>,
105}
106
107impl<T: Sample> ProcessorNode<T> {
108    pub fn new(processor: impl Processor<T> + Send + 'static) -> Self {
109        Self {
110            processor: Box::new(processor),
111        }
112    }
113
114    pub fn process(&mut self, context: &mut ProcessingContext<T>) {
115        self.processor.process(context);
116    }
117}
118
119#[derive(Clone)]
120#[doc(hidden)]
121pub struct EdgeData {
122    pub channel_selection: Option<ChannelSelection>,
123    pub enabled: bool,
124}
125
126/// Common interface for graph edge types
127#[doc(hidden)]
128pub trait GraphEdge: Clone {
129    const SUPPORTS_REWIRE: bool; // for compile-time differentiation between edge types
130
131    fn new(channel_selection: Option<ChannelSelection>) -> Self;
132    fn data(&self) -> &EdgeData;
133    fn data_mut(&mut self) -> &mut EdgeData;
134
135    fn get_rewire(&self) -> Option<&HashMap<usize, usize>> {
136        None
137    }
138}
139
140#[derive(Clone)]
141#[doc(hidden)]
142pub struct ProcessorChannel {
143    pub data: EdgeData,
144}
145
146impl GraphEdge for ProcessorChannel {
147    const SUPPORTS_REWIRE: bool = false;
148
149    fn new(channel_selection: Option<ChannelSelection>) -> Self {
150        Self {
151            data: EdgeData {
152                channel_selection,
153                enabled: true,
154            },
155        }
156    }
157
158    fn data(&self) -> &EdgeData {
159        &self.data
160    }
161
162    fn data_mut(&mut self) -> &mut EdgeData {
163        &mut self.data
164    }
165}
166
167#[derive(Clone)]
168#[doc(hidden)]
169pub struct RewireProcessorChannel {
170    pub data: EdgeData,
171    pub rewire: Option<HashMap<usize, usize>>,
172}
173
174impl GraphEdge for RewireProcessorChannel {
175    const SUPPORTS_REWIRE: bool = true;
176
177    fn new(channel_selection: Option<ChannelSelection>) -> Self {
178        Self {
179            data: EdgeData {
180                channel_selection,
181                enabled: true,
182            },
183            rewire: None,
184        }
185    }
186
187    fn data(&self) -> &EdgeData {
188        &self.data
189    }
190
191    fn data_mut(&mut self) -> &mut EdgeData {
192        &mut self.data
193    }
194
195    fn get_rewire(&self) -> Option<&HashMap<usize, usize>> {
196        self.rewire.as_ref()
197    }
198}
199
200type GraphVisitMap<T, E> = <StableDiGraph<ProcessorNode<T>, E> as petgraph::visit::Visitable>::Map;
201
202/// Directed graph structure for audio processing
203///
204/// Consists of processor nodes and typed edges that describe the signal flow.
205/// When constructing a [`DspGraph`], a capacity needs to be provided to preallocate internal data
206/// structures. This ensures realtime safety when adding nodes and edges at runtime.
207///
208/// Two graph variants are provided by the library, differentiated by their edge types:
209/// - [`BasicDspGraph`]: lightweight graph without rewiring support
210/// - [`RewireDspGraph`]: graph with channel rewiring capabilities
211///
212/// Most users should use the type aliases rather than `DspGraph` directly:
213///
214/// Nodes and edges can be added using [`DspGraph::add_processor`] and [`DspGraph::connect`].
215/// Connections can also be dynamically enabled, disabled or updated.
216///
217/// The graph owns the processing nodes as well as their corresponding output buffers. Conversely,
218/// the input and output buffers that the graph operates on must be managed outside of the graph structure.
219/// To run the audio processing graph, use the [`DspGraph::process`] method, which takes the input
220/// and output buffers as arguments.
221#[allow(private_bounds)]
222pub struct DspGraph<T: Sample, Edge: GraphEdge> {
223    graph: StableDiGraph<ProcessorNode<T>, Edge>,
224    topo_order: Vec<NodeIndex>, // Pre-allocated processing order vector
225    buffers: Vec<Option<MultiChannelBuffer<T>>>,
226    input_node: NodeIndex,
227    output_node: NodeIndex,
228    summing_buffer: MultiChannelBuffer<T>,
229    dfs_visitor: DfsPostOrder<NodeIndex, GraphVisitMap<T, Edge>>,
230    topo_dirty: bool,
231}
232
233#[allow(private_bounds)]
234impl<T: Sample, Edge: GraphEdge> DspGraph<T, Edge> {
235    /// Creates a new [`DspGraph`] with preallocated capacity for internal data structures
236    ///
237    /// - `num_channels`: Maximum number of channels a node will process. Needs to be known upfront for
238    ///   summing operations.
239    /// - `frame_size`: Maximum number of frames for block-wise processing.
240    /// - `max_num_edges`: Graph capacity used for preallocation. This value also bounds the maximum
241    ///   number of nodes that can be added. If `None`, defaults to 64.
242    pub fn new(num_channels: usize, frame_size: FrameSize, max_num_edges: Option<usize>) -> Self {
243        let max_num_edges = max_num_edges.unwrap_or(64);
244
245        let mut buffers = Vec::with_capacity(max_num_edges);
246        for _ in 0..max_num_edges {
247            buffers.push(None);
248        }
249
250        let mut graph = DspGraph {
251            graph: StableDiGraph::with_capacity(max_num_edges, max_num_edges),
252            topo_order: Vec::with_capacity(max_num_edges),
253            buffers,
254            input_node: NodeIndex::end(),
255            output_node: NodeIndex::end(),
256            summing_buffer: MultiChannelBuffer::new(num_channels, frame_size),
257            dfs_visitor: DfsPostOrder::default(),
258            topo_dirty: true,
259        };
260
261        graph.input_node = graph
262            .add_processor(NoOp, MultiChannelBuffer::new(0, FrameSize(0)))
263            .unwrap();
264        graph.output_node = graph
265            .add_processor(NoOp, MultiChannelBuffer::new(0, FrameSize(0)))
266            .unwrap();
267
268        graph.dfs_visitor = DfsPostOrder::empty(&graph.graph);
269
270        graph
271    }
272
273    /// Adds a processor node to the graph along with its associated output buffer
274    ///
275    /// Returns the `NodeIndex` of the newly added processor node (or an error if something went wrong).
276    /// The node index can be used to reference the node when adding an edge to it using [`DspGraph::connect`].
277    pub fn add_processor<P: Processor<T> + Send + 'static>(
278        &mut self,
279        processor: P,
280        output_buffer: MultiChannelBuffer<T>,
281    ) -> Result<NodeIndex, AudioGraphError> {
282        if self.graph.node_count() >= self.graph.capacity().0 {
283            return Err("Graph node capacity exceeded");
284        }
285
286        if output_buffer.num_frames().0 > self.summing_buffer.num_frames().0 {
287            return Err("Output buffer frame size exceeds graph capacity");
288        }
289
290        if output_buffer.num_channels() > self.summing_buffer.num_channels() {
291            return Err("Output buffer channel count exceeds graph capacity");
292        }
293
294        let node_index = self.graph.add_node(ProcessorNode::new(processor));
295        let buffer_index = node_index.index();
296
297        if buffer_index >= self.buffers.len() {
298            return Err("Buffer capacity exceeded");
299        }
300
301        self.buffers[buffer_index] = Some(output_buffer);
302
303        Ok(node_index)
304    }
305
306    /// Connects two nodes in the graph with an edge
307    ///
308    /// The direction of the edge is so that the `to` node will access the output buffer of the `from` node
309    /// as its input buffer during processing. These nodes can be either processor nodes added via
310    /// [`DspGraph::add_processor`], or the input or output nodes of the entire graph (see [`GraphNode`]).
311    ///
312    /// Optionally, a [`ChannelSelection`] can be provided to specify which channels of the `from` node's output buffer
313    /// should be processed by the `to` node. If no selection is provided, all channels are connected by default.
314    ///
315    /// Returns the `EdgeIndex` of the newly created edge (or an error if something went wrong).
316    /// The edge index can be used to reference the edge for further operations such as enabling/disabling
317    /// the connection.
318    pub fn connect(
319        &mut self,
320        from: GraphNode,
321        to: GraphNode,
322        channel_selection: Option<ChannelSelection>,
323    ) -> Result<EdgeIndex, AudioGraphError> {
324        if self.graph.edge_count() >= self.graph.capacity().1 {
325            return Err("Graph edge capacity exceeded");
326        }
327
328        // invalid combinations
329        if let (GraphNode::Input, GraphNode::Input)
330        | (GraphNode::Node(_), GraphNode::Input)
331        | (GraphNode::Output, _) = (from, to)
332        {
333            return Err("Invalid connection");
334        }
335
336        let from_index = match from {
337            GraphNode::Input => self.input_node,
338            GraphNode::Output => self.output_node,
339            GraphNode::Node(idx) => idx,
340        };
341        let to_index = match to {
342            GraphNode::Input => self.input_node,
343            GraphNode::Output => self.output_node,
344            GraphNode::Node(idx) => idx,
345        };
346
347        if self.graph.node_weight(from_index).is_none() {
348            return Err("Source node does not exist");
349        }
350        if self.graph.node_weight(to_index).is_none() {
351            return Err("Destination node does not exist");
352        }
353
354        let channel_selection = match channel_selection {
355            None => None,
356            Some(selection) => {
357                let mut channel_selection = selection;
358
359                if from_index != self.input_node {
360                    let source_buffer_channels = self.buffers[from_index.index()]
361                        .as_ref()
362                        .unwrap()
363                        .num_channels();
364                    channel_selection.clamp(source_buffer_channels);
365                }
366
367                if to_index != self.output_node {
368                    let destination_buffer_channels = self.buffers[to_index.index()]
369                        .as_ref()
370                        .unwrap()
371                        .num_channels();
372                    channel_selection.clamp(destination_buffer_channels);
373                }
374                Some(channel_selection)
375            }
376        };
377
378        let edge = Edge::new(channel_selection);
379
380        let edge_index = match self.graph.find_edge(from_index, to_index) {
381            Some(existing_edge_index) => {
382                *self.graph.edge_weight_mut(existing_edge_index).unwrap() = edge;
383                existing_edge_index
384            }
385            None => self.graph.add_edge(from_index, to_index, edge),
386        };
387
388        self.topo_dirty = true;
389        Ok(edge_index)
390    }
391
392    /// Removes an existing connection from the graph
393    ///
394    /// **NOT realtime safe for the [`RewireDspGraph`] variant**
395    ///
396    /// Alternative: Use [`DspGraph::disable_connection`] to temporarily disable a connection.
397    pub fn remove_connection(&mut self, edge: EdgeIndex) -> Result<(), AudioGraphError> {
398        self.graph.remove_edge(edge).ok_or("Edge not found")?;
399        self.topo_dirty = true;
400        Ok(())
401    }
402
403    /// Enables an existing connection in the graph
404    pub fn enable_connection(&mut self, edge: EdgeIndex) -> Result<(), AudioGraphError> {
405        if let Some(edge_weight) = self.graph.edge_weight_mut(edge) {
406            edge_weight.data_mut().enabled = true;
407            Ok(())
408        } else {
409            Err("Edge not found")
410        }
411    }
412
413    /// Disables an existing connection in the graph
414    pub fn disable_connection(&mut self, edge: EdgeIndex) -> Result<(), AudioGraphError> {
415        if let Some(edge_weight) = self.graph.edge_weight_mut(edge) {
416            edge_weight.data_mut().enabled = false;
417            Ok(())
418        } else {
419            Err("Edge not found")
420        }
421    }
422
423    fn ensure_topo_order_updated(&mut self) {
424        if self.topo_dirty {
425            self.topo_order.clear(); // clear but keep preallocated size
426
427            self.dfs_visitor.reset(&self.graph);
428            let reversed = Reversed(&self.graph);
429            for start_node in self
430                .graph
431                .neighbors_directed(self.output_node, Direction::Incoming)
432            {
433                self.dfs_visitor.move_to(start_node);
434                while let Some(node) = self.dfs_visitor.next(&reversed) {
435                    self.topo_order.push(node);
436                }
437            }
438
439            self.topo_dirty = false;
440        }
441    }
442
443    /// Processes audio data through the graph
444    pub fn process(
445        &mut self,
446        input: &dyn AudioBuffer<T>,
447        output: &mut dyn AudioBuffer<T>,
448        num_frames: FrameSize,
449    ) {
450        self.ensure_topo_order_updated(); // update node order if necessary
451
452        output.clear();
453
454        for &node_index in &self.topo_order {
455            if node_index == self.output_node {
456                continue;
457            }
458
459            let output_buffer_index = node_index.index();
460
461            let mut incoming_edges = self
462                .graph
463                .edges_directed(node_index, Direction::Incoming)
464                .filter(|edge| edge.weight().data().enabled);
465
466            let num_incoming_edges = incoming_edges.clone().count();
467
468            // If there are multiple inputs, sum them
469            if num_incoming_edges > 1 {
470                self.summing_buffer.clear();
471                let mut channel_selection: Option<ChannelSelection> = None;
472
473                for edge in incoming_edges {
474                    let input_node = edge.source();
475                    let input_buffer: &dyn AudioBuffer<T> = if input_node == self.input_node {
476                        input
477                    } else {
478                        let input_buffer_index = input_node.index();
479                        self.buffers[input_buffer_index].as_ref().unwrap()
480                    };
481
482                    let edge_selection = edge.weight().data().channel_selection.clone();
483
484                    if Edge::SUPPORTS_REWIRE {
485                        if let Some(rewire) = edge.weight().get_rewire() {
486                            let rewired_buffer_view = RewiredBufferView {
487                                buffer: input_buffer,
488                                rewire,
489                            };
490                            self.summing_buffer
491                                .add(&rewired_buffer_view, &edge_selection);
492                        } else {
493                            self.summing_buffer.add(input_buffer, &edge_selection);
494                        }
495                    } else {
496                        self.summing_buffer.add(input_buffer, &edge_selection);
497                    }
498
499                    if let Some(edge_selection) = &edge_selection {
500                        if let Some(existing_selection) = &mut channel_selection {
501                            existing_selection.combine(edge_selection);
502                        } else {
503                            channel_selection = Some(edge_selection.clone());
504                        }
505                    }
506                }
507
508                let output_buffer: &mut dyn AudioBuffer<T> =
509                    self.buffers[output_buffer_index].as_mut().unwrap();
510                let processor_node = self.graph.node_weight_mut(node_index).unwrap();
511
512                let mut context = ProcessingContext::create_unchecked(
513                    &self.summing_buffer,
514                    output_buffer,
515                    channel_selection,
516                    num_frames,
517                );
518
519                processor_node.process(&mut context);
520            } else if num_incoming_edges == 1 {
521                let (input_node, channel_selection, rewire_map) = {
522                    let edge = incoming_edges.next().unwrap();
523                    let rewire = if Edge::SUPPORTS_REWIRE {
524                        edge.weight().get_rewire().cloned()
525                    } else {
526                        None
527                    };
528                    (
529                        edge.source(),
530                        edge.weight().data().channel_selection.clone(),
531                        rewire,
532                    )
533                };
534
535                let (input_buffer, output_buffer): (&dyn AudioBuffer<T>, &mut dyn AudioBuffer<T>) =
536                    if input_node == self.input_node {
537                        let output_buffer = self.buffers[output_buffer_index].as_mut().unwrap();
538                        (input, output_buffer)
539                    } else {
540                        let input_buffer_index = input_node.index();
541                        let (low, high) = self.buffers.split_at_mut(output_buffer_index);
542                        (
543                            low[input_buffer_index].as_ref().unwrap(),
544                            high[0].as_mut().unwrap(),
545                        )
546                    };
547
548                let processor_node = self.graph.node_weight_mut(node_index).unwrap();
549
550                if Edge::SUPPORTS_REWIRE {
551                    if let Some(rewire) = &rewire_map {
552                        let rewired_buffer_view = RewiredBufferView {
553                            buffer: input_buffer,
554                            rewire,
555                        };
556                        let mut context = ProcessingContext::create_unchecked(
557                            &rewired_buffer_view,
558                            output_buffer,
559                            channel_selection,
560                            num_frames,
561                        );
562                        processor_node.process(&mut context);
563                    } else {
564                        let mut context = ProcessingContext::create_unchecked(
565                            input_buffer,
566                            output_buffer,
567                            channel_selection,
568                            num_frames,
569                        );
570                        processor_node.process(&mut context);
571                    }
572                } else {
573                    let mut context = ProcessingContext::create_unchecked(
574                        input_buffer,
575                        output_buffer,
576                        channel_selection,
577                        num_frames,
578                    );
579                    processor_node.process(&mut context);
580                }
581            }
582        }
583
584        for edge in self
585            .graph
586            .edges_directed(self.output_node, Direction::Incoming)
587            .filter(|e| e.weight().data().enabled)
588        {
589            let node = edge.source();
590            let node_buffer: &dyn AudioBuffer<T> = if node == self.input_node {
591                input
592            } else {
593                let input_buffer_index = node.index();
594                self.buffers[input_buffer_index].as_ref().unwrap()
595            };
596            output.add(node_buffer, &edge.weight().data().channel_selection.clone());
597        }
598    }
599}
600
601#[allow(private_bounds)]
602impl<T: Sample> RewireDspGraph<T> {
603    /// Rewires an existing connection in the graph to use a different channel mapping
604    /// between the edge's source and destination nodes.
605    ///
606    /// **NOT realtime safe**
607    ///
608    /// The `rewire_mapping` parameter is a slice of tuples where each tuple defines a channel mapping
609    /// in the form `(source_channel, destination_channel)`.
610    ///
611    /// Note: Mapping multiple source channels to the same destination channel returns an error.
612    ///
613    /// # Example
614    /// ```rust
615    /// use audiograph::{FrameSize, GraphNode, MultiChannelBuffer, NoOp, RewireDspGraph};
616    ///
617    /// let frame_size = FrameSize(1024);
618    ///
619    /// let mut dsp_graph = RewireDspGraph::<f32>::new(4, frame_size, None);
620    ///
621    /// let node1 = dsp_graph
622    ///     .add_processor(
623    ///         NoOp {},
624    ///         MultiChannelBuffer::new(4, frame_size), // 4 output channels
625    ///     )
626    ///     .unwrap();
627    ///
628    /// let node2 = dsp_graph
629    ///     .add_processor(
630    ///         NoOp {},
631    ///         MultiChannelBuffer::new(4, frame_size), // 4 output channels
632    ///     )
633    ///     .unwrap();
634    ///
635    /// // Connect nodes with default channel selection (i.e., all channels in order)
636    /// dsp_graph
637    ///     .connect(GraphNode::Input, node1.into(), None)
638    ///     .unwrap();
639    ///
640    /// let edge = dsp_graph.connect(node1.into(), node2.into(), None).unwrap();
641    ///
642    /// dsp_graph
643    ///     .connect(node2.into(), GraphNode::Output, None)
644    ///     .unwrap();
645    ///
646    /// // Rewire the edge to swap channels 0 and 1, while keeping channels 2 and 3 the same
647    /// dsp_graph
648    ///     .rewire(edge, &[(0, 1), (1, 0), (2, 2), (3, 3)])
649    ///     .unwrap();
650    /// ```
651    ///
652    pub fn rewire(
653        &mut self,
654        edge_index: EdgeIndex,
655        rewire_mapping: &[(usize, usize)], // maps source channel to destination channel
656    ) -> Result<(), AudioGraphError> {
657        if let Some(edge) = self.graph.edge_weight_mut(edge_index) {
658            let mut channel_selection = ChannelSelection::new(0);
659            let mut rewire = HashMap::new();
660
661            for &(source, dest) in rewire_mapping {
662                channel_selection.connect(dest)?;
663
664                // we flip (source, dest) to have logical to physical mapping
665                if rewire.insert(dest, source).is_some() {
666                    return Err("Duplicate destination channel in rewire mapping");
667                }
668            }
669
670            edge.data_mut().channel_selection = Some(channel_selection);
671            edge.rewire = Some(rewire);
672
673            Ok(())
674        } else {
675            Err("Edge not found")
676        }
677    }
678
679    /// Removes rewiring from an existing connection, reverting to the default channel selection (i.e. all channels connected)
680    ///
681    /// **NOT realtime-safe**
682    pub fn remove_rewire(&mut self, edge_index: EdgeIndex) -> Result<(), AudioGraphError> {
683        if let Some(edge) = self.graph.edge_weight_mut(edge_index) {
684            edge.rewire = None;
685            edge.data.channel_selection = None;
686            Ok(())
687        } else {
688            Err("Edge not found")
689        }
690    }
691
692    /// Connects two nodes and creates a rewired mapping in one step
693    ///
694    /// **NOT realtime-safe**
695    ///
696    /// See [`RewireDspGraph::connect`] and [`RewireDspGraph::rewire`] for details.
697    pub fn connect_rewired(
698        &mut self,
699        from: GraphNode,
700        to: GraphNode,
701        wiring: &[(usize, usize)],
702    ) -> Result<EdgeIndex, AudioGraphError> {
703        let edge = self.connect(from, to, None)?;
704        self.rewire(edge, wiring)?;
705        Ok(edge)
706    }
707}
708
709/// Type alias for a basic graph without rewiring support
710#[allow(private_interfaces)]
711pub type BasicDspGraph<T> = DspGraph<T, ProcessorChannel>;
712
713/// Type alias for a graph with rewiring support
714#[allow(private_interfaces)]
715pub type RewireDspGraph<T> = DspGraph<T, RewireProcessorChannel>;
716
717#[cfg(test)]
718mod tests {
719    use crate::processor::PassThrough;
720
721    use super::*;
722
723    struct FourtyTwo {}
724
725    impl Processor<f32> for FourtyTwo {
726        fn process(&mut self, context: &mut ProcessingContext<f32>) {
727            for channel in 0..context.output_buffer.num_channels() {
728                context
729                    .output_buffer
730                    .channel_mut(channel)
731                    .unwrap()
732                    .fill(42.0);
733            }
734        }
735    }
736
737    #[test]
738    fn test_simple_graph() {
739        let mut graph = BasicDspGraph::<f32>::new(1, FrameSize(10), None);
740        let fourty_two = graph
741            .add_processor(FourtyTwo {}, MultiChannelBuffer::new(1, FrameSize(10)))
742            .unwrap();
743        graph
744            .connect(GraphNode::Input, fourty_two.into(), None)
745            .unwrap();
746        graph
747            .connect(fourty_two.into(), GraphNode::Output, None)
748            .unwrap();
749
750        let input = MultiChannelBuffer::new(1, FrameSize(10));
751        let mut output = MultiChannelBuffer::new(1, FrameSize(10));
752        graph.process(&input, &mut output, FrameSize(10));
753
754        output.channel(0).unwrap().iter().for_each(|&x| {
755            assert_eq!(x, 42.0);
756        });
757    }
758
759    #[test]
760    fn test_passthrough() {
761        let mut graph = BasicDspGraph::<f32>::new(1, FrameSize(10), None);
762        graph
763            .connect(GraphNode::Input, GraphNode::Output, None)
764            .unwrap();
765        let mut input = MultiChannelBuffer::new(1, FrameSize(10));
766        input.channel_mut(0).unwrap().fill(2.0);
767
768        let mut output = MultiChannelBuffer::new(1, FrameSize(10));
769        graph.process(&input, &mut output, FrameSize(10));
770
771        output.channel(0).unwrap().iter().for_each(|&x| {
772            assert_eq!(x, 2.0);
773        });
774    }
775
776    #[test]
777    fn test_sum_at_output() {
778        let mut graph = BasicDspGraph::<f32>::new(1, FrameSize(10), None);
779        graph
780            .connect(GraphNode::Input, GraphNode::Output, None)
781            .unwrap();
782        let fourty_two = graph
783            .add_processor(FourtyTwo {}, MultiChannelBuffer::new(1, FrameSize(10)))
784            .unwrap();
785        graph
786            .connect(GraphNode::Input, fourty_two.into(), None)
787            .unwrap();
788        graph
789            .connect(fourty_two.into(), GraphNode::Output, None)
790            .unwrap();
791
792        let mut input = MultiChannelBuffer::new(1, FrameSize(10));
793        input.channel_mut(0).unwrap().fill(2.0);
794        let mut output = MultiChannelBuffer::new(1, FrameSize(10));
795
796        graph.process(&input, &mut output, FrameSize(10));
797
798        output.channel(0).unwrap().iter().for_each(|&x| {
799            assert_eq!(x, 44.0);
800        });
801    }
802
803    #[test]
804    fn test_partial_channel_routing() {
805        let mut graph = BasicDspGraph::<f32>::new(3, FrameSize(10), None);
806        let fourty_two = graph
807            .add_processor(FourtyTwo {}, MultiChannelBuffer::new(3, FrameSize(10)))
808            .unwrap();
809
810        let passthrough = graph
811            .add_processor(PassThrough {}, MultiChannelBuffer::new(3, FrameSize(10)))
812            .unwrap();
813
814        // all 3 channels connected
815        graph
816            .connect(GraphNode::Input, fourty_two.into(), None)
817            .unwrap();
818
819        let mut second_channel_only = ChannelSelection::new(0); // Start with no channels
820        second_channel_only.connect(1).unwrap();
821
822        graph
823            .connect(
824                fourty_two.into(),
825                passthrough.into(),
826                Some(second_channel_only),
827            )
828            .unwrap();
829
830        graph
831            .connect(passthrough.into(), GraphNode::Output, None)
832            .unwrap();
833
834        let mut input = MultiChannelBuffer::new(3, FrameSize(10));
835        input.channel_mut(0).unwrap().fill(1.0);
836        input.channel_mut(1).unwrap().fill(2.0);
837        input.channel_mut(2).unwrap().fill(3.0);
838
839        let mut output = MultiChannelBuffer::new(3, FrameSize(10));
840        graph.process(&input, &mut output, FrameSize(10));
841
842        // Only channel 1 should have the FourtyTwo output (42.0)
843        output.channel(0).unwrap().iter().for_each(|&x| {
844            assert_eq!(x, 0.0);
845        });
846        output.channel(1).unwrap().iter().for_each(|&x| {
847            assert_eq!(x, 42.0);
848        });
849        output.channel(2).unwrap().iter().for_each(|&x| {
850            assert_eq!(x, 0.0);
851        });
852    }
853
854    #[test]
855    fn test_processing_order_and_count() {
856        struct Adder {
857            value: f32,
858        }
859
860        impl Processor<f32> for Adder {
861            fn process(&mut self, context: &mut ProcessingContext<f32>) {
862                for ch in 0..context.output_buffer.num_channels() {
863                    let input_channel = context.input_buffer.channel(ch).unwrap();
864                    let output_channel = context.output_buffer.channel_mut(ch).unwrap();
865                    for (o, &i) in output_channel.iter_mut().zip(input_channel.iter()) {
866                        *o = i + self.value;
867                    }
868                }
869            }
870        }
871
872        let mut graph = BasicDspGraph::<f32>::new(1, FrameSize(8), None);
873
874        let add1 = graph
875            .add_processor(
876                Adder { value: 1.0 },
877                MultiChannelBuffer::new(1, FrameSize(8)),
878            )
879            .unwrap();
880
881        let add3 = graph
882            .add_processor(
883                Adder { value: 3.0 },
884                MultiChannelBuffer::new(1, FrameSize(8)),
885            )
886            .unwrap();
887        let add5 = graph
888            .add_processor(
889                Adder { value: 5.0 },
890                MultiChannelBuffer::new(1, FrameSize(8)),
891            )
892            .unwrap();
893
894        graph.connect(GraphNode::Input, add1.into(), None).unwrap();
895        graph.connect(add1.into(), add3.into(), None).unwrap();
896        graph.connect(add1.into(), add5.into(), None).unwrap();
897        graph.connect(add3.into(), GraphNode::Output, None).unwrap();
898        graph.connect(add5.into(), GraphNode::Output, None).unwrap();
899
900        let input = MultiChannelBuffer::new(1, FrameSize(8));
901        let mut output = MultiChannelBuffer::new(1, FrameSize(8));
902
903        graph.process(&input, &mut output, FrameSize(8));
904
905        output.channel(0).unwrap().iter().for_each(|&x| {
906            assert_eq!(x, 10.0);
907        });
908    }
909
910    #[test]
911    fn test_reconnect() {
912        let frame_size = FrameSize(10);
913
914        let mut graph = BasicDspGraph::<f32>::new(2, frame_size, Some(16));
915
916        let node_a = graph
917            .add_processor(PassThrough {}, MultiChannelBuffer::new(2, frame_size))
918            .unwrap();
919
920        // Initial connection
921        let input_edge = graph
922            .connect(
923                GraphNode::Input,
924                node_a.into(),
925                Some(ChannelSelection::new(2)),
926            )
927            .unwrap();
928
929        let output_edge = graph
930            .connect(
931                node_a.into(),
932                GraphNode::Output,
933                Some(ChannelSelection::new(2)),
934            )
935            .unwrap();
936
937        let mut input = MultiChannelBuffer::new(2, frame_size);
938        input.channel_mut(0).unwrap().fill(1.0);
939        input.channel_mut(1).unwrap().fill(2.0);
940
941        let mut output = MultiChannelBuffer::new(2, frame_size);
942
943        graph.process(&input, &mut output, frame_size);
944        assert_eq!(output.channel(0).unwrap()[0], 1.0);
945        assert_eq!(output.channel(1).unwrap()[0], 2.0);
946
947        // reconnect with different channel selection
948        let new_input_edge = graph
949            .connect(
950                GraphNode::Input,
951                node_a.into(),
952                Some(ChannelSelection::from_indices(&[0])), // Only channel 0
953            )
954            .unwrap();
955        let new_output_edge = graph
956            .connect(
957                node_a.into(),
958                GraphNode::Output,
959                Some(ChannelSelection::from_indices(&[0])), // Only channel 0
960            )
961            .unwrap();
962
963        assert_eq!(input_edge, new_input_edge);
964        assert_eq!(output_edge, new_output_edge);
965
966        output.clear();
967        graph.process(&input, &mut output, frame_size);
968        assert_eq!(output.channel(0).unwrap()[0], 1.0);
969        assert_eq!(output.channel(1).unwrap()[0], 0.0); // Channel 1 disconnected
970
971        graph
972            .connect(
973                GraphNode::Input,
974                node_a.into(),
975                Some(ChannelSelection::from_indices(&[1])), // Only channel 1
976            )
977            .unwrap();
978        graph
979            .connect(
980                node_a.into(),
981                GraphNode::Output,
982                Some(ChannelSelection::from_indices(&[1])), // Only channel 1
983            )
984            .unwrap();
985
986        output.clear();
987        graph.process(&input, &mut output, frame_size);
988        assert_eq!(output.channel(0).unwrap()[0], 0.0); // Channel 0 disconnected
989        assert_eq!(output.channel(1).unwrap()[0], 2.0);
990    }
991
992    #[test]
993    fn test_capacity_limits() {
994        let mut graph = BasicDspGraph::<f32>::new(1, FrameSize(10), Some(4));
995
996        // Capacity is 4, input and output are already created (2 nodes), so we can add 2 more
997        let n1 = graph
998            .add_processor(PassThrough {}, MultiChannelBuffer::new(1, FrameSize(10)))
999            .unwrap();
1000        let n2 = graph
1001            .add_processor(PassThrough {}, MultiChannelBuffer::new(1, FrameSize(10)))
1002            .unwrap();
1003
1004        assert!(
1005            graph
1006                .add_processor(PassThrough {}, MultiChannelBuffer::new(1, FrameSize(10)))
1007                .is_err()
1008        );
1009
1010        graph.connect(GraphNode::Input, n1.into(), None).unwrap();
1011        graph.connect(n1.into(), n2.into(), None).unwrap();
1012        graph.connect(n2.into(), GraphNode::Output, None).unwrap();
1013        graph.connect(GraphNode::Input, n2.into(), None).unwrap();
1014
1015        let result = graph.connect(n1.into(), GraphNode::Output, None);
1016        assert!(result.is_err());
1017    }
1018
1019    #[test]
1020    fn test_enable_disable_connection() {
1021        let frame_size = FrameSize(10);
1022        let mut graph = BasicDspGraph::<f32>::new(1, frame_size, None);
1023
1024        let node = graph
1025            .add_processor(FourtyTwo {}, MultiChannelBuffer::new(1, frame_size))
1026            .unwrap();
1027
1028        graph.connect(GraphNode::Input, node.into(), None).unwrap();
1029
1030        let output_edge = graph.connect(node.into(), GraphNode::Output, None).unwrap();
1031
1032        let mut input = MultiChannelBuffer::new(1, frame_size);
1033        input.channel_mut(0).unwrap().fill(1.0);
1034
1035        let mut output = MultiChannelBuffer::new(1, frame_size);
1036
1037        graph.process(&input, &mut output, frame_size);
1038        assert_eq!(output.channel(0).unwrap()[0], 42.0);
1039
1040        graph.disable_connection(output_edge).unwrap();
1041
1042        output.clear();
1043        graph.process(&input, &mut output, frame_size);
1044        assert_eq!(output.channel(0).unwrap()[0], 0.0);
1045
1046        graph.enable_connection(output_edge).unwrap();
1047
1048        output.clear();
1049        graph.process(&input, &mut output, frame_size);
1050        assert_eq!(output.channel(0).unwrap()[0], 42.0);
1051    }
1052}