maolan_engine/plugins/vst3/
host.rs1use super::default_vst3_search_roots;
2use super::interfaces::PluginFactory;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct Vst3PluginInfo {
8 pub id: String, pub name: String,
10 pub vendor: String,
11 pub path: String, pub category: String,
13 pub version: String,
14 pub audio_inputs: usize,
15 pub audio_outputs: usize,
16 pub has_midi_input: bool,
17 pub has_midi_output: bool,
18}
19
20pub struct Vst3Host {
21 plugins: Vec<Vst3PluginInfo>,
22}
23
24impl Default for Vst3Host {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl Vst3Host {
31 pub fn new() -> Self {
32 Self {
33 plugins: Vec::new(),
34 }
35 }
36
37 pub fn list_plugins(&mut self) -> Vec<Vst3PluginInfo> {
38 let mut roots = default_vst3_search_roots();
39
40 if let Ok(extra) = std::env::var("VST3_PATH") {
42 for p in std::env::split_paths(&extra) {
43 if !p.as_os_str().is_empty() {
44 roots.push(p);
45 }
46 }
47 }
48
49 let mut out = Vec::new();
50 for root in roots {
51 collect_vst3_plugins(&root, &mut out);
52 }
53
54 out.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
56
57 out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));
59
60 self.plugins = out.clone();
61 out
62 }
63
64 pub fn get_plugin_info(&self, path: &str) -> Option<&Vst3PluginInfo> {
65 self.plugins.iter().find(|p| p.path == path)
66 }
67}
68
69fn collect_vst3_plugins(root: &Path, out: &mut Vec<Vst3PluginInfo>) {
70 let Ok(entries) = std::fs::read_dir(root) else {
71 return;
72 };
73 for entry in entries.flatten() {
74 let path = entry.path();
75 let Ok(ft) = entry.file_type() else {
76 continue;
77 };
78 if ft.is_symlink() {
79 continue;
80 }
81
82 if !ft.is_dir() {
83 continue;
84 }
85
86 if path
88 .extension()
89 .is_some_and(|ext| ext.eq_ignore_ascii_case("vst3"))
90 {
91 if let Some(info) = scan_vst3_bundle(&path) {
92 out.push(info);
93 }
94 } else {
95 collect_vst3_plugins(&path, out);
97 }
98 }
99}
100
101fn scan_vst3_bundle(bundle_path: &Path) -> Option<Vst3PluginInfo> {
102 let factory = PluginFactory::from_module(bundle_path).ok()?;
103 let class_count = factory.count_classes();
104 if class_count == 0 {
105 return None;
106 }
107
108 let mut fallback = None;
109 for index in 0..class_count {
110 let Some(class_info) = factory.get_class_info(index) else {
111 continue;
112 };
113 if fallback.is_none() {
114 fallback = Some(class_info_to_plugin_info(&class_info, bundle_path, None));
115 }
116
117 let Ok(mut instance) = factory.create_instance(&class_info.cid) else {
118 continue;
119 };
120 let capabilities = match instance.initialize(&factory) {
121 Ok(()) => {
122 let (audio_inputs, audio_outputs) = instance.main_audio_channel_counts();
123 let (midi_inputs, midi_outputs) = instance.event_bus_counts();
124 let _ = instance.terminate();
125 Some((
126 audio_inputs,
127 audio_outputs,
128 midi_inputs > 0,
129 midi_outputs > 0,
130 ))
131 }
132 Err(_) => None,
133 };
134 return Some(class_info_to_plugin_info(
135 &class_info,
136 bundle_path,
137 capabilities,
138 ));
139 }
140
141 fallback
142}
143
144fn class_info_to_plugin_info(
145 class_info: &super::interfaces::ClassInfo,
146 bundle_path: &Path,
147 capabilities: Option<(usize, usize, bool, bool)>,
148) -> Vst3PluginInfo {
149 let (audio_inputs, audio_outputs, has_midi_input, has_midi_output) =
150 capabilities.unwrap_or((0, 0, false, false));
151
152 Vst3PluginInfo {
153 id: tuid_to_string(&class_info.cid),
154 name: class_info.name.clone(),
155 vendor: String::new(),
156 path: bundle_path.to_string_lossy().to_string(),
157 category: class_info.category.clone(),
158 version: String::new(),
159 audio_inputs,
160 audio_outputs,
161 has_midi_input,
162 has_midi_output,
163 }
164}
165
166fn tuid_to_string(tuid: &[i8; 16]) -> String {
167 tuid.iter()
169 .map(|&b| format!("{:02X}", b as u8))
170 .collect::<Vec<_>>()
171 .join("")
172}
173
174pub fn list_plugins() -> Vec<Vst3PluginInfo> {
176 let mut host = Vst3Host::new();
177 host.list_plugins()
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::plugins::vst3::interfaces::ClassInfo;
184 use std::path::PathBuf;
185
186 #[test]
187 fn class_info_to_plugin_info_uses_capabilities_when_present() {
188 let class_info = ClassInfo {
189 name: "Synth".to_string(),
190 category: "Instrument".to_string(),
191 cid: [0x12_i8; 16],
192 };
193
194 let info = class_info_to_plugin_info(
195 &class_info,
196 Path::new("/tmp/Test.vst3"),
197 Some((2, 4, true, false)),
198 );
199
200 assert_eq!(info.id, "12121212121212121212121212121212");
201 assert_eq!(info.name, "Synth");
202 assert_eq!(info.category, "Instrument");
203 assert_eq!(info.path, "/tmp/Test.vst3");
204 assert_eq!(info.audio_inputs, 2);
205 assert_eq!(info.audio_outputs, 4);
206 assert!(info.has_midi_input);
207 assert!(!info.has_midi_output);
208 }
209
210 #[test]
211 fn class_info_to_plugin_info_defaults_capabilities_when_missing() {
212 let class_info = ClassInfo {
213 name: "Fx".to_string(),
214 category: "Audio Module".to_string(),
215 cid: [0; 16],
216 };
217
218 let info = class_info_to_plugin_info(&class_info, Path::new("/tmp/Fx.vst3"), None);
219
220 assert_eq!(info.audio_inputs, 0);
221 assert_eq!(info.audio_outputs, 0);
222 assert!(!info.has_midi_input);
223 assert!(!info.has_midi_output);
224 }
225
226 #[test]
227 fn tuid_to_string_formats_bytes_as_uppercase_hex() {
228 let tuid = [
229 0x00_i8, 0x01, 0x23, 0x45, 0x67, 0x7F, -0x80, -0x01, 0x10, 0x20, 0x30, 0x40, 0x50,
230 0x60, 0x70, 0x7E,
231 ];
232
233 assert_eq!(tuid_to_string(&tuid), "00012345677F80FF102030405060707E");
234 }
235
236 #[test]
237 fn get_plugin_info_returns_cached_plugin_by_exact_path() {
238 let mut host = Vst3Host::new();
239 host.plugins = vec![
240 Vst3PluginInfo {
241 id: "id-1".to_string(),
242 name: "First".to_string(),
243 vendor: String::new(),
244 path: "/tmp/First.vst3".to_string(),
245 category: "Instrument".to_string(),
246 version: String::new(),
247 audio_inputs: 2,
248 audio_outputs: 2,
249 has_midi_input: true,
250 has_midi_output: false,
251 },
252 Vst3PluginInfo {
253 id: "id-2".to_string(),
254 name: "Second".to_string(),
255 vendor: String::new(),
256 path: "/tmp/Second.vst3".to_string(),
257 category: "Fx".to_string(),
258 version: String::new(),
259 audio_inputs: 2,
260 audio_outputs: 2,
261 has_midi_input: false,
262 has_midi_output: false,
263 },
264 ];
265
266 let found = host
267 .get_plugin_info("/tmp/Second.vst3")
268 .map(|p| p.name.clone());
269 let missing = host.get_plugin_info(&PathBuf::from("/tmp/missing.vst3").to_string_lossy());
270
271 assert_eq!(found.as_deref(), Some("Second"));
272 assert!(missing.is_none());
273 }
274}