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 viainvoke() 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)
Thesongbird-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:
0x01RT frame,0x02audio clip peaks,0x03bird 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)
Thesongbird-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:
| Channel | Authority | Key Events |
|---|---|---|
mixer | Shared | mixer:track_state, mixer:notes_changed, mixer:audio_clip_peaks, mixer:track_mixer_update, mixer:state |
transport | Shared | transport:state, transport:link_status_changed |
bird | Shared | bird:content_changed, bird:history_changed |
plugin | Engine | plugin:state |
ai | React | ai:lyria, ai:generate, ai:generate_progress, ai:generate_complete |
notifications | Engine | 13 notification events (e.g., notifications:log, notifications:loading_progress) |
settings | React | settings:changed, settings:notes |
meters | Engine | meters:rt_frame |
track | Shared | track:added, track:removed, track:renamed, track:reordered, track:recording_cleared, track:take_lane_changed |
project | Shared | project:state, project:section_added, project:section_deleted, project:section_reordered |
react_ui/src/sync/channels/). Each TS channel file also defines typed commands via XCommands interfaces and buildXCommands() factories.
Naming Conventions
| Type | Format | Examples |
|---|---|---|
| Events (Engine → JS) | channel:snake_case | mixer:track_state, transport:link_status_changed, notifications:loading_progress |
| Commands (JS → Engine) | channel.snake_case | mixer.view_mode, transport.set_bpm, recording.midi_arm |
| JS method names | camelCase | viewMode(), setBpm(), midiArm() |
| Rust method names | snake_case | view_mode(), set_bpm(), midi_arm() |
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
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
mixer:track_state | Full track state from engine (bird file load) | |
mixer:notes_changed | Lightweight MIDI note deltas | |
mixer:audio_clip_peaks | Async waveform peak data per clip | |
mixer:track_mixer_update | Per-track volume/pan/mute/solo from engine | |
mixer:state | Full mixer state sync | |
| Commands (JS → Engine) | ||
mixer.volume | Set track volume (trackIdx, param, value) | |
mixer.pan | Set track pan (trackIdx, param, value) | |
mixer.mute | Toggle track mute (trackIdx, value) | |
mixer.solo | Toggle track solo (trackIdx, value) | |
mixer.view_mode | Set mixer view mode (value) | |
mixer.melodyne | Close Melodyne overlay | |
mixer.keyboard | Toggle MIDI keyboard mode (value) | |
mixer.send_level | Set send level (trackIdx, param, value) | |
mixer.send_mode | Set send mode (trackIdx, value) | |
mixer.sidechain | Set sidechain source (trackIdx, value) | |
mixer.plugin_param | Set plugin parameter RT (trackIdx, param, value) | |
mixer.set_track_mixer | Batch set track mixer state (trackIdx, volumeDb, pan, mute, solo) |
transport — Playback & Timing
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
transport:state | Full transport state (BPM, position, key, scale, loop) | |
transport:link_status_changed | Ableton Link peer status update | |
| Commands (JS → Engine) | ||
transport.play | Start playback | |
transport.pause | Pause playback | |
transport.stop | Stop and return to start | |
transport.record | Toggle recording (value) | |
transport.scrub | Scrub playhead (value) | |
transport.position | Set playhead position RT (value) | |
transport.set_bpm | Set tempo (bpm) | |
transport.set_looping | Toggle loop (value) | |
transport.set_loop_range | Set loop bounds (startBar, endBar) | |
transport.enable_link | Toggle Ableton Link (value) | |
transport.enable_link_start_stop_sync | Toggle Link start/stop sync (value) | |
transport.set_link_custom_offset | Set Link custom offset (ms) |
track — Track Management (+ sub-domains: clip, take_lane, recording)
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
track:added | A track was added | |
track:removed | A track was removed | |
track:renamed | A track was renamed | |
track:reordered | Track order changed | |
track:recording_cleared | Recorded content cleared | |
track:take_lane_changed | Take lane added or auditioned | |
| Commands (JS → Engine) | ||
track.add_audio | Create new audio track | |
track.add_midi | Create new MIDI track | |
track.remove | Remove track (trackIdx) | |
track.rename | Rename track (trackIdx, value) | |
track.clear_recorded | Clear recorded MIDI (trackIdx) | |
track.reorder | Reorder tracks (value: JSON array of IDs) | |
clip.delete | Delete audio clip (trackIdx, param) — routes to track channel | |
take_lane.add | Add take lane (trackIdx) — routes to track channel | |
take_lane.audition | Audition take lane (trackIdx, value) — routes to track channel | |
recording.midi_arm | Toggle MIDI record arm (trackIdx, value) — routes to track channel | |
recording.audio_arm | Toggle audio record arm (trackIdx, value) — routes to track channel | |
recording.audio_source | Set audio input source (trackIdx, param, value) — routes to track channel | |
recording.midi_input | Set MIDI input device (trackIdx, value) — routes to track channel | |
recording.monitor | Set input monitoring mode (trackIdx, value) — routes to track channel |
project — Project Settings & Sections (+ sub-domain: section)
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
project:state | Full project settings (BPM, key, scale, time sig) | |
project:section_added | A section was added | |
project:section_deleted | A section was deleted | |
project:section_reordered | Section order changed | |
| Commands (JS → Engine) | ||
section.add | Add section (param: name, value: bar count) — routes to project channel | |
section.delete | Delete section (param: name) — routes to project channel | |
section.rename | Rename section (param: old name, value: new name) — routes to project channel | |
section.reorder | Reorder sections (value: JSON array) — routes to project channel |
bird — .bird File Content
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
bird:content_changed | Full .bird file content update | |
bird:history_changed | Git history changed (undo/redo, new commit) | |
| Commands | None — bird mutations go through the bird mutator pipeline, not the command system. |
ai — AI Generation
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
ai:lyria | Lyria music generation config state | |
ai:generate | Generate job state | |
ai:generate_progress | Generation progress (ephemeral, via CustomEvent) | |
ai:generate_complete | Generation completed (ephemeral, via CustomEvent) | |
| Commands (JS → Engine) | ||
ai.plugin_param | Set plugin parameter via AI path (trackIdx, param, value) |
plugin — Plugin State
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
plugin:state | Plugin parameter/state update from engine | |
| Commands (JS → Engine) | ||
plugin.change | Swap plugin on a track slot (trackIdx, param, value) | |
plugin.bypass | Toggle plugin bypass (trackIdx, param, value) | |
plugin.open | Open plugin editor window (trackIdx, param) |
chat — AI Chat State
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
chat:state | Full chat state sync (messages, threads, panel visibility) | |
| Commands | None — chat state is written directly to the Zustand store, not via commands. |
settings — App Settings
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
settings:changed | Settings update from engine | |
settings:notes | Notes/comments state update (undo/redo, collab) | |
| Commands | None — settings are persisted directly via the StateStorage bridge. |
notifications — Ephemeral Engine Notifications (fire-and-forget)
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
notifications:log | Debug log forwarding from engine to browser console | |
notifications:dropout_detected | Audio dropout/glitch warning | |
notifications:loading_progress | Project loading progress | |
notifications:show_project_picker | Signal to show project picker UI | |
notifications:recording_started | Recording session started | |
notifications:recording_stopped | Recording session stopped | |
notifications:live_note_on | Live MIDI note on (ghost notes, activity) | |
notifications:live_note_off | Live MIDI note off | |
notifications:terminal_output | Terminal process output streaming | |
notifications:export_progress | Stem export progress | |
notifications:export_done | Stem export completed | |
notifications:melodyne_overlay_opened | Melodyne ARA overlay opened | |
notifications:melodyne_overlay_closed | Melodyne ARA overlay closed | |
| Commands | None — notifications are one-directional (Engine → JS). |
meters — Real-Time Audio Metering
| Direction | Name | Description |
|---|---|---|
| Events (Engine → JS) | ||
meters:rt_frame | Real-time meter data at ~30Hz (levels, spectrum, stereo, CPU, position) | |
| Commands | None — 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:
| Guard | Purpose |
|---|---|
loadFinished | Block until project loading is complete |
notUndoRedo | Block echo during undo/redo operations |
notMidiEditing | Block during MIDI edit transactions |
notStreaming | Block during AI chat streaming |
notSliderDragging | Block persist during slider gestures |
notHydrating | Prevent updates during initialization |
notLoopCoolingDown | Debounce loop range changes |
notLoopRangeOwnedByUi | UI owns loop range during drag |
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:| Source | Description |
|---|---|
React | UI action (user interaction) |
Engine | Audio engine state change |
Collab | Remote peer via collaboration |
FileWatcher | Git 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
Transport Implementations
| Transport | Direction | Description |
|---|---|---|
DirectEngineTransport | Rust → Engine | Zero-serialization hot path for continuous gestures (slider drags, scrub). Pre-resolves dispatcher functions at init time. |
NativeFunctionTransport | Rust → Engine | Structured state commits via native function bridge. Maps channels to native function names. |
WebSocketTransport | Bidirectional | Text frames (JSON) + binary frames with tag-byte routing. Supports the headless server and browser WebSocket connections. |
CollabTransport | Bidirectional | Remote sync via collab server WebSocket. Handles connect/disconnect lifecycle and incoming message dispatch. |
NullTransport | No-op | For channels that don’t use a particular direction. |
Binary Frame Protocol
Binary frames use a tag-byte prefix for zero-overhead routing:| Tag | Name | Content |
|---|---|---|
0x01 | RT Frame | Metering, transport position, spectrum data (~30fps) |
0x02 | Audio Clip Peaks | Waveform peak data for display |
0x03 | Bird Mutation | Result of a .bird file edit |
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:| File | Tracked | Purpose |
|---|---|---|
daw.bird | ✅ Git | Composition — notes, arrangement, structure |
daw.mixer.json | ✅ Git | Mixer state — volumes, pans, mutes, solos, sends |
daw.state.json | ✅ Git | Project state — transport, markers, tempo/key/time-sig maps |
daw.plugins.json | ✅ Git | Plugin state — VST3/AU presets as structured JSON |
daw.ai.json | ✅ Git | AI state — chat threads, Lyria config, generation history |
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
| Mode | Description |
|---|---|
Native | Running inside Tauri WebView — uses window.__SONGBIRD__ native interop |
WebSocket | Running in a standard browser — uses WebSocket bridge |
PersistGate
Centralized gating logic that decides whether a store’ssetItem call should proceed to disk:
- Hydration gate — During initial load, don’t persist back (data just came from backend)
- Slider drag gate — During mixer slider drags, suppress mixer persist (audio feedback via RT bypass)
- Streaming gate — During AI chat streaming, suppress chat persist (flush at end)
SliderDragGuard
Per-fader persist suppression using anAtomicU32 counter:
begin_drag()→ increments counterend_drag()→ decrements counter, returnstruewhen all drags endedis_dragging()→truewhile any slider is active
Zustand Stores (React)
Persisted stores synced to the Rust backend:| Store | ID | Persisted To |
|---|---|---|
useTransportStore | transport:state | daw.state.json |
useMixerStore | mixer:state | daw.mixer.json |
useChatStore | chat:state | daw.ai.json |
useGenerateStore | ai:generate | daw.ai.json |
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.- Batched Rust to JS: Real-time telemetry is grouped into a single payload and emitted at ~30Hz.
- Direct-DOM Buffer (
rtBuffer): Data is written directly to a shared mutable object (getRtBuffer()). This avoids React re-renders completely. - Ballistic Smoothing: A single
requestAnimationFrameloop handles smoothing (e.g. meter decay) and notifies direct subscribers (canvas, plain DOM nodes). - Zustand Throttling: The full Zustand store (
useMeterStore) is only updated every N frames (~20Hz) for React components that need reactive bindings. - DirectEngineTransport: Slider drags use a real-time bypass path that skips guards and persistence, writing directly to the engine.
Smoothing Constants
| Constant | Value | Purpose |
|---|---|---|
LEVEL_RELEASE | 0.78 | Meter decay rate |
SPECTRUM_RELEASE | 0.68 | Spectrum analyzer decay |
STEREO_SMOOTH | 0.72 | Stereo field smoothing |
CPU_SMOOTH | 0.90 | CPU usage smoothing |
Loading Sequence
GuardFlags During Load
| Flag | Set When | Purpose |
|---|---|---|
store_hydrated | All React stores hydrate | Gates ALL state commits |
hydrating | During initial hydration | Prevents updates during initialization |
undo_redo_in_progress | During undo/redo | Blocks echo commits during state restoration |
slider_dragging | During slider gesture | Blocks 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/redorefs/redo-tip— created on first undo, points to the “newest” undone commit
Operations
Commit:- Check for uncommitted changes — skip if working tree is clean
- Delete
refs/redo-tip(new change invalidates redo) - Create commit on
main - Emit
bird:history_changedto React
- Block if HEAD message contains “Project loaded” or “Initial project state”
- If no
refs/redo-tip, create it pointing to current HEAD - Get HEAD’s parent commit
- Diff HEAD vs parent → get changed files
- Restore working directory from parent
- Move
refs/heads/mainto parent
- Look up
refs/redo-tip— if absent, nothing to redo - Walk backward from redo-tip to find the child of current HEAD
- Diff HEAD vs child → get changed files
- Restore working directory from child
- Move
refs/heads/mainto child - If HEAD now equals redo-tip, delete the ref
Undo/Redo Orchestration
- Set
undo_redo_in_progressguard flag - Flush pending state to disk
- Perform undo/redo via git2
- 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
- Emit
bird:history_changedto update UI - Clear
undo_redo_in_progressguard (blocks React persist echoes)
Commit Sources
Every commit message is tagged with a source:| Tag | Source | Example |
|---|---|---|
[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:- Version counter echo — Each channel tracks a monotonic version; subscribers skip updates they’ve already seen
- JSON compare echo — For channels using
JsonComparestrategy, incoming JSON is compared against cached state (with configurable field rounding for floats like volume/pan) undo_redo_in_progressguard — Blocks all channel commits during undo/redostore_hydratedguard — Blocks ALL commits until initial hydration completesslider_draggingguard — Blocks persistence while user is dragging (usesSliderDragGuardatomic counter)- UpdateSource tagging — Each change carries its origin (React, Engine, Collab); subscribers can filter by source
- 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
getHistoryIPC command (reads git via git2 revwalk) - Auto-refreshes on
bird:history_changedevents - Expandable toggle at bottom of app
Key Files
Sync Engine (Rust — songbird-sync)
| File | Role |
|---|---|
rust/crates/state/songbird-sync/src/engine_core.rs | Central state router: guards → echo → subscribers → persist |
rust/crates/state/songbird-sync/src/engine.rs | Top-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.rs | Event routing table + WiredSyncEngine |
rust/crates/state/songbird-sync/src/wiring_orchestration.rs | Orchestrate wiring across multiple components |
rust/crates/state/songbird-sync/src/transports.rs | Transport trait + 5 implementations (DirectEngine, NativeFunction, WebSocket, Collab, Null) |
rust/crates/state/songbird-sync/src/profiler.rs | Channel-aware performance profiler |
rust/crates/state/songbird-sync/src/batch_throttle.rs | Batch and throttle middleware for high-frequency events |
State Management (Rust — songbird-state)
| File | Role |
|---|---|
rust/crates/state/songbird-state/src/sync/dispatch.rs | EventDispatcher pipeline with DispatchResult |
rust/crates/state/songbird-state/src/sync/store_bridge.rs | Bridge between SyncEngine and AppStore (hydrate, snapshot, partialize) |
rust/crates/state/songbird-state/src/command_dispatch/invoke.rs | Protocol-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.rs | BridgeMode, SliderDragGuard, PersistGate, WsBridgeProtocol |
rust/crates/state/songbird-state/src/bridges/native_dispatch.rs | Native function dispatch + RT message building |
rust/crates/state/songbird-state/src/undo_redo.rs | Git-based undo/redo via git2 |
rust/crates/state/songbird-state/src/project_mgmt/bird_mutator.rs | Typed mutation descriptors + MutationApplicator trait |
rust/crates/state/songbird-state/src/store_slices.rs | Zustand store slice types mirroring React frontend |
rust/crates/state/songbird-state/src/project.rs | Plain serializable data model (tracks, plugins, clips) |
rust/crates/state/songbird-state/src/collab_mgmt/collab.rs | Collaboration state sync |
Headless & CLI (Rust)
| File | Role |
|---|---|
rust/crates/app/songbird-headless/src/main.rs | WebSocket server entry point |
rust/crates/app/songbird-headless/src/command_handler.rs | Processes text-frame commands |
rust/crates/app/songbird-headless/src/rt_frame.rs | Binary RT frame encoding + broadcast |
rust/crates/app/songbird-cli/src/main.rs | CLI entry point for scripted operations |
React UI
| File | Role |
|---|---|
react_ui/src/sync/engine.ts | Sync engine initialization + typed send() |
react_ui/src/sync/commandMap.ts | CommandMap union type, resolveCommand() domain→channel router |
react_ui/src/sync/api.ts | Clean send() and sendRT() helpers |
react_ui/src/sync/channels/ | Per-channel definitions (XCommands interfaces + buildXCommands factories) |
react_ui/src/sync/wiring.ts | Channel registration + transport wiring |
react_ui/src/sync/transports/ | DirectEngine + WebSocket transport implementations |
react_ui/src/data/store.ts | Zustand stores, hydration tracking, event listeners |
react_ui/src/data/meters.ts | Real-time batched event store, RT Buffer, ballistic smoothing |
react_ui/src/data/slices/mixer.ts | Mixer state actions (setVolume, setPan with rounding) |
react_ui/src/components/panels/HistoryPanel.tsx | Git log UI |
Tauri App (Rust)
| File | Role |
|---|---|
rust/crates/app/songbird-app/src/main.rs | Tauri IPC command handlers |
rust/crates/app/songbird-app/src/native_invoke.rs | Tauri invoke bridge (loadState, command routing) |
rust/crates/app/songbird-app/src/emit_state.rs | Push state updates from engine to React |