diff --git a/README.md b/README.md index b6baaff..673fca7 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,12 @@ Built with **Rust** + **NIH-plug** (VST3 + CLAP output) + **egui** for the UI. ``` 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 + ├─ Band 1 (low) → pre-gain → look-ahead delay → compressor VCA → makeup ─┐ (bypassable) + ├─ Band 2 (mid) → pre-gain → look-ahead delay → compressor VCA → makeup ─┤ (bypassable) + └─ Band 3 (high) → pre-gain → look-ahead delay → compressor VCA → makeup ─┤ (bypassable) + │ + Sum of bands ◄─────────────────────────────────────────────────────------┘ + └─ 'All' channel → pre-gain → look-ahead delay → compressor VCA → makeup └─ output brickwall limiter (true-peak, 4x oversampled) → output ``` @@ -62,10 +62,11 @@ a first-class mode, not an afterthought. - 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 +- **Pre-gain (drive)**: scales the band *before* the detector, so it pushes harder into compression and feeds the sum/limiter hotter — a mild "compressed semi-distortion" without a dedicated saturator. Applied in the wiring (the compressor itself is untouched). Pairs with makeup for full input/output gain-staging - 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 +- Makeup gain per band (−24…+24 dB — attenuates as well as boosts) - 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 @@ -85,20 +86,20 @@ a first-class mode, not an afterthought. ## Parameters ### 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 (0–5 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) +- `pre_gain_db` — drive into the compressor (−24…+36 dB, smoothed) - `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` +- `makeup_db` — makeup gain (−24…+24 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. @@ -110,23 +111,21 @@ 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 + lib.rs # ✅ Plugin trait + DSP wiring + process() + params.rs # ✅ Params structs, defaults, build_settings() + editor.rs # ✅ egui editor: meter panel + rolling plot (drawn via Painter) + slider columns + meters.rs # ✅ lock-free Meters (atomics): decayed bar values + raw plot feed 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 # ✅ look-ahead brickwall limiter (sample-peak; true-peak pending) - delay.rs # (planned) look-ahead delay (currently inside compressor.rs / limiter.rs) + limiter.rs # ✅ look-ahead brickwall limiter (true-peak via oversampler) oversampler.rs # ✅ 4x polyphase oversampler for true-peak detection (detection-only) - 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 ``` + +The editor's meters and plot are drawn directly with egui's `Painter` in `editor.rs` rather than +as separate widget files; a `gain_curve` display and draggable crossover handles are still to come. --- @@ -176,12 +175,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-19):** Stages 1–4 done — the full signal chain works: 3-band LR4 crossover → -per-band compressors (peak/RMS) → 'All' channel → **true-peak brickwall limiter** (4× oversampled -detection), with a basic 4-column UI. **Next: split `params.rs`/`editor/` out of `lib.rs`, then -Stage 6 visualisers (meters, gain curve).** DSP is in `src/dsp/` (`biquad.rs`, `crossover.rs`, -`compressor.rs`, `limiter.rs`, `oversampler.rs`); params and the egui editor are still inline in -`src/lib.rs`. +**Status (2026-06-23):** Stages 1–4 done — the full signal chain works: 3-band LR4 crossover → +per-band pre-gain + compressors (peak/RMS) → 'All' channel → **true-peak brickwall limiter** (4× +oversampled detection). `lib.rs` has been split into `params.rs`, `editor.rs`, and `meters.rs`. +Stage 6 metering is underway: per-channel **|L | GR | R| meters**, a **latching ceiling lamp**, and +a **rolling in/out/gain-reduction plot** (per-channel tabs + flow-speed selector). **Next: gain-curve +display and draggable crossover handles, then replace the placeholder slider UI.** ### Stage 1 — Skeleton plugin ✅ - [x] NIH-plug "passthrough" compiling and loading in DAW @@ -211,13 +210,15 @@ Stage 6 visualisers (meters, gain curve).** DSP is in `src/dsp/` (`biquad.rs`, ` ### 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] Per-band bypass toggles - [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) +- [x] Per-channel level meters (output level, `|L | GR | R|` cluster) +- [x] Per-channel gain-reduction meters (vertical bars) + latching ceiling lamp +- [x] Rolling in/out/gain-reduction plot (per-channel tabs, flow-speed selector) +- [ ] Static gain-curve display per band (threshold/ratio/knee) - [ ] Draggable crossover handles on a frequency display +- [ ] Replace the placeholder slider columns with the real UI --- ## Key Implementation Notes @@ -240,8 +241,10 @@ far below audibility. We therefore do **not** set the register ourselves or flus 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>` meter data. Never pass DSP state to the UI directly. +Params are atomics. The editor and audio thread communicate only through params and a shared +`Arc` of **lock-free atomics** (`meters.rs`) — never a mutex on the audio path. `process()` +publishes one value per meter per block (gated on the editor being open); the editor reads them +each frame. Never pass DSP state to the UI directly. ### VST3 licensing You must accept Steinberg's VST3 SDK licence before distributing VST3 binaries.