Skip to main content

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.
A few things worth calling out from the graph:
  • songbird-orchestrator is the shared EngineBackendsongbird-app and songbird-headless both depend on it rather than duplicating session-build, device-management, and plugin-editor code.
  • songbird-sync is 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-state does not depend on songbird-sync at runtime — the dependency is one-way (sync → state). Echo suppression and undo/redo live in state; routing lives in sync.
  • songbird-mcp has 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 MCP tools/call requests reach the same dispatch without a compile-time link.
  • songbird-voices and songbird-composition are isolatedvoices has no in-tree consumers yet; composition only depends on theory. 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 traitprocess() 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:
  1. Add a variant to the appropriate channel’s defs.rs command enum.
  2. Add a dispatch arm in the channel’s commands.rs.
  3. Run ./utils/sync-types to regenerate TypeScript bindings.
  4. Use send('channel.action', payload) from the React side.
The full skill is in .claude/skills/add-sync-path/SKILL.md.

State Management

The state pipeline is a single clean design:
  • 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/main and refs/redo-tip.
See crates/data/songbird-state/SPEC.md for the full invariant set.

Testing

The workspace has 5,240 tests organized by purpose:

Key Design Decisions

Graph Topology

Songbird uses a connection-based DAG with Connection 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 via rayon::scope. All metadata is cached so process() does zero allocation.

Nested Device Chains (Bitwig-inspired)

The PluginChain 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 same Plugin 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

The songbird-headless crate runs a standalone WebSocket server that serves the same protocol as the Tauri app, enabling browser-based UI without Tauri:
Protocol: Text frames use { "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.