From 5853e5fc595282480211b187575432f14f25a755 Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Sun, 31 May 2020 13:23:21 +0200 Subject: [PATCH] Add a `dasp_graph` crate. This adds a new crate for working with dynamic audio graphs. From the new docs: > `dasp_graph` is targeted towards users who require an efficient yet > flexible and dynamically configurable audio graph. Use cases might > include virtual mixers, digital audio workstations, game audio systems, > virtual modular synthesizers and related usecases. This work has been a long-time coming and is the result of many discussions in the rust audio community and many lessons learned over the last few years of working with rust and audio. In particular: - the development of and reflection on [dsp-chain](https://crates.io/crates/dsp-chain) and its shortcomings. - The (reasonable) limitations of [dasp_signal](https://crates.io/crates/dasp_signal) when dynamically configuring graphs. - Discussion on the design of audio graphs with @raphlinus at RustAudio/dsp-chain#141. - The development of the [spatial audio server](https://github.com/museumsvictoria/spatial_audio_server). - A recent email discussion with Sami Perttu on DASP and audio graphs. `dasp_graph` is of course not a one-size-fits-all solution. Instead, it is designed specifically to work well alongside (and fill a gap within) the rest of the `dasp` crate ecosystem. Please refer to the "Comparing `dasp_signal`" section of the `dasp_graph` root documentation for a more detailed overview of the design choices between the two, what applications each are best suited toward and how the two best interoperate together. A small suite of node implementations are provided out of the box including a `Delay`, `Sum`, `Pass`, `GraphNode` and `BoxedNode`, all of which can be enabled/disabled via their associated features. Following this, I have some ideas for adding an optional `sync` module to the crate, aimed at controlling and monitoring a dasp graph and it's nodes from a separate thread (i.e. for convenient use alongside a GUI) in a non-dynamically-allocating, non-blocking manner. The work so far has been performed with these plans in mind. The ideas are for the most part based on the discussion at RustAudio/dsp-chain#141. Also, `no_std` support for `dasp_graph` is currently blocked on petgraph support for `no_std`. A PR is open for adding `no_std` support at petgraph/petgraph#238. In the meantime, the `std` feature must be enabled to use the new `dasp::graph` module. This is also noted in the updated docs. For more information about the crate and inner workings feel free to read through the new `dasp_graph` docs. I'm yet to add examples, but hopefully the added tests can give a good idea of how to use the crate in the meantime. --- Cargo.toml | 1 + dasp/Cargo.toml | 16 +- dasp/src/lib.rs | 19 ++ dasp_graph/Cargo.toml | 31 +++ dasp_graph/src/buffer.rs | 59 +++++ dasp_graph/src/lib.rs | 374 ++++++++++++++++++++++++++++++++ dasp_graph/src/node/boxed.rs | 123 +++++++++++ dasp_graph/src/node/delay.rs | 30 +++ dasp_graph/src/node/graph.rs | 59 +++++ dasp_graph/src/node/mod.rs | 163 ++++++++++++++ dasp_graph/src/node/pass.rs | 23 ++ dasp_graph/src/node/signal.rs | 19 ++ dasp_graph/src/node/sum.rs | 64 ++++++ dasp_graph/tests/graph_send.rs | 23 ++ dasp_graph/tests/graph_types.rs | 32 +++ dasp_graph/tests/sum.rs | 132 +++++++++++ 16 files changed, 1167 insertions(+), 1 deletion(-) create mode 100644 dasp_graph/Cargo.toml create mode 100644 dasp_graph/src/buffer.rs create mode 100644 dasp_graph/src/lib.rs create mode 100644 dasp_graph/src/node/boxed.rs create mode 100644 dasp_graph/src/node/delay.rs create mode 100644 dasp_graph/src/node/graph.rs create mode 100644 dasp_graph/src/node/mod.rs create mode 100644 dasp_graph/src/node/pass.rs create mode 100644 dasp_graph/src/node/signal.rs create mode 100644 dasp_graph/src/node/sum.rs create mode 100644 dasp_graph/tests/graph_send.rs create mode 100644 dasp_graph/tests/graph_types.rs create mode 100644 dasp_graph/tests/sum.rs diff --git a/Cargo.toml b/Cargo.toml index 223b9ca8..920f3cdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "dasp", "dasp_envelope", "dasp_frame", + "dasp_graph", "dasp_interpolate", "dasp_peak", "dasp_ring_buffer", diff --git a/dasp/Cargo.toml b/dasp/Cargo.toml index ccd7d280..148e2b03 100644 --- a/dasp/Cargo.toml +++ b/dasp/Cargo.toml @@ -13,6 +13,7 @@ edition = "2018" [dependencies] dasp_envelope = { version = "0.11", path = "../dasp_envelope", default-features = false, optional = true } dasp_frame = { version = "0.11", path = "../dasp_frame", default-features = false } +dasp_graph = { version = "0.11", path = "../dasp_graph", default-features = false, optional = true } dasp_interpolate = { version = "0.11", path = "../dasp_interpolate", default-features = false, optional = true } dasp_peak = { version = "0.11", path = "../dasp_peak", default-features = false, optional = true } dasp_ring_buffer = { version = "0.11", path = "../dasp_ring_buffer", default-features = false, optional = true } @@ -24,7 +25,13 @@ dasp_window = { version = "0.11", path = "../dasp_window", default-features = fa [features] default = ["std"] -all = ["std", "all-no-std"] +all = [ + "std", + "all-no-std", + # TODO: Move these into `all-no-std` once `dasp_graph` gains `no_std` support. + "graph", + "graph-all-nodes", +] all-no-std = [ "envelope", "envelope-peak", @@ -65,6 +72,13 @@ std = [ envelope = ["dasp_envelope"] envelope-peak = ["dasp_envelope/peak"] envelope-rms = ["dasp_envelope/rms"] +graph = ["dasp_graph"] +graph-all-nodes = ["dasp_graph/all-nodes"] +graph-node-boxed = ["dasp_graph/node-boxed"] +graph-node-delay = ["dasp_graph/node-delay"] +graph-node-graph = ["dasp_graph/node-graph"] +graph-node-pass = ["dasp_graph/node-pass"] +graph-node-sum = ["dasp_graph/node-sum"] interpolate = ["dasp_interpolate"] interpolate-floor = ["dasp_interpolate/floor"] interpolate-linear = ["dasp_interpolate/linear"] diff --git a/dasp/src/lib.rs b/dasp/src/lib.rs index 95112bb9..acb26a53 100644 --- a/dasp/src/lib.rs +++ b/dasp/src/lib.rs @@ -23,6 +23,7 @@ //! - See the [**Converter** type](./signal/interpolate/struct.Converter.html) for sample rate //! conversion and scaling. //! - See the [**ring_buffer** module](./ring_buffer/index.html) for fast FIFO queue options. +//! - See the [**graph** module](./graph/index.html) for working with dynamic audio graphs. //! //! ## Optional Features //! @@ -40,6 +41,16 @@ //! [envelope](./envelope/index.html) module. //! - The **envelope-peak** feature enables peak envelope detection. //! - The **envelope-rms** feature enables RMS envelope detection. +//! - The **graph** feature enables the `dasp_graph` crate via the [graph](./graph/index.html) +//! module. +//! - The **node-boxed** feature provides a `Node` implementation for `Box`. +//! - The **node-delay** feature provides a simple multi-channel `Delay` node. +//! - The **node-graph** feature provides an implementation of `Node` for a type that encapsulates +//! another `dasp` graph type. +//! - The **node-pass** feature provides a `Pass` node that simply passes audio from its +//! inputs to its outputs. +//! - The **node-signal** feature provides an implementation of `Node` for `dyn Signal`. +//! - The **node-sum** feature provides `Sum` and `SumBuffers` `Node` implementations. //! - The **interpolate** feature enables the `dasp_interpolate` crate via the //! [interpolate](./interpolate/index.html) module. //! - The **interpolate-floor** feature enables a floor interpolation implementation. @@ -83,6 +94,10 @@ //! `--no-default-features`. //! //! To enable all of the above features in a `no_std` context, enable the **all-no-std** feature. +//! +//! *Note: The **graph** module is currently only available with the **std** feature enabled. +//! Adding support for `no_std` is pending the addition of support for `no_std` in petgraph. See +//! [this PR](https://github.com/petgraph/petgraph/pull/238). #![cfg_attr(not(feature = "std"), no_std)] @@ -91,6 +106,10 @@ pub use dasp_envelope as envelope; #[doc(inline)] pub use dasp_frame::{self as frame, Frame}; +// TODO: Remove `std` requirement once `dasp_graph` gains `no_std` support. +#[cfg(all(feature = "graph", feature = "std"))] +#[doc(inline)] +pub use dasp_graph as graph; #[cfg(feature = "interpolate")] #[doc(inline)] pub use dasp_interpolate as interpolate; diff --git a/dasp_graph/Cargo.toml b/dasp_graph/Cargo.toml new file mode 100644 index 00000000..85e9cfc1 --- /dev/null +++ b/dasp_graph/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dasp_graph" +description = "A digital audio signal processing graph." +version = "0.11.0" +authors = ["mitchmindtree "] +readme = "../README.md" +keywords = ["dsp", "audio", "graph", "pcm", "audio"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rustaudio/dasp.git" +homepage = "https://github.com/rustaudio/dasp" +edition = "2018" + +[features] +default = ["all-nodes"] +all-nodes = ["node-boxed", "node-delay", "node-graph", "node-pass", "node-signal", "node-sum"] +node-boxed = [] +node-delay = ["dasp_ring_buffer"] +node-graph = [] +node-pass = [] +node-signal = ["dasp_frame", "dasp_signal"] +node-sum = ["dasp_slice"] + +[dependencies] +dasp_frame = { version = "0.11", default-features = false, features = ["std"], optional = true } +dasp_ring_buffer = { version = "0.11", default-features = false, features = ["std"], optional = true } +dasp_signal = { version = "0.11", default-features = false, features = ["std"], optional = true } +dasp_slice = { version = "0.11", default-features = false, features = ["std"], optional = true } +petgraph = { version = "0.5", default-features = false } + +[dev-dependencies] +petgraph = { version = "0.5", features = ["stable_graph"] } diff --git a/dasp_graph/src/buffer.rs b/dasp_graph/src/buffer.rs new file mode 100644 index 00000000..aed0a7b6 --- /dev/null +++ b/dasp_graph/src/buffer.rs @@ -0,0 +1,59 @@ +use core::fmt; +use core::ops::{Deref, DerefMut}; + +/// The fixed-size buffer used for processing the graph. +#[derive(Clone)] +pub struct Buffer { + data: [f32; Self::LEN], +} + +impl Buffer { + /// The fixed length of the **Buffer** type. + pub const LEN: usize = 64; + /// A silent **Buffer**. + pub const SILENT: Self = Buffer { + data: [0.0; Self::LEN], + }; + + /// Short-hand for writing silence to the whole buffer. + pub fn silence(&mut self) { + self.data.copy_from_slice(&Self::SILENT) + } +} + +impl Default for Buffer { + fn default() -> Self { + Self::SILENT + } +} + +impl From<[f32; Self::LEN]> for Buffer { + fn from(data: [f32; Self::LEN]) -> Self { + Buffer { data } + } +} + +impl fmt::Debug for Buffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.data[..], f) + } +} + +impl PartialEq for Buffer { + fn eq(&self, other: &Self) -> bool { + &self[..] == &other[..] + } +} + +impl Deref for Buffer { + type Target = [f32]; + fn deref(&self) -> &Self::Target { + &self.data[..] + } +} + +impl DerefMut for Buffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data[..] + } +} diff --git a/dasp_graph/src/lib.rs b/dasp_graph/src/lib.rs new file mode 100644 index 00000000..2dcb145e --- /dev/null +++ b/dasp_graph/src/lib.rs @@ -0,0 +1,374 @@ +//! A crate for dynamically creating and editing audio graphs. +//! +//! `dasp_graph` is targeted towards users who require an efficient yet flexible and dynamically +//! configurable audio graph. Use cases might include virtual mixers, digital audio workstations, +//! game audio systems, virtual modular synthesizers and more. +//! +//! # Overview +//! +//! A `dasp` graph is composed of **nodes** and **edges**. +//! +//! Each node contains an instance of a type that implements the [`Node` +//! trait](./node/trait.Node.html). This is normally an audio source (input), processor (effect) or +//! sink (output). The `Node` trait is the core abstraction of `dasp_graph` and allows for trivial +//! re-use of audio nodes between projects and libraries. By implementing `Node` for your audio +//! instruments, effects, generators and processors, they can be easily composed together within a +//! graph and shared with future projects or other `dasp` users. `dasp_graph` provides a suite of +//! popular node implementations out of the box, each of which may be accessed by enabling [their +//! associated features](./index.html#optional-features). +//! +//! The edges of a `dasp` graph are empty and simply describe the direction of audio flow +//! through the graph. That is, the edge *a -> b* describes that the audio output of node *a* will +//! be used as an input to node *b*. +//! +//! Once we have added our nodes and edges describing the flow of audio through our graph, we can +//! repeatedly process and retrieve audio from it using the [`Processor`](./struct.Processor.html) +//! type. +//! +//! # Comparison to `dasp_signal` +//! +//! While [`dasp_signal`](https://docs.rs/dasp_signal) and its [`Signal` +//! trait](https://docs.rs/dasp_signal/latest/dasp_signal/trait.Signal.html) are already well +//! suited towards composing audio graphs, there are certain use cases where they can cause +//! friction. Use cases that require dynamically adding or removing nodes, mapping between +//! dynamically changing channel layouts, or writing the output of one node to multiple others are +//! all difficult to achieve in an elegant manner using `dasp_signal`. +//! +//! `dasp_graph` is designed in a manner that better handles these cases. The flat ownership model +//! where the graph owns all nodes makes it trivial to add or remove nodes and edges at runtime. +//! Nodes can specify the number of buffers that they support during construction, making it easy +//! to handle different channel layouts. Adding multiple outputs to a node (including predecessors +//! to enable cycles) is trivial due to `dasp_graph`'s requirement for a fixed sample rate across +//! the whole graph. +//! +//! On the other hand, `dasp_graph`'s requirement for a fixed sample rate can also be a limitation. +//! A `dasp_graph` cannot be composed of nodes with differing input sample rates meaning it is +//! unsuitable for writing a streaming sample rate converter. `dasp_graph`'s fixed buffer size +//! results in another limitation. It implies that when creating a cycle within the graph, a +//! minimum delay of `Buffer::LEN` is incurred at the edge causing the cycle. This makes it +//! tricky to compose per-sample feedback delays by using cycles in the graph. +//! +//! | Feature | `dasp_graph` | `dasp_signal` | +//! | ------------------------------------------------- |:-------------:|:-------------:| +//! | Easily dynamically add/remove nodes/edges | ✓ | ✗ | +//! | Easily write output of node to multiple others | ✓ | ✗ | +//! | Dynamic channel layout | ✓ | ✗ | +//! | Efficiently implement per-sample feedback | ✗ | ✓ | +//! | Support variable input sample rate per node | ✗ | ✓ | +//! +//! In general, `dasp_signal` tends to be better suited towards the composition of fixed or static +//! graphs where the number of channels are known ahead of time. It is perfect for small, fixed, +//! static graph structures like a simple standalone synthesizer/sampler or small +//! processors/effects like sample-rate converters or pitch shifters. `dasp_graph` on the other +//! hand is better suited at a higher level where flexibility is a priority, e.g. a virtual mixing +//! console or, the underlying graph for a digital audio workstation or a virtual modular +//! synthesizer. +//! +//! Generally, it is likely that `dasp_signal` will be more useful for writing `Node` +//! implementations for audio sources and effects, while `dasp_graph` will be well suited to +//! dynamically composing these nodes together in a flexible manner. +//! +//! # Graph types +//! +//! Rather than providing a fixed type of graph to work with, `dasp_graph` utilises the `petgraph` +//! traits to expose a generic interface allowing users to select the graph type that bests suits +//! their application or implement their own. +//! +//! **Graph** +//! +//! The [`petgraph::graph::Graph`](https://docs.rs/petgraph/latest/petgraph/graph/struct.Graph.html) +//! type is a standard graph type exposed by `petgraph`. The type is simply an interface around two +//! `Vec`s, one containing the nodes and one containing the edges. Adding nodes returns a unique +//! identifier that can be used to index into the graph. As long as the graph is intialised with a +//! sufficient capacity for both `Vec`s, adding nodes while avoiding dynamic allocation is simple. +//! +//! **StableGraph** +//! +//! One significant caveat with the `Graph` type is that removing a node invalidates any existing +//! indices that refer to the following nodes stored within the graph's node `Vec`. The +//! [`petgraph::stable_graph::StableGraph`](https://docs.rs/petgraph/latest/petgraph/stable_graph/struct.StableGraph.html) +//! type avoids this issue by storing each node in and enum. When a node is "removed", the element +//! simply switches to a variant that indicates its slot is available for use the next time +//! `add_node` is called. +//! +//! In summary, if you require the ability to dynamically remove nodes from your graph you should +//! prefer the `StableGraph` type. Otherwise, the `Graph` type is likely well suited. +//! +//! If neither of these graphs fit your use case, consider implementing the necessary petgraph +//! traits for your own graph type. You can find the necessary traits by checking the trait bounds +//! on the graph argument to the `dasp_graph` functions you intend to use. +//! +//! # Optional Features +//! +//! Each of the provided node implementations are available by default, however these may be +//! disabled by disabling default features. You can then enable only the implementations you +//! require with the following features: +//! +//! - The **node-boxed** feature provides a `Node` implementation for `Box`. This is +//! particularly useful for working with a graph composed of many different node types. +//! - The **node-graph** feature provides an implementation of `Node` for a type that encapsulates +//! another `dasp` graph type. This allows for composing individual nodes from graphs of other +//! nodes. +//! - The **node-signal** feature provides an implementation of `Node` for `dyn Signal`. This is +//! useful when designing nodes using `dasp_signal`. +//! - The **node-delay** feature provides a simple multi-channel `Delay` node. +//! - The **node-pass** feature provides a `Pass` node that simply passes audio from its +//! inputs to its outputs. +//! - The **node-sum** feature provides `Sum` and `SumBuffers` `Node` implementations. These are +//! useful for mixing together multiple inputs, and for simple mappings between different channel +//! layouts. +//! +//! ### no_std +//! +//! *TODO: Adding support for `no_std` is pending the addition of support for `no_std` in petgraph. +//! See https://github.com/petgraph/petgraph/pull/238. + +pub use buffer::Buffer; +pub use node::{Input, Node}; +use petgraph::data::{DataMap, DataMapMut}; +use petgraph::visit::{ + Data, DfsPostOrder, GraphBase, IntoNeighborsDirected, NodeCount, NodeIndexable, Reversed, + Visitable, +}; +use petgraph::{Incoming, Outgoing}; + +#[cfg(feature = "node-boxed")] +pub use node::{BoxedNode, BoxedNodeSend}; + +mod buffer; +pub mod node; + +/// State related to the processing of an audio graph of type `G`. +/// +/// The **Processor** allows for the re-use of resources related to traversal and requesting audio +/// from the graph. This makes it easier to avoid dynamic allocation within a high-priority audio +/// context. +/// +/// # Example +/// +/// ``` +/// use dasp_graph::{Node, NodeData}; +/// # use dasp_graph::{Buffer, Input}; +/// use petgraph; +/// # +/// # // The node type. (Hint: Use existing node impls by enabling their associated features). +/// # struct MyNode; +/// +/// // Chose a type of graph for audio processing. +/// type Graph = petgraph::graph::DiGraph, (), u32>; +/// // Create a short-hand for our processor type. +/// type Processor = dasp_graph::Processor; +/// # +/// # impl Node for MyNode { +/// # // ... +/// # fn process(&mut self, _inputs: &[Input], _output: &mut [Buffer]) { +/// # } +/// # } +/// +/// fn main() { +/// // Create a graph and a processor with some suitable capacity to avoid dynamic allocation. +/// let max_nodes = 1024; +/// let max_edges = 1024; +/// let mut g = Graph::with_capacity(max_nodes, max_edges); +/// let mut p = Processor::with_capacity(max_nodes); +/// +/// // Add some nodes and edges... +/// # let n_id = g.add_node(NodeData::new1(MyNode)); +/// +/// // Process all nodes within the graph that output to the node at `n_id`. +/// p.process(&mut g, n_id); +/// } +/// ``` +pub struct Processor +where + G: Visitable, +{ + // State related to the traversal of the audio graph starting from the output node. + dfs_post_order: DfsPostOrder, + // Solely for collecting the inputs of a node in order to apply its `Node::process` method. + inputs: Vec, +} + +/// For use as the node weight within a dasp graph. Contains the node and its buffers. +/// +/// For a graph to be compatible with a graph **Processor**, its node weights must be of type +/// `NodeData`, where `T` is some type that implements the `Node` trait. +pub struct NodeData { + /// The buffers to which the `node` writes audio data during a call to its `process` method. + /// + /// Generally, each buffer stored within `buffers` corresponds to a unique audio channel. E.g. + /// a node processing mono data would store one buffer, a node processing stereo data would + /// store two, and so on. + pub buffers: Vec, + pub node: T, +} + +impl Processor +where + G: Visitable, +{ + /// Construct a new graph processor from the given maximum anticipated node count. + /// + /// As long as this node count is not exceeded, the **Processor** should never require dynamic + /// allocation following construction. + pub fn with_capacity(max_nodes: usize) -> Self + where + G::Map: Default, + { + let mut dfs_post_order = DfsPostOrder::default(); + dfs_post_order.stack = Vec::with_capacity(max_nodes); + let inputs = Vec::with_capacity(max_nodes); + Self { + dfs_post_order, + inputs, + } + } + + /// Process audio through the subgraph ending at the node with the given ID. + /// + /// Specifically, this traverses nodes in depth-first-search *post* order where the edges of + /// the graph are reversed. This is equivalent to the topological order of all nodes that are + /// connected to the inputs of the given `node`. This ensures that all inputs of each node are + /// visited before the node itself. + /// + /// The `Node::process` method is called on each node as they are visited in the traversal. + /// + /// Upon returning, the buffers of each visited node will contain the audio processed by their + /// respective nodes. + /// + /// Supports all graphs that implement the necessary petgraph traits and whose nodes are of + /// type `NodeData` where `T` implements the `Node` trait. + /// + /// **Panics** if there is no node for the given index. + pub fn process(&mut self, graph: &mut G, node: G::NodeId) + where + G: Data> + DataMapMut, + for<'a> &'a G: GraphBase + IntoNeighborsDirected, + T: Node, + { + process(self, graph, node) + } +} + +impl NodeData { + /// Construct a new **NodeData** from an instance of its node type and buffers. + pub fn new(node: T, buffers: Vec) -> Self { + NodeData { node, buffers } + } + + /// Creates a new **NodeData** with a single buffer. + pub fn new1(node: T) -> Self { + Self::new(node, vec![Buffer::SILENT]) + } + + /// Creates a new **NodeData** with two buffers. + pub fn new2(node: T) -> Self { + Self::new(node, vec![Buffer::SILENT; 2]) + } +} + +#[cfg(feature = "node-boxed")] +impl NodeData { + /// The same as **new**, but boxes the given node data before storing it. + pub fn boxed(node: T, buffers: Vec) -> Self + where + T: 'static + Node, + { + NodeData::new(BoxedNode(Box::new(node)), buffers) + } + + /// The same as **new1**, but boxes the given node data before storing it. + pub fn boxed1(node: T) -> Self + where + T: 'static + Node, + { + Self::boxed(node, vec![Buffer::SILENT]) + } + + /// The same as **new2**, but boxes the given node data before storing it. + pub fn boxed2(node: T) -> Self + where + T: 'static + Node, + { + Self::boxed(node, vec![Buffer::SILENT, Buffer::SILENT]) + } +} + +/// Process audio through the subgraph ending at the node with the given ID. +/// +/// Specifically, this traverses nodes in depth-first-search *post* order where the edges of +/// the graph are reversed. This is equivalent to the topological order of all nodes that are +/// connected to the inputs of the given `node`. This ensures that all inputs of each node are +/// visited before the node itself. +/// +/// The `Node::process` method is called on each node as they are visited in the traversal. +/// +/// Upon returning, the buffers of each visited node will contain the audio processed by their +/// respective nodes. +/// +/// Supports all graphs that implement the necessary petgraph traits and whose nodes are of +/// type `NodeData` where `T` implements the `Node` trait. +/// +/// **Panics** if there is no node for the given index. +pub fn process(processor: &mut Processor, graph: &mut G, node: G::NodeId) +where + G: Data> + DataMapMut + Visitable, + for<'a> &'a G: GraphBase + IntoNeighborsDirected, + T: Node, +{ + const NO_NODE: &str = "no node exists for the given index"; + processor.dfs_post_order.reset(Reversed(&*graph)); + processor.dfs_post_order.move_to(node); + while let Some(n) = processor.dfs_post_order.next(Reversed(&*graph)) { + let data: *mut NodeData = graph.node_weight_mut(n).expect(NO_NODE) as *mut _; + processor.inputs.clear(); + for in_n in (&*graph).neighbors_directed(n, Incoming) { + // Skip edges that connect the node to itself to avoid aliasing `node`. + if n == in_n { + continue; + } + let input_container = graph.node_weight(in_n).expect(NO_NODE); + let input = node::Input::new(&input_container.buffers); + processor.inputs.push(input); + } + // Here we deference our raw pointer to the `NodeData`. The only references to the graph at + // this point in time are the input references and the node itself. We know that the input + // references do not alias our node's mutable reference as we explicitly check for it while + // looping through the inputs above. + unsafe { + (*data) + .node + .process(&processor.inputs, &mut (*data).buffers); + } + } +} + +/// Produce an iterator yielding IDs for all **source** nodes within the graph. +/// +/// A node is considered to be a source node if it has no incoming edges. +pub fn sources<'a, G>(g: &'a G) -> impl 'a + Iterator +where + G: IntoNeighborsDirected + NodeCount + NodeIndexable, +{ + (0..g.node_count()) + .map(move |ix| g.from_index(ix)) + .filter_map(move |id| match g.neighbors_directed(id, Incoming).next() { + None => Some(id), + _ => None, + }) +} + +/// Produce an iterator yielding IDs for all **sink** nodes within the graph. +/// +/// A node is considered to be a **sink** node if it has no outgoing edges. +pub fn sinks<'a, G>(g: &'a G) -> impl 'a + Iterator +where + G: IntoNeighborsDirected + NodeCount + NodeIndexable, +{ + (0..g.node_count()) + .map(move |ix| g.from_index(ix)) + .filter_map(move |id| match g.neighbors_directed(id, Outgoing).next() { + None => Some(id), + _ => None, + }) +} diff --git a/dasp_graph/src/node/boxed.rs b/dasp_graph/src/node/boxed.rs new file mode 100644 index 00000000..7501d555 --- /dev/null +++ b/dasp_graph/src/node/boxed.rs @@ -0,0 +1,123 @@ +use crate::{Buffer, Input, Node}; +use core::fmt; +use core::ops::{Deref, DerefMut}; + +/// A wrapper around a `Box`. +/// +/// Provides the necessary `Sized` implementation to allow for compatibility with the graph process +/// function. +pub struct BoxedNode(pub Box); + +/// A wrapper around a `Box`. +/// +/// Provides the necessary `Sized` implementation to allow for compatibility with the graph process +/// function. +/// +/// Useful when the ability to send nodes from one thread to another is required. E.g. this is +/// common when initialising nodes or the audio graph itself on one thread before sending them to +/// the audio thread. +pub struct BoxedNodeSend(pub Box); + +impl BoxedNode { + /// Create a new `BoxedNode` around the given `node`. + /// + /// This is short-hand for `BoxedNode::from(Box::new(node))`. + pub fn new(node: T) -> Self + where + T: 'static + Node, + { + Self::from(Box::new(node)) + } +} + +impl BoxedNodeSend { + /// Create a new `BoxedNode` around the given `node`. + /// + /// This is short-hand for `BoxedNode::from(Box::new(node))`. + pub fn new(node: T) -> Self + where + T: 'static + Node + Send, + { + Self::from(Box::new(node)) + } +} + +impl Node for BoxedNode { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + self.0.process(inputs, output) + } +} + +impl Node for BoxedNodeSend { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + self.0.process(inputs, output) + } +} + +impl From> for BoxedNode +where + T: 'static + Node, +{ + fn from(n: Box) -> Self { + BoxedNode(n as Box) + } +} + +impl From> for BoxedNodeSend +where + T: 'static + Node + Send, +{ + fn from(n: Box) -> Self { + BoxedNodeSend(n as Box) + } +} + +impl Into> for BoxedNode { + fn into(self) -> Box { + self.0 + } +} + +impl Into> for BoxedNodeSend { + fn into(self) -> Box { + self.0 + } +} + +impl fmt::Debug for BoxedNode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("BoxedNode").finish() + } +} + +impl fmt::Debug for BoxedNodeSend { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("BoxedNodeSend").finish() + } +} + +impl Deref for BoxedNode { + type Target = Box; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for BoxedNodeSend { + type Target = Box; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BoxedNode { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl DerefMut for BoxedNodeSend { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/dasp_graph/src/node/delay.rs b/dasp_graph/src/node/delay.rs new file mode 100644 index 00000000..8e18d6b4 --- /dev/null +++ b/dasp_graph/src/node/delay.rs @@ -0,0 +1,30 @@ +use crate::{Buffer, Input, Node}; +use dasp_ring_buffer as ring_buffer; + +/// A delay node, where the delay duration for each channel is equal to the length of the inner +/// ring buffer associated with that channel. +/// +/// Assumes that there is one input node, and that the number of input buffers, output buffers and +/// ring buffers all match. +#[derive(Clone, Debug, PartialEq)] +pub struct Delay(pub Vec>); + +impl Node for Delay +where + S: ring_buffer::SliceMut, +{ + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + // Retrieve the single input, ignore any others. + let input = match inputs.get(0) { + Some(input) => input, + None => return, + }; + + // Apply the delay across each channel. + for ((ring_buf, in_buf), out_buf) in self.0.iter_mut().zip(input.buffers()).zip(output) { + for (i, out) in out_buf.iter_mut().enumerate() { + *out = ring_buf.push(in_buf[i]); + } + } + } +} diff --git a/dasp_graph/src/node/graph.rs b/dasp_graph/src/node/graph.rs new file mode 100644 index 00000000..e4b61017 --- /dev/null +++ b/dasp_graph/src/node/graph.rs @@ -0,0 +1,59 @@ +//! Implementation of `Node` for a graph of nodes. +//! +//! Allows for nesting subgraphs within nodes of a graph. + +use crate::{Buffer, Input, Node, NodeData, Processor}; +use core::marker::PhantomData; +use petgraph::data::DataMapMut; +use petgraph::visit::{Data, GraphBase, IntoNeighborsDirected, Visitable}; + +pub struct GraphNode +where + G: Visitable, +{ + pub processor: Processor, + pub graph: G, + pub input_nodes: Vec, + pub output_node: G::NodeId, + pub node_type: PhantomData, +} + +impl Node for GraphNode +where + G: Data> + DataMapMut + Visitable, + for<'a> &'a G: GraphBase + IntoNeighborsDirected, + T: Node, +{ + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + let GraphNode { + ref mut processor, + ref mut graph, + ref input_nodes, + output_node, + .. + } = *self; + + // Write the input buffers to the input nodes. + for (input, &in_n) in inputs.iter().zip(input_nodes) { + let in_node_bufs = &mut graph + .node_weight_mut(in_n) + .expect("no node for graph node's input node ID") + .buffers; + for (in_node_buf, in_buf) in in_node_bufs.iter_mut().zip(input.buffers()) { + in_node_buf.copy_from_slice(in_buf); + } + } + + // Process the graph. + processor.process(graph, output_node); + + // Write the output node buffers to the output buffers. + let out_node_bufs = &mut graph + .node_weight_mut(output_node) + .expect("no node for graph node's output node ID") + .buffers; + for (out_buf, out_node_buf) in output.iter_mut().zip(out_node_bufs) { + out_buf.copy_from_slice(out_node_buf); + } + } +} diff --git a/dasp_graph/src/node/mod.rs b/dasp_graph/src/node/mod.rs new file mode 100644 index 00000000..953f6406 --- /dev/null +++ b/dasp_graph/src/node/mod.rs @@ -0,0 +1,163 @@ +use crate::buffer::Buffer; +use core::fmt; + +#[cfg(feature = "node-boxed")] +pub use boxed::{BoxedNode, BoxedNodeSend}; +#[cfg(feature = "node-delay")] +pub use delay::Delay; +#[cfg(feature = "node-graph")] +pub use graph::GraphNode; +#[cfg(feature = "node-pass")] +pub use pass::Pass; +#[cfg(feature = "node-sum")] +pub use sum::{Sum, SumBuffers}; + +#[cfg(feature = "node-boxed")] +mod boxed; +#[cfg(feature = "node-delay")] +mod delay; +#[cfg(feature = "node-graph")] +mod graph; +#[cfg(feature = "node-pass")] +mod pass; +#[cfg(feature = "node-signal")] +mod signal; +#[cfg(feature = "node-sum")] +mod sum; + +/// The `Node` type used within a dasp graph must implement this trait. +/// +/// The implementation describes how audio is processed from its inputs to outputs. +/// +/// - Audio **sources** or **inputs** may simply ignore the `inputs` field and write their source +/// data directly to the `output` buffers. +/// - Audio **processors**, **effects** or **sinks** may read from their `inputs`, apply some +/// custom processing and write the result to their `output` buffers. +/// +/// Multiple `Node` implementations are provided and can be enabled or disabled via [their +/// associated features](../index.html#optional-features). +/// +/// # Example +/// +/// The following demonstrates how to implement a simple node that sums each of its inputs onto the +/// output. +/// +/// ```rust +/// use dasp_graph::{Buffer, Input, Node}; +/// +/// // Our new `Node` type. +/// pub struct Sum; +/// +/// // Implement the `Node` trait for our new type. +/// # #[cfg(feature = "dasp_slice")] +/// impl Node for Sum { +/// fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { +/// // Fill the output with silence. +/// for out_buffer in output.iter_mut() { +/// out_buffer.silence(); +/// } +/// // Sum the inputs onto the output. +/// for (channel, out_buffer) in output.iter_mut().enumerate() { +/// for input in inputs { +/// let in_buffers = input.buffers(); +/// if let Some(in_buffer) = in_buffers.get(channel) { +/// dasp_slice::add_in_place(out_buffer, in_buffer); +/// } +/// } +/// } +/// } +/// } +/// ``` +pub trait Node { + /// Process some audio given a list of the node's `inputs` and write the result to the `output` + /// buffers. + /// + /// `inputs` represents a list of all nodes with direct edges toward this node. Each + /// [`Input`](./struct.Input.html) within the list can providee a reference to the output + /// buffers of their corresponding node. + /// + /// The `inputs` may be ignored if the implementation is for a source node. Alternatively, if + /// the `Node` only supports a specific number of `input`s, it is up to the user to decide how + /// they wish to enforce this or provide feedback at the time of graph and edge creation. + /// + /// This `process` method is called by the [`Processor`](../struct.Processor.html) as it + /// traverses the graph during audio rendering. + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]); +} + +/// A reference to another node that is an input to the current node. +/// +/// *TODO: It may be useful to provide some information that can uniquely identify the input node. +/// This could be useful to allow to distinguish between side-chained and regular inputs for +/// example.* +pub struct Input { + buffers_ptr: *const Buffer, + buffers_len: usize, +} + +impl Input { + // Constructor solely for use within the graph `process` function. + pub(crate) fn new(slice: &[Buffer]) -> Self { + let buffers_ptr = slice.as_ptr(); + let buffers_len = slice.len(); + Input { + buffers_ptr, + buffers_len, + } + } + + /// A reference to the buffers of the input node. + pub fn buffers(&self) -> &[Buffer] { + // As we know that an `Input` can only be constructed during a call to the graph `process` + // function, we can be sure that our slice is still valid as long as the input itself is + // alive. + unsafe { std::slice::from_raw_parts(self.buffers_ptr, self.buffers_len) } + } +} + +// Inputs can only be created by the `dasp_graph::process` implementation and only ever live as +// long as the lifetime of the call to the function. Thus, it's safe to implement this so that +// `Send` closures can be stored within the graph and sent between threads. +unsafe impl Send for Input {} + +impl fmt::Debug for Input { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self.buffers(), f) + } +} + +impl<'a, T> Node for &'a mut T +where + T: Node, +{ + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + (**self).process(inputs, output) + } +} + +impl Node for Box +where + T: Node, +{ + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + (**self).process(inputs, output) + } +} + +impl Node for dyn Fn(&[Input], &mut [Buffer]) { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + (*self)(inputs, output) + } +} + +impl Node for dyn FnMut(&[Input], &mut [Buffer]) { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + (*self)(inputs, output) + } +} + +impl Node for fn(&[Input], &mut [Buffer]) { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + (*self)(inputs, output) + } +} diff --git a/dasp_graph/src/node/pass.rs b/dasp_graph/src/node/pass.rs new file mode 100644 index 00000000..8872719a --- /dev/null +++ b/dasp_graph/src/node/pass.rs @@ -0,0 +1,23 @@ +use crate::{Buffer, Input, Node}; + +/// A simple node that passes an input directly to the output. +/// +/// Works by mem-copying each buffer of the first input to each buffer of the output respectively. +/// +/// This can be useful as an intermediary node when feeding the output of a node back into one of +/// its inputs. It can also be useful for discarding excess input channels by having a `Pass` with +/// less output buffers than its input. +#[derive(Clone, Debug, PartialEq)] +pub struct Pass; + +impl Node for Pass { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + let input = match inputs.get(0) { + None => return, + Some(input) => input, + }; + for (out_buf, in_buf) in output.iter_mut().zip(input.buffers()) { + out_buf.copy_from_slice(in_buf); + } + } +} diff --git a/dasp_graph/src/node/signal.rs b/dasp_graph/src/node/signal.rs new file mode 100644 index 00000000..cd8285bc --- /dev/null +++ b/dasp_graph/src/node/signal.rs @@ -0,0 +1,19 @@ +use crate::{Buffer, Input, Node}; +use dasp_frame::Frame; +use dasp_signal::Signal; + +impl Node for dyn Signal +where + F: Frame, +{ + fn process(&mut self, _inputs: &[Input], output: &mut [Buffer]) { + let channels = std::cmp::min(F::CHANNELS, output.len()); + for ix in 0..Buffer::LEN { + let frame = self.next(); + for ch in 0..channels { + // Safe, as we verify the number of channels at the beginning of the function. + output[ch][ix] = unsafe { *frame.channel_unchecked(ch) }; + } + } + } +} diff --git a/dasp_graph/src/node/sum.rs b/dasp_graph/src/node/sum.rs new file mode 100644 index 00000000..9a078d40 --- /dev/null +++ b/dasp_graph/src/node/sum.rs @@ -0,0 +1,64 @@ +use crate::{Buffer, Input, Node}; + +/// A stateless node that sums each of the inputs onto the output. +/// +/// Assumes that the number of buffers per input is equal to the number of output buffers. +#[derive(Clone, Debug, PartialEq)] +pub struct Sum; + +/// A stateless node that sums all of the buffers of all of the inputs onto each of the output +/// buffers. +/// +/// E.g. Given two inputs with three buffers each, all 6 input buffers will be summed onto the +/// first output buffer. If there is more than one output buffer, the result is copied to the +/// remaining output buffers. +/// +/// After a call to `Node::process`, each of the output buffers will always have the same contents. +/// +/// Common use cases: +/// +/// - Summing multiple input channels down to a single output channel. +/// - Writing a single input channel to multiple output channels. +#[derive(Clone, Debug, PartialEq)] +pub struct SumBuffers; + +impl Node for Sum { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + // Fill the output with silence. + for out_buffer in output.iter_mut() { + out_buffer.silence(); + } + // Sum the inputs onto the output. + for (channel, out_buffer) in output.iter_mut().enumerate() { + for input in inputs { + let in_buffers = input.buffers(); + if let Some(in_buffer) = in_buffers.get(channel) { + dasp_slice::add_in_place(out_buffer, in_buffer); + } + } + } + } +} + +impl Node for SumBuffers { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + // Get the first output buffer. + let mut out_buffers = output.iter_mut(); + let out_buffer_first = match out_buffers.next() { + None => return, + Some(buffer) => buffer, + }; + // Fill it with silence. + out_buffer_first.silence(); + // Sum all input buffers onto the first output buffer. + for input in inputs { + for in_buffer in input.buffers() { + dasp_slice::add_in_place(out_buffer_first, in_buffer); + } + } + // Write the first output buffer to the rest. + for out_buffer in out_buffers { + out_buffer.copy_from_slice(out_buffer_first); + } + } +} diff --git a/dasp_graph/tests/graph_send.rs b/dasp_graph/tests/graph_send.rs new file mode 100644 index 00000000..8bde1188 --- /dev/null +++ b/dasp_graph/tests/graph_send.rs @@ -0,0 +1,23 @@ +//! Small test to make sure `Processor` and graph types stay `Send`. +//! +//! We only need to know they compile. + +#![cfg(feature = "node-boxed")] +#![allow(unreachable_code, unused_variables)] + +use dasp_graph::{BoxedNodeSend, NodeData}; +use petgraph::visit::GraphBase; + +#[test] +#[should_panic] +fn test_graph_send() { + type Graph = petgraph::Graph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + let mut g: Graph = unimplemented!(); + let mut p: Processor = unimplemented!(); + let n: ::NodeId = unimplemented!(); + + std::thread::spawn(move || { + p.process(&mut g, n); + }); +} diff --git a/dasp_graph/tests/graph_types.rs b/dasp_graph/tests/graph_types.rs new file mode 100644 index 00000000..a7b0212d --- /dev/null +++ b/dasp_graph/tests/graph_types.rs @@ -0,0 +1,32 @@ +//! Check that we properly support the major petgraph types. +//! +//! We only need to know they compile. + +#![cfg(feature = "node-boxed")] +#![allow(unreachable_code, unused_variables)] + +use dasp_graph::{BoxedNode, NodeData}; +use petgraph::visit::GraphBase; + +#[test] +#[should_panic] +fn test_graph() { + type Graph = petgraph::Graph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + let mut g: Graph = unimplemented!(); + let mut p: Processor = unimplemented!(); + let n: ::NodeId = unimplemented!(); + p.process(&mut g, n); +} + +#[test] +#[should_panic] +fn test_stable_graph() { + type Graph = + petgraph::stable_graph::StableGraph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + let mut g: Graph = unimplemented!(); + let mut p: Processor = unimplemented!(); + let n: ::NodeId = unimplemented!(); + p.process(&mut g, n); +} diff --git a/dasp_graph/tests/sum.rs b/dasp_graph/tests/sum.rs new file mode 100644 index 00000000..4c27a69f --- /dev/null +++ b/dasp_graph/tests/sum.rs @@ -0,0 +1,132 @@ +#![cfg(all(feature = "node-boxed", feature = "node-sum"))] + +use dasp_graph::{node, Buffer, Input, Node, NodeData}; + +type BoxedNode = dasp_graph::BoxedNode; + +// A simple source node that just writes `0.1` to the output. We'll use this to test the sum node. +fn src_node(_inputs: &[Input], output: &mut [Buffer]) { + for o in output { + o.iter_mut().for_each(|s| *s = 0.1); + } +} + +#[test] +fn test_sum() { + // The type of graph to use for this test. + type Graph = petgraph::Graph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + + // Create a graph and a processor. + let max_nodes = 6; + let max_edges = 5; + let mut g = Graph::with_capacity(max_nodes, max_edges); + let mut p = Processor::with_capacity(max_nodes); + + // Create and add the nodes to the graph. + let src_node_ptr = src_node as fn(&[Input], &mut [Buffer]); + let src_a = g.add_node(NodeData::new1(BoxedNode::new(src_node_ptr))); + let src_b = g.add_node(NodeData::new1(BoxedNode::new(src_node_ptr))); + let sum = g.add_node(NodeData::new1(BoxedNode::new(node::Sum))); + + // Plug the source nodes into the sum node. + g.add_edge(src_a, sum, ()); + g.add_edge(src_b, sum, ()); + + // Process the graph from the sum node. + p.process(&mut g, sum); + + // Check that `sum` actually contains the sum. + let expected = Buffer::from([0.2; Buffer::LEN]); + assert_eq!(&g[sum].buffers[..], &[expected][..]); + + // Plug in some more sources. + let src_c = g.add_node(NodeData::new1(BoxedNode::new(src_node_ptr))); + let src_d = g.add_node(NodeData::new1(BoxedNode::new(src_node_ptr))); + let src_e = g.add_node(NodeData::new1(BoxedNode::new(src_node_ptr))); + g.add_edge(src_c, sum, ()); + g.add_edge(src_d, sum, ()); + g.add_edge(src_e, sum, ()); + + // Check that the result is consistent. + p.process(&mut g, sum); + let expected = Buffer::from([0.5; Buffer::LEN]); + assert_eq!(&g[sum].buffers[..], &[expected][..]); +} + +#[test] +fn test_sum2() { + // The type of graph to use for this test. + type Graph = petgraph::Graph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + + // Create a graph and a processor. + let mut g = Graph::new(); + let mut p = Processor::with_capacity(g.node_count()); + + // Create a small tree where we first sum a and b, then sum the result with c. + // This time, using two buffers (channels) per node. + let src_node_ptr = src_node as fn(&[Input], &mut [Buffer]); + let src_a = g.add_node(NodeData::new2(BoxedNode::new(src_node_ptr))); + let src_b = g.add_node(NodeData::new2(BoxedNode::new(src_node_ptr))); + let src_c = g.add_node(NodeData::new2(BoxedNode::new(src_node_ptr))); + let sum_a_b = g.add_node(NodeData::new2(BoxedNode::new(node::Sum))); + let sum_ab_c = g.add_node(NodeData::new2(BoxedNode::new(node::Sum))); + g.add_edge(src_a, sum_a_b, ()); + g.add_edge(src_b, sum_a_b, ()); + g.add_edge(sum_a_b, sum_ab_c, ()); + g.add_edge(src_c, sum_ab_c, ()); + + // Process the graph. + p.process(&mut g, sum_ab_c); + + // sum_a_b should be 0.2. + let expected = vec![Buffer::from([0.2; Buffer::LEN]); 2]; + assert_eq!(&g[sum_a_b].buffers[..], &expected[..]); + // sum_ab_c should be 0.3. + let expected = vec![Buffer::from([0.3; Buffer::LEN]); 2]; + assert_eq!(&g[sum_ab_c].buffers[..], &expected[..]); +} + +#[test] +fn test_sum_unboxed() { + // Prove to ourselves we also support unboxed node types with a custom node type. + enum TestNode { + SourceFnPtr(fn(&[Input], &mut [Buffer])), + Sum(node::Sum), + } + + impl Node for TestNode { + fn process(&mut self, inputs: &[Input], output: &mut [Buffer]) { + match *self { + TestNode::SourceFnPtr(ref mut f) => (*f)(inputs, output), + TestNode::Sum(ref mut sum) => sum.process(inputs, output), + } + } + } + + // The type of graph to use for this test. + type Graph = petgraph::Graph, (), petgraph::Directed, u32>; + type Processor = dasp_graph::Processor; + + // Create a graph and a processor. + let mut g = Graph::new(); + let mut p = Processor::with_capacity(g.node_count()); + + // Add two source nodes and a sum node. + let src_node_ptr = src_node as _; + let src_a = g.add_node(NodeData::new1(TestNode::SourceFnPtr(src_node_ptr))); + let src_b = g.add_node(NodeData::new1(TestNode::SourceFnPtr(src_node_ptr))); + let sum = g.add_node(NodeData::new1(TestNode::Sum(node::Sum))); + + // Plug the source nodes into the sum node. + g.add_edge(src_a, sum, ()); + g.add_edge(src_b, sum, ()); + + // Process the graph from the sum node. + p.process(&mut g, sum); + + // Check that `sum` actually contains the sum. + let expected = Buffer::from([0.2; Buffer::LEN]); + assert_eq!(&g[sum].buffers[..], &[expected][..]); +}