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
WebSocket clients (React UI, scripts, tests)

        ▼  ws://host:port
┌──────────────────────────────────────────┐
│  songbird-headless                       │
│  ├─ ServerState (Arc<Mutex<>>)           │
│  │   ├─ StateStore (project model)       │
│  │   └─ EngineSession                    │
│  ├─ command_handler.rs — text frames     │
│  ├─ rt_frame.rs — binary RT broadcast    │
│  └─ broadcast channel → all clients      │
│                                          │
│  audio_engine.rs                         │
│  ├─ cpal audio I/O                       │
│  ├─ ring buffers ↔ engine                │
│  └─ meter polling (~30fps)               │
└──────────────────────────────────────────┘

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/

Eleven domain channels, each defined in its own file under channels/. Each channel specifies its authority and event list:
ChannelAuthorityKey Events
mixerSharedmixer:track_state, mixer:notes_changed, mixer:audio_clip_peaks, mixer:track_mixer_update, mixer:state
transportSharedtransport:state, transport:link_status_changed
birdSharedbird:content_changed, bird:history_changed
pluginEngineplugin:state
aiReactai:lyria, ai:generate, ai:generate_progress, ai:generate_complete
notificationsEngine13 notification events (e.g., notifications:log, notifications:loading_progress)
settingsReactsettings:changed, settings:notes
metersEnginemeters:rt_frame
trackSharedtrack:added, track:removed, track:renamed, track:reordered, track:recording_cleared, track:take_lane_changed
projectSharedproject:state, project:section_added, project:section_deleted, project:section_reordered
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

TypeFormatExamples
Events (Engine → JS)channel:snake_casemixer:track_state, transport:link_status_changed, notifications:loading_progress
Commands (JS → Engine)channel.snake_casemixer.view_mode, transport.set_bpm, recording.midi_arm
JS method namescamelCaseviewMode(), setBpm(), midiArm()
Rust method namessnake_caseview_mode(), set_bpm(), midi_arm()
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

DirectionNameDescription
Events (Engine → JS)
mixer:track_stateFull track state from engine (bird file load)
mixer:notes_changedLightweight MIDI note deltas
mixer:audio_clip_peaksAsync waveform peak data per clip
mixer:track_mixer_updatePer-track volume/pan/mute/solo from engine
mixer:stateFull mixer state sync
Commands (JS → Engine)
mixer.volumeSet track volume (trackIdx, param, value)
mixer.panSet track pan (trackIdx, param, value)
mixer.muteToggle track mute (trackIdx, value)
mixer.soloToggle track solo (trackIdx, value)
mixer.view_modeSet mixer view mode (value)
mixer.melodyneClose Melodyne overlay
mixer.keyboardToggle MIDI keyboard mode (value)
mixer.send_levelSet send level (trackIdx, param, value)
mixer.send_modeSet send mode (trackIdx, value)
mixer.sidechainSet sidechain source (trackIdx, value)
mixer.plugin_paramSet plugin parameter RT (trackIdx, param, value)
mixer.set_track_mixerBatch set track mixer state (trackIdx, volumeDb, pan, mute, solo)

transport — Playback & Timing

DirectionNameDescription
Events (Engine → JS)
transport:stateFull transport state (BPM, position, key, scale, loop)
transport:link_status_changedAbleton Link peer status update
Commands (JS → Engine)
transport.playStart playback
transport.pausePause playback
transport.stopStop and return to start
transport.recordToggle recording (value)
transport.scrubScrub playhead (value)
transport.positionSet playhead position RT (value)
transport.set_bpmSet tempo (bpm)
transport.set_loopingToggle loop (value)
transport.set_loop_rangeSet loop bounds (startBar, endBar)
transport.enable_linkToggle Ableton Link (value)
transport.enable_link_start_stop_syncToggle Link start/stop sync (value)
transport.set_link_custom_offsetSet Link custom offset (ms)

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

