Mikkeli Matlock 1d939535be Stage 3: 3-band LR4 crossover + per-band compressors into the 'All' channel
Splits the input into low/mid/high with a Linkwitz-Riley 24 dB/oct crossover,
compresses each band, sums them, then runs the sum through a fourth 'All'
compressor. Bypassing the three bands collapses the plugin to a simple full-band
comp driven by 'All' (the crossover sums flat in magnitude).

- src/dsp/biquad.rs: generic RBJ biquad (Transposed Direct Form II), LP/HP/AP
- src/dsp/crossover.rs: 3-band LR4 filterbank; lower band all-pass-compensated at
  the higher crossover so the bands sum to flat magnitude (an all-pass, not a
  bit-exact null — that only holds for linear-phase FIR). Mirrors nih-plug's
  crossover plugin design.
- src/lib.rs: 4 Compressor instances (low/mid/high/all) + Crossover; params
  restructured to 4 nested CompressorParams (id_prefix low/mid/high/all) plus
  global crossover_low_hz/crossover_high_hz/look_ahead_ms; 4-column lo|mid|hi|all
  egui UI; latency = two series stages (bands + all), constant, reported once
- 10 unit tests (adds biquad LP/AP magnitude, crossover flat-magnitude
  reconstruction, band-split sanity)
- README: Stage 3 marked done; corrected the 'sum flat' expectation to flat
  magnitude (IIR LR sums to an all-pass, not a time-domain null)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:04:24 +09:00

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 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 (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    # ✅ 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 (rustupwinget 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.

Status (2026-06-17): Stages 13 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

  • 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)
  • process() passes audio through untouched (since superseded by the compressor)
  • 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)
  • Level detector — switchable peak / RMS (RMS window hardcoded for now)
  • Implement gain computer (threshold, ratio, soft knee)
  • Implement attack/release envelope (smooth decoupled peak detector)
  • Wire into process(); covered by unit tests (static curve, knee continuity, steady state, RMS, constant latency)

Stage 3 — Crossover filterbank

  • Implement LR4 LP/HP biquad chains in crossover.rs (+ generic biquad.rs, Transposed Direct Form II)
  • 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
  • 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
  • Apply per-band compressor to each band
  • Sum bands back together
  • Run the summed signal through the 'All' channel compressor before output

Stage 4 — Output brickwall limiter + oversampler ⬅ next (look-ahead + latency already done)

  • Look-ahead delay (circular buffer) — inside compressor.rs, no separate delay.rs
  • Wire look-ahead: detector reads N samples ahead of the VCA
  • 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)

  • Add nih_plug_egui editor
  • Sliders for all current parameters (ParamSlider grid)
  • Per-band bypass toggles (partial — single-band bypass present; per-band arrives with Stage 3)
  • 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 — 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)
S
Description
No description provided
Readme GPL-3.0 410 KiB
Languages
Rust 95.8%
PowerShell 4%
Batchfile 0.2%