Stage 4a: base-rate look-ahead brickwall limiter

Adds the output limiter stage after the 'All' channel. Guarantees the output
never exceeds the ceiling: fixed 1.5 ms look-ahead, stereo-linked sliding-max
peak detection over the look-ahead window -> gain = ceiling/window_max, decoupled
smoothing (fast attack / user release), and a final clamp as the hard guarantee.

- src/dsp/limiter.rs: Limiter (sample-peak; true-peak via oversampling is 4b)
- src/lib.rs: wired as final stage; new globals output_ceiling_db (-24..0) and
  limiter_release_ms; latency now the constant three-stage total (bands+All+limiter);
  two UI sliders added to the global row
- 14 unit tests (4 new: ceiling guarantee on spikes, loud-sine limiting,
  transparency below ceiling, latency)
- README/docs updated (Stage 4 split into 4a done / 4b oversampling)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-06-19 14:45:26 +09:00
parent ada0b00313
commit f6c45123fa
4 changed files with 266 additions and 20 deletions
+17 -16
View File
@@ -73,9 +73,9 @@ a first-class mode, not an afterthought.
- 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
- Brickwall, ceiling = 0 dBFS or user-defined (`output_ceiling`). Look-ahead + sliding-max peak detection + a ceiling clamp guarantee the output never exceeds the ceiling
- Short attack (≤ 0.1 ms), auto-release (release time user-set)
- **Sample-peak today**; 4x-oversampled true-peak (inter-sample) detection is the remaining Stage-4 work
### 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
@@ -87,6 +87,7 @@ a first-class mode, not an afterthought.
### Global
- `input_gain` — pre-gain before filterbank (dB)
- `output_ceiling` — brickwall ceiling (dBFS, default 0.0)
- `limiter_release_ms` — output limiter release time
- `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
@@ -116,8 +117,8 @@ src/
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)
limiter.rs # ✅ look-ahead brickwall limiter (sample-peak; true-peak pending)
delay.rs # (planned) look-ahead delay (currently inside compressor.rs / limiter.rs)
oversampler.rs # (planned) 4x oversampler for true-peak detection
editor/
mod.rs # (planned) egui editor split out of lib.rs
@@ -175,12 +176,11 @@ 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-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/`).
**Status (2026-06-19):** Stages 13 plus the **base-rate brickwall limiter** (Stage 4a) are done:
3-band LR4 crossover per-band compressors (peak/RMS) → 'All' channel → look-ahead brickwall
limiter, with a basic 4-column UI. **Next: Stage 4b — 4× oversampling for true-peak (inter-sample)
limiting** (deferred as the CPU-heavy part). DSP is in `src/dsp/` (`biquad.rs`, `crossover.rs`,
`compressor.rs`, `limiter.rs`); params and the egui editor are still inline in `src/lib.rs`.
### Stage 1 — Skeleton plugin ✅
- [x] NIH-plug "passthrough" compiling and loading in DAW
@@ -200,12 +200,13 @@ the 3-band LR4 crossover feeding per-band compressors summed into the 'All' chan
- [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`
### Stage 4 — Output brickwall limiter + oversampler *(4a done; 4b = oversampling)*
- [x] Look-ahead delay (circular buffer) — inside `compressor.rs` and `limiter.rs`, no separate `delay.rs`
- [x] Wire look-ahead: detector reads N samples ahead of the VCA
- [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
- [x] Report latency — `context.set_latency_samples()` once; now the constant three-stage total (bands + 'All' + limiter)
- [x] Brickwall output limiter (`limiter.rs`): look-ahead + sliding-max peak detect + ceiling clamp guarantee; limits **sample** peaks
- [ ] `oversampler.rs` (4x, polyphase FIR / windowed sinc) ⬅ next
- [ ] True-peak (inter-sample) limiting on top of the brickwall, via the oversampler
### Stage 5 — Basic egui UI *(basic version done early)*
- [x] Add `nih_plug_egui` editor
- [x] Sliders for all current parameters (`ParamSlider` grid)