feat(ui): MAIN and ADVANCED pages, Anvil main page around Mode, Split and Sub level

Producers found the panels crowded: every control had the same weight. The
header now carries a MAIN / ADVANCED switch (common-ui header_paged). The
main page keeps the controls a first result needs; the advanced page shows
everything, as before.

- Anvil: Mode (Replace or Enhance), Split and Sub level, plus Octave, Ghost,
  Keep and Output. Kick lock, Dynamics, Drive, Glide, Phase, Gate and the
  pitch switches move to Advanced. The note readout says when the original
  sub plays because tracking is unsure.
- Lathe, Temper, Knurl, Planish, Braze: tracking and fine-tuning controls
  (Gate, Smooth, Snap, Harmonics, Push at, Floor, Bright, Normalize, band
  gains, Phase) move to Advanced. Lathe labels the octaves of the note in Hz
  on the harmonic axis.
- Forge passes the page to its module pages.
- Weld: the main page shows the level won back at full height, Mode,
  Response and Output; the phase plot, Sections, Low, High and Gate move to
  Advanced.
- editor-check renders the advanced pages with --advanced.
This commit is contained in:
nvrl committed 2026-11-06 21:16:11 +01:00
1 parent d6939a753a
commit 01fd3241eb
10 files changed
+594 -227

No files matched your search

