Skip to main content

Audio Graph Architecture

Last updated: May 2026 — Gain/pan/spatial removed from Processor trait; now handled by dedicated Mixer and Spatial plugin nodes. This document is the canonical reference for any AI agent or developer working on the audio graph. Read it before making any changes.

1. Overview

The audio graph is a directed acyclic graph (DAG) of processing nodes. Each node holds exactly one Box<dyn Processor> that handles all DSP. The graph handles topology, buffer management, connection routing, topological sort, and optional parallel execution via Rayon.

Key Invariants

  1. All DSP state (mute, plugins) is accessed through the Processor trait. AudioNode is a thin metadata wrapper — it does NOT have public gain, pan, muted, or plugins fields.
  2. Gain, pan, and spatial positioning are NOT on the Processor trait. They are handled by dedicated plugin nodes:
    • Mixer plugin (songbird-plugins): gain + equal-power pan
    • SpatialPanner plugin (songbird-plugins): azimuth/elevation/distance for HRTF
  3. The signal chain within a node is: instrument → effects → strip → mixer The Mixer is always the last plugin in the chain.

2. File Structure


3. Architecture Intent & Design Decisions

3.1 Why a Processor Trait?

The Processor trait decouples the processing model from the graph:
  • PluginChainProcessor — traditional DAW track: runs a chain of plugins in series. Muted state is stored on the processor. Gain and pan are handled by a Mixer plugin at the end of the chain. This is the default — every graph.add_node("name") creates one.
  • SingleProcessor — modular mode: wraps exactly one plugin per node. For gain control, add a separate Mixer plugin node. Enables modular routing: split signals, parallel FX chains, per-plugin metering.

3.2 Mixer & Spatial Plugins (Modular DAW Pattern)

Following Bitwig and modular DAW conventions, mixer controls are dedicated nodes in the signal graph rather than baked into every processor:
  • Mixer (songbirdMixer): params[0] = gain (linear), params[1] = pan (-1..1). Uses equal-power pan law: cos/sin(pan01 * π/2).
  • SpatialPanner (songbirdSpatialPanner): params[0] = azimuth, params[1] = elevation, params[2] = distance. Metadata-only for HRTF.
Runtime commands (SetTrackGain, SetTrackPan, SetNodeGain, etc.) find the Mixer/Spatial plugin by type_name() and update its params directly.

3.3 Performance Characteristics

One dyn Processor dispatch per node per block adds ~1ns (a single vtable lookup). The existing code already does N vtable dispatches per node (one per plugin.process() call), so this is negligible.

3.4 Why Solo Stays on AudioNode

Solo is a graph-level concern: when any node is solo’d, the graph skips processing all non-solo’d nodes entirely (doesn’t even call process()). It can’t live in the processor because the graph needs to read it before deciding whether to call process().

3.5 plugins_mut() vs plugins_vec_mut()

  • plugins_mut() -> &mut [Box<dyn Plugin>] — Returns a mutable slice. Use for iterating, indexing, get_mut(). Cannot call push(), remove(), insert(), or clear() because slices don’t own the data.
  • plugins_vec_mut() -> Option<&mut Vec<Box<dyn Plugin>>> — Returns the underlying Vec. Use when you need push(), remove(), insert(), clear(), or mem::swap(). Returns None for processors that don’t use a dynamic Vec.
  • push_plugin(plugin) — Convenience for the common case. Panics on SingleProcessor (which holds exactly one plugin).

4. Processing Pipeline

Each audio callback:

5. Processor Model

Node vs. Processor Responsibilities


6. Gain/Pan Access Patterns

Reading gain/pan from a node

Setting gain/pan on a node

Adding a Mixer to a new node


7. Cross-Crate Access Patterns

The Processor trait is used by multiple crates outside the engine. Here’s how each crate accesses node state:

What NOT to do

Watch out: Not everything has a .processor

Several types have their own plugins, gain, pan, muted fields that are not AudioNode. Do NOT change these to use processor.xxx(): If you see data.tracks[0].plugins or track.gain, those are project data — they’re fine as-is.

8. Migration Status

✅ COMPLETE — Plugin-Based Gain/Pan/Spatial (May 2026)

Gain, pan, and spatial positioning have been removed from the Processor trait and PluginChainProcessor. They are now handled by dedicated plugin nodes following the Bitwig/modular DAW pattern. What was changed:

⏳ PENDING — Next Steps


9. Testing the Graph

Quick validation

Full test suite

Headless server (integration testing)


10. Adding a New Processor Type

  1. Create a new file in graph/, e.g., my_processor.rs
  2. Implement the Processor trait (see chain_processor.rs as template)
  3. Add pub mod my_processor; to graph/mod.rs
  4. Use it: AudioNode::with_processor(id, "name", Box::new(MyProcessor::new()))
  5. Update build_graph_from_project() in songbird-sync/graph_sync.rs if this processor type should be created from project data

Trait methods to implement