From 2a8c60922fb94d7e52bd3ab4f5ac6513b58177e2 Mon Sep 17 00:00:00 2001 From: ebbicott Date: Thu, 8 Oct 2026 15:39:23 -0400 Subject: [PATCH 1/2] Plugin menus: hosted plugins by vendor in scrollable lists; Audio Units insert The Mix window's insert menu listed each format's plugins as one flat column. With many installed (53 VST3 and 95 Audio Units effects on a composer's Mac) it ran past the bottom of the screen and egui menus don't scroll, so the lower entries couldn't be reached. CLAP, VST3 and Audio Units now open one submenu per vendor, as Pro Tools does when plug-in menus are organized by manufacturer, and every list scrolls within the screen. Choosing an Audio Unit did nothing: the engine's plugin lookup, which mix.insert uses, covered built-in, CLAP and VST3 plugins only, so "au:" ids were "unknown plugin" and the UI dropped the error. The lookup now includes Audio Units (the engine depends on soundcraft-au-host, like the mixer and the UI), and the menu shows a failed insert in the status bar. Closes #21 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + crates/engine/Cargo.toml | 1 + crates/engine/src/cmd/clap.rs | 19 +++++++- crates/ui-egui/src/mix_window.rs | 84 ++++++++++++++++++++++---------- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6719efc..4f0c98ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3448,6 +3448,7 @@ dependencies = [ "proptest", "serde", "serde_json", + "soundcraft-au-host", "soundcraft-audio-io", "soundcraft-clap-host", "soundcraft-dsp", diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 18dacdc0..b698856c 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -14,6 +14,7 @@ soundcraft-midi.workspace = true soundcraft-dsp.workspace = true soundcraft-clap-host.workspace = true soundcraft-vst3-host.workspace = true +soundcraft-au-host.workspace = true soundcraft-model.workspace = true soundcraft-mix.workspace = true soundcraft-video.workspace = true diff --git a/crates/engine/src/cmd/clap.rs b/crates/engine/src/cmd/clap.rs index 7b39c1f4..15ddcd74 100644 --- a/crates/engine/src/cmd/clap.rs +++ b/crates/engine/src/cmd/clap.rs @@ -25,9 +25,13 @@ pub fn specs() -> Vec { )] } -/// A built-in plugin's description, else a hosted CLAP (`clap:`) or VST3 (`vst3:`) plugin's. +/// A built-in plugin's description, else a hosted CLAP (`clap:`), VST3 (`vst3:`) or +/// Audio Units (`au:::`) plugin's. pub fn plugin_info(id: &str) -> Option<&'static PluginInfo> { - soundcraft_dsp::plugin_info(id).or_else(|| soundcraft_clap_host::plugin_info(id)).or_else(|| soundcraft_vst3_host::plugin_info(id)) + soundcraft_dsp::plugin_info(id) + .or_else(|| soundcraft_clap_host::plugin_info(id)) + .or_else(|| soundcraft_vst3_host::plugin_info(id)) + .or_else(|| soundcraft_au_host::plugin_info(id)) } #[cfg(test)] @@ -52,4 +56,15 @@ mod tests { assert!(super::plugin_info("eq_7band").is_some()); assert!(super::plugin_info("clap:no.such.plugin").is_none()); } + + /// Apple's own Audio Units ship with every Mac, so `mix.insert` can be checked against one. + #[cfg(target_os = "macos")] + #[test] + fn audio_units_insert_by_id() { + let mut e = Engine::default(); + e.execute("track.new", &json!({})).unwrap(); + let t = e.session().tracks[0].id.0; + let r = e.execute("mix.insert", &json!({"track": t, "plugin": "au:aufx:bpas:appl"})); + assert_eq!(r.map(|v| v["plugin"].clone()), Ok(json!("au:aufx:bpas:appl")), "Apple AUBandpass"); + } } diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index f7a9aace..d0ad0ba1 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -457,44 +457,74 @@ pub fn plugin_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId, slot: usize, oc } }); } - // Third-party CLAP plugins (scanned once, cached by soundcraft-clap-host). + if let Some(plugin) = hosted_menus(ui, false) + && let Err(e) = app.run("mix.insert", json!({"track": id.0, "slot": slot, "plugin": plugin})) + { + app.ui.status = e; + } +} + +/// A hosted plugin as the plugin menus list it. +struct MenuPlugin { + id: String, + name: String, + vendor: String, +} + +/// The CLAP, VST3 and (on macOS) Audio Units submenus, of effects or of instruments; returns the +/// id picked. Each format is scanned the first time its submenu opens, then cached by its host. +fn hosted_menus(ui: &mut Ui, instruments: bool) -> Option { + let mut picked = None; ui.menu_button("CLAP", |ui| { - let found: Vec<_> = soundcraft_clap_host::scan().into_iter().filter(|d| !d.is_instrument).collect(); - if found.is_empty() { - ui.label("No CLAP plugins found"); - } - for d in found { - if ui.button(format!("{} ({})", d.name, d.vendor)).clicked() { - let _ = app.run("mix.insert", json!({"track": id.0, "slot": slot, "plugin": d.id})); - } + let list = soundcraft_clap_host::scan().into_iter().filter(|d| d.is_instrument == instruments); + if let Some(p) = vendor_menus(ui, list.map(|d| MenuPlugin { id: d.id, name: d.name, vendor: d.vendor }).collect(), "No CLAP plugins found") { + picked = Some(p); } }); - // Third-party VST3 plugins (scanned once, cached by soundcraft-vst3-host). ui.menu_button("VST3", |ui| { - let found: Vec<_> = soundcraft_vst3_host::scan().into_iter().filter(|d| !d.is_instrument).collect(); - if found.is_empty() { - ui.label("No VST3 plugins found"); - } - for d in found { - if ui.button(format!("{} ({})", d.name, d.vendor)).clicked() { - let _ = app.run("mix.insert", json!({"track": id.0, "slot": slot, "plugin": d.id})); - } + let list = soundcraft_vst3_host::scan().into_iter().filter(|d| d.is_instrument == instruments); + if let Some(p) = vendor_menus(ui, list.map(|d| MenuPlugin { id: d.id, name: d.name, vendor: d.vendor }).collect(), "No VST3 plugins found") { + picked = Some(p); } }); - // Audio Units (macOS; scanned once, cached by soundcraft-au-host). if cfg!(target_os = "macos") { ui.menu_button("Audio Units", |ui| { - let found: Vec<_> = soundcraft_au_host::scan().into_iter().filter(|d| !d.is_instrument).collect(); - if found.is_empty() { - ui.label("No Audio Units found"); - } - for d in found { - if ui.button(format!("{} ({})", d.name, d.vendor)).clicked() { - let _ = app.run("mix.insert", json!({"track": id.0, "slot": slot, "plugin": d.id})); - } + let list = soundcraft_au_host::scan().into_iter().filter(|d| d.is_instrument == instruments); + if let Some(p) = vendor_menus(ui, list.map(|d| MenuPlugin { id: d.id, name: d.name, vendor: d.vendor }).collect(), "No Audio Units found") + { + picked = Some(p); } }); } + picked +} + +/// One submenu per vendor (as Pro Tools lists plug-ins by manufacturer), each list scrolling +/// within the screen so that every plugin stays reachable however many are installed. +fn vendor_menus(ui: &mut Ui, mut list: Vec, none: &str) -> Option { + if list.is_empty() { + ui.label(none); + return None; + } + list.sort_by_cached_key(|p| (p.vendor.to_lowercase(), p.name.to_lowercase())); + let mut vendors: Vec<&str> = list.iter().map(|p| p.vendor.as_str()).collect(); + vendors.dedup(); + let max_h = (ui.ctx().content_rect().height() - 80.0).max(160.0); + let mut picked = None; + egui::ScrollArea::vertical().id_salt("vendors").max_height(max_h).show(ui, |ui| { + for v in vendors { + ui.menu_button(if v.is_empty() { "Unknown vendor" } else { v }, |ui| { + egui::ScrollArea::vertical().id_salt("plugins").max_height(max_h).show(ui, |ui| { + for p in list.iter().filter(|p| p.vendor == v) { + if ui.button(&p.name).clicked() { + picked = Some(p.id.clone()); + } + } + }); + }); + } + }); + picked } /// A built-in plugin's description, else a hosted CLAP (`clap:`), VST3 (`vst3:`) or From ed47ec0fbea5eb9195106bdc68e72e4afe112542 Mon Sep 17 00:00:00 2001 From: ebbicott Date: Thu, 8 Oct 2026 15:40:37 -0400 Subject: [PATCH 2/2] Instrument picker: choose an instrument track's instrument and open it There was no way in the app to choose a CLAP, VST3 or Audio Units instrument, or to open an instrument's editor: - the insert menus leave instruments out; - an instrument was only set by track.new; - the Mix window's INSTRUMENT row looked for instrument inserts rather than the track's instrument; - plugin windows opened only for insert slots. Changes: - mix.instrument {track?, plugin} sets an instrument track's instrument, with the plugin's default parameters, and is undoable. It refuses effects and other track kinds. - The INST label on an instrument track's header opens an instrument menu: the current instrument and its editor, then the built-in, CLAP, VST3 and Audio Units instruments, by vendor. - The Mix window's INSTRUMENT row shows the track's instrument. A click opens a hosted instrument's plugin window, and a right-click changes it. - Plugin windows open for the instrument slot, with only the editor button, because the bypass, preset and parameter commands address insert slots. They no longer derive a slot letter for the instrument slot. Closes #22 Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/instrument.rs | 83 +++++++++++++++++++++++++++++ crates/engine/src/cmd/mod.rs | 2 + crates/ui-egui/src/edit_window.rs | 10 +++- crates/ui-egui/src/mix_window.rs | 69 +++++++++++++++++++++++- crates/ui-egui/src/panels.rs | 31 +++++++---- 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 crates/engine/src/cmd/instrument.rs diff --git a/crates/engine/src/cmd/instrument.rs b/crates/engine/src/cmd/instrument.rs new file mode 100644 index 00000000..6b34657a --- /dev/null +++ b/crates/engine/src/cmd/instrument.rs @@ -0,0 +1,83 @@ +//! An instrument track's instrument (`mix.instrument`): the plugin that turns the track's MIDI +//! into audio, built in or hosted (CLAP, VST3, Audio Units). The mixer plays `Track::instrument`; +//! the instrument is otherwise only set when the track is created (`track.new`). + +use super::*; +use crate::cmd; +use serde_json::json; +use soundcraft_model::{Insert, TrackKind}; + +pub fn specs() -> Vec { + vec![cmd!( + "mix.instrument", + "Set Instrument", + [], + None, + "{track?, plugin: id} — an instrument track's instrument: built-in, `clap:`, `vst3:` or `au:` id", + has_selection, + set_instrument + )] +} + +fn set_instrument(e: &mut Engine, p: &Value) -> Result { + let plugin = str_param(p, "plugin").ok_or_else(|| bad("mix.instrument", "`plugin` required"))?; + let info = super::clap::plugin_info(plugin).ok_or_else(|| bad("mix.instrument", format!("unknown plugin `{plugin}`")))?; + if !info.is_instrument { + return Err(bad("mix.instrument", format!("`{plugin}` is not an instrument"))); + } + let t = tracks_required(e, "mix.instrument", p)?.first().copied().ok_or_else(|| bad("mix.instrument", "no track"))?; + if e.session().track(t).map(|tr| tr.kind) != Some(TrackKind::Instrument) { + return Err(bad("mix.instrument", "only instrument tracks have an instrument")); + } + let mut ins = Insert::new(info.id); + for pi in info.params { + ins.params.insert(pi.id.to_string(), pi.default); + } + if let Some(tr) = e.session_mut().track_mut(t) { + tr.instrument = Some(ins); + } + Ok(json!({"track": t, "plugin": info.id})) +} + +#[cfg(test)] +mod tests { + use crate::Engine; + use serde_json::json; + use soundcraft_model::TrackId; + + fn track(e: &mut Engine, kind: &str) -> TrackId { + e.execute("track.new", &json!({"kind": kind})).unwrap(); + e.session().tracks.last().unwrap().id + } + + fn instrument_of(e: &Engine, t: TrackId) -> Option { + e.session().track(t).and_then(|tr| tr.instrument.as_ref()).map(|i| i.plugin.clone()) + } + + #[test] + fn sets_an_instrument_tracks_instrument_with_defaults_and_undoes() { + let mut e = Engine::default(); + let t = track(&mut e, "instrument"); + assert_eq!(instrument_of(&e, t).as_deref(), Some("subtractive_synth")); + let r = e.execute("mix.instrument", &json!({"track": t.0, "plugin": "drum_synth"})).unwrap(); + assert_eq!(r["plugin"], "drum_synth"); + assert_eq!(instrument_of(&e, t).as_deref(), Some("drum_synth")); + let params = e.session().track(t).and_then(|tr| tr.instrument.as_ref()).map(|i| i.params.len()); + assert_eq!(params, soundcraft_dsp::plugin_info("drum_synth").map(|p| p.params.len()), "every parameter at its default"); + e.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(instrument_of(&e, t).as_deref(), Some("subtractive_synth")); + } + + #[test] + fn refuses_effects_unknown_plugins_and_other_track_kinds() { + let mut e = Engine::default(); + let inst = track(&mut e, "instrument"); + let audio = track(&mut e, "audio"); + assert!(e.execute("mix.instrument", &json!({"track": inst.0, "plugin": "eq_7band"})).is_err(), "an effect"); + assert!(e.execute("mix.instrument", &json!({"track": inst.0, "plugin": "vst3:00000000000000000000000000000000"})).is_err()); + assert!(e.execute("mix.instrument", &json!({"track": inst.0})).is_err(), "no plugin"); + assert!(e.execute("mix.instrument", &json!({"track": audio.0, "plugin": "drum_synth"})).is_err(), "an audio track"); + assert_eq!(instrument_of(&e, inst).as_deref(), Some("subtractive_synth")); + assert_eq!(instrument_of(&e, audio), None); + } +} diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 665056ae..57ea4cfc 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -16,6 +16,7 @@ mod edit_more; mod event; mod event_more; mod file; +mod instrument; mod midi; mod mix; mod more_util; @@ -119,6 +120,7 @@ pub fn command_specs() -> &'static [CommandSpec] { v.extend(vst3::specs()); v.extend(surround::specs()); v.extend(plugin_state::specs()); + v.extend(instrument::specs()); v.extend(video::specs()); v }) diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index 17a54b08..94ca4217 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -833,7 +833,15 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele TrackKind::Video => "VIDEO", }; if !kind_label.is_empty() { - ui.painter().text(pos2(pl_r.max.x + 4.0, name_r.center().y), Align2::LEFT_CENTER, kind_label, bold(9.0), t.text_dim); + let kr = ui.painter().text(pos2(pl_r.max.x + 4.0, name_r.center().y), Align2::LEFT_CENTER, kind_label, bold(9.0), t.text_dim); + if track.kind == TrackKind::Instrument { + // The instrument picker (and the instrument's editor) behind the INST label. + let name = track.instrument.as_ref().and_then(|i| crate::mix_window::plugin_info(&i.plugin)).map_or("none", |p| p.name); + let resp = ui + .interact(kr.expand(3.0), ui.id().with(("instrument", id.0)), Sense::click()) + .on_hover_text(format!("Instrument: {name} (click to change or open its editor)")); + egui::Popup::menu(&resp).show(|ui| crate::mix_window::instrument_menu(app, ui, id)); + } } if h < 30.0 { return; diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index d0ad0ba1..ea0b05c0 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -140,8 +140,9 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh let sr = Rect::from_min_size(pos2(x0 + 2.0, sec.min.y + 16.0), vec2(inner_w - 4.0, 15.0)); let slot = track.mixer.inserts.iter().position(|i| i.as_ref().is_some_and(|i| plugin_info(&i.plugin).is_some_and(|p| p.is_instrument))); match slot { + _ if track.kind == TrackKind::Instrument => instrument_slot(app, ui, &track, sr), Some(k) => insert_slot(app, ui, &track, k, sr), - None if matches!(track.kind, TrackKind::Instrument | TrackKind::Midi) => { + None if track.kind == TrackKind::Midi => { ui.painter().text(sr.center(), Align2::CENTER_CENTER, "MIDI in: all", regular(9.0), t.text_dim); } None => {} @@ -401,6 +402,32 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh ui.painter().line_segment([pos2(r.max.x, r.min.y), pos2(r.max.x, r.max.y)], Stroke::new(1.0, t.border)); } +/// An instrument track's INSTRUMENT row: click opens a hosted instrument's plugin window, +/// right-click (or click, when none is set) picks another instrument. +fn instrument_slot(app: &mut SoundApp, ui: &mut Ui, track: &Track, r: Rect) { + let t = Tokens::DARK; + let (id, slot) = (track.id, soundcraft_mix::INSTRUMENT_SLOT); + let ins = track.instrument.as_ref(); + let info = ins.and_then(|i| plugin_info(&i.plugin)); + let resp = ui.interact(r, ui.id().with(("instrument", id.0)), Sense::click()); + let fill = if info.is_some() { Color32::from_rgb(52, 62, 80) } else { t.slot_bg }; + ui.painter().rect( + r, + CornerRadius::same(2), + if resp.hovered() { fill.gamma_multiply(1.3) } else { fill }, + Stroke::new(1.0, Color32::from_rgb(16, 16, 16)), + StrokeKind::Inside, + ); + ui.painter().with_clip_rect(r).text(r.center(), Align2::CENTER_CENTER, info.map_or("no instrument", |p| p.short_name), regular(10.5), t.text); + let hosted = ins.is_some_and(|i| soundcraft_mix::is_third_party(&i.plugin)); + if resp.clicked() && hosted && !app.ui.plugin_windows.contains(&(id, slot)) { + app.ui.plugin_windows.push((id, slot)); + } + let menu_resp = if hosted { resp.clone().on_hover_text("Click: open instrument · right-click: change") } else { resp.clone() }; + let popup = if hosted { egui::Popup::context_menu(&menu_resp) } else { egui::Popup::menu(&menu_resp) }; + popup.show(|ui| instrument_menu(app, ui, id)); +} + fn insert_slot(app: &mut SoundApp, ui: &mut Ui, track: &Track, slot: usize, r: Rect) { let t = Tokens::DARK; let id = track.id; @@ -464,6 +491,46 @@ pub fn plugin_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId, slot: usize, oc } } +/// The instrument picker of an instrument track: its instrument's editor, then the built-in, +/// CLAP, VST3 and Audio Units instruments. +pub fn instrument_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) { + let slot = soundcraft_mix::INSTRUMENT_SLOT; + let current = app.engine.session().track(id).and_then(|t| t.instrument.clone()); + if let Some(info) = current.as_ref().and_then(|i| plugin_info(&i.plugin)) { + ui.label(egui::RichText::new(info.name).strong()); + if let Some(p) = &app.player + && p.has_editor(id, slot) + { + if p.editor_open(id, slot) { + if ui.button("Close Plugin Editor").clicked() { + p.close_editor(id, slot); + } + } else if ui.button("Open Plugin Editor").clicked() + && let Err(e) = p.open_editor(id, slot) + { + app.ui.status = format!("{}: {e}", info.name); + } + } + ui.separator(); + } + let mut picked = None; + ui.menu_button("Built-in", |ui| { + for p in soundcraft_dsp::plugins().iter().filter(|p| p.is_instrument) { + if ui.button(p.name).clicked() { + picked = Some(p.id.to_string()); + } + } + }); + if let Some(p) = hosted_menus(ui, true) { + picked = Some(p); + } + if let Some(plugin) = picked + && let Err(e) = app.run("mix.instrument", json!({"track": id.0, "plugin": plugin})) + { + app.ui.status = e; + } +} + /// A hosted plugin as the plugin menus list it. struct MenuPlugin { id: String, diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 1bdb1847..37a90570 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -373,26 +373,33 @@ fn plugin_windows(app: &mut SoundApp, ctx: &egui::Context) { let wins = app.ui.plugin_windows.clone(); let mut keep = Vec::new(); for (tid, slot) in wins { - let Some((tname, ins)) = - app.engine.session().track(tid).and_then(|t| t.mixer.inserts.get(slot).cloned().flatten().map(|i| (t.name.clone(), i))) - else { + // The instrument slot (an instrument track's instrument) or an insert slot. + let instrument = slot == soundcraft_mix::INSTRUMENT_SLOT; + let Some((tname, ins)) = app.engine.session().track(tid).and_then(|t| { + let ins = if instrument { t.instrument.clone() } else { t.mixer.inserts.get(slot).cloned().flatten() }; + ins.map(|i| (t.name.clone(), i)) + }) else { continue; }; let Some(info) = crate::mix_window::plugin_info(&ins.plugin) else { continue }; + let place = if instrument { "instrument".to_string() } else { char::from(b'a'.saturating_add(slot as u8)).to_string() }; let mut open = true; - egui::Window::new(format!("{tname} · {} · {}", (b'a' + slot as u8) as char, info.name)) + egui::Window::new(format!("{tname} · {place} · {}", info.name)) .id(egui::Id::new(("plugin", tid.0, slot))) .open(&mut open) .default_width(340.0) .default_pos(egui::pos2(ctx.content_rect().width() - 380.0, 120.0)) .show(ctx, |ui| { ui.horizontal(|ui| { - ui.label(egui::RichText::new(&ins.preset).color(Tokens::DARK.text_dim)); - let label = if ins.bypass { "BYPASSED" } else { "Bypass" }; - if ui.button(label).clicked() { - let _ = app.run("mix.insert_bypass", json!({"track": tid.0, "slot": slot})); + // Bypass and presets act on insert slots. + if !instrument { + ui.label(egui::RichText::new(&ins.preset).color(Tokens::DARK.text_dim)); + let label = if ins.bypass { "BYPASSED" } else { "Bypass" }; + if ui.button(label).clicked() { + let _ = app.run("mix.insert_bypass", json!({"track": tid.0, "slot": slot})); + } + ui.menu_button("Presets ▾", |ui| preset_menu(app, ui, tid, slot, info, &ins)); } - ui.menu_button("Presets ▾", |ui| preset_menu(app, ui, tid, slot, info, &ins)); if soundcraft_mix::is_third_party(&ins.plugin) { let open = app.player.as_ref().is_some_and(|p| p.editor_open(tid, slot)); let available = app.player.as_ref().is_some_and(|p| p.has_editor(tid, slot)); @@ -411,6 +418,12 @@ fn plugin_windows(app: &mut SoundApp, ctx: &egui::Context) { } } }); + if instrument { + // The parameter commands address insert slots: an instrument is played and + // edited in its own editor. + ui.label(egui::RichText::new("Load and edit sounds in the plugin's own editor.").color(Tokens::DARK.text_dim)); + return; + } if info.id == "eq_7band" || info.id == "eq_1band" { eq_curve(ui, info.id, &ins); }