Skip to main content

maolan_engine/plugins/vst3/
host.rs

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