From 2a8c60922fb94d7e52bd3ab4f5ac6513b58177e2 Mon Sep 17 00:00:00 2001 From: ebbicott Date: Thu, 8 Oct 2026 15:39:23 -0400 Subject: [PATCH] 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 e6719ef..4f0c98e 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 18dacdc..b698856 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 7b39c1f..15ddcd7 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 f7a9aac..d0ad0ba 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