feat: per-channel meters, ceiling lamp, and pre-gain drive
Stage 6 begins. Add lock-free Meters (atomics shared audio->GUI) with a peak-with-decay ballistic, published once per block and gated on the editor being open. Editor draws a per-channel level + gain-reduction meter panel and a ceiling lamp fed by the output limiter. Compressor/Limiter expose gain_reduction_db() for this. Also add a smoothed per-channel pre-gain applied before each compressor (and before the All compressor), driving the signal into compression and on into the limiter for a compressed semi-distortion. Pairs with makeup for full per-channel input/output gain-staging. Compressor DSP untouched; 16 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+109
-3
@@ -4,30 +4,49 @@
|
||||
//! gain-curve view and draggable crossover handles. Built to stay usable meanwhile: a resizable
|
||||
//! window with a vertical scroll area so every control is reachable at any window size, global
|
||||
//! controls in a label|slider grid, and the four channels (low/mid/high/all) side by side.
|
||||
//!
|
||||
//! First real visualiser: a row of per-channel meters (output level + gain reduction) plus a
|
||||
//! ceiling lamp, fed by the lock-free [`Meters`] state the audio thread publishes each block.
|
||||
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::{
|
||||
create_egui_editor,
|
||||
egui::{self, Vec2},
|
||||
egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Painter, Rect, Sense, Vec2},
|
||||
resizable_window::ResizableWindow,
|
||||
widgets,
|
||||
};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::meters::{Meters, NUM_CHANNELS};
|
||||
use crate::params::{Codename206Params, CompressorParams};
|
||||
use crate::Codename206;
|
||||
|
||||
/// Build the plugin editor over a shared handle to the params.
|
||||
pub(crate) fn create(params: Arc<Codename206Params>) -> Option<Box<dyn Editor>> {
|
||||
/// Bottom of the level meter's dB scale (top is 0 dBFS).
|
||||
const METER_FLOOR_DB: f32 = -60.0;
|
||||
/// Full-scale of the gain-reduction meter (bar fills downward from the top).
|
||||
const GR_FULL_DB: f32 = 24.0;
|
||||
/// Limiter gain reduction (dB) at which the ceiling lamp is fully lit.
|
||||
const LAMP_FULL_DB: f32 = 3.0;
|
||||
/// Height of the meter panel.
|
||||
const METER_PANEL_H: f32 = 130.0;
|
||||
|
||||
/// Build the plugin editor over shared handles to the params and meter state.
|
||||
pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Option<Box<dyn Editor>> {
|
||||
let egui_state = params.editor_state.clone();
|
||||
create_egui_editor(
|
||||
params.editor_state.clone(),
|
||||
(),
|
||||
|_, _| {},
|
||||
move |egui_ctx, setter, _state| {
|
||||
// Keep frames coming so the meters animate while the editor is open.
|
||||
egui_ctx.request_repaint();
|
||||
|
||||
// One column of controls for a single compressor channel.
|
||||
let band_col = |ui: &mut egui::Ui, title: &str, p: &CompressorParams| {
|
||||
ui.strong(title);
|
||||
ui.label("Pre-gain");
|
||||
ui.add(widgets::ParamSlider::for_param(&p.pre_gain_db, setter));
|
||||
ui.add(widgets::ParamSlider::for_param(&p.detection, setter));
|
||||
ui.label("Threshold");
|
||||
ui.add(widgets::ParamSlider::for_param(&p.threshold_db, setter));
|
||||
@@ -51,6 +70,8 @@ pub(crate) fn create(params: Arc<Codename206Params>) -> Option<Box<dyn Editor>>
|
||||
.show(egui_ctx, egui_state.as_ref(), |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.heading(Codename206::NAME);
|
||||
draw_meters(ui, &meters);
|
||||
ui.separator();
|
||||
// Global controls stacked vertically so they never overflow sideways.
|
||||
egui::Grid::new("globals").num_columns(2).show(ui, |ui| {
|
||||
ui.label("Xover Lo/Mid");
|
||||
@@ -81,3 +102,88 @@ pub(crate) fn create(params: Arc<Codename206Params>) -> Option<Box<dyn Editor>>
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Draw the meter panel: one channel group per column (mono output level + gain reduction),
|
||||
/// plus the ceiling lamp driven by the output limiter.
|
||||
fn draw_meters(ui: &mut egui::Ui, meters: &Meters) {
|
||||
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
||||
let (rect, _) =
|
||||
ui.allocate_exact_size(vec2(ui.available_width(), METER_PANEL_H), Sense::hover());
|
||||
let p = ui.painter_at(rect);
|
||||
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(20, 20, 24));
|
||||
|
||||
let top = rect.top() + 10.0;
|
||||
let bottom = rect.bottom() - 18.0; // leave a row for the labels
|
||||
let cell_w = rect.width() / NUM_CHANNELS as f32;
|
||||
let bar_w = (cell_w * 0.26).min(22.0);
|
||||
let gap = (cell_w * 0.08).min(8.0);
|
||||
|
||||
for i in 0..NUM_CHANNELS {
|
||||
let cell_left = rect.left() + i as f32 * cell_w;
|
||||
let group_w = bar_w * 2.0 + gap;
|
||||
let bx = cell_left + (cell_w - group_w) * 0.5;
|
||||
|
||||
// Level bar (mono = max of L/R), fills upward; colour warns as it nears 0 dBFS.
|
||||
let level_lin =
|
||||
meters.level_l[i].load(Ordering::Relaxed).max(meters.level_r[i].load(Ordering::Relaxed));
|
||||
let level_db = util::gain_to_db(level_lin);
|
||||
let level_frac = ((level_db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
||||
v_bar(&p, bx, bar_w, top, bottom, level_frac, level_color(level_db), false);
|
||||
|
||||
// Gain-reduction bar, fills downward from the top.
|
||||
let gr_db = meters.gain_reduction_db[i].load(Ordering::Relaxed);
|
||||
let gr_frac = (gr_db / GR_FULL_DB).clamp(0.0, 1.0);
|
||||
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true);
|
||||
|
||||
p.text(
|
||||
pos2(cell_left + cell_w * 0.5, rect.bottom() - 2.0),
|
||||
Align2::CENTER_BOTTOM,
|
||||
labels[i],
|
||||
FontId::proportional(12.0),
|
||||
Color32::from_gray(200),
|
||||
);
|
||||
}
|
||||
|
||||
// Ceiling lamp (top-right): dark when idle, bright red while the limiter is catching peaks.
|
||||
let lit = (meters.limiter_gr_db.load(Ordering::Relaxed) / LAMP_FULL_DB).clamp(0.0, 1.0);
|
||||
let lamp = Color32::from_rgb(
|
||||
(40.0 + 215.0 * lit) as u8,
|
||||
(12.0 + 28.0 * lit) as u8,
|
||||
(12.0 + 28.0 * lit) as u8,
|
||||
);
|
||||
let center = pos2(rect.right() - 14.0, rect.top() + 14.0);
|
||||
p.circle_filled(center, 7.0, lamp);
|
||||
p.text(
|
||||
pos2(center.x - 14.0, center.y),
|
||||
Align2::RIGHT_CENTER,
|
||||
"CEILING",
|
||||
FontId::proportional(11.0),
|
||||
Color32::from_gray(180),
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw a vertical bar within `[top, bottom]`. `frac` is 0..1; `from_top` fills downward from the
|
||||
/// top (gain reduction) instead of upward from the bottom (level).
|
||||
fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Color32, from_top: bool) {
|
||||
let track = Color32::from_rgb(34, 34, 40);
|
||||
p.rect_filled(Rect::from_min_max(pos2(x, top), pos2(x + w, bottom)), CornerRadius::ZERO, track);
|
||||
|
||||
let h = (bottom - top) * frac.clamp(0.0, 1.0);
|
||||
let filled = if from_top {
|
||||
Rect::from_min_max(pos2(x, top), pos2(x + w, top + h))
|
||||
} else {
|
||||
Rect::from_min_max(pos2(x, bottom - h), pos2(x + w, bottom))
|
||||
};
|
||||
p.rect_filled(filled, CornerRadius::ZERO, fill);
|
||||
}
|
||||
|
||||
/// Level-bar colour: green below -6 dB, yellow approaching, red near 0 dBFS.
|
||||
fn level_color(db: f32) -> Color32 {
|
||||
if db >= -1.0 {
|
||||
Color32::from_rgb(235, 70, 60)
|
||||
} else if db >= -6.0 {
|
||||
Color32::from_rgb(230, 200, 70)
|
||||
} else {
|
||||
Color32::from_rgb(90, 200, 110)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user