Skip to main content

Sync Engine

For humans and LLMs contributing to Songbird.

Overview

Songbird’s Sync Engine is the central nervous system of the application. It routes all state changes between the UI, the audio engine, the file system, and collaboration peers. Every meaningful change flows through a single pipeline — guard check → echo suppression → subscriber notification → persistence — and is committed to an in-process Git repo for undo/redo. The sync engine is UI-agnostic: the same core runs whether the frontend is a Tauri WebView (desktop), a browser connecting over WebSocket (headless server), or a CLI script rendering stems on a VM.

UI Client Modes

The sync engine supports multiple UI frontends connecting to the same Rust engine core. This enables desktop development, remote server deployments, and headless batch processing with the same codebase.

Tauri WebView (Desktop)

The primary desktop app. React runs inside Tauri’s WebView, communicating with the Rust backend via invoke() IPC. This is the full-featured mode with GUI, metering, plugin UIs, and real-time audio.
  • Transport: Tauri IPC (window.__SONGBIRD__.invoke())
  • Real-time data: Tauri events at ~30Hz (metering, transport position)
  • Use case: Day-to-day music production

WebSocket Headless (Server / VM)

The songbird-headless binary runs the full audio engine as a WebSocket server with no GUI. Any WebSocket client — including the same React UI running in a standard browser — can connect and control the engine remotely.
  • Transport: WebSocket text frames (JSON commands) + binary frames (RT data)
  • Binary frame tags: 0x01 RT frame, 0x02 audio clip peaks, 0x03 bird mutation
  • Protocol: Same command/event schema as Tauri IPC — the React UI is backend-agnostic
  • Use case: Running on cloud VMs, remote collaboration servers, CI/CD render farms, headless recording rigs

CLI (Scripted Operations)

The songbird-cli binary provides direct Rust API access for batch operations — export, render, validation. No UI, no WebSocket, no audio I/O.
  • Transport: Direct Rust function calls (no IPC overhead)
  • Use case: ./songbird-cli render project.bird -o output.wav, CI pipelines, automated stem exports

Sync Engine Architecture

SyncEngineCore — songbird-sync/src/engine_core.rs

Central router for all state updates. Every inbound event flows through a fixed pipeline:

Channels — songbird-sync/src/channels/

Twenty domain channels, each defined as its own submodule under channels/ with a defs.rs (events / command names / wiring) and commands.rs (dispatch arms). Each channel specifies its authority and event list: The TS side additionally configures echo strategy, persistence, and guard flags per channel (see react_ui/src/sync/channels/). Each TS channel file also defines typed commands via XCommands interfaces and buildXCommands() factories.

Naming Conventions

Command strings use snake_case for legibility, but JS handler method names remain camelCase per JS convention. The resolveCommand() function in commandMap.ts converts 'mixer.view_mode'['mixer', 'viewMode'] for handler lookup. Sub-domain commands (e.g., clip.delete, take_lane.add, recording.midi_arm, section.add) route to their parent channel (track or project) with a prefixed lookup key (e.g., 'clip:delete', 'take_lane:add').

Per-Channel Reference

Below is the complete list of events and commands for each channel, grouped by direction.

mixer — Track Audio Parameters


transport — Playback & Timing


recording — Track Management & Recording (+ sub-domains: clip, take_lane)

Renamed from track. The standalone clip, take_lane, and recording namespaces now route through this channel. The legacy track:* event names have moved under recording:*.

project — Project Settings & Sections (+ sub-domain: section)


clip — Clip Content (replaces the old bird channel)


ml — ML / AI Generation (replaces the old ai channel)

Covers Lyria music generation, Veo video generation, local LLM chat inference, and model-download progress.

plugin — Plugin State


chat — AI Chat State


settings — App Settings


notifications — Ephemeral Engine Notifications (fire-and-forget)


meters — Real-Time Audio Metering


GuardFlags — songbird-sync/src/guards.rs

Shared state gates that block updates in specific scenarios: Guards are implemented as Arc<AtomicBool> flags in Rust, providing thread-safe, lock-free gating. Each channel specifies which guards must pass before an update is accepted.

UpdateSource

Every change is tagged with its origin for echo suppression:

Transport Layer

The sync engine uses trait-based transports to abstract how data flows between system components. This is what enables the same engine to serve Tauri, WebSocket, and CLI frontends.

Transport Trait

Transport Implementations

Binary Frame Protocol

Binary frames use a tag-byte prefix for zero-overhead routing: The WebSocketTransport automatically decodes binary frames via tag routing and dispatches to both binary and text (parsed JSON) handlers.

Files on Disk

Each project directory contains: Rule: All daw.* files are git-tracked and participate in undo/redo.

Bridge Layer — songbird-state/src/bridge_layer.rs

The bridge layer sits between the UI stores and the sync engine, handling persistence gating and mode detection.

