8d9eaeaffc
Implements the single full-band feed-forward compressor (the engine that will be reused per band + for the 'All' channel). Design follows Giannoulis et al. 2012: log-domain gain computer with a quadratic soft knee feeding a smooth decoupled peak detector for attack/release ballistics. Stereo-linked peak detection. Look-ahead uses a fixed audio delay with a constant reported latency (set once in initialize); the knob only moves the detector tap within that delay. This avoids renegotiating latency from process(), which crashed FL Studio when the look-ahead was adjusted during playback. - src/dsp/compressor.rs: Compressor + CompressorSettings, RT-safe (no alloc in process; buffers sized in prepare; envelope denormals flushed in-code) - src/lib.rs: nested CompressorParams (threshold/ratio/knee/attack/release/makeup/ bypass) + global look-ahead; egui ParamSlider grid; latency reported once - 5 unit tests (static curve, knee continuity, steady-state convergence, constant latency); Cargo.toml lib crate-type added so tests link - README: 'All' channel architecture already documented; look-ahead spec updated Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
238 lines
11 KiB
Markdown
238 lines
11 KiB
Markdown
# 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 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–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)
|
||
- `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
|
||
|
||
```
|
||
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)
|
||
|
||
```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.
|
||
|
||
### Stage 1 — Skeleton plugin
|
||
- [ ] NIH-plug "passthrough" compiling and loading in DAW
|
||
- [ ] `Params` struct 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.rs` circular 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_egui` editor
|
||
- [ ] 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 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
|
||
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](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) |