Files
codename-206/README.md
T
Mikkeli Matlock 0f66e9638c Add peak/RMS detection switch; sweep docs to match code
- Compressor: switchable peak / RMS detection (EnumParam<DetectionMode> in lib.rs
  -> use_rms bool in CompressorSettings; DSP stays framework-agnostic). RMS is a
  one-pole running mean of the linked squared level with a hardcoded 5 ms window,
  updated whenever active so peak<->RMS switching is seamless. New unit test
  (RMS compresses a sine less than peak); 6 tests total.
- README: reconciled Implementation Order checklists with actual progress
  (Stages 1-2 done; look-ahead/latency + basic UI pulled forward), annotated the
  project structure (implemented vs planned), and corrected the Latency and
  Denormal-flushing notes to match the code (set_latency_samples once / constant
  latency; in-code denormal flush). Overview and goals left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:02:23 +09:00

250 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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 (05 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 # (planned) LR4 filterbank (biquad chains)
limiter.rs # (planned) output true-peak brickwall limiter
biquad.rs # (planned) generic biquad (Direct Form II transposed)
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-15):** Stages 12 are complete. Look-ahead + latency reporting (from Stage 4)
and a basic slider UI (from Stage 5) were pulled forward and already work. **Next: Stage 3 —
crossover filterbank.** DSP currently lives in `src/dsp/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 ⬅ next
- [ ] 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 *(look-ahead + latency done early)*
- [x] Look-ahead delay (circular buffer) — currently inside `compressor.rs`, no separate `delay.rs` yet
- [x] Wire look-ahead: detector reads N samples ahead of the VCA
- [x] Report latency — via `context.set_latency_samples()`, reported once as a constant (see Latency note)
- [ ] 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<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)