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 <noreply@anthropic.com>
This commit is contained in:
ebbicott 2026-10-08 15:39:23 -04:00
parent ae2362752c
commit 2a8c60922f
4 changed files with 76 additions and 29 deletions

1
Cargo.lock generated
View File

@ -3448,6 +3448,7 @@ dependencies = [
"proptest",
"serde",
"serde_json",
"soundcraft-au-host",
"soundcraft-audio-io",
"soundcraft-clap-host",
"soundcraft-dsp",

View File

@ -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

View File

@ -25,9 +25,13 @@ pub fn specs() -> Vec<CommandSpec> {
)]
}
/// A built-in plugin's description, else a hosted CLAP (`clap:<id>`) or VST3 (`vst3:<class id>`) plugin's.
/// A built-in plugin's description, else a hosted CLAP (`clap:<id>`), VST3 (`vst3:<class id>`) or
/// Audio Units (`au:<type>:<subtype>:<manufacturer>`) 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");
}
}

View File

@ -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<String> {
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<MenuPlugin>, none: &str) -> Option<String> {
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:<id>`), VST3 (`vst3:<class id>`) or