2026-06-14 20:31:27 +09:00
# Codename 206
2026-06-14 21:24:43 +09:00
* 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. *
2026-06-14 20:31:27 +09:00
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
2026-06-14 21:22:34 +09:00
- 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)
2026-06-14 20:31:27 +09:00
- 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)
2026-06-14 21:22:34 +09:00
├─ 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
2026-06-14 20:31:27 +09:00
```
The detector for each band reads `look_ahead_ms` ahead of the VCA, so gain reduction is already ramping when the transient arrives.
2026-06-14 21:22:34 +09:00
**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.
2026-06-14 20:31:27 +09:00
---
## DSP Architecture
### Crossover Filterbank
- Linkwitz-Riley 4th-order (LR4) filters at each crossover frequency
- LR4 = two cascaded biquads (Butterworth LP or HP)
2026-06-17 20:04:24 +09:00
- 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)
2026-06-14 20:31:27 +09:00
- Crossover frequencies are user-adjustable parameters
### Per-Band Compressor
2026-06-15 20:02:23 +09:00
- Level detection: switchable peak / RMS (RMS window currently hardcoded small; can be exposed later)
2026-06-14 20:31:27 +09:00
- 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
2026-06-14 21:22:34 +09:00
### '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
2026-06-15 20:02:23 +09:00
- Has its own look-ahead; the plugin reports a single **constant ** total latency (the fixed band + 'All' look-ahead), set once — see Latency below
2026-06-14 20:31:27 +09:00
### Output Limiter
2026-06-19 14:45:26 +09:00
- Brickwall, ceiling = 0 dBFS or user-defined (`output_ceiling` ). Look-ahead + sliding-max peak detection + a ceiling clamp guarantee the output never exceeds the ceiling
- Short attack (≤ 0.1 ms), auto-release (release time user-set)
- **Sample-peak today**; 4x-oversampled true-peak (inter-sample) detection is the remaining Stage-4 work
2026-06-14 20:31:27 +09:00
### Latency
2026-06-15 20:02:23 +09:00
- 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
2026-06-14 20:31:27 +09:00
- 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)
2026-06-19 14:45:26 +09:00
- `limiter_release_ms` — output limiter release time
2026-06-15 19:33:31 +09:00
- `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)
2026-06-14 20:31:27 +09:00
- `crossover_low_hz` — low/mid crossover frequency
- `crossover_high_hz` — mid/high crossover frequency
2026-06-14 21:22:34 +09:00
### Per-Channel Compressor (× 4: low, mid, high, **all** — one `#[nested]` params struct reused)
2026-06-15 20:02:23 +09:00
- `detection` — peak / RMS level detection
2026-06-14 20:31:27 +09:00
- `threshold_db`
- `ratio` — 1.0 (off) to ∞ (limiting)
- `attack_ms`
- `release_ms`
- `knee_db` — soft knee width
- `makeup_gain_db`
2026-06-14 21:22:34 +09:00
- `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.
2026-06-14 20:31:27 +09:00
---
## Project Structure
2026-06-15 20:02:23 +09:00
Target layout (✅ = exists today; the rest is planned):
2026-06-14 20:31:27 +09:00
```
src/
2026-06-15 20:02:23 +09:00
lib.rs # ✅ Plugin trait + Params + egui editor (all inline for now)
params.rs # (planned) split Params out of lib.rs
2026-06-14 20:31:27 +09:00
dsp/
2026-06-15 20:02:23 +09:00
mod.rs # ✅ module declarations
compressor.rs # ✅ full-band comp: peak/RMS detector, gain computer, ballistics, look-ahead delay
2026-06-17 20:04:24 +09:00
crossover.rs # ✅ LR4 3-band filterbank with all-pass phase compensation
biquad.rs # ✅ generic biquad (Transposed Direct Form II)
2026-06-19 14:45:26 +09:00
limiter.rs # ✅ look-ahead brickwall limiter (sample-peak; true-peak pending)
delay.rs # (planned) look-ahead delay (currently inside compressor.rs / limiter.rs)
2026-06-15 20:02:23 +09:00
oversampler.rs # (planned) 4x oversampler for true-peak detection
2026-06-14 20:31:27 +09:00
editor/
2026-06-15 20:02:23 +09:00
mod.rs # (planned) egui editor split out of lib.rs
2026-06-14 20:31:27 +09:00
widgets/
2026-06-15 20:02:23 +09:00
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
2026-06-14 20:31:27 +09:00
```
---
## Build Steps
2026-06-14 21:22:34 +09:00
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
2026-06-14 20:31:27 +09:00
```
2026-06-14 21:22:34 +09:00
### 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.)
2026-06-19 12:42:22 +09:00
> **Known issue (deferred):** the **CLAP** build shows its name/vendor/type correctly in FL, but
> the **VST3** still displays stale/missing metadata there. Suspected cause is FL caching the VST3
> by its unchanged `VST3_CLASS_ID`. Likely fix is to regenerate that class ID (and/or clear FL's
> plugin DB); low priority for now — use the CLAP build meanwhile.
2026-06-14 20:31:27 +09:00
---
## Implementation Order
Work through these stages in order — each stage produces a loadable, audible plugin.
2026-06-15 20:02:23 +09:00
2026-06-19 14:45:26 +09:00
**Status (2026-06-19): ** Stages 1– 3 plus the **base-rate brickwall limiter ** (Stage 4a) are done:
3-band LR4 crossover → per-band compressors (peak/RMS) → 'All' channel → look-ahead brickwall
limiter, with a basic 4-column UI. **Next: Stage 4b — 4× oversampling for true-peak (inter-sample)
limiting** (deferred as the CPU-heavy part). DSP is in `src/dsp/` (`biquad.rs` , `crossover.rs` ,
`compressor.rs` , `limiter.rs` ); params and the egui editor are still inline in `src/lib.rs` .
2026-06-15 20:02:23 +09:00
### 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)
2026-06-17 20:04:24 +09:00
### 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
2026-06-19 14:45:26 +09:00
### Stage 4 — Output brickwall limiter + oversampler *(4a done; 4b = oversampling)*
- [x] Look-ahead delay (circular buffer) — inside `compressor.rs` and `limiter.rs` , no separate `delay.rs`
2026-06-15 20:02:23 +09:00
- [x] Wire look-ahead: detector reads N samples ahead of the VCA
2026-06-19 14:45:26 +09:00
- [x] Report latency — `context.set_latency_samples()` once; now the constant three-stage total (bands + 'All' + limiter)
- [x] Brickwall output limiter (`limiter.rs` ): look-ahead + sliding-max peak detect + ceiling clamp guarantee; limits **sample ** peaks
- [ ] `oversampler.rs` (4x, polyphase FIR / windowed sinc) ⬅ next
- [ ] True-peak (inter-sample) limiting on top of the brickwall, via the oversampler
2026-06-15 20:02:23 +09:00
### 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
2026-06-14 20:31:27 +09:00
### 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
2026-06-18 01:34:56 +09:00
Handled by the framework — no plugin code needed. NIH-plug wraps `process()` and `reset()` in
`process_wrapper` , which enables the CPU's **Flush-To-Zero ** mode for the duration via its
`ScopedFtz` guard (x86 `MXCSR` bit 15 / AArch64 `FPCR` bit 24, set with inline asm and restored
on drop). FTZ has a fixed threshold at the normal/subnormal boundary (~− 759 dB for f32), so the
decaying envelope/RMS tails and all the IIR filter state are flushed to zero automatically,
far below audibility. We therefore do **not ** set the register ourselves or flush values in code.
(Note: NIH-plug sets FTZ but not DAZ; for our feed-forward IIR work FTZ on results is sufficient.)
2026-06-14 20:31:27 +09:00
### 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<Mutex<...>>` 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)