Audio Processing (app/audio/)
Real-time audio analysis, recording, metering, and plugin state tracking.
Files
| File | Purpose |
|---|---|
PlaybackInfo.cpp/.h | 30Hz timer + background analysis thread for transport, FFT spectrum, stereo analysis, and metering. Emits batched rtFrame JSON to the WebView. |
AudioRecorder.cpp/.h | Real-time audio recording to Tracktion clips. |
AudioQuantizer.cpp/.h | Audio clip quantization to beat grid. |
MasterAnalyzerPlugin.cpp/.h | Tracktion plugin inserted on master track for raw audio tap (feeds FFT/stereo analysis). |
LevelMeterBridge.cpp/.h | Legacy per-track level metering (disabled — superseded by PlaybackInfo rtFrame batching). |
PluginStateTracker.cpp | Reactive plugin parameter change tracking. Listens to audioProcessorParameterChanged, marks dirty plugins, debounces via timer, then flushes changes to daw.edit.json and commits to git. |
DropoutDetector.h | Monitors audio callback timing. Detects buffer underruns (xruns) and timing gaps, emits dropoutDetected events to the WebView for display in the debug panel. |
Real-Time Metering Architecture
Data Flow
-
Audio thread (
MasterAnalyzerPlugin::applyToBuffer): Writes raw samples into a lock-free ring buffer and accumulates RMS values. Zero allocations, no locks. -
Background thread (
AnalysisThread, ~30Hz): Pulls from ring buffer, runs FFT + windowing → 16-band spectrum. Computes stereo width, phase correlation, and balance from RMS accumulators. Reads theLevelSnapshot(populated by the timer) and builds the complete JSON payload usingsnprintfinto a double-bufferedchar[8192]. Zero heap allocations. -
Timer callback (message thread, 30Hz): Snapshots per-track levels (
getAndClearAudioLevel) and transport position into aLevelSnapshotstruct. Reads the pre-built JSON pointer from the background thread. Posts the WebView emit viacallAsync(non-blocking — the timer returns immediately). -
JS side: Writes to a mutable
rtBuffer. ArequestAnimationFrameloop applies ballistic smoothing (instant attack, ~300ms exponential decay) and notifies DOM subscribers at display refresh rate (~60Hz).
Key Design Decisions
- 30Hz C++ → 60Hz visual: C++ only sends data 30×/sec to avoid competing with the audio thread. JS-side ballistic smoothing at display refresh rate makes meters appear 60Hz+.
- Zero allocations in steady state: Timer uses pre-allocated
juce::String(preallocateBytes). Background thread usessnprintfinto stack buffers. Nojuce::Stringconcatenation. callAsyncfor emit: The WebView IPC (emitEventIfBrowserIsVisible) is posted to the next message loop iteration, so the timer callback returns immediately and the audio thread can be scheduled sooner.- Double-buffered handoffs:
LevelSnapshot(timer→background) and JSONchar[](background→timer) use atomic pointer swaps — no locks, no contention.
Design Principles
- Audio thread safety — Never allocate memory, lock mutexes, or call non-real-time-safe functions on the audio thread.
applyToBuffer()uses only atomic operations and ring buffer writes. - Three-thread pipeline — Audio → Background → Message thread separation keeps each thread’s work minimal and predictable. Don’t collapse threads.
- Gain ramps — Audio start/stop uses 10ms gain ramps to prevent pops. See
SongbirdEditortransport handlers for ramp orchestration. - Plugin state tracking —
PluginStateTrackerusesaudioProcessorParameterChangedcallbacks (audio thread) →callAsync→ dirty set accumulation → timer-based debounced flush. This avoids committing on every tiny parameter tweak.