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:
@@ -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 1–2 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 1–3 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)*
|
||||
|
||||
Reference in New Issue
Block a user