Files
bass-foundry/plugins/quench/ui/src/lib.rs
T
nvrl 3d316cbe49 feat(quench): add Quench sub-safe clipper
Quench splits the signal with a Linkwitz-Riley crossover and lets the sub
pass unclipped. The top is clipped symmetrically at the ceiling minus the
sub's held peak, so it does not pump at the sub's rate and the sum stays
under the ceiling. What the clip removes is high-passed at the split, so its
products stay out of the low band. Two correction passes and an oversampled
hard clip hold the ceiling; a safety clip catches the downsampler's ringing.

- Controls: Drive, Ceiling, Split, Hold, Soft, Bypass.
- Listen to Quench, a plain clipper at the same settings, or the delta.
- Editor meter compares how much the low band changed for Quench and for a
  plain clip, with a scope of the output, the sub and the top's room.
- Measured: sub level +0.04 dB against -2.2 dB for a plain clip, low-band
  products -35 dB against -16 dB, at 0.5-1.2 dB less RMS.
- Registered in the catalog, workflows, editor-check, README, chain and
  development docs, and the website copy.
2026-11-06 15:51:36 +01:00

328 lines
10 KiB
Rust

//! 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<f32>,
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<Pos2> = (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<Pos2> = (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.",
);
});
}