a4c542b2d9
Add a per-channel below-threshold shaper composed in series ahead of the comp: gain = low_shape(level) + comp(level + low_shape(level)). The compressor's threshold now sees the shaped level, so a Low Slope boost lifts quiet material up into compression (and a cut pulls it out). Anchored at the -60 dB silence floor. Low Curve bends the shaper toward a bounded saturation so the serial composition doesn't blow up (0 = straight line). gain_computer split into comp_gain_db + low_gain_db and composed; shared with the editor gain-curve display. Slider order rearranged to read in signal order (pre-gain -> low shaper -> compressor -> output). Defaults (slope 1, curve 0) reproduce the plain compressor; 17 tests pass. Known: the bipolar behaviour isn't final yet (milestone commit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
5.5 KiB
Rust
117 lines
5.5 KiB
Rust
//! 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 crossover;
|
|
mod gain_curve;
|
|
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| {
|
|
// Roughly in signal order: input drive -> low shaper -> compressor -> output.
|
|
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("Low Slope");
|
|
ui.add(widgets::ParamSlider::for_param(&p.low_slope, setter));
|
|
ui.label("Low Curve");
|
|
ui.add(widgets::ParamSlider::for_param(&p.low_curve, 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.label("Mix");
|
|
ui.add(widgets::ParamSlider::for_param(&p.mix, 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();
|
|
// Gain curve (left, square) beside the scrolling plot (right, fills the rest).
|
|
let selected = state.plot.selected;
|
|
ui.horizontal_top(|ui| {
|
|
ui.vertical(|ui| gain_curve::draw(ui, ¶ms, selected));
|
|
ui.vertical(|ui| 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("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);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
)
|
|
}
|