Skip to main content

Crate audio_graph_bsd

Crate audio_graph_bsd 

Source
Expand description

Real-time-safe directed node-graph audio processing engine for Rust.

audio-graph schedules and drives a directed acyclic graph (DAG) of AudioNodes on a dedicated real-time (RT) audio thread. The engine performs topological sorting at compile time, pre-allocates every scratch buffer, and then runs Graph::process_cycle over the pre-scheduled order without ever allocating, locking, or panicking.

§Architecture

  1. Build phase (non-RT): the caller adds nodes with Graph::add_node and wires output ports to input ports with Graph::link.
  2. Compile phase (non-RT): Graph::compile runs Kahn’s topological sort, rejects cycles, fixes the execution order, and pre-allocates a per-port scratch AudioFrame for every node.
  3. RT phase: Graph::process_cycle is invoked once per audio cycle on the RT thread. It copies upstream outputs into downstream inputs (bounded, alloc-free) and calls each node’s process in dependency order.

§Real-time safety boundary — CRITICAL

Graph::process_cycle is the RT entry point. It MUST NOT allocate, lock, panic, or perform any system call. This is achieved by:

  • Running the topological sort in Graph::compile (never in process_cycle).
  • Pre-allocating all scratch frames in compile() so process_cycle never grows a Vec.
  • Using only bounded for loops and slice indexing over pre-sized buffers.

Conformance can be verified at test time with a counting allocator that fails on any allocation observed inside process_cycle.

§Example

A minimal two-node graph (source → gain) driven for one cycle. The source is a no-op node whose output is seeded via Graph::feed; the gain node scales it by 0.5:

use audio_core_bsd::{
    AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
};
use audio_graph_bsd::{Graph, GraphConfig};

/// A source node with one output and a no-op `process`: its output scratch
/// is seeded externally via `Graph::feed` and left untouched each cycle.
struct SourceNode {
    out_p: [PortDescriptor; 1],
}
impl SourceNode {
    fn new() -> Self {
        Self {
            out_p: [PortDescriptor::output(1, SampleFormat::F32)],
        }
    }
}
impl AudioNode for SourceNode {
    fn inputs(&self) -> &[PortDescriptor] { &[] }
    fn outputs(&self) -> &[PortDescriptor] { &self.out_p }
    fn process(&mut self, _ctx: &mut ProcessContext, _i: &[AudioFrame], _o: &mut [AudioFrame]) {}
}

/// Scales its single mono input by a fixed gain.
struct GainNode {
    gain: f32,
    in_p: [PortDescriptor; 1],
    out_p: [PortDescriptor; 1],
}
impl GainNode {
    fn new(gain: f32) -> Self {
        Self {
            gain,
            in_p: [PortDescriptor::input(1, SampleFormat::F32)],
            out_p: [PortDescriptor::output(1, SampleFormat::F32)],
        }
    }
}
impl AudioNode for GainNode {
    fn inputs(&self) -> &[PortDescriptor] { &self.in_p }
    fn outputs(&self) -> &[PortDescriptor] { &self.out_p }
    fn process(&mut self, _ctx: &mut ProcessContext, i: &[AudioFrame], o: &mut [AudioFrame]) {
        let (Some(inp), Some(out)) = (i.first(), o.get_mut(0)) else { return };
        let n = inp.samples.len().min(out.samples.len());
        for k in 0..n {
            out.samples[k] = inp.samples[k] * self.gain;
        }
    }
}

let mut g = Graph::new();
let src = g.add_node(Box::new(SourceNode::new()));
let dst = g.add_node(Box::new(GainNode::new(0.5)));
g.link((src, 0), (dst, 0)).unwrap();
g.compile(GraphConfig::new(8, 48_000, 1)).unwrap();

// Seed the source output with all-ones (survives process_cycle because
// SourceNode::process is a no-op), run one cycle.
g.feed(src, 0, &AudioFrame::from_planar(1, 48_000, vec![1.0; 8]));
let mut ctx = ProcessContext::new(8, 0, 48_000);
g.process_cycle(&mut ctx).unwrap();
assert!(g
    .read_output(dst, 0)
    .unwrap()
    .samples
    .iter()
    .all(|&x| (x - 0.5).abs() < 1e-6));

Structs§

Graph
A real-time-safe directed acyclic graph of audio nodes.
GraphConfig
Compile-time configuration fixing the size of every scratch buffer.
RingSink
A sink node that stashes audio from the graph for a worker thread to drain.
RingSource
A source node that drains audio from a worker thread via a lock-free ring.

Enums§

GraphError
Errors raised while building or compiling an audio Graph.

Type Aliases§

LinkId
Identifier of a link returned by Graph::link. Equal to the link’s position in insertion order.
NodeId
Identifier of a node within a Graph. Stable for the lifetime of the graph.
PortIdx
Index of a port on a node, in the order reported by the node’s inputs / outputs.