Songbird Rust Engine (rust/)
The Rust backend provides the complete audio engine, DSP plugins, clip management, state synchronization, and the dispatch layer for Songbird. The audio engine and entire backend are pure Rust; a thin C ABI FFI bridge (songbird-host-ffi) hosts third-party VST3/AU plugins via a JUCE wrapper. The same Rust core powers the Tauri desktop app, the headless WebSocket server, the CLI, and the WASM/web build.
Tech Stack
- Rust 2021 edition — Systems language with memory safety guarantees
- Tauri v2 — Desktop app shell
- cpal — Cross-platform audio I/O
- midir — Cross-platform MIDI I/O
- rayon — Data-parallel graph processing
- serde / serde_json — Serialization (project files, IPC, state sync)
- ts-rs — TypeScript bindings generated from Rust enums
- hound — WAV file reading/writing
- git2 (libgit2) — Git operations for sync engine
- crossbeam-channel — Lock-free communication between audio and UI threads
Workspace Structure
The workspace is organized into seven layers, bottom-up:Crate Dependency Graph
Every edge below is an actual[dependencies] entry in the relevant
Cargo.toml (dev-dependencies excluded). Crates are clustered by the
crates/<group>/ directory they live in.
songbird-orchestratoris the sharedEngineBackend—songbird-appandsongbird-headlessboth depend on it rather than duplicating session-build, device-management, and plugin-editor code.songbird-syncis the dispatch hub — it’s the only crate that depends on the ML, generation (Lyria, Veo, separator), and analytics crates, because it routes commands from those channels to their handlers. The engine and state crates know nothing about ML.songbird-statedoes not depend onsongbird-syncat runtime — the dependency is one-way (sync → state). Echo suppression and undo/redo live instate; routing lives insync.songbird-mcphas zero Rust dependencies — it’s a standalone binary that talks to the running desktop app over a Unix Domain Socket bridge (~/.songbird/mcp.sock), so MCPtools/callrequests reach the samedispatchwithout a compile-time link.songbird-voicesandsongbird-compositionare isolated —voiceshas no in-tree consumers yet;compositiononly depends ontheory. Both are scaffolding for upcoming work.- The
core/layer (types,dsp,theory,ffi) is zero-dep — every other crate ultimately roots into it, but it depends on nothing in the workspace.
Architecture Overview
Build & Development
Audio Processing Pipeline
The audio callback processes data through this pipeline each block:Real-Time Safety
The audio callback path is designed for zero allocation:- AudioBuffer — inline
[*mut f32; 8], no heap allocation - ScratchPool — pre-allocated per-node buffers, assigned in
rebuild_topology() - SpscRingBuffer — lock-free single-producer single-consumer for engine↔UI commands
- No Mutex in audio path — all shared state accessed via lock-free ring buffers
- Plugin trait —
process()operates on pre-allocated buffers
Dispatch Layer
The Tauri app exposes a single#[tauri::command] (native_invoke)
that funnels every UI ↔ backend call through
songbird_sync::dispatch::dispatch(). Headless mode uses the same
dispatch over a WebSocket transport; the WASM build uses an in-process
transport. This is enforced as a Hard Rule — no new #[tauri::command]
is allowed outside the sync engine.
Commands are addressed by "channel.action" strings (e.g.
transport.play, mixer.volume, clip.delete). Events flow the
other way as "channel:event" (e.g. transport:state,
mixer:audio_clip_peaks). The 20 channels live under
rust/crates/data/songbird-sync/src/channels/; each has a defs.rs
(events / command enums via ts-rs) and a commands.rs (dispatch arms).
To add a new command:
- Add a variant to the appropriate channel’s
defs.rscommand enum. - Add a dispatch arm in the channel’s
commands.rs. - Run
./utils/sync-typesto regenerate TypeScript bindings. - Use
send('channel.action', payload)from the React side.
.claude/skills/add-sync-path/SKILL.md.
State Management
- Typed mutations — channels declare command/event enums via ts-rs; TS types are generated, not hand-written.
- Echo suppression — version counters and source tags prevent loops between the React and engine sides.
- Guard flags — slider-drag guard, persistence gate, and other guards configured per-channel.
- Undo/redo — every undoable mutation is committed to an in-process libgit2 repo; undo/redo are git operations against
refs/heads/mainandrefs/redo-tip.
crates/data/songbird-state/SPEC.md for the full invariant set.
Testing
The workspace has 5,240 tests organized by purpose:| Test Suite | Location | Count | What It Tests |
|---|---|---|---|
| Unit tests | Each crate’s mod tests | ~2,500 | Individual module correctness |
| E2E engine tests | engine_e2e_tests.rs | 15 | Full pipeline: transport → graph → clips → output |
| Audio output tests | audio_output_tests.rs | 25 | Waveform correctness, DSP parity, golden output |
| IPC roundtrip tests | ipc_roundtrip_tests.rs | 87 | Serialize/deserialize all IPC response types |
| IPC workflow tests | ipc_workflow_tests.rs | 15 | Full user workflows through IPC layer |
| IPC coverage tests | ipc_handler_coverage_tests.rs | 54 | Every sync-engine channel command wired correctly |
| Engine controller tests | engine_controller.rs | 179 | Offline rendering, callbacks, cross-crate integration, robustness hardening |
| Edge case tests | edge_case_tests.rs | 25 | NaN, Inf, empty buffers, extreme values |
| Stress tests | stress_tests.rs | 15 | Concurrent mutations, MIDI floods, large graphs |
| Performance tests | perf_tests.rs | 15 | Graph throughput, parallel speedup, zero-alloc |
| RT safety tests | rt_safety_tests.rs | 25 | Zero allocation in audio callback path |
| Project format tests | project_format_tests.rs | 15 | JSON round-trip, forward compat, Unicode |
| Plugin factory tests | songbird-plugins/lib.rs | 3 | Create, round-trip state for all stock plugins |
Key Design Decisions
Graph Topology
Songbird uses a connection-based DAG withConnection structs carrying port indices, rather than a flat track-centric model. This enables sidechain routing, parallel compression, and multi-bus routing without architecture changes. Topological sort via Kahn’s algorithm is cached in rebuild_topology().
Parallel Processing (via rayon)
Independent branches of the audio graph execute concurrently. The topological sort assigns levels — nodes at the same level have no dependencies and can be processed in parallel viarayon::scope. All metadata is cached so process() does zero allocation.
Nested Device Chains (Bitwig-inspired)
ThePluginChain supports ChainNode variants that can be either a Plugin or a Container holding sub-chains. This enables parallel processing chains, multiband splits, and mid/side processing at the track level.
Plugin Trait
All plugins — stock and external — implement the samePlugin trait:
FFI Bridge (for C++ Plugin Hosting)
Rust owns the audio thread. Third-party VST3/AU plugins are hosted via JUCE through a C ABI FFI layer (songbird-host-ffi). The bridge uses non-interleaved buffer layout matching JUCE’s AudioBuffer<float>. The Plugin trait does not assume shared memory, enabling future out-of-process hosting.
Headless WebSocket Server
Thesongbird-headless crate runs a standalone WebSocket server that serves the same protocol as the Tauri app, enabling browser-based UI without Tauri:
{ "eventId": "<command>", "payload": <json> } format (same as the desktop dispatch). Binary RT frames broadcast at ~30fps with metering, transport position, recording peaks, and Link status.
See ARCHITECTURE.md for the full system design.