10 KiB
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 |
| 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 back to flat
- Crossover frequencies are user-adjustable parameters
Per-Band Compressor
- Level detection: switchable RMS / peak, with configurable window
- 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 delay; total reported latency = max(band look-ahead) + 'All' look-ahead
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
- Look-ahead duration must be reported via
Plugin::latency()for DAW compensation - 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–10 ms)crossover_low_hz— low/mid crossover frequencycrossover_high_hz— mid/high crossover frequency
Per-Channel Compressor (× 4: low, mid, high, all — one #[nested] params struct reused)
threshold_dbratio— 1.0 (off) to ∞ (limiting)attack_msrelease_msknee_db— soft knee widthmakeup_gain_dbbypass— 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
src/
lib.rs # Plugin entry point, implements Plugin trait
params.rs # Params struct with NIH-plug #[id] attributes
dsp/
mod.rs
crossover.rs # LR4 filterbank (biquad chains)
compressor.rs # Per-band compressor + look-ahead
limiter.rs # Output true-peak brickwall limiter
biquad.rs # Generic biquad filter (Direct Form II transposed)
delay.rs # Circular buffer for look-ahead delay lines
oversampler.rs # 4x oversampler for true-peak detection
editor/
mod.rs # egui editor setup via nih_plug_egui
widgets/
gain_curve.rs # Custom egui Widget: gain curve display
band_meter.rs # Per-band gain reduction meter
level_meter.rs# 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)
# 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):
.\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.
Stage 1 — Skeleton plugin
- NIH-plug "passthrough" compiling and loading in DAW
Paramsstruct with all parameters declared (no DSP yet)process()passes audio through untouched- Verify plugin loads and parameters appear in DAW
Stage 2 — Single-band compressor (no look-ahead, no UI)
- Implement
biquad.rs— generic biquad, Direct Form II transposed - Implement basic RMS level detector
- Implement gain computer (threshold, ratio, knee)
- Implement attack/release envelope on gain reduction
- Wire into
process(), test with a sine sweep
Stage 3 — Crossover filterbank
- Implement LR4 LP and HP biquad chains in
crossover.rs - Verify bands sum flat (null test: sum vs dry should be silence)
- Add per-band bypass; with all bands bypassed, output must null against dry (proves the "simple comp" mode path)
- Apply per-band compressor to each band
- Sum bands back together
- Run the summed signal through the 'All' channel comp/lim (reuse the per-band compressor) before output
Stage 4 — Look-ahead + brickwall limiter
- Implement
delay.rscircular buffer - Wire look-ahead: detector reads N samples ahead of VCA
- Report latency via
Plugin::latency() - Implement
oversampler.rs(4x, use a polyphase FIR or windowed sinc) - Implement brickwall output limiter with true-peak detection
Stage 5 — Basic egui UI
- Add
nih_plug_eguieditor - Knobs / sliders for all parameters
- Per-band bypass toggles
- Confirm UI controls update DSP in real time
Stage 6 — Custom visualisations
level_meter.rs— input/output RMS + peak metersband_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
Add #[cfg(target_arch = "x86_64")] std::arch::x86_64::_MM_SET_FLUSH_ZERO_MODE(...) in
initialize(), or add a small DC offset (1e-25) to filter inputs.
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 — read the
plugins/examples first - NIH-plug docs
- Cookiecutter template
- egui docs
- 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)