//! Quench editor, shared by the CLAP wrapper and the headless renderer. use common_ui::*; use egui::{self, Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2}; use quench_core::{ AnalyzerFrame, Monitor, Settings, CEILING_MIN, DRIVE_MAX, HOLD_MAX, HOLD_MIN, SCOPE, SPLIT_MAX, SPLIT_MIN, }; pub use common_ui::style; pub const WIDTH: u32 = 980; pub const HEIGHT: u32 = 640; pub struct State { pub analyzer: AnalyzerFrame, /// What the output plays. Not saved: the wrapper resets it when the editor opens. pub monitor: Monitor, /// True while a pointer button is held, so knob drags become one host gesture. pub gesture_active: bool, pub help: String, } impl Default for State { fn default() -> Self { Self { analyzer: AnalyzerFrame::default(), monitor: Monitor::Quench, gesture_active: false, help: DEFAULT_HELP.into(), } } } const DEFAULT_HELP: &str = "Put Quench last on the master or the bass bus. Raise Drive until the top clips as hard as you want."; impl State { pub fn accept_frame(&mut self, frame: AnalyzerFrame) { self.analyzer = frame; } } fn hint(r: &egui::Response, state: &mut State, text: &str) { if r.hovered() || r.has_focus() { state.help = text.into(); } } #[allow(clippy::too_many_arguments)] fn knob( ui: &mut egui::Ui, state: &mut State, name: &str, value: &mut f32, range: std::ops::RangeInclusive, default: f32, unit: &str, help: &str, ) { ui.vertical_centered(|ui| { let r = rotary(ui, name, value, range, default, unit, 72., ACCENT); hint(&r, state, help); }); } /// The selected output with the ceiling, the sub band, and the room the sub leaves the top. fn scope(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame, height: f32) { let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), height), Sense::hover()); let p = ui.painter_at(rect); p.rect_filled(rect, 2., SCREEN); p.text( rect.left_top() + Vec2::new(10., 8.), egui::Align2::LEFT_TOP, "OUTPUT · SUB · ROOM FOR THE TOP", FontId::proportional(11.), MUTED, ); p.text( rect.right_top() + Vec2::new(-12., 8.), egui::Align2::RIGHT_TOP, format!( "{:.0} ms", SCOPE as f32 * 8. / frame.sample_rate.max(1.) * 1000. ), FontId::proportional(11.), MUTED, ); let area = Rect::from_min_max( rect.left_top() + Vec2::new(12., 28.), rect.right_bottom() - Vec2::new(12., 10.), ); let mid = area.center().y; let scale = area.height() * 0.5 / 1.08; let y = |v: f32| mid - v.clamp(-1.08, 1.08) * scale; let x = |i: usize| area.left() + area.width() * i as f32 / (SCOPE - 1) as f32; p.line_segment( [Pos2::new(area.left(), mid), Pos2::new(area.right(), mid)], Stroke::new(1.0_f32, Color32::from_rgb(32, 35, 40)), ); let c = 10f32.powf(settings.ceiling / 20.); for v in [c, -c] { p.line_segment( [Pos2::new(area.left(), y(v)), Pos2::new(area.right(), y(v))], Stroke::new(1.0_f32, ACCENT.gamma_multiply(0.6)), ); } p.text( Pos2::new(area.right() - 4., y(c) - 3.), egui::Align2::RIGHT_BOTTOM, format!("CEILING {:.1} dB", settings.ceiling), FontId::proportional(10.), ACCENT, ); for i in 0..SCOPE { let (lo, hi) = (frame.scope_min[i], frame.scope_max[i]); let top = y(hi); let bottom = y(lo).max(top + 1.); p.line_segment( [Pos2::new(x(i), top), Pos2::new(x(i), bottom)], Stroke::new(2.0_f32, STEEL.gamma_multiply(0.55)), ); } // Where the output may go: the sub plus or minus the top's room. for sign in [1., -1.] { let line: Vec = (0..SCOPE) .map(|i| Pos2::new(x(i), y(frame.scope_sub[i] + sign * frame.scope_room[i]))) .collect(); p.add(egui::Shape::line(line, Stroke::new(1.0_f32, MUTED))); } let sub: Vec = (0..SCOPE) .map(|i| Pos2::new(x(i), y(frame.scope_sub[i]))) .collect(); p.add(egui::Shape::line(sub, Stroke::new(2.0_f32, FOCUS))); p.rect_stroke( rect, 2., Stroke::new(1.0_f32, EDGE), egui::StrokeKind::Inside, ); } pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { backdrop(ui); state.help = DEFAULT_HELP.into(); state.gesture_active = ui.input(|i| i.pointer.any_down()); ui.add_space(2.); header(ui, "QUENCH", "SUB-SAFE CLIPPER", |ui| { let r = ui .selectable_label(settings.bypass, RichText::new("BYPASS").size(13.)) .on_hover_text("Crossfade to the input, delayed by the latency."); if r.clicked() { settings.bypass = !settings.bypass; } ui.separator(); for (monitor, label, text) in [ ( Monitor::Delta, "DELTA", "Hear what Quench takes away. Not saved.", ), ( Monitor::Plain, "PLAIN", "Hear a plain clipper at the same settings, to compare the low end. Not saved.", ), (Monitor::Quench, "QUENCH", "Hear Quench."), ] { let r = ui .selectable_label(state.monitor == monitor, RichText::new(label).size(13.)) .on_hover_text(text); if r.clicked() { state.monitor = monitor; } } caption(ui, "LISTEN"); }); ui.add_space(6.); let frame = state.analyzer; ui.horizontal_top(|ui| { panel(ui, |ui| { ui.set_width(250.); ui.set_min_height(TOP); ui.vertical(|ui| readings(ui, settings, &frame)); }); ui.add_space(4.); panel(ui, |ui| { ui.set_width(ui.available_width()); scope(ui, settings, &frame, TOP); }); }); ui.add_space(6.); panel(ui, |ui| { ui.set_width(ui.available_width()); ui.vertical(|ui| controls(ui, settings, state)); }); ui.separator(); ui.label(RichText::new(&state.help).color(MUTED)); } const TOP: f32 = 330.; fn readings(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame) { caption(ui, "LOW END CHANGED BY"); let has_sub = frame.low_quench.is_finite(); ui.label( RichText::new(if has_sub { format!("{:.0} dB", frame.low_quench) } else { "—".into() }) .font(FontId::new(44., egui::FontFamily::Name("medium".into()))) .color(if has_sub { TEXT } else { DIM }), ); ui.label( RichText::new(if has_sub { format!("plain clip: {:.0} dB", frame.low_plain) } else { "no sub playing".into() }) .monospace() .size(13.) .color(MUTED), ); ui.add_space(12.); readout( ui, "TOP CLIPPED BY", if frame.top_clip >= 40. { "no room left".into() } else if frame.top_clip > 0. { format!("{:.1} dB", frame.top_clip) } else { "not clipping".into() }, ACCENT, ); let room = settings.ceiling - frame.sub_peak; readout( ui, "SUB PEAK · ROOM LEFT", if frame.sub_peak > -119. { format!("{:.1} dBFS · {:.1} dB", frame.sub_peak, room.max(0.)) } else { "—".into() }, FOCUS, ); if frame.sub_peak > settings.ceiling - 1. { ui.label( RichText::new( "The sub alone reaches the ceiling: the top has no room and the safety clip \ cuts the sub. Lower Drive or the sub.", ) .size(12.) .color(ACCENT), ); ui.add_space(6.); } ui.add_space(4.); meter(ui, "IN", frame.input_peak, STEEL, 150.); meter(ui, "OUT", frame.output_peak, ACCENT, 150.); ui.add_space(6.); ui.label( RichText::new(format!( "safety clip {} · {:.1} ms latency", if frame.safety > 0. { format!("{:.2} dB", frame.safety) } else { "idle".into() }, frame.latency as f32 / frame.sample_rate.max(1.) * 1000. )) .size(12.) .color(MUTED), ); } fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) { let d = Settings::default(); ui.columns(5, |c| { knob( &mut c[0], state, "Drive", &mut settings.drive, 0. ..=DRIVE_MAX, d.drive, " dB", "Gain into the clipper. The sub rises with it: watch the room it leaves.", ); knob( &mut c[1], state, "Ceiling", &mut settings.ceiling, CEILING_MIN..=0., d.ceiling, " dB", "Highest output peak. The sub keeps its level; the top gets what is left.", ); knob( &mut c[2], state, "Split", &mut settings.split, SPLIT_MIN..=SPLIT_MAX, d.split, " Hz", "Below this the sub passes without clipping. Set it above the highest sub note, \ or at Anvil's Split.", ); knob( &mut c[3], state, "Hold", &mut settings.hold, HOLD_MIN..=HOLD_MAX, d.hold, " ms", "How long the sub's peak keeps its room. Longer than half a period of the lowest \ note keeps the top from pumping with the sub.", ); knob( &mut c[4], state, "Soft", &mut settings.soft, 0. ..=100., d.soft, " %", "Knee of the top's clip. 0 % is a hard clip, 100 % a soft curve.", ); }); }