A zero-latency, spectrum-analyzer-integrated parametric equalizer for broadcast, streaming, live, and mastering work. 11 fixed-slot bands of minimum-phase IIR (2× HPF + Low Shelf + 6× Bell + High Shelf + LPF), with per-band drag, overlaid on a Pre / Post FFT analyzer. Built with JUCE + WebView (Vite / React 19 / MUI 7). Ships as VST3 / AU / AAX / Standalone on Windows / macOS and VST3 / LV2 / CLAP / Standalone on Linux, plus a WebAssembly browser demo that reuses the exact same DSP.
You can find the demo site running on WebAssembly here. https://zeroeq-demo.web.app/
- 0-sample latency — all bands are minimum-phase IIR biquads. No lookahead, no oversampling, no linear-phase FFT. Null-tests bit-identical with the input when every band is OFF (or Bypass is on).
- 11 fixed-slot bands — 2× High Pass (left) + Low Shelf + 6× Bell + High Shelf + Low Pass (right). Each band is colour-coded and draggable directly on the spectrum. No add/remove operations; the layout is always the same, which makes muscle memory transferable across sessions.
- High Pass / Low Pass with proper Butterworth Q — slopes of 6 / 12 / 18 / 24 / 36 / 48 dB/oct. Each cascaded biquad stage is assigned the correct Butterworth Q (e.g. 24 dB/oct uses
Q = 0.5412, 1.3066) so the default response is maximally flat at unity "resonance". The node'sGaincontrol multiplies each stage's Q uniformly, acting as a continuous resonance dial. - Integrated Pre / Post analyzer — 4096-point Hann-windowed FFT, 256 log-frequency display bins (20 Hz..22 kHz). Pre (source) and Post (post-EQ) are both emitted and drawn as translucent fills with a Post outline. Analyzer can be toggled Off from the top-right button for zero-CPU headroom mode.
- Interactive node editor — drag any node to change Freq + Gain (or "peak height" for HP/LP). Scroll-wheel over a node adjusts Q (for Bell/Shelf/Notch) or steps through slope values (for HP/LP). Ctrl/Cmd+click resets a single band.
Reset Allclears everything back to defaults. - Variable vertical scale — ±3 / ±6 / ±12 / ±24 / ±32 dB toggle in the top-left. The grid and node hit-testing follow the selected range instantly.
- Adaptive node tooltip — hovering or dragging a node shows Freq / Gain / Q (or Slope) on a frosted-glass chip rendered in a Portal, so it always stays on top and never gets clipped by the canvas.
- 60 Hz UI — meters and spectrum refresh at 60 Hz (vsync-friendly). Smoothing coefficients are matched to the new rate so perceived attack/release feel identical to the previous 30 Hz build.
- Output stage — dedicated -24..+24 dB Output Gain fader on the right, independent from the band gains.
- I/O metering — Peak / RMS / Momentary LKFS (ITU-R BS.1770-4) are all computed every block and broadcast together. Out meter is displayed with its own dB scale.
- Stereo-true — identical coefficients and envelope state per band, applied channel-by-channel, so the stereo image is preserved end-to-end.
- WebAssembly demo — the same C++ DSP (11-band EQ + Pre/Post FFT analyzer + meters) compiled to WASM and driven by an
AudioWorklet. The React UI is reused verbatim via a Vite alias that swapsjuce-framework-frontend-mirrorfor a web shim that forwards parameter changes to the worklet.
- Main canvas — overlaid Pre/Post spectrum + EQ transfer curve + 11 coloured band nodes. X = log-frequency (20 Hz..22 kHz), Y = gain (±N dB depending on scale toggle). Drag a node to change Freq + Gain; scroll over a node for Q (or slope). Double-click to toggle On/Off.
- Top-left overlay — scale toggle (±3 / ±6 / ±12 / ±24 / ±32).
- Top-right overlay —
Reset AllandSpectrum(Off/On) toggles. - Right strip — OUT L / R level meters with a -30..0 dB scale.
- Bottom rail — per-band column: On/Off switch (filter-icon), Gain knob, Freq knob, Q knob or Slope select, each with inline numeric input.
- Right column — Output Gain fader (-24..+24 dB).
The plugin window is resizable (minimum 875 × 450, default 875 × 450).
| Type | Use | Notes |
|---|---|---|
| High Pass | Rumble / subsonic removal | Slopes 6 / 12 / 18 / 24 / 36 / 48 dB/oct. Per-stage Butterworth Q — maximally flat at "Gain = 0 dB". Positive Gain acts as a resonance boost on the cutoff. |
| Low Shelf | Warmth, bass tilt | RBJ shelf biquad at the specified corner. |
| Bell | Surgical cuts, broad sweeps | RBJ peaking biquad. Q controls bandwidth. |
| High Shelf | Air, top-end tilt | RBJ shelf biquad. |
| Low Pass | De-essing, lo-fi | Slopes 6 / 12 / 18 / 24 / 36 / 48 dB/oct. Same Butterworth design as HP. |
| Notch | Surgical kill (60 Hz hum, feedback) | Full extinction at fc. Selected via the per-band BAND{i}_TYPE parameter (not directly exposed by the fixed layout, but supported by the DSP). |
Linear-phase / natural-phase / dynamic-EQ / mid-side modes are on the roadmap; current implementation is minimum-phase, full-stereo only.
- CMake 3.22+
- C++17 toolchain
- Windows: Visual Studio 2022 with the C++ workload
- macOS: Xcode 14+
- Linux: gcc 13+ / clang + the apt packages listed under Building on Linux
- Node.js 18+ and npm (for the WebUI)
- JUCE (included as a submodule)
clap-juce-extensions(also a git submodule, used only for the Linux CLAP target)- Optional: AAX SDK for Pro Tools builds (drop at
aax-sdk/) - Optional: Inno Setup 6 for the Windows installer
- Optional: Emscripten for the WebAssembly demo
# 1. Clone with submodules
git clone <this-repo>
cd ZeroEQ
git submodule update --init --recursive
# 2. WebUI dependencies
cd webui && npm install && cd ..
# 3. Build (Windows, Release distribution)
powershell -ExecutionPolicy Bypass -File build_windows.ps1 -Configuration Release
# → produces releases/<VERSION>/ZeroEQ_<VERSION>_Windows_VST3_AAX_Standalone.zip
# and (if Inno Setup 6 is installed) ZeroEQ_<VERSION>_Windows_Setup.exe
# 4. Build (macOS)
./build_macos.zsh
# 5. Build (Linux — see "Building on Linux" below)
bash build_linux.shTested on WSL2 Ubuntu 24.04, but should work on any modern glibc-based distro with webkit2gtk-4.1 available.
sudo apt update
sudo apt install -y \
build-essential pkg-config cmake ninja-build git \
libasound2-dev libjack-jackd2-dev libcurl4-openssl-dev \
libfreetype-dev libfontconfig1-dev \
libx11-dev libxcomposite-dev libxcursor-dev libxext-dev \
libxinerama-dev libxrandr-dev libxrender-dev \
libwebkit2gtk-4.1-dev libglu1-mesa-dev mesa-common-dev libgtk-3-dev
git submodule update --init --recursive # JUCE + clap-juce-extensions
bash build_linux.sh # Release VST3 / LV2 / CLAP / Standalone + zipOutput:
- Build artefacts:
build-linux/plugin/ZeroEQ_artefacts/Release/{VST3,LV2,CLAP,Standalone}/ - Auto-installed:
~/.vst3/ZeroEQ.vst3,~/.lv2/ZeroEQ.lv2,~/.clap/ZeroEQ.clap - Distribution zip:
releases/<VERSION>/ZeroEQ_<VERSION>_Linux_VST3_LV2_CLAP_Standalone.zip
LV2 and CLAP are gated behind if(UNIX AND NOT APPLE) in CMake, so existing Windows / macOS release flows are unaffected. AU and AAX are skipped on Linux as expected.
# Windows (Debug)
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Debug --target ZeroEQ_VST3
# macOS (Debug)
cmake -B build -G Xcode
cmake --build build --config Debug --target ZeroEQ_VST3# Terminal A: Vite dev server
cd webui && npm run dev
# Terminal B: Debug build of the plugin
cmake --build build --config Debug --target ZeroEQ_StandaloneDebug builds load the WebUI from http://127.0.0.1:5173. Release builds embed the bundled assets via juce_add_binary_data.
The same C++ EQ + analyzer DSP is compiled to WebAssembly and driven by an AudioWorklet in the browser. The React UI is reused verbatim; a Vite alias swaps juce-framework-frontend-mirror for a local shim that owns the parameter state (58 params: 3 global + 11 bands × 5-6 fields each) and forwards every change to the AudioWorklet.
# Build the WASM module (requires emsdk activated in the shell)
cd wasm
bash build.sh # emits webui/public-web/wasm/zeroeq_dsp.wasm
# Windows alternative (system Python 3.9 fails; use the emsdk-bundled 3.13):
# $env:PATH = 'D:\...\emsdk\python\3.13.3_64bit;' + $env:PATH
# & 'D:\...\emsdk\emsdk_env.ps1'
# Remove-Item -Recurse -Force build -ErrorAction SilentlyContinue
# mkdir build; cd build
# emcmake cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
# cmake --build .
# Copy-Item -Force dist\zeroeq_dsp.wasm ..\dist\
# Copy-Item -Force dist\zeroeq_dsp.wasm ..\..\webui\public-web\wasm\
# Start the web-demo dev server
cd ../webui
npm run dev:web # http://127.0.0.1:5174
# Production bundle
npm run build:web # dist/ ready for static hosting
# Firebase Hosting deploy
npm run deploy:web # requires firebase CLI + zeroeq-demo projectThe WASM binary is ~44 KB (Release -O3) and contains the full 11-band EQ + Pre/Post 4096-point FFT analyzer + meters. On Web, the UI sits inside a fixed-size card (960 × 650) with a transport bar above and a short description below.
Global:
| ID | Type | Range | Default | Notes |
|---|---|---|---|---|
BYPASS |
bool | off / on | off | Bit-identical passthrough when on. |
OUTPUT_GAIN |
float | -24..+24 dB | 0 dB | Applied after the band chain. |
ANALYZER_MODE |
choice | Off / Pre / Post / Pre+Post | Pre+Post | Controls which FFTs run and are emitted to the UI. |
Per band (i = 0..10; slot roles are fixed in the UI layout):
| ID | Type | Range | Default (slot-dependent) |
|---|---|---|---|
BAND{i}_ON |
bool | off / on | HPF/LPF: off, others: on |
BAND{i}_TYPE |
choice | Bell / LowShelf / HighShelf / HighPass / LowPass / Notch | per slot (fixed visually) |
BAND{i}_FREQ |
float | 20..20000 Hz (log) | log-distributed |
BAND{i}_GAIN |
float | -32..+32 dB | 0 dB |
BAND{i}_Q |
float | 0.1..18 (log) | 1.0 (Bell) / 0.707 (Shelf) |
BAND{i}_SLOPE |
choice | 6 / 12 / 18 / 24 / 36 / 48 dB/oct | 18 dB/oct |
BAND{i}_Q is used by Bell / LowShelf / HighShelf / Notch. BAND{i}_SLOPE and BAND{i}_GAIN (as a resonance-multiplier) are used by HighPass / LowPass.
Each band instantiates up to 5 cascaded biquad stages (enough for an 8th-order filter = 4 biquads + optional 1st-order). All filtering is done in Transposed Direct Form II with per-channel state. Coefficients come from the RBJ cookbook and are rebuilt whenever a band's parameters change; the old state is kept to avoid click-on-change artefacts.
Slopes 6 / 12 / 18 / 24 / 36 / 48 dB/oct decompose as:
| dB/oct | Order | Biquads (Q values) | 1st-order |
|---|---|---|---|
| 6 | 1 | — | ✓ |
| 12 | 2 | 0.7071 | — |
| 18 | 3 | 1.0 | ✓ |
| 24 | 4 | 1.3066, 0.5412 | — |
| 36 | 6 | 1.9319, 0.7071, 0.5178 | — |
| 48 | 8 | 2.5629, 0.9000, 0.6013, 0.5098 | — |
At Gain = 0 dB the response is exactly Butterworth (maximally flat, no resonance peak). Moving Gain up/down multiplies every stage's Q by 10^(gainDb/20), preserving the per-stage Q ratio and widening/narrowing the knee uniformly.
4096-point Hann-windowed FFT with hop = 2048. Audio samples are downmixed to mono and pushed into a power-of-two ring; the UI timer drains the ring at 60 Hz, runs the FFT (a custom radix-2 implementation in the WASM build, juce::dsp::FFT in the plugin), and log-resamples the magnitude to 256 display bins (20 Hz..min(22 kHz, Nyquist)).
Per-bin smoothing uses an asymmetric attack (0.37) / release (0.025) calibrated for 60 Hz calls so peaks stay legible without flickering. When the spectrum outline crosses the floor (-90 dB) it is broken at the exact boundary to avoid "stuck to the bottom" horizontal lines; the downward/upward transition draws one more point against the floor so the outline never appears to vanish mid-slope.
Peak, RMS, and Momentary (BS.1770-4) are all computed every block and broadcast together at 60 Hz. Meter decay is 0.965 per-frame (≈ 20 dB/sec release, matching the previous 30 Hz build's 0.93). The UI decides which to display — switching never resets the DSP.
ZeroEQ reports 0 samples to the host. To confirm in your DAW:
- Check the plugin info / delay compensation display (e.g. Cubase MixConsole shows
Latency: 0 samples). - Null test — duplicate a clip on two tracks; insert ZeroEQ with Bypass ON on one (or all bands OFF); invert polarity on the other; sum. The result is silence.
- Pro Tools (AAX) on Windows: the editor injects
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--force-device-scale-factor=1before constructing WebView2 so the UI renders at 1× inside Pro Tools' DPI-virtualised window. It also defers asetSize()enforcement on the next message-loop iteration to overcome hosts that open the plugin below the declared minimum size. - All hosts: a per-monitor DPI poll runs on a 60 Hz timer and forces a re-layout when the DPI scale factor changes (useful when dragging the plugin window between monitors).
- Custom resize grip: a 24×24 transparent overlay sits in the bottom-right corner with a small dot pattern; dragging it posts
window_action("resizeTo", w, h)back to the native side, which clamps tokMinWidth × kMinHeight = 875 × 450and applies the new size to the host window.
ZeroEQ/
├─ plugin/ # JUCE plugin (C++)
│ ├─ src/
│ │ ├─ PluginProcessor.* # APVTS (3 global + 11 bands × 6 params), DSP chain entry
│ │ ├─ PluginEditor.* # WebView init, Web↔APVTS relays, DPI polling, 60 Hz timer
│ │ ├─ ParameterIDs.h # All parameter IDs + per-slot defaults
│ │ ├─ KeyEventForwarder.* # WebView → host DAW key forwarding
│ │ └─ dsp/
│ │ ├─ Equalizer.* # 11-band minimum-phase IIR EQ, Butterworth-cascaded HP/LP
│ │ ├─ Analyzer.* # Pre / Post FFT analyzer (4096 pt, log-freq 256 bin)
│ │ └─ MomentaryProcessor.* # ITU-R BS.1770-4 Momentary LKFS
│ └─ CMakeLists.txt
├─ wasm/ # C++ DSP ported to pure-standard-library for Emscripten
│ ├─ src/
│ │ ├─ wasm_exports.cpp # C ABI consumed by the AudioWorklet
│ │ ├─ dsp_engine.h # Orchestrator (source, transport, EQ, analyzer, meters)
│ │ ├─ equalizer.h # Pure-C++ port of Equalizer (RBJ biquads, Butterworth Q)
│ │ ├─ analyzer.h # Pure-C++ port of Analyzer (radix-2 FFT + log resample)
│ │ ├─ momentary_processor.h # Pure-C++ port of MomentaryProcessor
│ │ └─ fft.h # Tiny in-house Cooley-Tukey radix-2 forward FFT
│ ├─ CMakeLists.txt
│ └─ build.sh # emcmake + emmake, copies to webui/public-web/wasm/
├─ webui/ # Vite + React 19 + MUI 7 frontend (plugin + web demo)
│ ├─ src/
│ │ ├─ App.tsx # Layout, grid/card switching, dev-mode gating
│ │ ├─ components/
│ │ │ ├─ eq/
│ │ │ │ ├─ SpectrumEditor.tsx # Canvas: spectrum + EQ curve + draggable band nodes + tooltip
│ │ │ │ ├─ BandControlColumn.tsx # Per-band UI column (switch, 3 knobs, inputs, slope select)
│ │ │ │ ├─ InteractiveKnob.tsx # Drag / wheel / reset with modifier-key fine-adjust
│ │ │ │ ├─ InlineNumberInput.tsx # Editable numeric field with unit suffix
│ │ │ │ ├─ eqCurve.ts # TS mirror of the biquad math (for curve drawing)
│ │ │ │ └─ BandDefs.ts # Per-slot defaults + Butterworth Q tables
│ │ │ ├─ OutputMeterWidget.tsx # OUT L/R meters (internal meterUpdate subscription)
│ │ │ ├─ ParameterFader.tsx # Vertical OUTPUT fader
│ │ │ ├─ WebTransportBar.tsx # Web-demo only: play/pause/seek/loop/file upload
│ │ │ ├─ WebDemoMenu.tsx # Web-demo only: hamburger drawer (plugin downloads / sources)
│ │ │ └─ ...
│ │ ├─ bridge/juce.ts # Plugin: juce-framework-frontend-mirror wrapper
│ │ ├─ bridge/web/ # Web demo: Vite alias targets
│ │ │ ├─ WebAudioEngine.ts # AudioContext + worklet bridge, message queue until WASM ready
│ │ │ ├─ juce-shim.ts # Parameter-state drop-in for the frontend-mirror API (58 params)
│ │ │ └─ WebParamState.ts # Local WebSliderState / WebToggleState / WebComboBoxState
│ │ └─ hooks/useBandParam.ts # Reactive APVTS subscription per band (useSyncExternalStore)
│ ├─ public-web/ # Web-demo static assets (WASM, worklet, sample.mp3)
│ │ ├─ wasm/zeroeq_dsp.wasm # Compiled WASM DSP (~44 KB)
│ │ └─ worklet/dsp-processor.js # AudioWorkletProcessor (instantiates WASM, drives buffers)
│ ├─ vite.config.ts # Plugin build (embedded into the native binary)
│ ├─ vite.config.web.ts # Web-demo SPA build (VITE_RUNTIME=web)
│ ├─ firebase.json / .firebaserc # Firebase Hosting config (project: zeroeq-demo)
│ └─ package.json
├─ cmake/ # Version.cmake, icon
├─ scripts/ # AAX signing helper, WebView2 download, etc.
├─ docs/ # AAX signing guide, SDK README, developer build guide
├─ JUCE/ # Submodule
├─ aax-sdk/ # Optional — place the AAX SDK here to enable AAX builds
├─ installer.iss # Inno Setup script for Windows installer
├─ build_windows.ps1 # Windows release build pipeline (WebUI + VST3 / AAX / Standalone + signing + installer)
├─ build_macos.zsh # macOS release build pipeline
├─ VERSION # Single source of truth for the version string
└─ LICENSE
This project is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see the LICENSE file for the full text.
It uses JUCE under the AGPLv3 option of its dual-licensing scheme. Other third-party SDKs (VST3 / AAX / WebView2 / etc.) are governed by their own licenses; the runtime dependency list is shown in the in-app Licenses dialog.
Developed by Jun Murakami. Built on JUCE with an embedded WebView2 / WKWebView frontend, and a WebAssembly build of the same DSP for the browser demo.