Skip to main content

vst3_host/
discovery.rs

1//! VST3 plugin discovery functionality
2
3use crate::{error::Result, plugin::PluginInfo};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use std::ptr;
7
8/// Factory-level metadata (the plugin vendor's identity).
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct FactoryInfo {
11    /// Vendor / manufacturer name.
12    pub vendor: String,
13    /// Vendor URL.
14    pub url: String,
15    /// Vendor contact email.
16    pub email: String,
17    /// Raw factory flags.
18    pub flags: i32,
19}
20
21/// One class exported by a plugin's factory.
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct ClassInfo {
24    /// Class display name.
25    pub name: String,
26    /// Class category (e.g. "Audio Module Class").
27    pub category: String,
28    /// Class id, hex-encoded.
29    pub class_id: String,
30    /// Instantiation cardinality.
31    pub cardinality: i32,
32    /// Version string (if available).
33    pub version: String,
34}
35
36/// One audio or event bus.
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct BusInfo {
39    /// Bus display name.
40    pub name: String,
41    /// Bus type (Main = 0, Aux = 1).
42    pub bus_type: i32,
43    /// Raw bus flags.
44    pub flags: i32,
45    /// Number of channels on this bus.
46    pub channel_count: i32,
47}
48
49/// The plugin's full bus layout.
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct BusLayout {
52    /// Audio input buses.
53    pub audio_inputs: Vec<BusInfo>,
54    /// Audio output buses.
55    pub audio_outputs: Vec<BusInfo>,
56    /// Event (MIDI) input buses.
57    pub event_inputs: Vec<BusInfo>,
58    /// Event (MIDI) output buses.
59    pub event_outputs: Vec<BusInfo>,
60}
61
62/// A deep introspection report for a VST3 plugin — factory, classes, and bus layout.
63/// This is the static metadata a plugin *inspector* UI needs, beyond the lightweight
64/// [`PluginInfo`]. For the parameter list, load the plugin and call
65/// [`crate::Plugin::get_parameters`] (which runs the full controller logic).
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DetailedPluginInfo {
68    /// The basic metadata (also part of this report for convenience).
69    pub info: PluginInfo,
70    /// Factory / vendor identity.
71    pub factory: FactoryInfo,
72    /// All classes exported by the factory.
73    pub classes: Vec<ClassInfo>,
74    /// Full audio + event bus layout.
75    pub buses: BusLayout,
76}
77
78/// A complete, serializable report of a plugin: static introspection plus its parameter
79/// list. Build it after loading the plugin and serialize to JSON for export (e.g. the
80/// inspector's "Copy JSON", or feeding plugin metadata to other tools).
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PluginReport {
83    /// Static introspection: factory, classes, bus layout, basic info.
84    pub detailed: DetailedPluginInfo,
85    /// The plugin's parameters (normalized values + metadata).
86    pub parameters: Vec<crate::parameters::Parameter>,
87}
88
89impl PluginReport {
90    /// Bundle a [`DetailedPluginInfo`] with a parameter list (from
91    /// [`crate::Plugin::get_parameters`]).
92    pub fn new(
93        detailed: DetailedPluginInfo,
94        parameters: Vec<crate::parameters::Parameter>,
95    ) -> Self {
96        Self {
97            detailed,
98            parameters,
99        }
100    }
101
102    /// Serialize the report to pretty-printed JSON.
103    pub fn to_json(&self) -> serde_json::Result<String> {
104        serde_json::to_string_pretty(self)
105    }
106}
107
108/// Scan standard VST3 directories for plugins
109pub fn scan_standard_paths() -> Vec<PathBuf> {
110    let mut paths = Vec::new();
111
112    #[cfg(target_os = "macos")]
113    {
114        paths.push(PathBuf::from("/Library/Audio/Plug-Ins/VST3"));
115        if let Ok(home) = std::env::var("HOME") {
116            paths.push(PathBuf::from(format!(
117                "{}/Library/Audio/Plug-Ins/VST3",
118                home
119            )));
120        }
121    }
122
123    #[cfg(target_os = "windows")]
124    {
125        paths.push(PathBuf::from(r"C:\Program Files\Common Files\VST3"));
126        paths.push(PathBuf::from(r"C:\Program Files (x86)\Common Files\VST3"));
127    }
128
129    #[cfg(target_os = "linux")]
130    {
131        paths.push(PathBuf::from("/usr/lib/vst3"));
132        paths.push(PathBuf::from("/usr/local/lib/vst3"));
133        if let Ok(home) = std::env::var("HOME") {
134            paths.push(PathBuf::from(format!("{}/.vst3", home)));
135        }
136    }
137
138    paths
139}
140
141/// Scan directories for VST3 plugins
142pub fn scan_directories(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
143    let mut plugins = Vec::new();
144
145    for path in paths {
146        if path.exists() {
147            scan_directory(path, &mut plugins)?;
148        }
149    }
150
151    // Remove duplicates and sort
152    plugins.sort();
153    plugins.dedup();
154
155    Ok(plugins)
156}
157
158/// Check if a plugin should be blacklisted
159fn is_blacklisted(path: &Path) -> bool {
160    if let Some(file_name) = path.file_name() {
161        if let Some(name_str) = file_name.to_str() {
162            let name_lower = name_str.to_lowercase();
163            // Blacklist plugins known to cause issues (removed wave blacklisting)
164            return name_lower.contains("ozone"); // Only ozone for now
165        }
166    }
167    false
168}
169
170/// Recursively scan a directory for VST3 plugins
171fn scan_directory(dir: &Path, plugins: &mut Vec<PathBuf>) -> Result<()> {
172    if let Ok(entries) = std::fs::read_dir(dir) {
173        for entry in entries.flatten() {
174            let path = entry.path();
175
176            // Check if it's a VST3 bundle/file
177            if let Some(ext) = path.extension() {
178                if ext == "vst3" {
179                    // Skip blacklisted plugins
180                    if !is_blacklisted(&path) {
181                        plugins.push(path.clone());
182                    } else {
183                        eprintln!("Skipping blacklisted plugin: {}", path.display());
184                    }
185                }
186            }
187
188            // Recursively scan subdirectories (but not .vst3 bundles)
189            if path.is_dir() && path.extension() != Some(std::ffi::OsStr::new("vst3")) {
190                scan_directory(&path, plugins)?;
191            }
192        }
193    }
194
195    Ok(())
196}
197
198/// Get metadata for a VST3 plugin without fully loading it
199pub fn get_plugin_info(path: &Path) -> Result<PluginInfo> {
200    use vst3::Steinberg::Vst::BusDirections_::*;
201    use vst3::Steinberg::Vst::MediaTypes_::*;
202    use vst3::{ComPtr, Interface, Steinberg::Vst::*, Steinberg::*};
203
204    unsafe {
205        // Load the module using our VST3-compliant module loader
206        let module = crate::internal::module_loader::load_module(path)?;
207
208        // Get factory using the proper VST3 loading sequence
209        let factory_ptr = module.get_factory()?;
210
211        let factory = ComPtr::<IPluginFactory>::from_raw(factory_ptr).ok_or_else(|| {
212            crate::Error::PluginLoadFailed("Failed to create factory ComPtr".to_string())
213        })?;
214
215        // Get factory info
216        let mut factory_info: PFactoryInfo = std::mem::zeroed();
217        factory.getFactoryInfo(&mut factory_info);
218
219        let vendor = crate::internal::utils::c_str_to_string(&factory_info.vendor);
220
221        // Find audio component
222        let num_classes = factory.countClasses();
223        let mut plugin_name = String::new();
224        let mut category = String::new();
225        let mut version = String::new();
226        let mut uid = String::new();
227        let mut has_midi_input = false;
228        let mut has_midi_output = false;
229        let mut audio_inputs = 0u32;
230        let mut audio_outputs = 0u32;
231        let mut has_gui = false;
232
233        for i in 0..num_classes {
234            let mut class_info: PClassInfo = std::mem::zeroed();
235            if factory.getClassInfo(i, &mut class_info) == kResultOk {
236                let class_category = crate::internal::utils::c_str_to_string(&class_info.category);
237
238                if class_category.contains("Audio Module Class") {
239                    plugin_name = crate::internal::utils::c_str_to_string(&class_info.name);
240
241                    // Real version + sub-categories via IPluginFactory2 (PClassInfo.category
242                    // is just "Audio Module Class"; the useful sub-categories live in
243                    // PClassInfo2.subCategories). Left empty rather than faked when absent.
244                    if let Some(f2) = factory.cast::<IPluginFactory2>() {
245                        let mut info2: PClassInfo2 = std::mem::zeroed();
246                        if f2.getClassInfo2(i, &mut info2) == kResultOk {
247                            version = crate::internal::utils::c_str_to_string(&info2.version);
248                            category =
249                                crate::internal::utils::c_str_to_string(&info2.subCategories);
250                        }
251                    }
252
253                    // Convert UID to string
254                    // cid is an array of bytes, convert to hex string
255                    uid = class_info
256                        .cid
257                        .iter()
258                        .map(|b| format!("{:02X}", b))
259                        .collect::<String>();
260
261                    // Try to create component to get more info
262                    let mut component_ptr: *mut IComponent = ptr::null_mut();
263                    let result = factory.createInstance(
264                        class_info.cid.as_ptr() as *const std::os::raw::c_char,
265                        IComponent::IID.as_ptr() as *const std::os::raw::c_char,
266                        &mut component_ptr as *mut _ as *mut _,
267                    );
268
269                    if result == kResultOk && !component_ptr.is_null() {
270                        let component =
271                            ComPtr::<IComponent>::from_raw(component_ptr).ok_or_else(|| {
272                                crate::error::Error::Other("Failed to wrap component".to_string())
273                            })?;
274
275                        // Initialize with a host context (null crashes u-he/Waves plugins).
276                        let host_app =
277                            crate::internal::com_implementations::create_host_application();
278                        let host_ctx = host_app.to_com_ptr::<IHostApplication>();
279                        let context = host_ctx
280                            .as_ref()
281                            .map(|p| p.as_ptr() as *mut FUnknown)
282                            .unwrap_or(ptr::null_mut());
283                        component.initialize(context);
284
285                        // Get bus counts
286                        audio_inputs = component.getBusCount(kAudio as i32, kInput as i32) as u32;
287                        audio_outputs = component.getBusCount(kAudio as i32, kOutput as i32) as u32;
288
289                        // MIDI capability from event bus presence.
290                        has_midi_input = component.getBusCount(kEvent as i32, kInput as i32) > 0;
291                        has_midi_output = component.getBusCount(kEvent as i32, kOutput as i32) > 0;
292
293                        // GUI detection (lightweight). A plugin has an editor when it provides
294                        // an edit controller — either the component itself implements
295                        // IEditController (single-component) or it names a separate controller
296                        // class. The previous check only handled the single-component case, so
297                        // it wrongly reported "no GUI" for the common separate-component
298                        // plugins. A precise createView probe needs the plugin's full setup
299                        // (component handler + activation) that only the load path performs;
300                        // controller presence is the reliable fast signal here.
301                        has_gui = component.cast::<IEditController>().is_some() || {
302                            let mut cid: [std::os::raw::c_char; 16] = [0; 16];
303                            component.getControllerClassId(&mut cid) == kResultOk
304                        };
305
306                        // Cleanup
307                        component.terminate();
308                    }
309
310                    break;
311                }
312            }
313        }
314
315        // If no audio component found, use first class
316        if plugin_name.is_empty() && num_classes > 0 {
317            let mut class_info: PClassInfo = std::mem::zeroed();
318            if factory.getClassInfo(0, &mut class_info) == kResultOk {
319                plugin_name = crate::internal::utils::c_str_to_string(&class_info.name);
320            }
321        }
322
323        Ok(PluginInfo {
324            path: path.to_path_buf(),
325            name: if plugin_name.is_empty() {
326                path.file_stem()
327                    .and_then(|s| s.to_str())
328                    .unwrap_or("Unknown")
329                    .to_string()
330            } else {
331                plugin_name
332            },
333            vendor,
334            version,
335            category,
336            uid,
337            audio_inputs,
338            audio_outputs,
339            has_midi_input,
340            has_midi_output,
341            has_gui,
342        })
343    }
344}
345
346/// Deep-introspect a VST3 plugin: factory identity, exported classes, and bus layout.
347///
348/// Heavier than [`get_plugin_info`] (it enumerates every class and bus) but still does
349/// not require driving audio. For the parameter list, load the plugin and call
350/// [`crate::Plugin::get_parameters`].
351pub fn get_detailed_plugin_info(path: &Path) -> Result<DetailedPluginInfo> {
352    use vst3::Steinberg::Vst::BusDirections_::*;
353    use vst3::Steinberg::Vst::BusInfo as VstBusInfo;
354    use vst3::Steinberg::Vst::MediaTypes_::*;
355    use vst3::{ComPtr, Interface, Steinberg::Vst::*, Steinberg::*};
356
357    // Reuse the lightweight pass for the basic info.
358    let info = get_plugin_info(path)?;
359
360    unsafe {
361        let module = crate::internal::module_loader::load_module(path)?;
362        let factory_ptr = module.get_factory()?;
363        let factory = ComPtr::<IPluginFactory>::from_raw(factory_ptr).ok_or_else(|| {
364            crate::Error::PluginLoadFailed("Failed to create factory ComPtr".to_string())
365        })?;
366
367        // Factory identity.
368        let mut fi: PFactoryInfo = std::mem::zeroed();
369        factory.getFactoryInfo(&mut fi);
370        let factory_info = FactoryInfo {
371            vendor: crate::internal::utils::c_str_to_string(&fi.vendor),
372            url: crate::internal::utils::c_str_to_string(&fi.url),
373            email: crate::internal::utils::c_str_to_string(&fi.email),
374            flags: fi.flags,
375        };
376
377        // Exported classes + locate the audio component class id.
378        let num_classes = factory.countClasses();
379        let mut classes = Vec::new();
380        let mut audio_cid: Option<[std::os::raw::c_char; 16]> = None;
381        for i in 0..num_classes {
382            let mut ci: PClassInfo = std::mem::zeroed();
383            if factory.getClassInfo(i, &mut ci) == kResultOk {
384                let category = crate::internal::utils::c_str_to_string(&ci.category);
385                let class_id = ci
386                    .cid
387                    .iter()
388                    .map(|b| format!("{:02X}", b))
389                    .collect::<String>();
390                if category.contains("Audio Module Class") && audio_cid.is_none() {
391                    audio_cid = Some(ci.cid);
392                }
393                classes.push(ClassInfo {
394                    name: crate::internal::utils::c_str_to_string(&ci.name),
395                    category,
396                    class_id,
397                    cardinality: ci.cardinality,
398                    version: String::new(), // not available in PClassInfo
399                });
400            }
401        }
402
403        // Bus layout from the audio component.
404        let mut buses = BusLayout::default();
405        if let Some(cid) = audio_cid {
406            let mut component_ptr: *mut IComponent = ptr::null_mut();
407            let result = factory.createInstance(
408                cid.as_ptr(),
409                IComponent::IID.as_ptr() as *const std::os::raw::c_char,
410                &mut component_ptr as *mut _ as *mut _,
411            );
412            if result == kResultOk && !component_ptr.is_null() {
413                if let Some(component) = ComPtr::<IComponent>::from_raw(component_ptr) {
414                    // Initialize with a host context (null crashes u-he/Waves plugins).
415                    let host_app = crate::internal::com_implementations::create_host_application();
416                    let host_ctx = host_app.to_com_ptr::<IHostApplication>();
417                    let context = host_ctx
418                        .as_ref()
419                        .map(|p| p.as_ptr() as *mut FUnknown)
420                        .unwrap_or(ptr::null_mut());
421                    component.initialize(context);
422
423                    let collect = |media: i32, dir: i32| -> Vec<crate::discovery::BusInfo> {
424                        let mut out = Vec::new();
425                        let count = component.getBusCount(media, dir);
426                        for i in 0..count {
427                            let mut bi: VstBusInfo = std::mem::zeroed();
428                            if component.getBusInfo(media, dir, i, &mut bi) == kResultOk {
429                                out.push(crate::discovery::BusInfo {
430                                    name: crate::internal::utils::vst_string_to_string(&bi.name),
431                                    bus_type: bi.busType,
432                                    flags: bi.flags as i32,
433                                    channel_count: bi.channelCount,
434                                });
435                            }
436                        }
437                        out
438                    };
439
440                    buses.audio_inputs = collect(kAudio as i32, kInput as i32);
441                    buses.audio_outputs = collect(kAudio as i32, kOutput as i32);
442                    buses.event_inputs = collect(kEvent as i32, kInput as i32);
443                    buses.event_outputs = collect(kEvent as i32, kOutput as i32);
444
445                    component.terminate();
446                }
447            }
448        }
449
450        Ok(DetailedPluginInfo {
451            info,
452            factory: factory_info,
453            classes,
454            buses,
455        })
456    }
457}
458
459/// Platform-specific VST3 binary path resolution
460pub fn get_vst3_binary_path(bundle_path: &Path) -> Result<PathBuf> {
461    // If it's already pointing to the binary, use it
462    if bundle_path.is_file() {
463        return Ok(bundle_path.to_path_buf());
464    }
465
466    // Platform-specific VST3 bundle handling
467    #[cfg(target_os = "macos")]
468    {
469        // macOS: .vst3 bundle structure
470        if bundle_path.extension() == Some(std::ffi::OsStr::new("vst3")) {
471            let contents_path = bundle_path.join("Contents").join("MacOS");
472            if let Ok(entries) = std::fs::read_dir(&contents_path) {
473                for entry in entries.flatten() {
474                    let file_path = entry.path();
475                    if file_path.is_file() {
476                        if let Some(name) = file_path.file_name() {
477                            if let Some(name_str) = name.to_str() {
478                                // Skip hidden files and common non-binary files
479                                if !name_str.starts_with('.')
480                                    && !name_str.ends_with(".plist")
481                                    && !name_str.ends_with(".txt")
482                                {
483                                    return Ok(file_path);
484                                }
485                            }
486                        }
487                    }
488                }
489            }
490        }
491    }
492
493    #[cfg(target_os = "windows")]
494    {
495        // Windows: .vst3 file or folder structure
496        if bundle_path.is_dir() {
497            // Look for the .vst3 in the per-arch Contents folder. VST3 uses `arm64-win`
498            // (and `arm64ec-win`) for ARM64 — not `aarch64-win`. Native arch first.
499            let contents = bundle_path.join("Contents");
500            let arm64_path = contents.join("arm64-win");
501            let arm64ec_path = contents.join("arm64ec-win");
502            let x64_path = contents.join("x86_64-win");
503            let x86_path = contents.join("x86-win");
504
505            for contents_path in &[arm64_path, arm64ec_path, x64_path, x86_path] {
506                if let Ok(entries) = std::fs::read_dir(contents_path) {
507                    for entry in entries.flatten() {
508                        let file_path = entry.path();
509                        if file_path.extension() == Some(std::ffi::OsStr::new("vst3")) {
510                            return Ok(file_path);
511                        }
512                    }
513                }
514            }
515        }
516    }
517
518    #[cfg(target_os = "linux")]
519    {
520        // Linux: Similar to Windows
521        if bundle_path.is_dir() {
522            let contents_path = bundle_path.join("Contents");
523            let arch_paths = [
524                contents_path.join("aarch64-linux"),
525                contents_path.join("x86_64-linux"),
526                contents_path.join("i386-linux"),
527            ];
528
529            for arch_path in &arch_paths {
530                if let Ok(entries) = std::fs::read_dir(arch_path) {
531                    for entry in entries.flatten() {
532                        let file_path = entry.path();
533                        if file_path.extension() == Some(std::ffi::OsStr::new("so")) {
534                            return Ok(file_path);
535                        }
536                    }
537                }
538            }
539        }
540    }
541
542    Err(crate::Error::PluginNotFound(format!(
543        "Could not find VST3 binary in bundle: {}",
544        bundle_path.display()
545    )))
546}
547
548#[cfg(test)]
549mod report_tests {
550    use super::*;
551    use crate::plugin::PluginInfo;
552
553    #[test]
554    fn plugin_report_serializes_and_round_trips() {
555        let detail = DetailedPluginInfo {
556            info: PluginInfo {
557                path: std::path::PathBuf::from("/x/Dexed.vst3"),
558                name: "Dexed".into(),
559                vendor: "Digital Suburban".into(),
560                version: "1.0.0".into(),
561                category: "Instrument|Synth".into(),
562                uid: "ABCD".into(),
563                audio_inputs: 0,
564                audio_outputs: 1,
565                has_midi_input: true,
566                has_midi_output: true,
567                has_gui: true,
568            },
569            factory: FactoryInfo {
570                vendor: "Digital Suburban".into(),
571                ..Default::default()
572            },
573            classes: vec![ClassInfo {
574                name: "Dexed".into(),
575                ..Default::default()
576            }],
577            buses: BusLayout::default(),
578        };
579        let report = PluginReport::new(detail, Vec::new());
580        let json = report.to_json().expect("to_json");
581        // The export round-trips and preserves the accurate metadata.
582        let back: PluginReport = serde_json::from_str(&json).expect("round-trip");
583        assert_eq!(back.detailed.info.name, "Dexed");
584        assert_eq!(back.detailed.info.category, "Instrument|Synth");
585        assert!(back.detailed.info.has_midi_output);
586        assert_eq!(back.detailed.classes.len(), 1);
587    }
588}