# Codename 206 *Called 206 because the Peugeot 206 has a 'maxi' variant. You'll know this is a Maximizer knockoff if you can follow that trail of thoughts.* Multiband Compressor / Limiter VST3 — Project Plan ## Overview A VST3 multiband compressor/limiter with a custom gain curve display, inspired by FL Studio's Maximizer. Built with **Rust** + **NIH-plug** (VST3 + CLAP output) + **egui** for the UI. **Goals:** - 3-band (configurable crossover points) compressor/limiter - An 'All' aggregate channel: a 4th comp/lim stack on the summed bands, so bypassing all bands turns the plugin into a simple full-band compressor (mirrors FL's Maximizer) - Look-ahead brickwall output limiter with true-peak detection - Real-time gain reduction metering per band - Custom gain curve visualiser - Fully resizable vector UI --- ## Tech Stack | Layer | Choice | |---|---| | Language | Rust (stable) | | Plugin framework | [NIH-plug](https://github.com/robbert-vdh/nih-plug) | | Plugin formats | VST3, CLAP | | UI framework | egui (via `nih_plug_egui`) | | Build tooling | `cargo xtask bundle` | --- ## Signal Flow ``` Input └─ Crossover filterbank (Linkwitz-Riley LR4 @ each crossover freq) ├─ Band 1 (low) → look-ahead delay → compressor VCA → gain stage ─┐ (bypassable) ├─ Band 2 (mid) → look-ahead delay → compressor VCA → gain stage ─┤ (bypassable) └─ Band 3 (high) → look-ahead delay → compressor VCA → gain stage ─┤ (bypassable) │ Sum of bands ◄──────────────────────────────────────────────------┘ └─ 'All' channel → look-ahead delay → compressor VCA → gain stage └─ output brickwall limiter (true-peak, 4x oversampled) → output ``` The detector for each band reads `look_ahead_ms` ahead of the VCA, so gain reduction is already ramping when the transient arrives. **The 'All' aggregate channel** (mirrors FL's Maximizer): the three bands are summed and the result passes through a *fourth*, full-band compressor/limiter stack before the output limiter. Because the LR4 filterbank sums phase-coherently flat, **bypassing all three bands leaves the summed signal identical to the input** — so the plugin collapses into a plain single-band compressor/limiter driven entirely by the 'All' channel. That makes "multiband off = simple comp" a first-class mode, not an afterthought. --- ## DSP Architecture ### Crossover Filterbank - Linkwitz-Riley 4th-order (LR4) filters at each crossover frequency - LR4 = two cascaded biquads (Butterworth LP or HP) - Bands sum phase-coherently to flat **magnitude** (the sum is an all-pass; lower bands get an all-pass at each later crossover to match phase — not a bit-exact time-domain null) - Crossover frequencies are user-adjustable parameters ### Per-Band Compressor - Level detection: switchable peak / RMS (RMS window currently hardcoded small; can be exposed later) - Gain computer: threshold, ratio, soft knee - Attack / release envelopes (logarithmic ballistics) - Makeup gain per band - Look-ahead: circular delay buffer on the audio path; detector reads ahead ### 'All' Aggregate Channel - Structurally **identical to a per-band compressor** — reuse the same comp/lim code/params, just fed the summed signal instead of a filtered band - Runs after the three bands are summed, before the output brickwall limiter - Bands are individually bypassable; with all three bypassed the (phase-coherent) crossover sum equals the dry input, so the 'All' channel alone acts as a full-band comp/lim - Has its own look-ahead; the plugin reports a single **constant** total latency (the fixed band + 'All' look-ahead), set once — see Latency below ### Output Limiter - True-peak brickwall (ceiling = 0 dBFS or user-defined) - 4x oversampling for inter-sample peak detection - Short attack (≤ 0.1 ms), auto-release ### Latency - Reported via `context.set_latency_samples()` in `initialize()` — **never** from `process()`; renegotiating latency mid-stream crashes some hosts (FL included) - Reported latency is a **constant** (the max look-ahead); the look-ahead control only moves the detector tap within that fixed delay - All bands use equal delay to preserve phase alignment --- ## Parameters ### Global - `input_gain` — pre-gain before filterbank (dB) - `output_ceiling` — brickwall ceiling (dBFS, default 0.0) - `look_ahead_ms` — look-ahead time (0–5 ms). Reported latency is **constant** (the max look-ahead); the knob only moves the detector tap within that fixed delay, so it is safe to adjust during playback (changing reported latency mid-stream crashes some hosts, FL included) - `crossover_low_hz` — low/mid crossover frequency - `crossover_high_hz` — mid/high crossover frequency ### Per-Channel Compressor (× 4: low, mid, high, **all** — one `#[nested]` params struct reused) - `detection` — peak / RMS level detection - `threshold_db` - `ratio` — 1.0 (off) to ∞ (limiting) - `attack_ms` - `release_ms` - `knee_db` — soft knee width - `makeup_gain_db` - `bypass` — per-channel bypass (bypassing low+mid+high = simple full-band comp via the 'all' channel) The 'all' channel uses the same struct so its UI and DSP are identical to a band; it just sits after the band sum. --- ## Project Structure Target layout (✅ = exists today; the rest is planned): ``` src/ lib.rs # ✅ Plugin trait + Params + egui editor (all inline for now) params.rs # (planned) split Params out of lib.rs dsp/ mod.rs # ✅ module declarations compressor.rs # ✅ full-band comp: peak/RMS detector, gain computer, ballistics, look-ahead delay crossover.rs # ✅ LR4 3-band filterbank with all-pass phase compensation biquad.rs # ✅ generic biquad (Transposed Direct Form II) limiter.rs # (planned) output true-peak brickwall limiter delay.rs # (planned) look-ahead delay (currently lives inside compressor.rs) oversampler.rs # (planned) 4x oversampler for true-peak detection editor/ mod.rs # (planned) egui editor split out of lib.rs widgets/ gain_curve.rs # (planned) custom egui Widget: gain curve display band_meter.rs # (planned) per-band gain reduction meter level_meter.rs# (planned) input/output level meter ``` --- ## Build Steps The project is already scaffolded (NIH-plug + nih_plug_egui, pinned to a fixed git rev in `Cargo.toml`). You do **not** need the Steinberg VST3 SDK — NIH-plug bundles its own bindings. **Prerequisites (Windows):** - Rust stable (`rustup` — `winget install Rustlang.Rustup`) - Visual Studio 2022 with the "Desktop development with C++" workload (provides the MSVC linker) ```powershell # Build + bundle the VST3 and CLAP cargo xtask bundle codename_206 --release # Output: target\bundled\Codename 206.vst3 and Codename 206.clap ``` ### Deployment FL Studio scans `C:\Program Files\Common Files\VST3` by default, **ignores directory junctions** (so a symlinked bundle is invisible to its scanner), and caches failed scans. So deployment must copy a *real* bundle into a folder FL scans, then FL must be told to rescan failed plugins. Use the provided script (no need to remember the details): ```powershell .\deploy.ps1 # build, then copy to the global VST3/CLAP folders (one UAC prompt) .\deploy.ps1 -SkipBuild # reinstall the last build without rebuilding .\deploy.ps1 -User # copy to %LOCALAPPDATA%\Programs\Common\VST3 instead (no admin) — best for a dev loop ``` `deploy.bat` is a double-click wrapper around the same script. After deploying, in FL Studio: **Options → Manage plugins → tick "Rescan previously failed plugins" → Find installed plugins**, then search for **Codename 206**. (The rescan-failed step is essential — without it FL silently skips a plugin it has seen before.) --- ## Implementation Order Work through these stages in order — each stage produces a loadable, audible plugin. **Status (2026-06-17):** Stages 1–3 complete — full-band compressor, peak/RMS detection, and now the 3-band LR4 crossover feeding per-band compressors summed into the 'All' channel (4 reusable `Compressor` instances). Look-ahead + latency (Stage 4) and a basic 4-column UI (Stage 5) are in. **Next: Stage 4 — output brickwall limiter + oversampler.** DSP is in `src/dsp/` (`biquad.rs`, `crossover.rs`, `compressor.rs`); params and the egui editor are still inline in `src/lib.rs` (not yet split into `params.rs` / `editor/`). ### Stage 1 — Skeleton plugin ✅ - [x] NIH-plug "passthrough" compiling and loading in DAW - [ ] `Params` struct with all parameters declared *(partial — compressor + look-ahead params done; global `input_gain`/`output_ceiling` and crossover params pending)* - [x] `process()` passes audio through untouched *(since superseded by the compressor)* - [x] Verify plugin loads and parameters appear in DAW *(verified in FL Studio)* ### Stage 2 — Single-band (full-band) compressor ✅ - [ ] Implement `biquad.rs` — generic biquad, Direct Form II transposed *(deferred to Stage 3 — not needed for the full-band comp)* - [x] Level detector — switchable **peak / RMS** (RMS window hardcoded for now) - [x] Implement gain computer (threshold, ratio, soft knee) - [x] Implement attack/release envelope (smooth decoupled peak detector) - [x] Wire into `process()`; covered by unit tests (static curve, knee continuity, steady state, RMS, constant latency) ### Stage 3 — Crossover filterbank ✅ - [x] Implement LR4 LP/HP biquad chains in `crossover.rs` (+ generic `biquad.rs`, Transposed Direct Form II) - [x] Verify bands sum flat — for IIR LR4 the sum is an **all-pass** (flat *magnitude*, phase-shifted), not a bit-exact null; lower bands get an all-pass at each later crossover to phase-match. Tested via `bands_sum_to_flat_magnitude` - [x] Per-band bypass — a bypassed band passes its delayed dry band; with all three bypassed the 'All' channel sees the flat-magnitude reconstruction = the simple-comp mode - [x] Apply per-band compressor to each band - [x] Sum bands back together - [x] Run the summed signal through the 'All' channel compressor before output ### Stage 4 — Output brickwall limiter + oversampler ⬅ next *(look-ahead + latency already done)* - [x] Look-ahead delay (circular buffer) — inside `compressor.rs`, no separate `delay.rs` - [x] Wire look-ahead: detector reads N samples ahead of the VCA - [x] Report latency — `context.set_latency_samples()` once; now the constant two-stage total (bands + 'All') - [ ] Implement `oversampler.rs` (4x, use a polyphase FIR or windowed sinc) - [ ] Implement brickwall output limiter with true-peak detection ### Stage 5 — Basic egui UI *(basic version done early)* - [x] Add `nih_plug_egui` editor - [x] Sliders for all current parameters (`ParamSlider` grid) - [ ] Per-band bypass toggles *(partial — single-band bypass present; per-band arrives with Stage 3)* - [x] Confirm UI controls update DSP in real time ### Stage 6 — Custom visualisations - [ ] `level_meter.rs` — input/output RMS + peak meters - [ ] `band_meter.rs` — per-band gain reduction meters (vertical bars) - [ ] `gain_curve.rs` — static gain curve display per band (threshold/ratio/knee) - [ ] Draggable crossover handles on a frequency display --- ## Key Implementation Notes ### No allocations in `process()` Rust's borrow checker will help, but be explicit. All buffers (delay lines, filter states) must be pre-allocated in `initialize()`. Use `assert_process_allocs` feature flag during development to catch violations. ### Denormal flushing The compressor flushes its envelope/RMS state to zero in code once it decays below audibility (`flush_denormal` in `compressor.rs`). The hardware `_MM_SET_FLUSH_ZERO_MODE` intrinsic is now deprecated and the matching DAZ helper isn't exposed by `std::arch`, so a global hardware FTZ/DAZ (via inline asm on the audio thread) is deferred until the IIR crossover/limiter filters land, where it matters more. ### Parameter smoothing NIH-plug provides `Smoother` — use it for all gain/threshold params to avoid zipper noise. ### Thread safety Params are atomics. The editor and audio thread communicate only through params and `Arc>` meter data. Never pass DSP state to the UI directly. ### VST3 licensing You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries. NIH-plug's VST3 bindings are GPLv3; if you distribute, the plugin must also be GPLv3 (or you need a commercial Steinberg licence). CLAP has no such restriction. --- ## Reference Material - [NIH-plug repo](https://github.com/robbert-vdh/nih-plug) — read the `plugins/` examples first - [NIH-plug docs](https://nih-plug.robbertvanderhelm.nl/) - [Cookiecutter template](https://github.com/robbert-vdh/nih-plug-template) - [egui docs](https://docs.rs/egui) - Zölzer, *DAFX: Digital Audio Effects* — biquad filter cookbook - Giannoulis et al., "Digital Dynamic Range Compressor Design" (JAES 2012) — compressor ballistics reference - AES paper on true-peak limiting / inter-sample peaks (ITU-R BS.1770)