Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eee94379bc | |||
| 3aeb09957a |
@@ -91,6 +91,12 @@ a first-class mode, not an afterthought.
|
||||
- `look_ahead_ms` — look-ahead time (0–5 ms). Reported latency is **constant** (the max look-ahead); the knob only moves the detector tap within that fixed delay, so it is safe to adjust during playback (changing reported latency mid-stream crashes some hosts, FL included)
|
||||
- `crossover_low_hz` — low/mid crossover frequency
|
||||
- `crossover_high_hz` — mid/high crossover frequency
|
||||
|
||||
> **Crossover automation caveat:** the lo ≤ hi limit is enforced in the **editor only** (the two
|
||||
> are independent params). Host automation writes them directly, so it can drive lo past hi and
|
||||
> momentarily invert the mid band. The DSP clamps to a monotonic split so it won't break audio,
|
||||
> but FL's automation can misbehave once inverted. Not fixed by design — just don't automate the
|
||||
> two across each other.
|
||||
### Per-Channel Compressor (× 4: low, mid, high, **all** — one `#[nested]` params struct reused)
|
||||
- `pre_gain_db` — drive into the compressor (−24…+36 dB, smoothed)
|
||||
- `detection` — peak / RMS level detection
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Draggable crossover handles — replaces the two plain crossover sliders.
|
||||
//!
|
||||
//! A horizontal log-frequency strip split into LOW / MID / HIGH by two draggable handles, plus a
|
||||
//! number box per crossover (double-click to type an exact value). The handles enforce a dynamic
|
||||
//! limit: lo/mid can never exceed mid/hi (and vice-versa), on top of each param's own range.
|
||||
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::egui::{
|
||||
self, pos2, vec2, Align2, Color32, CornerRadius, DragValue, FontId, Painter, Rect, Sense, Stroke,
|
||||
};
|
||||
|
||||
use crate::params::Codename206Params;
|
||||
|
||||
/// Height of the handle strip.
|
||||
const GRAPH_H: f32 = 54.0;
|
||||
/// Displayed frequency axis (log), independent of the params' own ranges.
|
||||
const DISP_MIN_HZ: f32 = 20.0;
|
||||
const DISP_MAX_HZ: f32 = 20_000.0;
|
||||
|
||||
fn log_span() -> f32 {
|
||||
DISP_MAX_HZ.ln() - DISP_MIN_HZ.ln()
|
||||
}
|
||||
|
||||
fn fmt_hz(hz: f32) -> String {
|
||||
if hz >= 1000.0 {
|
||||
format!("{:.2} kHz", hz / 1000.0)
|
||||
} else {
|
||||
format!("{:.0} Hz", hz)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn draw(ui: &mut egui::Ui, params: &Codename206Params, setter: &ParamSetter) {
|
||||
let lo = params.crossover_low_hz.value();
|
||||
let hi = params.crossover_high_hz.value();
|
||||
// Each param's own min/max (normalized 0/1 map to the range ends).
|
||||
let lo_min = params.crossover_low_hz.preview_plain(0.0);
|
||||
let lo_max = params.crossover_low_hz.preview_plain(1.0);
|
||||
let hi_min = params.crossover_high_hz.preview_plain(0.0);
|
||||
let hi_max = params.crossover_high_hz.preview_plain(1.0);
|
||||
// Dynamic limits so the two never cross: lo <= hi.
|
||||
let lo_upper = lo_max.min(hi);
|
||||
let hi_lower = hi_min.max(lo);
|
||||
|
||||
ui.label("Crossover");
|
||||
|
||||
let (rect, _) =
|
||||
ui.allocate_exact_size(vec2(ui.available_width(), GRAPH_H), Sense::hover());
|
||||
let p = ui.painter_at(rect);
|
||||
let (left, right, top, bottom) =
|
||||
(rect.left() + 2.0, rect.right() - 2.0, rect.top() + 2.0, rect.bottom() - 2.0);
|
||||
let width = right - left;
|
||||
let x_for = |hz: f32| left + (hz.max(1.0).ln() - DISP_MIN_HZ.ln()) / log_span() * width;
|
||||
let xlo = x_for(lo);
|
||||
let xhi = x_for(hi);
|
||||
|
||||
// Three band regions.
|
||||
p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, Color32::from_rgb(28, 38, 52));
|
||||
p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, Color32::from_rgb(30, 48, 36));
|
||||
p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, Color32::from_rgb(52, 38, 30));
|
||||
|
||||
let band_label = |cx: f32, t: &str| {
|
||||
p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), Color32::from_gray(160));
|
||||
};
|
||||
band_label((left + xlo) * 0.5, "LOW");
|
||||
band_label((xlo + xhi) * 0.5, "MID");
|
||||
band_label((xhi + right) * 0.5, "HIGH");
|
||||
|
||||
handle(ui, &p, setter, "xover_lo", xlo, lo, ¶ms.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, Color32::from_rgb(120, 170, 230));
|
||||
handle(ui, &p, setter, "xover_hi", xhi, hi, ¶ms.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, Color32::from_rgb(230, 150, 120));
|
||||
|
||||
// Number boxes (double-click to type). Clamped to the same dynamic limits.
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Lo/Mid");
|
||||
let mut v = lo as f64;
|
||||
if ui
|
||||
.add(DragValue::new(&mut v).range(lo_min as f64..=lo_upper as f64).speed(0.5).suffix(" Hz"))
|
||||
.changed()
|
||||
{
|
||||
set_clamped(setter, ¶ms.crossover_low_hz, v as f32, lo_min, lo_upper);
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
ui.label("Mid/Hi");
|
||||
let mut v = hi as f64;
|
||||
if ui
|
||||
.add(DragValue::new(&mut v).range(hi_lower as f64..=hi_max as f64).speed(2.0).suffix(" Hz"))
|
||||
.changed()
|
||||
{
|
||||
set_clamped(setter, ¶ms.crossover_high_hz, v as f32, hi_lower, hi_max);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// One draggable vertical handle. Drives `param` from the pointer's x (log-frequency), clamped to
|
||||
/// `[min, max]` (which already encodes the dynamic lo<=hi limit), with proper begin/end gestures.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle(
|
||||
ui: &egui::Ui,
|
||||
p: &Painter,
|
||||
setter: &ParamSetter,
|
||||
id_salt: &str,
|
||||
x: f32,
|
||||
hz: f32,
|
||||
param: &FloatParam,
|
||||
min: f32,
|
||||
max: f32,
|
||||
left: f32,
|
||||
width: f32,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
color: Color32,
|
||||
) {
|
||||
let hit = Rect::from_min_max(pos2(x - 5.0, top), pos2(x + 5.0, bottom));
|
||||
let resp = ui
|
||||
.interact(hit, ui.id().with(id_salt), Sense::drag())
|
||||
.on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
|
||||
|
||||
if resp.drag_started() {
|
||||
setter.begin_set_parameter(param);
|
||||
}
|
||||
if resp.dragged() {
|
||||
if let Some(pos) = resp.interact_pointer_pos() {
|
||||
let frac = ((pos.x - left) / width).clamp(0.0, 1.0);
|
||||
let new = (DISP_MIN_HZ.ln() + frac * log_span()).exp().clamp(min, max);
|
||||
setter.set_parameter(param, new);
|
||||
}
|
||||
}
|
||||
if resp.drag_stopped() {
|
||||
setter.end_set_parameter(param);
|
||||
}
|
||||
|
||||
let col = if resp.dragged() || resp.hovered() { Color32::WHITE } else { color };
|
||||
p.line_segment([pos2(x, top), pos2(x, bottom)], Stroke::new(2.0, col));
|
||||
p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), Color32::from_gray(220));
|
||||
}
|
||||
|
||||
/// Set a param to `value` clamped to `[min, max]`, wrapped in its own gesture (for the number box).
|
||||
fn set_clamped(setter: &ParamSetter, param: &FloatParam, value: f32, min: f32, max: f32) {
|
||||
setter.begin_set_parameter(param);
|
||||
setter.set_parameter(param, value.clamp(min, max));
|
||||
setter.end_set_parameter(param);
|
||||
}
|
||||
+3
-6
@@ -19,6 +19,7 @@ use crate::meters::Meters;
|
||||
use crate::params::{Codename206Params, CompressorParams};
|
||||
use crate::Codename206;
|
||||
|
||||
mod crossover;
|
||||
mod meter;
|
||||
mod plot;
|
||||
|
||||
@@ -75,14 +76,10 @@ pub(crate) fn create(params: Arc<Codename206Params>, meters: Arc<Meters>) -> Opt
|
||||
ui.separator();
|
||||
plot::draw(ui, &meters, &mut state.plot);
|
||||
ui.separator();
|
||||
crossover::draw(ui, ¶ms, setter);
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user