feat: live operating-point fill + -6 dB refs on the gain curve
Add a per-channel input_level meter (post pre-gain, peak-with-decay) and use it to shade the gain curve: a translucent fill under the curve from the floor up to the current input, its right edge riding the curve (width = input, height = output), plus a dot at the operating point. Add -6 dBFS reference lines on both axes with a tick label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
//! Static gain-curve display (visualisation only): output level vs input level for the channel
|
//! Static gain-curve display: output level vs input level for the channel currently selected in
|
||||||
//! currently selected in the plot. Plots the full channel transfer for the wet path:
|
//! the plot. Plots the wet transfer `out = (in + pre_gain) + gain_reduction(...) + makeup` (mix not
|
||||||
//! `out = (in + pre_gain) + gain_reduction(in + pre_gain) + makeup`. Mix is not folded in (the
|
//! folded in; output clamped at 0 dBFS), plus a live **operating-point fill** under the curve up to
|
||||||
//! curve shows the wet/100% path, matching the GR-meter convention); output is clamped at 0 dBFS.
|
//! the channel's current input level — its right edge rides the curve (width = input, height = out).
|
||||||
|
|
||||||
|
use nih_plug::prelude::util;
|
||||||
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke};
|
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use crate::dsp::compressor::Compressor;
|
use crate::dsp::compressor::Compressor;
|
||||||
|
use crate::meters::Meters;
|
||||||
use crate::params::Codename206Params;
|
use crate::params::Codename206Params;
|
||||||
|
|
||||||
/// Side length of the square plot.
|
/// Side length of the square plot.
|
||||||
@@ -13,7 +16,7 @@ const CURVE_SIZE: f32 = 150.0;
|
|||||||
/// dB extent of both axes (bottom/left = FLOOR_DB, top/right = 0 dBFS).
|
/// dB extent of both axes (bottom/left = FLOOR_DB, top/right = 0 dBFS).
|
||||||
const FLOOR_DB: f32 = -60.0;
|
const FLOOR_DB: f32 = -60.0;
|
||||||
|
|
||||||
pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usize) {
|
pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usize, meters: &Meters) {
|
||||||
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
||||||
let ch = selected.min(3);
|
let ch = selected.min(3);
|
||||||
let cp = match ch {
|
let cp = match ch {
|
||||||
@@ -43,6 +46,13 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
|
|||||||
let x_for = |db: f32| left + (db - FLOOR_DB) / -FLOOR_DB * w;
|
let x_for = |db: f32| left + (db - FLOOR_DB) / -FLOOR_DB * w;
|
||||||
let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h;
|
let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h;
|
||||||
|
|
||||||
|
// -6 dBFS reference lines on both axes.
|
||||||
|
let g6 = Color32::from_gray(38);
|
||||||
|
let x6 = x_for(-6.0);
|
||||||
|
let y6 = y_for(-6.0);
|
||||||
|
p.line_segment([pos2(x6, top), pos2(x6, bottom)], Stroke::new(1.0, g6));
|
||||||
|
p.line_segment([pos2(left, y6), pos2(right, y6)], Stroke::new(1.0, g6));
|
||||||
|
|
||||||
// Unity reference (out = in), bottom-left to top-right.
|
// 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)));
|
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).
|
// Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre).
|
||||||
@@ -59,9 +69,38 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, selected: usiz
|
|||||||
let out_db = (driven + gr + makeup).clamp(FLOOR_DB, 0.0);
|
let out_db = (driven + gr + makeup).clamp(FLOOR_DB, 0.0);
|
||||||
pts.push(pos2(x_for(in_db), y_for(out_db)));
|
pts.push(pos2(x_for(in_db), y_for(out_db)));
|
||||||
}
|
}
|
||||||
|
// Operating-point fill: shade under the curve from the floor up to the current input level.
|
||||||
|
let driven_now = util::gain_to_db(meters.input_level[ch].load(Ordering::Relaxed));
|
||||||
|
let ext_in = (driven_now - pre).clamp(FLOOR_DB, 0.0); // external input -> curve x
|
||||||
|
let x_now = x_for(ext_in);
|
||||||
|
let fill_col = Color32::from_rgba_unmultiplied(120, 200, 160, 45);
|
||||||
|
for seg in pts.windows(2) {
|
||||||
|
let a = seg[0];
|
||||||
|
let mut b = seg[1];
|
||||||
|
if a.x >= x_now {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if b.x > x_now {
|
||||||
|
let f = ((x_now - a.x) / (b.x - a.x)).clamp(0.0, 1.0); // clip the last quad at x_now
|
||||||
|
b = pos2(x_now, a.y + (b.y - a.y) * f);
|
||||||
|
}
|
||||||
|
p.add(egui::Shape::convex_polygon(
|
||||||
|
vec![pos2(a.x, bottom), a, b, pos2(b.x, bottom)],
|
||||||
|
fill_col,
|
||||||
|
Stroke::NONE,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160))));
|
p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160))));
|
||||||
|
|
||||||
// Corner dB ticks.
|
// Operating-point dot, on the curve at the current input.
|
||||||
|
let driven = ext_in + pre;
|
||||||
|
let gr = Compressor::gain_computer(driven, threshold, ratio, knee, low_slope, low_curve);
|
||||||
|
let out_op = (driven + gr + makeup).clamp(FLOOR_DB, 0.0);
|
||||||
|
p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, Color32::from_rgb(235, 240, 235));
|
||||||
|
|
||||||
|
// Corner dB ticks + the -6 dB reference.
|
||||||
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, 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));
|
p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), Color32::from_gray(90));
|
||||||
|
p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), Color32::from_gray(80));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Opt
|
|||||||
// Gain curve (left, square) beside the scrolling plot (right, fills the rest).
|
// Gain curve (left, square) beside the scrolling plot (right, fills the rest).
|
||||||
let selected = state.plot.selected;
|
let selected = state.plot.selected;
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
ui.vertical(|ui| gain_curve::draw(ui, ¶ms, selected));
|
ui.vertical(|ui| gain_curve::draw(ui, ¶ms, selected, &meters));
|
||||||
ui.vertical(|ui| plot::draw(ui, &meters, &mut state.plot));
|
ui.vertical(|ui| plot::draw(ui, &meters, &mut state.plot));
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ impl Plugin for Codename206 {
|
|||||||
let num_samples = buffer.samples();
|
let num_samples = buffer.samples();
|
||||||
let mut lvl_l = [0.0f32; meters::NUM_CHANNELS];
|
let mut lvl_l = [0.0f32; meters::NUM_CHANNELS];
|
||||||
let mut lvl_r = [0.0f32; meters::NUM_CHANNELS];
|
let mut lvl_r = [0.0f32; meters::NUM_CHANNELS];
|
||||||
|
let mut inp = [0.0f32; meters::NUM_CHANNELS]; // mono input level (detector / gain-curve x)
|
||||||
let mut gr = [0.0f32; meters::NUM_CHANNELS];
|
let mut gr = [0.0f32; meters::NUM_CHANNELS];
|
||||||
let mut lim_gr = 0.0f32;
|
let mut lim_gr = 0.0f32;
|
||||||
|
|
||||||
@@ -257,6 +258,7 @@ impl Plugin for Codename206 {
|
|||||||
let out_r = band_out[b][r].abs();
|
let out_r = band_out[b][r].abs();
|
||||||
// Wet gain reduction (what the comp computes), independent of the mix.
|
// Wet gain reduction (what the comp computes), independent of the mix.
|
||||||
let g = self.comps[b].gain_reduction_db();
|
let g = self.comps[b].gain_reduction_db();
|
||||||
|
inp[b] = inp[b].max(in_mono);
|
||||||
lvl_l[b] = lvl_l[b].max(out_l);
|
lvl_l[b] = lvl_l[b].max(out_l);
|
||||||
lvl_r[b] = lvl_r[b].max(out_r);
|
lvl_r[b] = lvl_r[b].max(out_r);
|
||||||
gr[b] = gr[b].max(g);
|
gr[b] = gr[b].max(g);
|
||||||
@@ -287,6 +289,7 @@ impl Plugin for Codename206 {
|
|||||||
let out_l = out_frame[0].abs();
|
let out_l = out_frame[0].abs();
|
||||||
let out_r = out_frame[r].abs();
|
let out_r = out_frame[r].abs();
|
||||||
let g = self.comps[ALL].gain_reduction_db();
|
let g = self.comps[ALL].gain_reduction_db();
|
||||||
|
inp[ALL] = inp[ALL].max(in_mono);
|
||||||
lvl_l[ALL] = lvl_l[ALL].max(out_l);
|
lvl_l[ALL] = lvl_l[ALL].max(out_l);
|
||||||
lvl_r[ALL] = lvl_r[ALL].max(out_r);
|
lvl_r[ALL] = lvl_r[ALL].max(out_r);
|
||||||
gr[ALL] = gr[ALL].max(g);
|
gr[ALL] = gr[ALL].max(g);
|
||||||
@@ -327,6 +330,7 @@ impl Plugin for Codename206 {
|
|||||||
for i in 0..meters::NUM_CHANNELS {
|
for i in 0..meters::NUM_CHANNELS {
|
||||||
meters::decay_store(&self.meters.level_l[i], lvl_l[i], w);
|
meters::decay_store(&self.meters.level_l[i], lvl_l[i], w);
|
||||||
meters::decay_store(&self.meters.level_r[i], lvl_r[i], w);
|
meters::decay_store(&self.meters.level_r[i], lvl_r[i], w);
|
||||||
|
meters::decay_store(&self.meters.input_level[i], inp[i], w);
|
||||||
meters::decay_store(&self.meters.gain_reduction_db[i], gr[i], w);
|
meters::decay_store(&self.meters.gain_reduction_db[i], gr[i], w);
|
||||||
}
|
}
|
||||||
meters::decay_store(&self.meters.limiter_gr_db, lim_gr, w);
|
meters::decay_store(&self.meters.limiter_gr_db, lim_gr, w);
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ pub struct Meters {
|
|||||||
pub level_l: [AtomicF32; NUM_CHANNELS],
|
pub level_l: [AtomicF32; NUM_CHANNELS],
|
||||||
/// Right output level per channel (== left for mono signals).
|
/// Right output level per channel (== left for mono signals).
|
||||||
pub level_r: [AtomicF32; NUM_CHANNELS],
|
pub level_r: [AtomicF32; NUM_CHANNELS],
|
||||||
|
/// Mono **input** level per channel (post pre-gain = what the compressor detects). Drives the
|
||||||
|
/// gain-curve operating-point fill. Peak-with-decay.
|
||||||
|
pub input_level: [AtomicF32; NUM_CHANNELS],
|
||||||
/// Compressor gain reduction per channel in **dB (>= 0)**. Mono by design — detection is
|
/// Compressor gain reduction per channel in **dB (>= 0)**. Mono by design — detection is
|
||||||
/// stereo-linked, so the same gain applies to both channels.
|
/// stereo-linked, so the same gain applies to both channels.
|
||||||
pub gain_reduction_db: [AtomicF32; NUM_CHANNELS],
|
pub gain_reduction_db: [AtomicF32; NUM_CHANNELS],
|
||||||
@@ -40,6 +43,7 @@ impl Default for Meters {
|
|||||||
Self {
|
Self {
|
||||||
level_l: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
level_l: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
||||||
level_r: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
level_r: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
||||||
|
input_level: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
||||||
gain_reduction_db: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
gain_reduction_db: std::array::from_fn(|_| AtomicF32::new(0.0)),
|
||||||
limiter_gr_db: AtomicF32::new(0.0),
|
limiter_gr_db: AtomicF32::new(0.0),
|
||||||
scope: ScopeRing::default(),
|
scope: ScopeRing::default(),
|
||||||
@@ -55,6 +59,7 @@ impl Meters {
|
|||||||
for i in 0..NUM_CHANNELS {
|
for i in 0..NUM_CHANNELS {
|
||||||
self.level_l[i].store(0.0, Ordering::Relaxed);
|
self.level_l[i].store(0.0, Ordering::Relaxed);
|
||||||
self.level_r[i].store(0.0, Ordering::Relaxed);
|
self.level_r[i].store(0.0, Ordering::Relaxed);
|
||||||
|
self.input_level[i].store(0.0, Ordering::Relaxed);
|
||||||
self.gain_reduction_db[i].store(0.0, Ordering::Relaxed);
|
self.gain_reduction_db[i].store(0.0, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
self.limiter_gr_db.store(0.0, Ordering::Relaxed);
|
self.limiter_gr_db.store(0.0, Ordering::Relaxed);
|
||||||
|
|||||||
Reference in New Issue
Block a user