//! 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)); }