+171 -11
View File
@@ -1,6 +1,6 @@
//! Anvil editor, shared by the CLAP wrapper and the headless renderer.
use anvil_core::{
AnalyzerFrame, Settings, Source, AUDITION, AUDITION_HITS, GHOST_COUNT, KICK_OFFSET_MAX,
AnalyzerFrame, Mode, Settings, Source, AUDITION, AUDITION_HITS, GHOST_COUNT, KICK_OFFSET_MAX,
PHONE_LOW, SCOPE,
};
use common_ui::*;
@@ -20,10 +20,13 @@ pub struct State {
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
/// The advanced page shows instead of the main one. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
Self {
advanced: false,
solo: false,
phone: false,
audition: false,
@@ -206,7 +209,8 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "ANVIL", "SUB ANCHOR", |ui| {
let mut advanced = state.advanced;
header_paged(ui, "ANVIL", "SUB ANCHOR", Some(&mut advanced), |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
@@ -232,6 +236,7 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
state.advanced = advanced;
ui.add_space(6.);
let frame = state.analyzer;
ui.horizontal_top(|ui| {
@@ -247,15 +252,24 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
});
});
ui.add_space(6.);
panel(ui, |ui| {
ui.set_width(ui.available_width());
ui.vertical(|ui| controls(ui, settings, state));
});
ui.add_space(6.);
panel(ui, |ui| {
ui.set_width(ui.available_width());
kick_panel(ui, settings, state);
});
if state.advanced {
panel(ui, |ui| {
ui.set_width(ui.available_width());
ui.vertical(|ui| controls(ui, settings, state));
});
ui.add_space(6.);
panel(ui, |ui| {
ui.set_width(ui.available_width());
kick_panel(ui, settings, state);
});
} else {
let height = ui.available_height() - 64.;
panel(ui, |ui| {
ui.set_width(ui.available_width());
ui.set_min_height(height);
ui.vertical(|ui| main_controls(ui, settings, state));
});
}
ui.separator();
ui.label(RichText::new(&state.help).color(MUTED));
}
@@ -264,6 +278,7 @@ fn note_panel(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame) {
caption(ui, "NOTE");
let (name, hz) = match frame.note {
Some(n) if frame.hz > 0. => (common_dsp::note_name(n), format!("{:.1} Hz", frame.hz)),
_ if frame.neutral => ("?".into(), "unsure: original sub plays".into()),
_ => ("—".into(), "no note".into()),
};
ui.label(
@@ -286,6 +301,131 @@ fn note_panel(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame) {
.color(MUTED),
);
}
/// The main page: what happens to the original sub, where the sub ends and how loud the
/// new one plays.
fn main_controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
let defaults = Settings::default();
ui.add_space(24.);
ui.columns(3, |c| {
c[0].vertical_centered(|ui| {
caption(ui, "MODE");
ui.add_space(6.);
ui.spacing_mut().item_spacing.x = 8.;
let r = choice(
ui,
&mut settings.mode,
&[
(
Mode::Replace,
"REPLACE",
"Swap the sub below Split for a clean sine: one level and phase on every note.",
),
(
Mode::Enhance,
"ENHANCE",
"Keep the original sub and add the clean sine under it for weight.",
),
],
);
if let Some(r) = r {
hint(&r, state, "Replace swaps the sub; Enhance keeps it and adds weight.");
}
ui.add_space(14.);
let text = match settings.mode {
Mode::Replace => {
"The band below Split is replaced by a clean sine on the played note. \
Every note gets the same level and starts at the same phase."
}
Mode::Enhance => {
"Your sub stays as it is. A clean sine on the played note is added \
under it. Try Octave −1 in Advanced for a sub-octave."
}
};
ui.label(RichText::new(text).size(13.).color(MUTED));
ui.add_space(10.);
ui.label(
RichText::new(
"When the note is unclear, Anvil plays your original sub instead of guessing.",
)
.size(12.)
.color(DIM),
);
});
knob(
&mut c[1],
state,
"Split",
&mut settings.split,
40. ..=200.,
defaults.split,
" Hz",
120.,
"Set it just above the sub. Below it the sub is replaced (or enhanced).",
);
knob(
&mut c[2],
state,
"Sub level",
&mut settings.level,
-30. ..=0.,
defaults.level,
" dBFS",
120.,
"Peak level of the new sub. Every note gets exactly this level.",
);
});
ui.add_space(28.);
ui.separator();
ui.add_space(12.);
ui.columns(4, |c| {
let mut octave = settings.octave as f32;
knob(
&mut c[0],
state,
"Octave",
&mut octave,
-2. ..=1.,
0.,
"",
64.,
"Move the new sub up or down an octave against the played note.",
);
settings.octave = octave.round() as i32;
knob(
&mut c[1],
state,
"Ghost",
&mut settings.ghost,
0. ..=100.,
defaults.ghost,
" %",
64.,
"Lets phones and laptops hear the sub note: adds its harmonics the patch lacks.",
);
knob(
&mut c[2],
state,
"Keep",
&mut settings.keep,
0. ..=100.,
defaults.keep,
" %",
64.,
"Blend some of the original sub back under the new one (Replace mode).",
);
knob(
&mut c[3],
state,
"Output",
&mut settings.output,
-12. ..=12.,
defaults.output,
" dB",
64.,
"Output trim.",
);
});
}
fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
let defaults = Settings::default();
ui.columns(6, |c| {
@@ -445,6 +585,26 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"SNAP TO SEMITONE",
"Round the detected pitch. Turn off for slides and pitch bends.",
);
ui.separator();
caption(ui, "MODE");
for (mode, label, help) in [
(
Mode::Replace,
"REPLACE",
"Swap the sub below Split for the clean sine.",
),
(
Mode::Enhance,
"ENHANCE",
"Keep the original sub and add the sine under it.",
),
] {
let r = ui.selectable_label(settings.mode == mode, RichText::new(label).size(13.));
if r.clicked() {
settings.mode = mode;
}
hint(&r, state, help);
}
});
}
fn kick_panel(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
+65 -46
View File
@@ -14,12 +14,15 @@ pub struct State {
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
Self {
analyzer: AnalyzerFrame::default(),
gesture_active: false,
advanced: false,
help: DEFAULT_HELP.into(),
}
}
@@ -194,18 +197,26 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "BRAZE", "TEMPO-LOCKED BEATING", |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
let mut advanced = state.advanced;
header_paged(
ui,
"BRAZE",
"TEMPO-LOCKED BEATING",
Some(&mut advanced),
|ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
},
);
state.advanced = advanced;
ui.add_space(6.);
let frame = state.analyzer;
ui.horizontal_top(|ui| {
@@ -319,7 +330,8 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
});
ui.add_space(8.);
ui.separator();
ui.columns(6, |c| {
let advanced = state.advanced;
ui.columns(if advanced { 6 } else { 4 }, |c| {
knob(
&mut c[0],
state,
@@ -355,30 +367,35 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Moves the right channel's beat against the left. 100 %: one side peaks while \
the other nulls. Check the mono sum.",
);
let at = if advanced {
knob(
&mut c[3],
state,
"Phase",
&mut settings.phase,
0. ..=360.,
d.phase,
"°",
72.,
"Where the beat peaks. 0° puts the peak of the chosen harmonic on the beat.",
);
knob(
&mut c[4],
state,
"Gate",
&mut settings.gate,
-60. ..=-20.,
d.gate,
" dB",
72.,
"Input level that counts as a note. Raise it if tails retrigger.",
);
5
} else {
3
};
knob(
&mut c[3],
state,
"Phase",
&mut settings.phase,
0. ..=360.,
d.phase,
"°",
72.,
"Where the beat peaks. 0° puts the peak of the chosen harmonic on the beat.",
);
knob(
&mut c[4],
state,
"Gate",
&mut settings.gate,
-60. ..=-20.,
d.gate,
" dB",
72.,
"Input level that counts as a note. Raise it if tails retrigger.",
);
knob(
&mut c[5],
&mut c[at],
state,
"Output",
&mut settings.output,
@@ -389,15 +406,17 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Output trim.",
);
});
ui.add_space(4.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
if state.advanced {
ui.add_space(4.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
}
hint(
&r,
state,
"Round the detected pitch. Off keeps the copy's jumps on the real period, which is \
seamless on detuned patches.",
);
}
hint(
&r,
state,
"Round the detected pitch. Off keeps the copy's jumps on the real period, which is \
seamless on detuned patches.",
);
}
+65
View File
@@ -312,6 +312,16 @@ pub fn readout(ui: &mut egui::Ui, label: &str, value: String, color: Color32) {
}
/// Product header: accent mark, product name, subtitle, then right-aligned controls.
pub fn header(ui: &mut egui::Ui, name: &str, subtitle: &str, right: impl FnOnce(&mut egui::Ui)) {
header_paged(ui, name, subtitle, None, right);
}
/// Product header with the MAIN / ADVANCED page switch after the subtitle.
pub fn header_paged(
ui: &mut egui::Ui,
name: &str,
subtitle: &str,
advanced: Option<&mut bool>,
right: impl FnOnce(&mut egui::Ui),
) {
ui.horizontal(|ui| {
let (mark, _) = ui.allocate_exact_size(Vec2::new(4., 26.), Sense::hover());
ui.painter().rect_filled(mark, 1., ACCENT);
@@ -321,6 +331,10 @@ pub fn header(ui: &mut egui::Ui, name: &str, subtitle: &str, right: impl FnOnce(
.color(TEXT),
);
ui.label(RichText::new(subtitle).size(12.).color(MUTED));
if let Some(advanced) = advanced {
ui.add_space(14.);
page_switch(ui, advanced);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), right);
});
}
@@ -526,3 +540,54 @@ fn pitch_note(ui: &mut egui::Ui, pitch: &mut Pitch, octave: bool) {
});
}
}
/// MAIN / ADVANCED page switch. The main page holds the few controls a first result needs;
/// the advanced page holds everything else. Returns true while the advanced page shows.
pub fn page_switch(ui: &mut egui::Ui, advanced: &mut bool) -> bool {
ui.horizontal(|ui| {
for (page, label, help) in [
(
false,
"MAIN",
"The controls that matter for a first result.",
),
(true, "ADVANCED", "Every control, for fine tuning."),
] {
let r = ui
.selectable_label(*advanced == page, RichText::new(label).size(12.))
.on_hover_text(help);
if r.clicked() {
*advanced = page;
}
}
});
*advanced
}
/// Choice drawn as large chips, centred, for a plugin's mode on its main page.
pub fn choice<T: PartialEq + Copy>(
ui: &mut egui::Ui,
value: &mut T,
options: &[(T, &str, &str)],
) -> Option<egui::Response> {
let mut hovered = None;
let n = options.len() as f32;
let width = n * 96. + (n - 1.) * ui.spacing().item_spacing.x;
ui.horizontal(|ui| {
ui.add_space(((ui.available_width() - width) / 2.).max(0.));
for (v, label, help) in options {
let r = ui
.add_sized(
[96., 30.],
egui::SelectableLabel::new(*value == *v, RichText::new(*label).size(14.)),
)
.on_hover_text(*help);
if r.clicked() {
*value = *v;
}
if r.hovered() {
hovered = Some(r);
}
}
});
hovered
}
+26 -12
View File
@@ -14,6 +14,8 @@ pub struct State {
pub knurl: knurl_ui::State,
/// Module whose page is shown.
pub page: Stage,
/// Module pages show their advanced controls. Not saved.
pub advanced: bool,
latency: u32,
sample_rate: f32,
/// True while a pointer button is held, so knob drags become one host gesture.
@@ -27,6 +29,7 @@ impl Default for State {
temper: temper_ui::State::default(),
knurl: knurl_ui::State::default(),
page: Stage::Lathe,
advanced: false,
latency: 0,
sample_rate: 48000.,
gesture_active: false,
@@ -121,18 +124,29 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.gesture_active = ui.input(|i| i.pointer.any_down());
settings.share();
ui.add_space(2.);
header(ui, "FORGE", "TRACKED CHANNEL STRIP", |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
let mut advanced = state.advanced;
header_paged(
ui,
"FORGE",
"TRACKED CHANNEL STRIP",
Some(&mut advanced),
|ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
},
);
state.advanced = advanced;
state.lathe.advanced = advanced;
state.temper.advanced = advanced;
state.knurl.advanced = advanced;
ui.add_space(6.);
panel(ui, |ui| {
ui.set_width(ui.available_width());
+67 -56
View File
@@ -18,12 +18,15 @@ pub struct State {
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
Self {
analyzer: AnalyzerFrame::default(),
gesture_active: false,
advanced: false,
help: DEFAULT_HELP.into(),
}
}
@@ -70,7 +73,8 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "KNURL", "CYCLE TEXTURE", |ui| {
let mut advanced = state.advanced;
header_paged(ui, "KNURL", "CYCLE TEXTURE", Some(&mut advanced), |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
@@ -82,6 +86,7 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
state.advanced = advanced;
body(ui, settings, state);
ui.separator();
ui.label(RichText::new(&state.help).color(MUTED));
@@ -397,20 +402,11 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
hint(&plus, state, help);
});
ui.add_space(4.);
ui.columns(10, |c| {
let advanced = state.advanced;
ui.columns(if advanced { 10 } else { 5 }, |c| {
knob(
&mut c[0],
state,
"Edge",
&mut settings.edge,
5. ..=50.,
d.edge,
" %",
"How soft each step change is, as a share of a cycle. Low is sharper and buzzier.",
);
knob(
&mut c[1],
state,
"Level",
&mut settings.level_jitter,
0. ..=6.,
@@ -419,7 +415,7 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Random level per cycle: a rough, living surface.",
);
knob(
&mut c[2],
&mut c[1],
state,
"Time",
&mut settings.time_jitter,
@@ -429,7 +425,7 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Random timing per cycle, in percent of a period: rattle and scrape.",
);
knob(
&mut c[3],
&mut c[2],
state,
"Grit",
&mut settings.grit,
@@ -438,48 +434,63 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
" dB",
"Level of the noise burst in each cycle, relative to the input. Fully left is off.",
);
let at = if advanced {
knob(
&mut c[3],
state,
"Edge",
&mut settings.edge,
5. ..=50.,
d.edge,
" %",
"How soft each step change is, as a share of a cycle. Low is sharper and buzzier.",
);
knob(
&mut c[4],
state,
"Tone",
&mut settings.tone,
4. ..=64.,
d.tone,
" ×f0",
"Lowest frequency of the grit, in harmonics of the note.",
);
knob(
&mut c[5],
state,
"Position",
&mut settings.position,
0. ..=100.,
d.position,
" %",
"Where in the cycle the grit burst sits.",
);
knob(
&mut c[6],
state,
"Length",
&mut settings.length,
2. ..=50.,
d.length,
" %",
"Length of the grit burst, as a share of a cycle.",
);
knob(
&mut c[7],
state,
"Gate",
&mut settings.gate,
-60. ..=-20.,
d.gate,
" dB",
"Input level that counts as a note (AUDIO). Raise it if tails retrigger.",
);
8
} else {
3
};
knob(
&mut c[4],
state,
"Tone",
&mut settings.tone,
4. ..=64.,
d.tone,
" ×f0",
"Lowest frequency of the grit, in harmonics of the note.",
);
knob(
&mut c[5],
state,
"Position",
&mut settings.position,
0. ..=100.,
d.position,
" %",
"Where in the cycle the grit burst sits.",
);
knob(
&mut c[6],
state,
"Length",
&mut settings.length,
2. ..=50.,
d.length,
" %",
"Length of the grit burst, as a share of a cycle.",
);
knob(
&mut c[7],
state,
"Gate",
&mut settings.gate,
-60. ..=-20.,
d.gate,
" dB",
"Input level that counts as a note (AUDIO). Raise it if tails retrigger.",
);
knob(
&mut c[8],
&mut c[at],
state,
"Mix",
&mut settings.mix,
@@ -489,7 +500,7 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Blend with the delayed dry signal.",
);
knob(
&mut c[9],
&mut c[at + 1],
state,
"Output",
&mut settings.output,
+52 -21
View File
@@ -14,6 +14,8 @@ pub struct State {
pub help: String,
/// Last point of a draw stroke in the harmonic editor: (harmonic index, dB).
stroke: Option<(usize, f32)>,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
@@ -22,6 +24,7 @@ impl Default for State {
gesture_active: false,
help: DEFAULT_HELP.into(),
stroke: None,
advanced: false,
}
}
}
@@ -77,6 +80,13 @@ fn y_gain(rect: Rect, y: f32) -> f32 {
}
/// Bar editor of the per-harmonic gains, with the effective curve and measured levels.
fn hz_label(hz: f32) -> String {
if hz >= 1000. {
format!("{:.1}k", hz / 1000.)
} else {
format!("{hz:.0}")
}
}
fn harmonic_editor(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State, height: f32) {
let size = Vec2::new(ui.available_width(), height);
let (outer, response) = ui.allocate_exact_size(size, Sense::click_and_drag());
@@ -199,6 +209,17 @@ fn harmonic_editor(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State
FontId::proportional(10.),
label_color,
);
// Where the harmonic sits now, at the octaves of the fundamental.
let hz = state.analyzer.hz;
if hz > 0. && (k + 1).is_power_of_two() {
p.text(
Pos2::new(x0 + col * 0.5, gains.bottom() - 2.),
egui::Align2::CENTER_BOTTOM,
hz_label(hz * (k + 1) as f32),
FontId::proportional(9.),
DIM,
);
}
// Measured levels: input (steel) and output (accent), side by side.
let f = &state.analyzer;
for (i, (db, color)) in [(f.input[k], STEEL), (f.output[k], ACCENT)]
@@ -269,7 +290,8 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "LATHE", "HARMONIC EQ", |ui| {
let mut advanced = state.advanced;
header_paged(ui, "LATHE", "HARMONIC EQ", Some(&mut advanced), |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
@@ -281,6 +303,7 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
state.advanced = advanced;
body(ui, settings, state);
ui.separator();
ui.label(RichText::new(&state.help).color(MUTED));
@@ -391,21 +414,24 @@ fn note_panel(
hint(&r, state, help);
}
});
ui.add_space(10.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
if state.advanced {
ui.add_space(10.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
}
hint(
&r,
state,
"Round the detected pitch. Turn off for slides and pitch bends.",
);
}
hint(
&r,
state,
"Round the detected pitch. Turn off for slides and pitch bends.",
);
}
fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
let d = Settings::default();
ui.columns(9, |c| {
let advanced = state.advanced;
ui.columns(if advanced { 9 } else { 6 }, |c| {
knob(&mut c[0], state, "Tilt", &mut settings.tilt, -12. ..=12., d.tilt, " dB/oct", 80.,
"Brighter or darker by harmonic number: dB per octave of harmonics, pivot on the fundamental.");
knob(&mut c[1], state, "Odd/Even", &mut settings.odd_even, -12. ..=12., d.odd_even, " dB", 80.,
@@ -414,17 +440,22 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Gain between the harmonics. Cut to clean noise and beating, lift for grit.");
knob(&mut c[3], state, "Width", &mut settings.width, 5. ..=50., d.width, " %", 80.,
"Bandwidth of every band as a share of the harmonic spacing.");
let mut harmonics = settings.harmonics as f32;
knob(&mut c[4], state, "Harmonics", &mut harmonics, 8. ..=32., d.harmonics as f32, "", 54.,
"How many harmonics are shaped. Higher harmonics pass unchanged.");
settings.harmonics = harmonics.round() as u32;
knob(&mut c[5], state, "Smooth", &mut settings.smooth, 0. ..=100., d.smooth, " ms", 54.,
"Retune time for slides and bends. New notes always retune at once.");
knob(&mut c[6], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
knob(&mut c[7], state, "Mix", &mut settings.mix, 0. ..=100., d.mix, " %", 54.,
let last = if advanced {
let mut harmonics = settings.harmonics as f32;
knob(&mut c[4], state, "Harmonics", &mut harmonics, 8. ..=32., d.harmonics as f32, "", 54.,
"How many harmonics are shaped. Higher harmonics pass unchanged.");
settings.harmonics = harmonics.round() as u32;
knob(&mut c[5], state, "Smooth", &mut settings.smooth, 0. ..=100., d.smooth, " ms", 54.,
"Retune time for slides and bends. New notes always retune at once.");
knob(&mut c[6], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
7
} else {
4
};
knob(&mut c[last], state, "Mix", &mut settings.mix, 0. ..=100., d.mix, " %", 54.,
"Blend with the delayed dry signal.");
knob(&mut c[8], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB", 54.,
knob(&mut c[last + 1], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB", 54.,
"Output trim.");
});
}
+29 -17
View File
@@ -17,12 +17,15 @@ pub struct State {
pub help: String,
/// Band edge being dragged on the harmonic ruler.
dragging: Option<usize>,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
Self {
analyzer: AnalyzerFrame::default(),
gesture_active: false,
advanced: false,
help: DEFAULT_HELP.into(),
dragging: None,
}
@@ -350,7 +353,8 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "PLANISH", "HARMONIC OTT", |ui| {
let mut advanced = state.advanced;
header_paged(ui, "PLANISH", "HARMONIC OTT", Some(&mut advanced), |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
@@ -362,6 +366,7 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
state.advanced = advanced;
ui.add_space(6.);
let frame = state.analyzer;
ui.horizontal_top(|ui| {
@@ -455,21 +460,25 @@ fn note_panel(
hint(&r, state, help);
}
});
ui.add_space(10.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
if state.advanced {
ui.add_space(10.);
let r = ui.selectable_label(settings.snap, RichText::new("SNAP TO SEMITONE").size(13.));
if r.clicked() {
settings.snap = !settings.snap;
}
hint(
&r,
state,
"Round the detected pitch. Off keeps the level window on the real period, which is steadier on detuned patches.",
);
}
hint(
&r,
state,
"Round the detected pitch. Off keeps the level window on the real period, which is steadier on detuned patches.",
);
}
fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
let d = Settings::default();
ui.columns(12, |c| {
// Main: the OTT controls. Advanced adds the per-band gains, Smooth and Gate.
let advanced = state.advanced;
ui.columns(if advanced { 12 } else { 6 }, |c| {
knob(&mut c[0], state, "Drive", &mut settings.drive, -24. ..=24., d.drive, " dB", 64.,
"Pushes the band levels against the thresholds: more drive, more squash. It does not change the level by itself.");
knob(&mut c[1], state, "Depth", &mut settings.depth, 0. ..=100., d.depth, " %", 64.,
@@ -480,7 +489,8 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Cut for bands above the downward threshold: holds loud harmonics down.");
knob(&mut c[4], state, "Time", &mut settings.time, 10. ..=1000., d.time, " %", 64.,
"Scales every attack and release. Lower bands are slower than higher ones.");
for b in 0..BANDS {
let at = if advanced { 9 } else { 5 };
for b in (0..BANDS).filter(|_| advanced) {
let name = format!("Band {}", b + 1);
let help = format!(
"Gain of band {} (harmonics {}) after its compressor, scaled by Depth.",
@@ -490,11 +500,13 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
knob(&mut c[5 + b], state, &name, &mut settings.gains[b], GAIN_MIN..=GAIN_MAX,
d.gains[b], " dB", 54., &help);
}
knob(&mut c[9], state, "Smooth", &mut settings.smooth, 0. ..=100., d.smooth, " ms", 54.,
"Retune time for slides and bends. New notes always retune at once.");
knob(&mut c[10], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
knob(&mut c[11], state, "Output", &mut settings.output, -24. ..=24., d.output, " dB", 54.,
if advanced {
knob(&mut c[9], state, "Smooth", &mut settings.smooth, 0. ..=100., d.smooth, " ms", 54.,
"Retune time for slides and bends. New notes always retune at once.");
knob(&mut c[10], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
}
knob(&mut c[if advanced { 11 } else { at }], state, "Output", &mut settings.output, -24. ..=24., d.output, " dB", 54.,
"Output trim.");
});
}
+50 -33
View File
@@ -13,12 +13,15 @@ pub struct State {
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
Self {
analyzer: AnalyzerFrame::default(),
gesture_active: false,
advanced: false,
help: DEFAULT_HELP.into(),
}
}
@@ -226,19 +229,27 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "TEMPER", "TRACKED DISTORTION", |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
toggles(ui, settings);
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
});
let mut advanced = state.advanced;
header_paged(
ui,
"TEMPER",
"TRACKED DISTORTION",
Some(&mut advanced),
|ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Crossfade to the delayed dry signal.");
if r.clicked() {
settings.bypass = !settings.bypass;
}
toggles(ui, settings);
ui.separator();
let mut mode = settings.source.mode();
pitch_from(ui, &mut mode, &mut settings.pitch);
settings.source = Source::from_mode(mode);
},
);
state.advanced = advanced;
body(ui, settings, state);
ui.separator();
ui.label(RichText::new(&state.help).color(MUTED));
@@ -371,7 +382,8 @@ fn note_panel(ui: &mut egui::Ui, settings: &Settings, frame: &AnalyzerFrame) {
fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
let d = Settings::default();
ui.columns(10, |c| {
let advanced = state.advanced;
ui.columns(if advanced { 10 } else { 5 }, |c| {
knob(&mut c[0], state, "Drive", &mut settings.drive, 0. ..=36., d.drive, " dB", 80.,
"Gain into the curve. With Normalize at 100 % every note starts from the same level.");
knob(&mut c[1], state, "Bias", &mut settings.bias, 0. ..=100., d.bias, " %", 80.,
@@ -380,25 +392,30 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
"Boost or cut one harmonic before the curve, so the distortion bites there on every note.");
knob(&mut c[3], state, "Mix", &mut settings.mix, 0. ..=100., d.mix, " %", 80.,
"Blend with the delayed dry signal.");
let mut push = settings.push_harmonic as f32;
knob(&mut c[4], state, "Push at", &mut push, 1. ..=16., d.push_harmonic as f32, "", 54.,
"Harmonic number that Push boosts or cuts.");
settings.push_harmonic = push.round().clamp(1., 16.) as u32;
knob(&mut c[5], state, "Floor", &mut settings.floor, 0. ..=2., d.floor, " ×f0", 54.,
"Highpass before the curve at this multiple of the note. 0 is off. Keeps sub mud out of the distortion.");
if settings.floor < 0.25 {
settings.floor = 0.;
}
knob(&mut c[6], state, "Bright", &mut settings.bright, 0. ..=64., d.bright, " ×f0", 54.,
"Lowpass after the curve at this harmonic. 0 is off. The top end stops at the same harmonic on every note.");
if settings.bright < 4. {
settings.bright = 0.;
}
knob(&mut c[7], state, "Normalize", &mut settings.normalize, 0. ..=100., d.normalize, " %", 54.,
"How far each note's level is evened out before Drive. 100 %: quiet and loud patches distort the same.");
knob(&mut c[8], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
knob(&mut c[9], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB", 54.,
let last = if advanced {
let mut push = settings.push_harmonic as f32;
knob(&mut c[4], state, "Push at", &mut push, 1. ..=16., d.push_harmonic as f32, "", 54.,
"Harmonic number that Push boosts or cuts.");
settings.push_harmonic = push.round().clamp(1., 16.) as u32;
knob(&mut c[5], state, "Floor", &mut settings.floor, 0. ..=2., d.floor, " ×f0", 54.,
"Highpass before the curve at this multiple of the note. 0 is off. Keeps sub mud out of the distortion.");
if settings.floor < 0.25 {
settings.floor = 0.;
}
knob(&mut c[6], state, "Bright", &mut settings.bright, 0. ..=64., d.bright, " ×f0", 54.,
"Lowpass after the curve at this harmonic. 0 is off. The top end stops at the same harmonic on every note.");
if settings.bright < 4. {
settings.bright = 0.;
}
knob(&mut c[7], state, "Normalize", &mut settings.normalize, 0. ..=100., d.normalize, " %", 54.,
"How far each note's level is evened out before Drive. 100 %: quiet and loud patches distort the same.");
knob(&mut c[8], state, "Gate", &mut settings.gate, -60. ..=-20., d.gate, " dB", 54.,
"Input level that counts as a note. Raise it if tails retrigger.");
9
} else {
4
};
knob(&mut c[last], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB", 54.,
"Output trim.");
});
}
+44 -26
View File
@@ -21,6 +21,8 @@ pub struct State {
/// True while a pointer button is held, so knob drags become one host gesture.
pub gesture_active: bool,
pub help: String,
/// The advanced page shows its extra controls. Not saved.
pub advanced: bool,
}
impl Default for State {
fn default() -> Self {
@@ -28,6 +30,7 @@ impl Default for State {
analyzer: AnalyzerFrame::default(),
report: Report::default(),
gesture_active: false,
advanced: false,
help: DEFAULT_HELP.into(),
}
}
@@ -86,7 +89,8 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
state.help = DEFAULT_HELP.into();
state.gesture_active = ui.input(|i| i.pointer.any_down());
ui.add_space(2.);
header(ui, "WELD", "LAYER ALIGN", |ui| {
let mut advanced = state.advanced;
header_paged(ui, "WELD", "LAYER ALIGN", Some(&mut advanced), |ui| {
let r = ui
.selectable_label(settings.bypass, RichText::new("BYPASS").size(13.))
.on_hover_text("Pass the main layer through unchanged.");
@@ -107,6 +111,7 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
}
caption(ui, "LISTEN");
});
state.advanced = advanced;
ui.add_space(6.);
ui.horizontal_top(|ui| {
panel(ui, |ui| {
@@ -119,9 +124,14 @@ pub fn show(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
ui.set_width(ui.available_width());
ui.vertical(|ui| {
let report = state.report;
band_plot(ui, &report, 190.);
ui.add_space(4.);
phase_plot(ui, &report, TOP - 194.);
// The phase plot is for fine tuning: the main page shows the level won back.
if state.advanced {
band_plot(ui, &report, 190.);
ui.add_space(4.);
phase_plot(ui, &report, TOP - 194.);
} else {
band_plot(ui, &report, TOP);
}
});
});
});
@@ -514,35 +524,43 @@ fn controls(ui: &mut egui::Ui, settings: &mut Settings, state: &mut State) {
}
hint(&r, state, help);
}
ui.add_space(30.);
caption(ui, "SECTIONS");
let minus = ui.button(RichText::new("−").size(14.));
if minus.clicked() && settings.sections > 1 {
settings.sections -= 1;
if state.advanced {
ui.add_space(30.);
caption(ui, "SECTIONS");
let minus = ui.button(RichText::new("−").size(14.));
if minus.clicked() && settings.sections > 1 {
settings.sections -= 1;
}
ui.label(RichText::new(format!("{}", settings.sections)).monospace().size(14.).color(TEXT));
let plus = ui.button(RichText::new("+").size(14.));
if plus.clicked() && settings.sections < MAX_SECTIONS {
settings.sections += 1;
}
let help = "Most allpass sections the fit may use. Fewer is gentler; Weld uses fewer when they are enough.";
hint(&minus, state, help);
hint(&plus, state, help);
}
ui.label(RichText::new(format!("{}", settings.sections)).monospace().size(14.).color(TEXT));
let plus = ui.button(RichText::new("+").size(14.));
if plus.clicked() && settings.sections < MAX_SECTIONS {
settings.sections += 1;
}
let help = "Most allpass sections the fit may use. Fewer is gentler; Weld uses fewer when they are enough.";
hint(&minus, state, help);
hint(&plus, state, help);
});
ui.add_space(4.);
ui.columns(6, |c| {
let advanced = state.advanced;
ui.columns(if advanced { 6 } else { 3 }, |c| {
knob(&mut c[0], state, "Response", &mut settings.response, 0.3..=5., d.response, " s",
"Averaging time in FOLLOW. Longer is steadier; shorter follows switch-ups faster.");
knob(&mut c[1], state, "Low", &mut settings.low, 30. ..=400., d.low, " Hz",
"Lowest frequency the fit listens to. Leave the sub to Anvil.");
knob(&mut c[2], state, "High", &mut settings.high, 1000. ..=12000., d.high, " Hz",
"Highest frequency the fit listens to.");
knob(&mut c[3], state, "Gate", &mut settings.gate, -70. ..=-30., d.gate, " dB",
"Moments where either layer is quieter than this are not measured.");
knob(&mut c[4], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB",
let at = if advanced {
knob(&mut c[1], state, "Low", &mut settings.low, 30. ..=400., d.low, " Hz",
"Lowest frequency the fit listens to. Leave the sub to Anvil.");
knob(&mut c[2], state, "High", &mut settings.high, 1000. ..=12000., d.high, " Hz",
"Highest frequency the fit listens to.");
knob(&mut c[3], state, "Gate", &mut settings.gate, -70. ..=-30., d.gate, " dB",
"Moments where either layer is quieter than this are not measured.");
4
} else {
1
};
knob(&mut c[at], state, "Output", &mut settings.output, -12. ..=12., d.output, " dB",
"Output trim.");
let report = state.report;
c[5].vertical(|ui| {
c[at + 1].vertical(|ui| {
caption(ui, "TIME OFFSET");
let clear = report.lag_strength > 0.3;
ui.label(big(
+25 -5
View File
@@ -24,6 +24,8 @@ fn main() {
}
let patina = plugin == "patina" || args.iter().any(|arg| arg == "--resonance");
let compact = std::env::args().any(|arg| arg == "--compact");
// Render the ADVANCED page of editors that have one.
let advanced = std::env::args().any(|arg| arg == "--advanced");
let anvil = plugin == "anvil";
let crucible = plugin == "crucible";
let lathe = plugin == "lathe";
@@ -105,7 +107,10 @@ fn main() {
}
}
let mut anvil_settings = anvil_core::Settings::default();
let mut anvil_state = anvil_ui::State::default();
let mut anvil_state = anvil_ui::State {
advanced,
..anvil_ui::State::default()
};
if anvil {
anvil_settings.ghost = 60.;
anvil_settings.kick_lock = true;
@@ -136,6 +141,7 @@ fn main() {
}
let mut lathe_settings = lathe_core::Settings::default();
let mut lathe_state = lathe_ui::State::default();
lathe_state.advanced = advanced;
if lathe {
// A growl-like map: cut the hollow region, lift the 5th, darker top.
lathe_settings.gains[1] = -4.;
@@ -168,7 +174,10 @@ fn main() {
bright: 12.,
..Default::default()
};
let mut temper_state = temper_ui::State::default();
let mut temper_state = temper_ui::State {
advanced,
..temper_ui::State::default()
};
if temper {
let mut engine = temper_core::Engine::new(48000., 2).unwrap();
engine.set_settings(temper_settings).unwrap();
@@ -183,7 +192,10 @@ fn main() {
temper_state.accept_frame(engine.take_analyzer());
}
let mut knurl_settings = knurl_core::Settings::default();
let mut knurl_state = knurl_ui::State::default();
let mut knurl_state = knurl_ui::State {
advanced,
..knurl_ui::State::default()
};
if knurl {
// An F1 saw bass with the default Fry pattern, jitter and grit.
let mut engine = knurl_core::Engine::new(48000., 2).unwrap();
@@ -208,6 +220,7 @@ fn main() {
forge_core::Stage::Temper,
];
let mut forge_state = forge_ui::State::default();
forge_state.advanced = advanced;
forge_state.page = match args
.iter()
.position(|a| a == "--page")
@@ -237,6 +250,7 @@ fn main() {
..Default::default()
};
let mut planish_state = planish_ui::State::default();
planish_state.advanced = advanced;
if planish {
// A distorted F1 saw bass: loud body, thin top, so both directions work.
let mut engine = planish_core::Engine::new(48000., 2).unwrap();
@@ -255,7 +269,10 @@ fn main() {
width: 30.,
..Default::default()
};
let mut braze_state = braze_ui::State::default();
let mut braze_state = braze_ui::State {
advanced,
..braze_ui::State::default()
};
if braze {
// A reese-like F1 saw at 140 BPM, 2.3 beats into the bar.
let mut engine = braze_core::Engine::new(48000., 2).unwrap();
@@ -324,7 +341,10 @@ fn main() {
ballast_state.accept_frame(engine.take_analyzer());
}
let mut weld_settings = weld_core::Settings::default();
let mut weld_state = weld_ui::State::default();
let mut weld_state = weld_ui::State {
advanced,
..weld_ui::State::default()
};
if weld {
// Two layers of an F1 saw bass. The sidechain layer is polarity-flipped and runs
// through an allpass at 400 Hz, as an EQ in its chain would leave it. The real