diff --git a/README.md b/README.md index c5b0c0c..e2b9588 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,22 @@ src/ oversampler.rs # ✅ 4x polyphase oversampler for true-peak detection (detection-only) ``` -The editor's meters and plot are currently drawn directly with egui's `Painter` inline in -`editor.rs`. When the UI is redesigned (gain curve, draggable crossover, real layout), the plan is -to split it into a widget module so each visualiser is self-contained and reusable: +The editor lives in an `editor/` module — one file per visualiser widget (each owns its GUI +state), with `mod.rs` as the aggregator/layout. Drawn directly with egui's `Painter`. ``` src/ editor/ - mod.rs # editor assembly + layout (replaces editor.rs) - widgets/ - meter.rs # |L | GR | R| level + gain-reduction cluster (extract from editor.rs) - plot.rs # rolling in/out/GR scope (extract from editor.rs) - gain_curve.rs # static gain-curve display per channel (threshold/ratio/knee) — planned - crossover.rs # frequency display with draggable crossover handles — planned + mod.rs # aggregator: create(), EditorState, layout, placeholder slider columns + meter.rs # |L | GR | R| level + gain-reduction bars + per-channel ceiling lamp + plot.rs # rolling in/out/GR scope (200 Hz ring feed) + ceiling-hit markers + crossover.rs # log-freq strip with draggable crossover handles + number boxes + gain_curve.rs # static gain-curve display (out vs in) for the selected channel ``` +Remaining UI work: replace the placeholder per-channel slider columns in `mod.rs` with the real +layout. + Deferred until the redesign — no need to split prematurely while the layout is still a placeholder. --- @@ -195,12 +196,13 @@ 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-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.** +**Status (2026-06-25):** Stages 1–4 done — the full signal chain works: 3-band LR4 crossover → +per-band pre-gain + compressors (peak/RMS) → per-channel dry/wet mix → 'All' channel → **true-peak +brickwall limiter** (4× oversampled detection). `lib.rs` is split into `params.rs`, `meters.rs`, and +an `editor/` widget module. Stage 6 visualisers are essentially complete: per-channel **|L | GR | R| +meters** + **per-channel ceiling lamps**, a **rolling in/out/GR plot** (200 Hz ring feed, flow-speed, +ceiling-hit markers), **draggable crossover handles**, and a **static gain-curve display**. **Next: +replace the placeholder slider columns with the real UI layout.** ### Stage 1 — Skeleton plugin ✅ - [x] NIH-plug "passthrough" compiling and loading in DAW @@ -236,8 +238,8 @@ display and draggable crossover handles, then replace the placeholder slider UI. - [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 +- [x] Static gain-curve display (out vs in; includes pre-gain + makeup) for the selected channel +- [x] Draggable crossover handles on a log-frequency display (with number boxes) - [ ] Replace the placeholder slider columns with the real UI --- diff --git a/src/dsp/compressor.rs b/src/dsp/compressor.rs index 82b0329..15f3a01 100644 --- a/src/dsp/compressor.rs +++ b/src/dsp/compressor.rs @@ -132,8 +132,9 @@ impl Compressor { } /// Static compressor curve. Returns gain reduction in dB (<= 0) for an input `level_db`. - /// Quadratic soft knee of width `knee_db`, centred on `threshold_db`. - fn gain_computer(level_db: f32, threshold_db: f32, ratio: f32, knee_db: f32) -> f32 { + /// Quadratic soft knee of width `knee_db`, centred on `threshold_db`. Also used by the editor's + /// gain-curve display, so it stays the single source of truth for the transfer shape. + pub fn gain_computer(level_db: f32, threshold_db: f32, ratio: f32, knee_db: f32) -> f32 { let slope = 1.0 / ratio - 1.0; // <= 0 for ratio >= 1 let over = level_db - threshold_db; diff --git a/src/editor/gain_curve.rs b/src/editor/gain_curve.rs new file mode 100644 index 0000000..76b93d0 --- /dev/null +++ b/src/editor/gain_curve.rs @@ -0,0 +1,65 @@ +//! Static gain-curve display (visualisation only): output level vs input level for the channel +//! currently selected in the plot. Plots the full channel transfer for the wet path: +//! `out = (in + pre_gain) + gain_reduction(in + pre_gain) + makeup`. Mix is not folded in (the +//! curve shows the wet/100% path, matching the GR-meter convention); output is clamped at 0 dBFS. + +use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke}; + +use crate::dsp::compressor::Compressor; +use crate::params::Codename206Params; + +/// Side length of the square plot. +const CURVE_SIZE: f32 = 150.0; +/// dB extent of both axes (bottom/left = FLOOR_DB, top/right = 0 dBFS). +const FLOOR_DB: f32 = -60.0; + +pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usize) { + let labels = ["LOW", "MID", "HIGH", "ALL"]; + let ch = selected.min(3); + let cp = match ch { + 0 => ¶ms.low, + 1 => ¶ms.mid, + 2 => ¶ms.high, + _ => ¶ms.all, + }; + let pre = cp.pre_gain_db.value(); + let threshold = cp.threshold_db.value(); + let ratio = cp.ratio.value(); + let knee = cp.knee_db.value(); + let makeup = cp.makeup_db.value(); + + ui.label(format!("Curve: {}", labels[ch])); + let (rect, _) = ui.allocate_exact_size(vec2(CURVE_SIZE, CURVE_SIZE), Sense::hover()); + let p = ui.painter_at(rect); + p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20)); + + let inset = 2.0; + let (left, right, top, bottom) = + (rect.left() + inset, rect.right() - inset, rect.top() + inset, rect.bottom() - inset); + let w = right - left; + let h = bottom - top; + let x_for = |db: f32| left + (db - FLOOR_DB) / -FLOOR_DB * w; + let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h; + + // Unity reference (out = in), bottom-left to top-right. + p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, Color32::from_gray(45))); + // Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre). + let tx = x_for((threshold - pre).clamp(FLOOR_DB, 0.0)); + p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, Color32::from_rgb(80, 60, 45))); + + // Full wet transfer: drive into the comp, then makeup. (Mix not folded in.) + let n = 96; + let mut pts = Vec::with_capacity(n + 1); + for i in 0..=n { + let in_db = FLOOR_DB + (i as f32 / n as f32) * -FLOOR_DB; // external input, -60..0 + let driven = in_db + pre; + let gr = Compressor::gain_computer(driven, threshold, ratio, knee); // <= 0 dB + let out_db = (driven + gr + makeup).clamp(FLOOR_DB, 0.0); + pts.push(pos2(x_for(in_db), y_for(out_db))); + } + p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160)))); + + // Corner dB ticks. + p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), Color32::from_gray(90)); + p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), Color32::from_gray(90)); +} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7bc15d4..8e029d1 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -20,6 +20,7 @@ use crate::params::{Codename206Params, CompressorParams}; use crate::Codename206; mod crossover; +mod gain_curve; mod meter; mod plot; @@ -75,7 +76,12 @@ pub(crate) fn create(params: Arc, meters: Arc) -> Opt ui.heading(Codename206::NAME); meter::draw(ui, &meters, &mut state.meter); ui.separator(); - plot::draw(ui, &meters, &mut state.plot); + // Gain curve (left, square) beside the scrolling plot (right, fills the rest). + let selected = state.plot.selected; + ui.horizontal_top(|ui| { + ui.vertical(|ui| gain_curve::draw(ui, ¶ms, selected)); + ui.vertical(|ui| plot::draw(ui, &meters, &mut state.plot)); + }); ui.separator(); crossover::draw(ui, ¶ms, setter); ui.separator(); diff --git a/src/editor/plot.rs b/src/editor/plot.rs index ba896c5..9fc38ac 100644 --- a/src/editor/plot.rs +++ b/src/editor/plot.rs @@ -62,8 +62,8 @@ impl PlotHistory { /// GUI-side state for the plot: selected channel, history ring, ring-drain cursor, and the /// column being assembled from drained buckets. pub(super) struct PlotState { - /// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all). - selected: usize, + /// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all). Also drives the gain curve. + pub(super) selected: usize, history: PlotHistory, /// Seconds of history shown across the full plot width — the flow speed (smaller = faster). window_s: f64,