DirectionNameDescription
Events (Engine → JS)
track:addedA track was added
track:removedA track was removed
track:renamedA track was renamed
track:reorderedTrack order changed
track:recording_clearedRecorded content cleared
track:take_lane_changedTake lane added or auditioned
Commands (JS → Engine)
track.add_audioCreate new audio track
track.add_midiCreate new MIDI track
track.removeRemove track (trackIdx)
track.renameRename track (trackIdx, value)
track.clear_recordedClear recorded MIDI (trackIdx)
track.reorderReorder tracks (value: JSON array of IDs)
clip.deleteDelete audio clip (trackIdx, param) — routes to track channel
take_lane.addAdd take lane (trackIdx) — routes to track channel
take_lane.auditionAudition take lane (trackIdx, value) — routes to track channel
recording.midi_armToggle MIDI record arm (trackIdx, value) — routes to track channel
recording.audio_armToggle audio record arm (trackIdx, value) — routes to track channel
recording.audio_sourceSet audio input source (trackIdx, param, value) — routes to track channel
recording.midi_inputSet MIDI input device (trackIdx, value) — routes to track channel
recording.monitorSet input monitoring mode (trackIdx, value) — routes to track channel

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

DirectionNameDescription
Events (Engine → JS)
project:stateFull project settings (BPM, key, scale, time sig)
project:section_addedA section was added
project:section_deletedA section was deleted
project:section_reorderedSection order changed
Commands (JS → Engine)
section.addAdd section (param: name, value: bar count) — routes to project channel
section.deleteDelete section (param: name) — routes to project channel
section.renameRename section (param: old name, value: new name) — routes to project channel
section.reorderReorder sections (value: JSON array) — routes to project channel

bird — .bird File Content

DirectionNameDescription
Events (Engine → JS)
bird:content_changedFull .bird file content update
bird:history_changedGit history changed (undo/redo, new commit)
CommandsNone — bird mutations go through the bird mutator pipeline, not the command system.

ai — AI Generation

DirectionNameDescription
Events (Engine → JS)
ai:lyriaLyria music generation config state
ai:generateGenerate job state
ai:generate_progressGeneration progress (ephemeral, via CustomEvent)
ai:generate_completeGeneration completed (ephemeral, via CustomEvent)
Commands (JS → Engine)
ai.plugin_paramSet plugin parameter via AI path (trackIdx, param, value)

plugin — Plugin State

DirectionNameDescription
Events (Engine → JS)
plugin:statePlugin parameter/state update from engine
Commands (JS → Engine)
plugin.changeSwap plugin on a track slot (trackIdx, param, value)
plugin.bypassToggle plugin bypass (trackIdx, param, value)
plugin.openOpen plugin editor window (trackIdx, param)

chat — AI Chat State

DirectionNameDescription
Events (Engine → JS)
chat:stateFull chat state sync (messages, threads, panel visibility)
CommandsNone — chat state is written directly to the Zustand store, not via commands.

settings — App Settings

DirectionNameDescription
Events (Engine → JS)
settings:changedSettings update from engine
settings:notesNotes/comments state update (undo/redo, collab)
CommandsNone — settings are persisted directly via the StateStorage bridge.

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

DirectionNameDescription
Events (Engine → JS)
notifications:logDebug log forwarding from engine to browser console
notifications:dropout_detectedAudio dropout/glitch warning
notifications:loading_progressProject loading progress
notifications:show_project_pickerSignal to show project picker UI
notifications:recording_startedRecording session started
notifications:recording_stoppedRecording session stopped
notifications:live_note_onLive MIDI note on (ghost notes, activity)
notifications:live_note_offLive MIDI note off
notifications:terminal_outputTerminal process output streaming
notifications:export_progressStem export progress
notifications:export_doneStem export completed
notifications:melodyne_overlay_openedMelodyne ARA overlay opened
notifications:melodyne_overlay_closedMelodyne ARA overlay closed
CommandsNone — notifications are one-directional (Engine → JS).

meters — Real-Time Audio Metering

DirectionNameDescription
Events (Engine → JS)
meters:rt_frameReal-time meter data at ~30Hz (levels, spectrum, stereo, CPU, position)
CommandsNone — meters are read-only from JS. Data arrives via binary transport.

GuardFlags — songbird-sync/src/guards.rs

Shared state gates that block updates in specific scenarios:
GuardPurpose
loadFinishedBlock until project loading is complete
notUndoRedoBlock echo during undo/redo operations
notMidiEditingBlock during MIDI edit transactions
notStreamingBlock during AI chat streaming
notSliderDraggingBlock persist during slider gestures
notHydratingPrevent updates during initialization
notLoopCoolingDownDebounce loop range changes
notLoopRangeOwnedByUiUI owns loop range during drag
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:
SourceDescription
ReactUI action (user interaction)
EngineAudio engine state change
CollabRemote peer via collaboration
FileWatcherGit file change on disk

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

