refactor: split editor.rs into editor/ widget modules
Move the egui editor from a single editor.rs into an editor/ module: mod.rs (aggregator: create(), EditorState, layout, placeholder slider columns), meter.rs (|L|GR|R| meters + ceiling lamp, owns MeterState), and plot.rs (rolling in/out/GR plot, owns PlotState + PlotHistory). Each visualiser owns its GUI state; the aggregator composes them. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-394
@@ -1,394 +0,0 @@
|
|||||||
//! egui editor.
|
|
||||||
//!
|
|
||||||
//! Placeholder control layout for now — Stage 6 will keep adding visualisers (a rolling
|
|
||||||
//! reduction/in/out plot next, then a gain-curve view and draggable crossover handles) and
|
|
||||||
//! eventually replace the slider columns. Built to stay usable meanwhile: a resizable window with
|
|
||||||
//! a vertical scroll area so every control is reachable at any size, global controls in a
|
|
||||||
//! label|slider grid, and the four channels (low/mid/high/all) side by side.
|
|
||||||
//!
|
|
||||||
//! Meters: a per-channel `|L | GR | R|` cluster (output level left/right + mono gain reduction in
|
|
||||||
//! the middle) plus a latching 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, pos2, vec2, Align2, Color32, CornerRadius, CursorIcon, FontId, Painter, Rect, Sense, Stroke, 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;
|
|
||||||
|
|
||||||
/// 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) above which the ceiling lamp latches on.
|
|
||||||
const LAMP_TRIGGER_DB: f32 = 0.1;
|
|
||||||
/// How long the ceiling lamp stays lit after the most recent catch (seconds).
|
|
||||||
const LAMP_HOLD_S: f64 = 3.0;
|
|
||||||
/// Height of the meter panel.
|
|
||||||
const METER_PANEL_H: f32 = 130.0;
|
|
||||||
/// Height of the scrolling-plot panel.
|
|
||||||
const PLOT_PANEL_H: f32 = 150.0;
|
|
||||||
/// Number of frames held in the scrolling-plot history (~a few seconds at ~60 fps).
|
|
||||||
const PLOT_N: usize = 256;
|
|
||||||
|
|
||||||
const COLOR_IN: Color32 = Color32::from_rgb(90, 170, 235);
|
|
||||||
const COLOR_OUT: Color32 = Color32::from_rgb(90, 200, 110);
|
|
||||||
const COLOR_GR: Color32 = Color32::from_rgb(240, 150, 60);
|
|
||||||
|
|
||||||
/// Rolling history for the in/out/GR plot — kept for ALL channels at once (cheap: ~12 KB), so
|
|
||||||
/// switching the selected tab shows that channel's existing history rather than restarting blank.
|
|
||||||
/// It's a per-channel ring sampled once per GUI frame (wall-clock, not sample-accurate).
|
|
||||||
struct PlotHistory {
|
|
||||||
in_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
|
||||||
out_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
|
||||||
gr_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
|
||||||
write: usize,
|
|
||||||
len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlotHistory {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
in_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
|
|
||||||
out_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
|
|
||||||
gr_db: [[0.0; PLOT_N]; NUM_CHANNELS],
|
|
||||||
write: 0,
|
|
||||||
len: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotHistory {
|
|
||||||
/// Append one frame of (in_db, out_db, gr_db) per channel.
|
|
||||||
fn push(&mut self, samples: &[(f32, f32, f32); NUM_CHANNELS]) {
|
|
||||||
for i in 0..NUM_CHANNELS {
|
|
||||||
self.in_db[i][self.write] = samples[i].0;
|
|
||||||
self.out_db[i][self.write] = samples[i].1;
|
|
||||||
self.gr_db[i][self.write] = samples[i].2;
|
|
||||||
}
|
|
||||||
self.write = (self.write + 1) % PLOT_N;
|
|
||||||
self.len = (self.len + 1).min(PLOT_N);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GUI-side editor state (not persisted): ceiling-lamp latch, selected plot channel, plot history,
|
|
||||||
/// and the time-based scroll cadence (flow speed = how many seconds span the plot width).
|
|
||||||
struct EditorState {
|
|
||||||
/// egui time (seconds) of the most recent ceiling catch, while the lamp is latched on.
|
|
||||||
/// `None` = lamp off (never caught, expired, or dismissed by a click).
|
|
||||||
ceiling_trigger: Option<f64>,
|
|
||||||
/// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all).
|
|
||||||
selected: usize,
|
|
||||||
history: PlotHistory,
|
|
||||||
/// Seconds of history shown across the full plot width — the flow speed (smaller = faster).
|
|
||||||
window_s: f64,
|
|
||||||
/// egui time of the last column pushed to the history ring (the cadence clock).
|
|
||||||
last_push: f64,
|
|
||||||
/// Per-channel max accumulator (in_db, out_db, gr_db) for the column currently being built.
|
|
||||||
acc: [(f32, f32, f32); NUM_CHANNELS],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for EditorState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
ceiling_trigger: None,
|
|
||||||
selected: 0,
|
|
||||||
history: PlotHistory::default(),
|
|
||||||
window_s: 5.0,
|
|
||||||
last_push: 0.0,
|
|
||||||
acc: [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(),
|
|
||||||
EditorState::default(),
|
|
||||||
|_, _| {},
|
|
||||||
move |egui_ctx, setter, state| {
|
|
||||||
// Keep frames coming so the meters animate and the lamp can time out while 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));
|
|
||||||
ui.label("Ratio");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.ratio, setter));
|
|
||||||
ui.label("Knee");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.knee_db, setter));
|
|
||||||
ui.label("Attack");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.attack_ms, setter));
|
|
||||||
ui.label("Release");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.release_ms, setter));
|
|
||||||
ui.label("Makeup");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.makeup_db, setter));
|
|
||||||
ui.add(widgets::ParamSlider::for_param(&p.bypass, setter));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resizable window; vertical scroll so every control stays reachable even when the
|
|
||||||
// window is small. (Placeholder layout — Stage 6 will replace it.)
|
|
||||||
ResizableWindow::new("editor")
|
|
||||||
.min_size(Vec2::new(480.0, 320.0))
|
|
||||||
.show(egui_ctx, egui_state.as_ref(), |ui| {
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
||||||
ui.heading(Codename206::NAME);
|
|
||||||
draw_meters(ui, &meters, state);
|
|
||||||
ui.separator();
|
|
||||||
draw_plot(ui, &meters, state);
|
|
||||||
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");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_low_hz, setter));
|
|
||||||
ui.end_row();
|
|
||||||
ui.label("Xover Mid/Hi");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
|
||||||
ui.end_row();
|
|
||||||
ui.label("Look-ahead");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter));
|
|
||||||
ui.end_row();
|
|
||||||
ui.label("Ceiling");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.output_ceiling_db, setter));
|
|
||||||
ui.end_row();
|
|
||||||
ui.label("Lim Release");
|
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.limiter_release_ms, setter));
|
|
||||||
ui.end_row();
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
ui.columns(4, |cols| {
|
|
||||||
band_col(&mut cols[0], "LOW", ¶ms.low);
|
|
||||||
band_col(&mut cols[1], "MID", ¶ms.mid);
|
|
||||||
band_col(&mut cols[2], "HIGH", ¶ms.high);
|
|
||||||
band_col(&mut cols[3], "ALL", ¶ms.all);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw the meter panel: one channel group per column as `|L | GR | R|` (output level left/right,
|
|
||||||
/// mono gain reduction in the middle), plus the latching ceiling lamp driven by the limiter.
|
|
||||||
fn draw_meters(ui: &mut egui::Ui, meters: &Meters, state: &mut EditorState) {
|
|
||||||
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
|
||||||
let now = ui.ctx().input(|i| i.time);
|
|
||||||
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;
|
|
||||||
// Three bars per cluster now, so they're narrower than the old two-bar layout.
|
|
||||||
let bar_w = (cell_w * 0.17).min(14.0);
|
|
||||||
let gap = (cell_w * 0.05).min(5.0);
|
|
||||||
|
|
||||||
for i in 0..NUM_CHANNELS {
|
|
||||||
let cell_left = rect.left() + i as f32 * cell_w;
|
|
||||||
let group_w = bar_w * 3.0 + gap * 2.0;
|
|
||||||
let bx = cell_left + (cell_w - group_w) * 0.5;
|
|
||||||
|
|
||||||
// L / R output level (upward); colour warns as it nears 0 dBFS.
|
|
||||||
let l_db = util::gain_to_db(meters.level_l[i].load(Ordering::Relaxed));
|
|
||||||
let r_db = util::gain_to_db(meters.level_r[i].load(Ordering::Relaxed));
|
|
||||||
let l_frac = ((l_db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
|
||||||
let r_frac = ((r_db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
// Mono gain reduction (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, top, bottom, l_frac, level_color(l_db), false);
|
|
||||||
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true);
|
|
||||||
v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, level_color(r_db), false);
|
|
||||||
|
|
||||||
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): latches on when the limiter catches a peak, then holds. It clears
|
|
||||||
// after LAMP_HOLD_S or when clicked. Re-arms while limiting is ongoing.
|
|
||||||
if meters.limiter_gr_db.load(Ordering::Relaxed) > LAMP_TRIGGER_DB {
|
|
||||||
state.ceiling_trigger = Some(now);
|
|
||||||
}
|
|
||||||
let center = pos2(rect.right() - 14.0, rect.top() + 14.0);
|
|
||||||
let lamp_rect = Rect::from_center_size(center, vec2(22.0, 22.0));
|
|
||||||
let resp = ui
|
|
||||||
.interact(lamp_rect, ui.id().with("ceiling_lamp"), Sense::click())
|
|
||||||
.on_hover_cursor(CursorIcon::PointingHand)
|
|
||||||
.on_hover_text("Ceiling reached — click to clear");
|
|
||||||
if resp.clicked() {
|
|
||||||
state.ceiling_trigger = None;
|
|
||||||
}
|
|
||||||
// Expire the latch once the hold time has passed.
|
|
||||||
if let Some(t) = state.ceiling_trigger {
|
|
||||||
if now - t >= LAMP_HOLD_S {
|
|
||||||
state.ceiling_trigger = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let lamp = if state.ceiling_trigger.is_some() {
|
|
||||||
Color32::from_rgb(255, 40, 40)
|
|
||||||
} else {
|
|
||||||
Color32::from_rgb(40, 12, 12)
|
|
||||||
};
|
|
||||||
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 the scrolling in/out/gain-reduction plot for the selected channel, plus the channel tabs.
|
|
||||||
/// History for all channels is sampled every frame (wall-clock), so it scrolls continuously while
|
|
||||||
/// the editor is open regardless of which tab is shown.
|
|
||||||
fn draw_plot(ui: &mut egui::Ui, meters: &Meters, state: &mut EditorState) {
|
|
||||||
let now = ui.ctx().input(|i| i.time);
|
|
||||||
// (Re)initialise the cadence clock on first use or after a long gap (e.g. tab hidden).
|
|
||||||
if state.last_push <= 0.0 || now - state.last_push > state.window_s {
|
|
||||||
state.last_push = now;
|
|
||||||
}
|
|
||||||
// Accumulate this frame's block peaks into the column currently being built.
|
|
||||||
for i in 0..NUM_CHANNELS {
|
|
||||||
let in_db = util::gain_to_db(meters.plot_in[i].load(Ordering::Relaxed));
|
|
||||||
let out_db = util::gain_to_db(meters.plot_out[i].load(Ordering::Relaxed));
|
|
||||||
let gr = meters.plot_gr[i].load(Ordering::Relaxed);
|
|
||||||
state.acc[i].0 = state.acc[i].0.max(in_db);
|
|
||||||
state.acc[i].1 = state.acc[i].1.max(out_db);
|
|
||||||
state.acc[i].2 = state.acc[i].2.max(gr);
|
|
||||||
}
|
|
||||||
// Emit columns on a fixed time grid so the window length stays accurate regardless of the
|
|
||||||
// frame rate. The while loop is bounded by PLOT_N thanks to the resync above.
|
|
||||||
let dt_col = state.window_s / PLOT_N as f64;
|
|
||||||
let mut pushed = false;
|
|
||||||
while now - state.last_push >= dt_col {
|
|
||||||
state.history.push(&state.acc);
|
|
||||||
state.last_push += dt_col;
|
|
||||||
pushed = true;
|
|
||||||
}
|
|
||||||
if pushed {
|
|
||||||
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel tabs + flow-speed selector + legend.
|
|
||||||
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
|
||||||
let speeds = [2.0f64, 5.0, 15.0, 45.0];
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Plot:");
|
|
||||||
for (i, l) in labels.iter().enumerate() {
|
|
||||||
ui.selectable_value(&mut state.selected, i, *l);
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
ui.label("Speed:");
|
|
||||||
let mut speed_changed = false;
|
|
||||||
for &w in &speeds {
|
|
||||||
if ui.selectable_value(&mut state.window_s, w, format!("{w:.0}s")).changed() {
|
|
||||||
speed_changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if speed_changed {
|
|
||||||
// Cadence changed: start the history fresh so the time axis is consistent.
|
|
||||||
state.history = PlotHistory::default();
|
|
||||||
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
|
|
||||||
state.last_push = now;
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
ui.colored_label(COLOR_IN, "in");
|
|
||||||
ui.colored_label(COLOR_OUT, "out");
|
|
||||||
ui.colored_label(COLOR_GR, "GR");
|
|
||||||
});
|
|
||||||
|
|
||||||
let (rect, _) =
|
|
||||||
ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover());
|
|
||||||
let p = ui.painter_at(rect);
|
|
||||||
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20));
|
|
||||||
|
|
||||||
let (top, bottom, left, right) =
|
|
||||||
(rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0);
|
|
||||||
let width = right - left;
|
|
||||||
let y_for_db = |db: f32| -> f32 {
|
|
||||||
let frac = ((db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
|
||||||
bottom - frac * (bottom - top)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gridlines (dB).
|
|
||||||
for &g in &[0.0f32, -12.0, -24.0, -48.0] {
|
|
||||||
let y = y_for_db(g);
|
|
||||||
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, Color32::from_gray(40)));
|
|
||||||
p.text(
|
|
||||||
pos2(left + 2.0, y),
|
|
||||||
Align2::LEFT_BOTTOM,
|
|
||||||
format!("{g:.0}"),
|
|
||||||
FontId::proportional(9.0),
|
|
||||||
Color32::from_gray(90),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let c = state.selected.min(NUM_CHANNELS - 1);
|
|
||||||
let (write, len) = (state.history.write, state.history.len);
|
|
||||||
if len >= 2 {
|
|
||||||
let draw_series = |series: &[f32; PLOT_N], to_db: &dyn Fn(f32) -> f32, color: Color32| {
|
|
||||||
let mut pts = Vec::with_capacity(len);
|
|
||||||
for k in 0..len {
|
|
||||||
let idx = (write + PLOT_N - len + k) % PLOT_N;
|
|
||||||
let pos = (PLOT_N - len + k) as f32 / (PLOT_N - 1) as f32; // newest hugs the right
|
|
||||||
pts.push(pos2(left + pos * width, y_for_db(to_db(series[idx]))));
|
|
||||||
}
|
|
||||||
p.add(egui::Shape::line(pts, Stroke::new(1.5, color)));
|
|
||||||
};
|
|
||||||
draw_series(&state.history.in_db[c], &|db| db, COLOR_IN);
|
|
||||||
draw_series(&state.history.out_db[c], &|db| db, COLOR_OUT);
|
|
||||||
// GR hangs from the 0 dB line: a reduction of X dB is drawn at the -X gridline.
|
|
||||||
draw_series(&state.history.gr_db[c], &|gr| -gr, COLOR_GR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
//! Per-channel level + gain-reduction meters and the latching ceiling lamp.
|
||||||
|
//!
|
||||||
|
//! Each channel is a `|L | GR | R|` cluster (output level left/right, mono gain reduction in the
|
||||||
|
//! middle). The ceiling lamp latches on a limiter catch and holds, clearing after `LAMP_HOLD_S`
|
||||||
|
//! or on a click. Fed by the lock-free [`Meters`] state the audio thread publishes each block.
|
||||||
|
|
||||||
|
use nih_plug::prelude::*;
|
||||||
|
use nih_plug_egui::egui::{
|
||||||
|
self, pos2, vec2, Align2, Color32, CornerRadius, CursorIcon, FontId, Painter, Rect, Sense,
|
||||||
|
};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use super::METER_FLOOR_DB;
|
||||||
|
use crate::meters::{Meters, NUM_CHANNELS};
|
||||||
|
|
||||||
|
/// Full-scale of the gain-reduction bar (fills downward from the top).
|
||||||
|
const GR_FULL_DB: f32 = 24.0;
|
||||||
|
/// Limiter gain reduction (dB) above which the ceiling lamp latches on.
|
||||||
|
const LAMP_TRIGGER_DB: f32 = 0.1;
|
||||||
|
/// How long the ceiling lamp stays lit after the most recent catch (seconds).
|
||||||
|
const LAMP_HOLD_S: f64 = 3.0;
|
||||||
|
/// Height of the meter panel.
|
||||||
|
const METER_PANEL_H: f32 = 130.0;
|
||||||
|
|
||||||
|
/// GUI-side state for the meter panel.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct MeterState {
|
||||||
|
/// egui time (seconds) of the most recent ceiling catch, while the lamp is latched on.
|
||||||
|
/// `None` = lamp off (never caught, expired, or dismissed by a click).
|
||||||
|
ceiling_trigger: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the meter panel: a `|L | GR | R|` cluster per channel plus the latching ceiling lamp.
|
||||||
|
pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) {
|
||||||
|
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
||||||
|
let now = ui.ctx().input(|i| i.time);
|
||||||
|
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;
|
||||||
|
// Three bars per cluster, so they're narrower than a two-bar layout.
|
||||||
|
let bar_w = (cell_w * 0.17).min(14.0);
|
||||||
|
let gap = (cell_w * 0.05).min(5.0);
|
||||||
|
|
||||||
|
for i in 0..NUM_CHANNELS {
|
||||||
|
let cell_left = rect.left() + i as f32 * cell_w;
|
||||||
|
let group_w = bar_w * 3.0 + gap * 2.0;
|
||||||
|
let bx = cell_left + (cell_w - group_w) * 0.5;
|
||||||
|
|
||||||
|
// L / R output level (upward); colour warns as it nears 0 dBFS.
|
||||||
|
let l_db = util::gain_to_db(meters.level_l[i].load(Ordering::Relaxed));
|
||||||
|
let r_db = util::gain_to_db(meters.level_r[i].load(Ordering::Relaxed));
|
||||||
|
let l_frac = ((l_db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
||||||
|
let r_frac = ((r_db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
// Mono gain reduction (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, top, bottom, l_frac, level_color(l_db), false);
|
||||||
|
v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true);
|
||||||
|
v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, level_color(r_db), false);
|
||||||
|
|
||||||
|
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): latches on when the limiter catches a peak, then holds. It clears
|
||||||
|
// after LAMP_HOLD_S or when clicked. Re-arms while limiting is ongoing.
|
||||||
|
if meters.limiter_gr_db.load(Ordering::Relaxed) > LAMP_TRIGGER_DB {
|
||||||
|
state.ceiling_trigger = Some(now);
|
||||||
|
}
|
||||||
|
let center = pos2(rect.right() - 14.0, rect.top() + 14.0);
|
||||||
|
let lamp_rect = Rect::from_center_size(center, vec2(22.0, 22.0));
|
||||||
|
let resp = ui
|
||||||
|
.interact(lamp_rect, ui.id().with("ceiling_lamp"), Sense::click())
|
||||||
|
.on_hover_cursor(CursorIcon::PointingHand)
|
||||||
|
.on_hover_text("Ceiling reached — click to clear");
|
||||||
|
if resp.clicked() {
|
||||||
|
state.ceiling_trigger = None;
|
||||||
|
}
|
||||||
|
// Expire the latch once the hold time has passed.
|
||||||
|
if let Some(t) = state.ceiling_trigger {
|
||||||
|
if now - t >= LAMP_HOLD_S {
|
||||||
|
state.ceiling_trigger = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let lamp = if state.ceiling_trigger.is_some() {
|
||||||
|
Color32::from_rgb(255, 40, 40)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(40, 12, 12)
|
||||||
|
};
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
//! egui editor: assembly + control layout.
|
||||||
|
//!
|
||||||
|
//! The aggregator. Builds the editor window and lays out the heading, the meter panel
|
||||||
|
//! ([`meter`]), the rolling plot ([`plot`]), and the (placeholder) per-channel slider columns.
|
||||||
|
//! Each visualiser owns its GUI state and drawing in its submodule; this module wires them
|
||||||
|
//! together and holds the shared [`EditorState`]. When the UI is redesigned the slider columns
|
||||||
|
//! get replaced and the visualisers stay as self-contained widgets.
|
||||||
|
|
||||||
|
use nih_plug::prelude::*;
|
||||||
|
use nih_plug_egui::{
|
||||||
|
create_egui_editor,
|
||||||
|
egui::{self, Vec2},
|
||||||
|
resizable_window::ResizableWindow,
|
||||||
|
widgets,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::meters::Meters;
|
||||||
|
use crate::params::{Codename206Params, CompressorParams};
|
||||||
|
use crate::Codename206;
|
||||||
|
|
||||||
|
mod meter;
|
||||||
|
mod plot;
|
||||||
|
|
||||||
|
/// Bottom of the dB scale shared by the meters and the plot (top is 0 dBFS).
|
||||||
|
const METER_FLOOR_DB: f32 = -60.0;
|
||||||
|
|
||||||
|
/// GUI-side editor state (not persisted): the per-widget state for the meter panel and the plot.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct EditorState {
|
||||||
|
meter: meter::MeterState,
|
||||||
|
plot: plot::PlotState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(),
|
||||||
|
EditorState::default(),
|
||||||
|
|_, _| {},
|
||||||
|
move |egui_ctx, setter, state| {
|
||||||
|
// Keep frames coming so the meters animate and the lamp can time out while open.
|
||||||
|
egui_ctx.request_repaint();
|
||||||
|
|
||||||
|
// One column of controls for a single compressor channel (placeholder layout).
|
||||||
|
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));
|
||||||
|
ui.label("Ratio");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.ratio, setter));
|
||||||
|
ui.label("Knee");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.knee_db, setter));
|
||||||
|
ui.label("Attack");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.attack_ms, setter));
|
||||||
|
ui.label("Release");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.release_ms, setter));
|
||||||
|
ui.label("Makeup");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.makeup_db, setter));
|
||||||
|
ui.add(widgets::ParamSlider::for_param(&p.bypass, setter));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resizable window; vertical scroll so every control stays reachable even when the
|
||||||
|
// window is small. (Placeholder layout — the redesign will replace the slider columns.)
|
||||||
|
ResizableWindow::new("editor")
|
||||||
|
.min_size(Vec2::new(480.0, 320.0))
|
||||||
|
.show(egui_ctx, egui_state.as_ref(), |ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.heading(Codename206::NAME);
|
||||||
|
meter::draw(ui, &meters, &mut state.meter);
|
||||||
|
ui.separator();
|
||||||
|
plot::draw(ui, &meters, &mut state.plot);
|
||||||
|
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");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_low_hz, setter));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Xover Mid/Hi");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.crossover_high_hz, setter));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Look-ahead");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Ceiling");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.output_ceiling_db, setter));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Lim Release");
|
||||||
|
ui.add(widgets::ParamSlider::for_param(¶ms.limiter_release_ms, setter));
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.columns(4, |cols| {
|
||||||
|
band_col(&mut cols[0], "LOW", ¶ms.low);
|
||||||
|
band_col(&mut cols[1], "MID", ¶ms.mid);
|
||||||
|
band_col(&mut cols[2], "HIGH", ¶ms.high);
|
||||||
|
band_col(&mut cols[3], "ALL", ¶ms.all);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
//! Rolling input/output/gain-reduction plot with per-channel tabs and a flow-speed selector.
|
||||||
|
//!
|
||||||
|
//! Histories for all four channels run continuously (cheap), so switching tabs shows that
|
||||||
|
//! channel's existing history. The scroll is time-based (a column every `window/PLOT_N` seconds)
|
||||||
|
//! so the window length — the flow speed — stays accurate regardless of frame rate, with
|
||||||
|
//! peak-preserving max accumulation between columns. Fed by the raw block-peak [`Meters`] feed.
|
||||||
|
|
||||||
|
use nih_plug::prelude::*;
|
||||||
|
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use super::METER_FLOOR_DB;
|
||||||
|
use crate::meters::{Meters, NUM_CHANNELS};
|
||||||
|
|
||||||
|
/// Height of the plot panel.
|
||||||
|
const PLOT_PANEL_H: f32 = 150.0;
|
||||||
|
/// Number of columns held in the history ring.
|
||||||
|
const PLOT_N: usize = 256;
|
||||||
|
|
||||||
|
const COLOR_IN: Color32 = Color32::from_rgb(90, 170, 235);
|
||||||
|
const COLOR_OUT: Color32 = Color32::from_rgb(90, 200, 110);
|
||||||
|
const COLOR_GR: Color32 = Color32::from_rgb(240, 150, 60);
|
||||||
|
|
||||||
|
/// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns.
|
||||||
|
struct PlotHistory {
|
||||||
|
in_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
||||||
|
out_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
||||||
|
gr_db: [[f32; PLOT_N]; NUM_CHANNELS],
|
||||||
|
write: usize,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlotHistory {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
in_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
|
||||||
|
out_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
|
||||||
|
gr_db: [[0.0; PLOT_N]; NUM_CHANNELS],
|
||||||
|
write: 0,
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlotHistory {
|
||||||
|
/// Append one column of (in_db, out_db, gr_db) per channel.
|
||||||
|
fn push(&mut self, samples: &[(f32, f32, f32); NUM_CHANNELS]) {
|
||||||
|
for i in 0..NUM_CHANNELS {
|
||||||
|
self.in_db[i][self.write] = samples[i].0;
|
||||||
|
self.out_db[i][self.write] = samples[i].1;
|
||||||
|
self.gr_db[i][self.write] = samples[i].2;
|
||||||
|
}
|
||||||
|
self.write = (self.write + 1) % PLOT_N;
|
||||||
|
self.len = (self.len + 1).min(PLOT_N);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GUI-side state for the plot: selected channel, history ring, and time-based scroll cadence.
|
||||||
|
pub(super) struct PlotState {
|
||||||
|
/// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all).
|
||||||
|
selected: usize,
|
||||||
|
history: PlotHistory,
|
||||||
|
/// Seconds of history shown across the full plot width — the flow speed (smaller = faster).
|
||||||
|
window_s: f64,
|
||||||
|
/// egui time of the last column pushed to the history ring (the cadence clock).
|
||||||
|
last_push: f64,
|
||||||
|
/// Per-channel max accumulator (in_db, out_db, gr_db) for the column currently being built.
|
||||||
|
acc: [(f32, f32, f32); NUM_CHANNELS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlotState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
selected: 0,
|
||||||
|
history: PlotHistory::default(),
|
||||||
|
window_s: 5.0,
|
||||||
|
last_push: 0.0,
|
||||||
|
acc: [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the scrolling in/out/gain-reduction plot for the selected channel, plus the channel tabs
|
||||||
|
/// and flow-speed selector. History for all channels advances every frame regardless of the tab.
|
||||||
|
pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
|
||||||
|
let now = ui.ctx().input(|i| i.time);
|
||||||
|
// (Re)initialise the cadence clock on first use or after a long gap (e.g. tab hidden).
|
||||||
|
if state.last_push <= 0.0 || now - state.last_push > state.window_s {
|
||||||
|
state.last_push = now;
|
||||||
|
}
|
||||||
|
// Accumulate this frame's block peaks into the column currently being built.
|
||||||
|
for i in 0..NUM_CHANNELS {
|
||||||
|
let in_db = util::gain_to_db(meters.plot_in[i].load(Ordering::Relaxed));
|
||||||
|
let out_db = util::gain_to_db(meters.plot_out[i].load(Ordering::Relaxed));
|
||||||
|
let gr = meters.plot_gr[i].load(Ordering::Relaxed);
|
||||||
|
state.acc[i].0 = state.acc[i].0.max(in_db);
|
||||||
|
state.acc[i].1 = state.acc[i].1.max(out_db);
|
||||||
|
state.acc[i].2 = state.acc[i].2.max(gr);
|
||||||
|
}
|
||||||
|
// Emit columns on a fixed time grid so the window length stays accurate regardless of the
|
||||||
|
// frame rate. The while loop is bounded by PLOT_N thanks to the resync above.
|
||||||
|
let dt_col = state.window_s / PLOT_N as f64;
|
||||||
|
let mut pushed = false;
|
||||||
|
while now - state.last_push >= dt_col {
|
||||||
|
state.history.push(&state.acc);
|
||||||
|
state.last_push += dt_col;
|
||||||
|
pushed = true;
|
||||||
|
}
|
||||||
|
if pushed {
|
||||||
|
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel tabs + flow-speed selector + legend.
|
||||||
|
let labels = ["LOW", "MID", "HIGH", "ALL"];
|
||||||
|
let speeds = [2.0f64, 5.0, 15.0, 45.0];
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Plot:");
|
||||||
|
for (i, l) in labels.iter().enumerate() {
|
||||||
|
ui.selectable_value(&mut state.selected, i, *l);
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.label("Speed:");
|
||||||
|
let mut speed_changed = false;
|
||||||
|
for &w in &speeds {
|
||||||
|
if ui.selectable_value(&mut state.window_s, w, format!("{w:.0}s")).changed() {
|
||||||
|
speed_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if speed_changed {
|
||||||
|
// Cadence changed: start the history fresh so the time axis is consistent.
|
||||||
|
state.history = PlotHistory::default();
|
||||||
|
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
|
||||||
|
state.last_push = now;
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.colored_label(COLOR_IN, "in");
|
||||||
|
ui.colored_label(COLOR_OUT, "out");
|
||||||
|
ui.colored_label(COLOR_GR, "GR");
|
||||||
|
});
|
||||||
|
|
||||||
|
let (rect, _) =
|
||||||
|
ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover());
|
||||||
|
let p = ui.painter_at(rect);
|
||||||
|
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20));
|
||||||
|
|
||||||
|
let (top, bottom, left, right) =
|
||||||
|
(rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0);
|
||||||
|
let width = right - left;
|
||||||
|
let y_for_db = |db: f32| -> f32 {
|
||||||
|
let frac = ((db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
|
||||||
|
bottom - frac * (bottom - top)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gridlines (dB).
|
||||||
|
for &g in &[0.0f32, -12.0, -24.0, -48.0] {
|
||||||
|
let y = y_for_db(g);
|
||||||
|
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, Color32::from_gray(40)));
|
||||||
|
p.text(
|
||||||
|
pos2(left + 2.0, y),
|
||||||
|
Align2::LEFT_BOTTOM,
|
||||||
|
format!("{g:.0}"),
|
||||||
|
FontId::proportional(9.0),
|
||||||
|
Color32::from_gray(90),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = state.selected.min(NUM_CHANNELS - 1);
|
||||||
|
let (write, len) = (state.history.write, state.history.len);
|
||||||
|
if len >= 2 {
|
||||||
|
let draw_series = |series: &[f32; PLOT_N], to_db: &dyn Fn(f32) -> f32, color: Color32| {
|
||||||
|
let mut pts = Vec::with_capacity(len);
|
||||||
|
for k in 0..len {
|
||||||
|
let idx = (write + PLOT_N - len + k) % PLOT_N;
|
||||||
|
let pos = (PLOT_N - len + k) as f32 / (PLOT_N - 1) as f32; // newest hugs the right
|
||||||
|
pts.push(pos2(left + pos * width, y_for_db(to_db(series[idx]))));
|
||||||
|
}
|
||||||
|
p.add(egui::Shape::line(pts, Stroke::new(1.5, color)));
|
||||||
|
};
|
||||||
|
draw_series(&state.history.in_db[c], &|db| db, COLOR_IN);
|
||||||
|
draw_series(&state.history.out_db[c], &|db| db, COLOR_OUT);
|
||||||
|
// GR hangs from the 0 dB line: a reduction of X dB is drawn at the -X gridline.
|
||||||
|
draw_series(&state.history.gr_db[c], &|gr| -gr, COLOR_GR);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user