Architecture
This document describes the current (v4.0.1) system design: how audio flows through the plugin, the threading and synchronization model, the DSP behind each analyzer, and a map of the source tree. It is written to be accurate to the code — if you find a discrepancy, the code is authoritative and this document is a bug.
High-level structure
Buffle Audio Visualize is a JUCE AudioProcessor (the
plugin) with an AudioProcessorEditor (the UI). The
processor owns four independent analysis engines and passes audio
through unchanged. The editor owns the display components, the
mix-health rule engine, and the session recorder.
┌───────────────────────────────────────────┐
Host / DAW ──audio──▶ WaveformVisualizerAudioProcessor │
(audio thread) │ processBlock(): │
│ • waveformBuffer.push() │
│ • stereoImageAnalyzer.process() │
│ • spectrumAnalyzer.process() │
│ • loudnessAnalyzer.process() │
│ • (audio copied to output unchanged) │
└───────┬───────────────────────────────────┘
│ atomics + short critical sections
│ (engine → GUI snapshots)
┌───────▼───────────────────────────────────┐
Screen ◀──repaint──▶ WaveformVisualizerAudioProcessorEditor │
(message thread) │ • WaveformComponent (60 fps) │
│ • VectorScopeComponent(30 fps) │
│ • SpectrumComponent (30 fps) │
│ • LoudnessMeterComponent (30 fps) │
│ • HealthMonitor + SessionRecorder(10 Hz)│
│ • TimelineComponent (15 fps) │
└────────────────────────────────────────────┘
The audio path is transparent: processBlock clears any
unused output channels, feeds the four engines, and does not modify the
samples that reach the output. There are no sound-altering parameters
and zero added latency.
Threading & synchronization model
Two threads matter: the audio (real-time) thread
that calls processBlock, and the message (GUI)
thread that runs timers and paints. Engines are written by the
audio thread and read by the GUI thread. Synchronization is explicit and
deliberately simple:
| Data | Writer | Reader | Mechanism |
|---|---|---|---|
| Scalar measurements (correlation, balance, LUFS, true peak, per-band values) | audio | GUI | std::atomic<float> |
| Spectrum magnitude / peak-hold / average arrays | audio | GUI | juce::CriticalSection (short hold) |
| Vectorscope point cloud | audio | GUI | juce::CriticalSection |
| Loudness gating-block history | audio | GUI | juce::CriticalSection |
| Health findings, session frames | GUI only | GUI only | none needed |
Real-time characteristics (disclosed honestly for review):
- All engine buffers are sized once in
prepare(); the steady-state audio path does not allocate for the spectrum, loudness (K-weighting/oversampling), or band-split analysis. - The audio path takes short critical sections (not
lock-free) when publishing spectrum arrays, vectorscope points, and
loudness history. Work under each lock is bounded (a fixed-size copy or
a single
push_back); locks are uncontended most of the time because GUI reads are brief snapshots. - Two amortized allocations exist on the audio path and are bounded:
the vectorscope point vector (
push_backuntilmaxPoints, then overwrite;updateTrailEffecterases faded points) and the loudness gating-block history (push_backroughly every 100 ms, capped atmaxHistoryBlocks). - The FFT is computed on the audio thread inside a critical section when a full 75 %-overlap hop has accumulated. It is O(n log n) with n = 4096 — bounded and fast, but not deferred to a background thread.
These are typical analyzer trade-offs; they are documented rather than hidden so a reviewer can weigh them. The HealthMonitor and SessionRecorder run only on the message thread and never touch the audio thread.
The analysis engines
WaveformBuffer
(WaveformBuffer.h/.cpp)
Thread-safe circular buffer of recent L/R samples for the waveform display. Configurable time window (2 / 5 / 10 s). Reports sample rate and data-present flag.
StereoImageAnalyzer
(StereoImageAnalyzer.h/.cpp)
- Full-band Pearson correlation over a 1024-sample sliding window (recomputed at a decimated rate to keep the audio path cheap), L/R balance from RMS, and a stereo width estimate.
- Vectorscope point cloud with a trail-decay age per point.
- Band-Split Stereo Guard: six bands split by
cascaded
juce::dsp::LinkwitzRileyFiltercrossovers (60 / 150 / 500 / 2 k / 6 k Hz). Per-band correlation and energy over ~400 ms windows, exposed as atomics; the mono-bass status is the worst correlation of the two bands below 150 Hz.
SpectrumAnalyzer
(SpectrumAnalyzer.h/.cpp)
- 4096-point real FFT (
juce::dsp::FFT), Hann window (juce::dsp::WindowingFunction), 75 % overlap (hop = fftSize/4). - Analyzes the mono sum. Publishes: exponentially smoothed magnitudes (dB), time-based peak-hold (adjustable 1–10 s), and a long-term linear-power average (the basis for tone-match snapshots).
- Averaging modes (fast / medium / slow) change the smoothing coefficients.
LoudnessAnalyzer
(LoudnessAnalyzer.h/.cpp)
- ITU-R BS.1770-4 K-weighting implemented as two biquad stages whose coefficients are derived at the session sample rate (reproducing the 48 kHz reference coefficients; same approach as libebur128).
- Momentary (400 ms), short-term (3 s), and gated integrated LUFS (absolute −70 LUFS gate + relative −10 LU gate) via 100 ms sub-blocks.
- True peak via 4× oversampling
(
juce::dsp::Oversampling, 2 IIR stages), with peak hold. - Loudness range (LRA) per EBU Tech 3342 from the short-term distribution.
UI structure
The editor lays out a header bar, a 2×2 analyzer grid, a Flight Recorder timeline strip, and a footer:
- Header: logo + control buttons
FREEZE,RESET,AVG,HOLD,WIN. - Grid: Waveform (top-left), Vectorscope (top-right), Spectrum (bottom-left), Loudness (bottom-right).
- Timeline: Flight Recorder strip with
REPORT/CLR/LIVE. - Footer: mix-health severity dot + headline (click → feed), version, logo (click → About).
Custom look-and-feel (BuffleLookAndFeel) provides the
dark theme, rounded buttons, and tooltip styling. A
juce::TooltipWindow hosts hover help.
UI controls
| Control | Location | Action |
|---|---|---|
| FREEZE | header | Pause all displays (analysis keeps running) |
| RESET | header | Reset integrated loudness, peak holds, spectrum average/peaks |
| AVG | header | Cycle spectrum averaging: fast / medium / slow |
| HOLD | header | Spectrum peak-hold duration: 1 / 3 / 5 / 10 s |
| WIN | header | Waveform time window: 2 / 5 / 10 s |
| PRESET | header | Factory presets (Streaming/Broadcast/Podcast/Club) or save/load a
custom .bavpreset |
| click scope | vectorscope | Toggle M/S Lissajous ↔︎ polar dome |
| SNAP / LOAD | spectrum | Capture average as reference / load a reference file |
| REF / DELTA | spectrum | Toggle reference overlay / difference view |
| A–D / CLR | spectrum | Select reference slot / clear the library |
| LENS | spectrum | Cycle Translation Lens: off / phone / buds / car / club |
| MASK | spectrum | Toggle the Masking Radar overlay (beta) |
| Shift+click | spectrum | Place measurement cursor A, then B; delta-freq/delta-dB readout |
| click/drag | timeline | Scrub the session; replays the recorded spectrum shape at that moment |
| REPORT / CLR / LIVE | timeline | Export HTML report / clear session / return to live |
| click health line | footer | Open the Mix Health feed |
| click N INST badge | footer | Open the multi-instance session panel (shown once 2+ instances share this process) |
| click logo | footer | Open the About box |
Decision-support layer (message thread)
- HealthMonitor (
HealthMonitor.h): a set of debounced rules over aHealthMetricssnapshot, producing severity-tagged, timestamped findings. - SessionRecorder (
SessionRecorder.h): a bounded, self-decimating log ofFrames; computes summary statistics and generates the self-contained HTML report. - The editor builds a metrics snapshot at 10 Hz and drives both.
Session intelligence (multi-instance, v4.0.0)
- InstanceRegistry (
InstanceRegistry.h): ajuce::SharedResourcePointer-held singleton, refcounted per OS process (created on first plugin instance, destroyed when the last one releases it). No shared memory or IPC — every instance in the same process shares the same C++ object. - Each
PluginProcessorregisters anEntry(id, display name, and lambdas wrapping its ownLoudnessAnalyzer/StereoImageAnalyzergetters) in its constructor and unregisters in its destructor.updateTrackProperties()forwards the host-reported track name (when the host provides one) into the registry. - The editor's footer badge (
N INST) and its CallOutBox panel read ajuce::CriticalSection-protected snapshot of the registry at 4 Hz. - Limitation: only instances sharing the same OS process are visible to each other. Some hosts sandbox each plugin/instance into its own process; there is no reliable way to detect this from inside the plugin, so it is not surfaced in the UI.
Source map
WaveformVisualizer/Source/
├── PluginProcessor.{h,cpp} AudioProcessor: owns engines, passes audio through
├── PluginEditor.{h,cpp} Editor: layout, controls, health/recorder ticks
├── PluginVersion.h Single source of version truth
├── WaveformBuffer.{h,cpp} Circular sample buffer
├── WaveformComponent.{h,cpp} Waveform display
├── StereoImageAnalyzer.{h,cpp} Correlation, balance, band-split stereo guard
├── VectorScopeComponent.{h,cpp} Lissajous + polar dome + meters + band strip
├── SpectrumAnalyzer.{h,cpp} FFT engine, peak hold, average
├── SpectrumComponent.{h,cpp} Spectrum, references, lens, masking radar, tilt/sharpness
├── LoudnessAnalyzer.{h,cpp} BS.1770-4 LUFS, true peak, LRA
├── LoudnessMeterComponent.{h,cpp} Loudness meter display
├── HealthMonitor.h Mix-health rule engine (header-only)
├── SessionRecorder.h Flight Recorder + HTML report (header-only)
├── TimelineComponent.{h,cpp} Flight Recorder timeline strip
└── InstanceRegistry.h Multi-instance session registry (header-only)
Build system
- Project is defined by
WaveformVisualizer/buffleaudio_visualize.jucer(Projucer). - Editing the
.jucerrequires a Projucer--resaveto regenerate the Xcode project underWaveformVisualizer/Builds/MacOSX/. - JUCE modules used:
juce_audio_basics/devices/formats/processors/utils,juce_audio_plugin_client,juce_core,juce_data_structures,juce_dsp,juce_events,juce_graphics,juce_gui_basics,juce_gui_extra. - The installer (
Installer/build_installer.sh) builds Release VST3 + Standalone and packages them withpkgbuild/productbuild(non-relocatable components).