pub trait Transport: Send + Sync {
    fn id(&self) -> &str;
    fn send_event(&self, channel: &str, payload: &serde_json::Value);
    fn on_receive(&self, channel: &str, handler: ...) -> SubscriptionId;
    fn unsubscribe(&self, id: SubscriptionId);
}

// Extended for binary frames (RT data, waveform peaks)
pub trait BinaryTransport: Transport {
    fn send_binary(&self, channel: &str, buffer: &[u8]);
    fn on_receive_binary(&self, channel: &str, handler: ...) -> SubscriptionId;
}

Transport Implementations

TransportDirectionDescription
DirectEngineTransportRust → EngineZero-serialization hot path for continuous gestures (slider drags, scrub). Pre-resolves dispatcher functions at init time.
NativeFunctionTransportRust → EngineStructured state commits via native function bridge. Maps channels to native function names.
WebSocketTransportBidirectionalText frames (JSON) + binary frames with tag-byte routing. Supports the headless server and browser WebSocket connections.
CollabTransportBidirectionalRemote sync via collab server WebSocket. Handles connect/disconnect lifecycle and incoming message dispatch.
NullTransportNo-opFor channels that don’t use a particular direction.

Binary Frame Protocol

Binary frames use a tag-byte prefix for zero-overhead routing:
TagNameContent
0x01RT FrameMetering, transport position, spectrum data (~30fps)
0x02Audio Clip PeaksWaveform peak data for display
0x03Bird MutationResult of a .bird file edit
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:
FileTrackedPurpose
daw.bird✅ GitComposition — notes, arrangement, structure
daw.mixer.json✅ GitMixer state — volumes, pans, mutes, solos, sends
daw.state.json✅ GitProject state — transport, markers, tempo/key/time-sig maps
daw.plugins.json✅ GitPlugin state — VST3/AU presets as structured JSON
daw.ai.json✅ GitAI state — chat threads, Lyria config, generation history
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

ModeDescription
NativeRunning inside Tauri WebView — uses window.__SONGBIRD__ native interop
WebSocketRunning in a standard browser — uses WebSocket bridge

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:
StoreIDPersisted To
useTransportStoretransport:statedaw.state.json
useMixerStoremixer:statedaw.mixer.json
useChatStorechat:statedaw.ai.json
useGenerateStoreai:generatedaw.ai.json

Persist Flow

React setState → Zustand persist middleware → Bridge Layer
  → PersistGate check → Sync Engine → guard check → echo check → subscribers → persist

Hydration Flow (Load)

Rust loads state files from disk
  → Zustand persist getItem → IPC → Rust returns cached JSON
  → Store hydrates synchronously
  → onRehydrateStorage callback → counter++ → all stores done → reactReady()

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

ConstantValuePurpose
LEVEL_RELEASE0.78Meter decay rate
SPECTRUM_RELEASE0.68Spectrum analyzer decay
STEREO_SMOOTH0.72Stereo field smoothing
CPU_SMOOTH0.90CPU usage smoothing

Loading Sequence

1. App launches (Tauri or headless WebSocket server)
2. uiReady() from React → start background loading
3. Scan for plugins (stock + VST3/AU via FFI)
4. Load .bird file
   - bird_tokenizer → bird_parser → bird_populator → EngineSession
   - Load plugins, build mixer state
   - Apply mixer state to engine
5. Push track state to React via events
6. Save state cache → defer commit until plugins settle
7. [Meanwhile] React hydrates stores → reactReady() → sets store_hydrated
8. Plugins settle
9. Commit "Project loaded" → commits now enabled

GuardFlags During Load

FlagSet WhenPurpose
store_hydratedAll React stores hydrateGates ALL state commits
hydratingDuring initial hydrationPrevents updates during initialization
undo_redo_in_progressDuring undo/redoBlocks echo commits during state restoration
slider_draggingDuring slider gestureBlocks persist until release

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 bird: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 bird: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:
TagSourceExample
[auto]System[auto] Project loaded
[mixer]Fader/knob change[mixer] 'drums' vol 80→65
[LLM]AI copilot[LLM] Pre-LLM state
[user]Manual save/revert[user] Reverted last AI change

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 bird:history_changed events
  • Expandable toggle at bottom of app

