feat: static gain-curve display for the selected channel
Add editor/gain_curve.rs: a square panel beside the scrolling plot showing the channel transfer (out vs in, -60..0 dB) for whichever channel the plot tab selects. Plots the full wet path — out = (in + pre_gain) + gain_reduction + makeup — using the shared Compressor::gain_computer (now pub) so it matches the DSP and the GR meter. Unity-reference diagonal + threshold marker; mix not folded in. Update README structure/status to reflect the editor/ widget module and the completed Stage 6 visualisers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -130,21 +130,22 @@ src/
|
|||||||
oversampler.rs # ✅ 4x polyphase oversampler for true-peak detection (detection-only)
|
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
|
The editor lives in an `editor/` module — one file per visualiser widget (each owns its GUI
|
||||||
`editor.rs`. When the UI is redesigned (gain curve, draggable crossover, real layout), the plan is
|
state), with `mod.rs` as the aggregator/layout. Drawn directly with egui's `Painter`.
|
||||||
to split it into a widget module so each visualiser is self-contained and reusable:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
editor/
|
editor/
|
||||||
mod.rs # editor assembly + layout (replaces editor.rs)
|
mod.rs # aggregator: create(), EditorState, layout, placeholder slider columns
|
||||||
widgets/
|
meter.rs # |L | GR | R| level + gain-reduction bars + per-channel ceiling lamp
|
||||||
meter.rs # |L | GR | R| level + gain-reduction cluster (extract from editor.rs)
|
plot.rs # rolling in/out/GR scope (200 Hz ring feed) + ceiling-hit markers
|
||||||
plot.rs # rolling in/out/GR scope (extract from editor.rs)
|
crossover.rs # log-freq strip with draggable crossover handles + number boxes
|
||||||
gain_curve.rs # static gain-curve display per channel (threshold/ratio/knee) — planned
|
gain_curve.rs # static gain-curve display (out vs in) for the selected channel
|
||||||
crossover.rs # frequency display with draggable crossover handles — planned
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
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.
|
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 →
|
**Status (2026-06-25):** 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×
|
per-band pre-gain + compressors (peak/RMS) → per-channel dry/wet mix → 'All' channel → **true-peak
|
||||||
oversampled detection). `lib.rs` has been split into `params.rs`, `editor.rs`, and `meters.rs`.
|
brickwall limiter** (4× oversampled detection). `lib.rs` is split into `params.rs`, `meters.rs`, and
|
||||||
Stage 6 metering is underway: per-channel **|L | GR | R| meters**, a **latching ceiling lamp**, and
|
an `editor/` widget module. Stage 6 visualisers are essentially complete: per-channel **|L | GR | R|
|
||||||
a **rolling in/out/gain-reduction plot** (per-channel tabs + flow-speed selector). **Next: gain-curve
|
meters** + **per-channel ceiling lamps**, a **rolling in/out/GR plot** (200 Hz ring feed, flow-speed,
|
||||||
display and draggable crossover handles, then replace the placeholder slider UI.**
|
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 ✅
|
### Stage 1 — Skeleton plugin ✅
|
||||||
- [x] NIH-plug "passthrough" compiling and loading in DAW
|
- [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 level meters (output level, `|L | GR | R|` cluster)
|
||||||
- [x] Per-channel gain-reduction meters (vertical bars) + latching ceiling lamp
|
- [x] Per-channel gain-reduction meters (vertical bars) + latching ceiling lamp
|
||||||
- [x] Rolling in/out/gain-reduction plot (per-channel tabs, flow-speed selector)
|
- [x] Rolling in/out/gain-reduction plot (per-channel tabs, flow-speed selector)
|
||||||
- [ ] Static gain-curve display per band (threshold/ratio/knee)
|
- [x] Static gain-curve display (out vs in; includes pre-gain + makeup) for the selected channel
|
||||||
- [ ] Draggable crossover handles on a frequency display
|
- [x] Draggable crossover handles on a log-frequency display (with number boxes)
|
||||||
- [ ] Replace the placeholder slider columns with the real UI
|
- [ ] Replace the placeholder slider columns with the real UI
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -132,8 +132,9 @@ impl Compressor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Static compressor curve. Returns gain reduction in dB (<= 0) for an input `level_db`.
|
/// 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`.
|
/// Quadratic soft knee of width `knee_db`, centred on `threshold_db`. Also used by the editor's
|
||||||
fn gain_computer(level_db: f32, threshold_db: f32, ratio: f32, knee_db: f32) -> f32 {
|
/// 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 slope = 1.0 / ratio - 1.0; // <= 0 for ratio >= 1
|
||||||
let over = level_db - threshold_db;
|
let over = level_db - threshold_db;
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
+7
-1
@@ -20,6 +20,7 @@ use crate::params::{Codename206Params, CompressorParams};
|
|||||||
use crate::Codename206;
|
use crate::Codename206;
|
||||||
|
|
||||||
mod crossover;
|
mod crossover;
|
||||||
|
mod gain_curve;
|
||||||
mod meter;
|
mod meter;
|
||||||
mod plot;
|
mod plot;
|
||||||
|
|
||||||
@@ -75,7 +76,12 @@ pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Opt
|
|||||||
ui.heading(Codename206::NAME);
|
ui.heading(Codename206::NAME);
|
||||||
meter::draw(ui, &meters, &mut state.meter);
|
meter::draw(ui, &meters, &mut state.meter);
|
||||||
ui.separator();
|
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();
|
ui.separator();
|
||||||
crossover::draw(ui, ¶ms, setter);
|
crossover::draw(ui, ¶ms, setter);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|||||||
+2
-2
@@ -62,8 +62,8 @@ impl PlotHistory {
|
|||||||
/// GUI-side state for the plot: selected channel, history ring, ring-drain cursor, and the
|
/// GUI-side state for the plot: selected channel, history ring, ring-drain cursor, and the
|
||||||
/// column being assembled from drained buckets.
|
/// column being assembled from drained buckets.
|
||||||
pub(super) struct PlotState {
|
pub(super) struct PlotState {
|
||||||
/// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all).
|
/// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all). Also drives the gain curve.
|
||||||
selected: usize,
|
pub(super) selected: usize,
|
||||||
history: PlotHistory,
|
history: PlotHistory,
|
||||||
/// Seconds of history shown across the full plot width — the flow speed (smaller = faster).
|
/// Seconds of history shown across the full plot width — the flow speed (smaller = faster).
|
||||||
window_s: f64,
|
window_s: f64,
|
||||||
|
|||||||
Reference in New Issue
Block a user