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][..]); +}