Key Files

Sync Engine (Rust — songbird-sync)

FileRole
rust/crates/state/songbird-sync/src/engine_core.rsCentral state router: guards → echo → subscribers → persist
rust/crates/state/songbird-sync/src/engine.rsTop-level SyncEngine with connect/disconnect lifecycle
rust/crates/state/songbird-sync/src/channels/Per-channel definitions (11 files: mixer.rs, transport.rs, bird.rs, ai.rs, plugin.rs, notifications.rs, settings.rs, meters.rs, track.rs, project.rs)
rust/crates/state/songbird-sync/src/wiring.rsEvent routing table + WiredSyncEngine
rust/crates/state/songbird-sync/src/wiring_orchestration.rsOrchestrate wiring across multiple components
rust/crates/state/songbird-sync/src/transports.rsTransport trait + 5 implementations (DirectEngine, NativeFunction, WebSocket, Collab, Null)
rust/crates/state/songbird-sync/src/profiler.rsChannel-aware performance profiler
rust/crates/state/songbird-sync/src/batch_throttle.rsBatch and throttle middleware for high-frequency events

State Management (Rust — songbird-state)

FileRole
rust/crates/state/songbird-state/src/sync/dispatch.rsEventDispatcher pipeline with DispatchResult
rust/crates/state/songbird-state/src/sync/store_bridge.rsBridge between SyncEngine and AppStore (hydrate, snapshot, partialize)
rust/crates/state/songbird-state/src/command_dispatch/invoke.rsProtocol-agnostic command dispatcher (routes to per-channel modules)
rust/crates/state/songbird-state/src/command_dispatch/channels/Per-channel command handlers (11 files)
rust/crates/state/songbird-state/src/bridges/bridge_layer.rsBridgeMode, SliderDragGuard, PersistGate, WsBridgeProtocol
rust/crates/state/songbird-state/src/bridges/native_dispatch.rsNative function dispatch + RT message building
rust/crates/state/songbird-state/src/undo_redo.rsGit-based undo/redo via git2
rust/crates/state/songbird-state/src/project_mgmt/bird_mutator.rsTyped mutation descriptors + MutationApplicator trait
rust/crates/state/songbird-state/src/store_slices.rsZustand store slice types mirroring React frontend
rust/crates/state/songbird-state/src/project.rsPlain serializable data model (tracks, plugins, clips)
rust/crates/state/songbird-state/src/collab_mgmt/collab.rsCollaboration state sync

Headless & CLI (Rust)

FileRole
rust/crates/app/songbird-headless/src/main.rsWebSocket server entry point
rust/crates/app/songbird-headless/src/command_handler.rsProcesses text-frame commands
rust/crates/app/songbird-headless/src/rt_frame.rsBinary RT frame encoding + broadcast
rust/crates/app/songbird-cli/src/main.rsCLI entry point for scripted operations

React UI

FileRole
react_ui/src/sync/engine.tsSync engine initialization + typed send()
react_ui/src/sync/commandMap.tsCommandMap union type, resolveCommand() domain→channel router
react_ui/src/sync/api.tsClean send() and sendRT() helpers
react_ui/src/sync/channels/Per-channel definitions (XCommands interfaces + buildXCommands factories)
react_ui/src/sync/wiring.tsChannel registration + transport wiring
react_ui/src/sync/transports/DirectEngine + WebSocket transport implementations
react_ui/src/data/store.tsZustand stores, hydration tracking, event listeners
react_ui/src/data/meters.tsReal-time batched event store, RT Buffer, ballistic smoothing
react_ui/src/data/slices/mixer.tsMixer state actions (setVolume, setPan with rounding)
react_ui/src/components/panels/HistoryPanel.tsxGit log UI

Tauri App (Rust)

FileRole
rust/crates/app/songbird-app/src/main.rsTauri IPC command handlers
rust/crates/app/songbird-app/src/native_invoke.rsTauri invoke bridge (loadState, command routing)
rust/crates/app/songbird-app/src/emit_state.rsPush state updates from engine to React