BridgeMode

PersistGate

Centralized gating logic that decides whether a store’s setItem call should proceed to disk:
  1. Hydration gate — During initial load, don’t persist back (data just came from backend)
  2. Slider drag gate — During mixer slider drags, suppress mixer persist (audio feedback via RT bypass)
  3. Streaming gate — During AI chat streaming, suppress chat persist (flush at end)

SliderDragGuard

Per-fader persist suppression using an AtomicU32 counter:
  • begin_drag() → increments counter
  • end_drag() → decrements counter, returns true when all drags ended
  • is_dragging()true while any slider is active

Zustand Stores (React)

Persisted stores synced to the Rust backend:

Persist Flow

Hydration Flow (Load)


Real-Time Events (Engine → React)

High-frequency telemetry data (audio levels, transport position, stereo analysis, CPU stats) bypasses the standard state sync to minimize overhead.
  1. Batched Rust to JS: Real-time telemetry is grouped into a single payload and emitted at ~30Hz.
  2. Direct-DOM Buffer (rtBuffer): Data is written directly to a shared mutable object (getRtBuffer()). This avoids React re-renders completely.
  3. Ballistic Smoothing: A single requestAnimationFrame loop handles smoothing (e.g. meter decay) and notifies direct subscribers (canvas, plain DOM nodes).
  4. Zustand Throttling: The full Zustand store (useMeterStore) is only updated every N frames (~20Hz) for React components that need reactive bindings.
  5. DirectEngineTransport: Slider drags use a real-time bypass path that skips guards and persistence, writing directly to the engine.

Smoothing Constants


Loading Sequence

GuardFlags During Load


Undo/Redo System — songbird-state/src/undo_redo.rs

Uses libgit2 (via the git2 crate, in-process, zero fork) for git operations.

Branch Structure

  • refs/heads/main — current position (HEAD), moves on undo/redo
  • refs/redo-tip — created on first undo, points to the “newest” undone commit

Operations

Commit:
  1. Check for uncommitted changes — skip if working tree is clean
  2. Delete refs/redo-tip (new change invalidates redo)
  3. Create commit on main
  4. Emit clip:history_changed to React
Undo:
  1. Block if HEAD message contains “Project loaded” or “Initial project state”
  2. If no refs/redo-tip, create it pointing to current HEAD
  3. Get HEAD’s parent commit
  4. Diff HEAD vs parent → get changed files
  5. Restore working directory from parent
  6. Move refs/heads/main to parent
Redo:
  1. Look up refs/redo-tip — if absent, nothing to redo
  2. Walk backward from redo-tip to find the child of current HEAD
  3. Diff HEAD vs child → get changed files
  4. Restore working directory from child
  5. Move refs/heads/main to child
  6. If HEAD now equals redo-tip, delete the ref

Undo/Redo Orchestration

  1. Set undo_redo_in_progress guard flag
  2. Flush pending state to disk
  3. Perform undo/redo via git2
  4. Reload changed files:
    • .bird → re-parse via songbird-clips pipeline
    • .plugins.json → restore plugin state
    • .mixer.json → reload mixer and apply to engine + push to React
  5. Emit clip:history_changed to update UI
  6. Clear undo_redo_in_progress guard (blocks React persist echoes)

Commit Sources

Every commit message is tagged with a source:

Echo Prevention (Critical)

The sync engine uses multiple mechanisms to prevent feedback loops:
  1. Version counter echo — Each channel tracks a monotonic version; subscribers skip updates they’ve already seen
  2. JSON compare echo — For channels using JsonCompare strategy, incoming JSON is compared against cached state (with configurable field rounding for floats like volume/pan)
  3. undo_redo_in_progress guard — Blocks all channel commits during undo/redo
  4. store_hydrated guard — Blocks ALL commits until initial hydration completes
  5. slider_dragging guard — Blocks persistence while user is dragging (uses SliderDragGuard atomic counter)
  6. UpdateSource tagging — Each change carries its origin (React, Engine, Collab); subscribers can filter by source
  7. PersistGate — Centralized check that gates disk writes on hydration, slider drag, and streaming state

History Panel — React

HistoryPanel.tsx displays live git history in git log --oneline format.
  • Fetches via getHistory IPC command (reads git via git2 revwalk)
  • Auto-refreshes on clip:history_changed events
  • Expandable toggle at bottom of app

Key Files

Sync Engine (Rust — songbird-sync)

State Management (Rust — songbird-state)

The state crate now lives at rust/crates/data/songbird-state/. Its public surface is the StateManager (channel-aligned slices), the bridges/ layer (slider-drag guard, persist gate, WS bridge protocol), and the persistence/ layer (bird IO, clips IO, git-based session history). See its SPEC.md for the full pipeline.

Headless & CLI (Rust)

React UI

Tauri App (Rust)