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>
This commit is contained in:
Mikkeli Matlock
2026-06-17 20:04:24 +09:00
parent 0f66e9638c
commit 1d939535be
5 changed files with 544 additions and 99 deletions
+19 -17
View File
@@ -59,7 +59,7 @@ a first-class mode, not an afterthought.
### 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
- 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)
@@ -114,9 +114,9 @@ src/
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)
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
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/
@@ -170,10 +170,12 @@ is essential — without it FL silently skips a plugin it has seen before.)
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/`).
**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 ✅
- [x] NIH-plug "passthrough" compiling and loading in DAW
@@ -186,17 +188,17 @@ editor are still inline in `src/lib.rs` (not yet split into `params.rs` / `edito
- [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
### 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
### Stage 4 — Output brickwall limiter + oversampler ⬅ next *(look-ahead + latency already done)*
- [x] Look-ahead delay (circular buffer) — inside `compressor.rs`, no separate `delay.rs`
- [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)
- [x] 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)*