diff --git a/plugins/anvil/ui/src/lib.rs b/plugins/anvil/ui/src/lib.rs index 55a4f10..569caef 100644 --- a/plugins/anvil/ui/src/lib.rs +++ b/plugins/anvil/ui/src/lib.rs @@ -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) { diff --git a/plugins/braze/ui/src/lib.rs b/plugins/braze/ui/src/lib.rs index e5c3fe5..83c3534 100644 --- a/plugins/braze/ui/src/lib.rs +++ b/plugins/braze/ui/src/lib.rs @@ -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.", - ); } diff --git a/plugins/common/ui/src/lib.rs b/plugins/common/ui/src/lib.rs index 2e90e9e..001ac92 100644 --- a/plugins/common/ui/src/lib.rs +++ b/plugins/common/ui/src/lib.rs @@ -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( + ui: &mut egui::Ui, + value: &mut T, + options: &[(T, &str, &str)], +) -> Option { + 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 +} diff --git a/plugins/forge/ui/src/lib.rs b/plugins/forge/ui/src/lib.rs index e4a8f04..a18cc3c 100644 --- a/plugins/forge/ui/src/lib.rs +++ b/plugins/forge/ui/src/lib.rs @@ -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()); diff --git a/plugins/knurl/ui/src/lib.rs b/plugins/knurl/ui/src/lib.rs index 46848d9..071ec8b 100644 --- a/plugins/knurl/ui/src/lib.rs +++ b/plugins/knurl/ui/src/lib.rs @@ -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, diff --git a/plugins/lathe/ui/src/lib.rs b/plugins/lathe/ui/src/lib.rs index 103e67f..7b924ba 100644 --- a/plugins/lathe/ui/src/lib.rs +++ b/plugins/lathe/ui/src/lib.rs @@ -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."); }); } diff --git a/plugins/planish/ui/src/lib.rs b/plugins/planish/ui/src/lib.rs index e91337c..dd04be1 100644 --- a/plugins/planish/ui/src/lib.rs +++ b/plugins/planish/ui/src/lib.rs @@ -17,12 +17,15 @@ pub struct State { pub help: String, /// Band edge being dragged on the harmonic ruler. dragging: Option, + /// 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."); }); } diff --git a/plugins/temper/ui/src/lib.rs b/plugins/temper/ui/src/lib.rs index 0530c36..773471d 100644 --- a/plugins/temper/ui/src/lib.rs +++ b/plugins/temper/ui/src/lib.rs @@ -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."); }); } diff --git a/plugins/weld/ui/src/lib.rs b/plugins/weld/ui/src/lib.rs index dc0b27d..f9c67f7 100644 --- a/plugins/weld/ui/src/lib.rs +++ b/plugins/weld/ui/src/lib.rs @@ -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( diff --git a/tools/editor-check/src/main.rs b/tools/editor-check/src/main.rs index fa824c6..c46071d 100644 --- a/tools/editor-check/src/main.rs +++ b/tools/editor-check/src/main.rs @@ -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