1use crate::{error::Result, plugin::PluginInfo};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use std::ptr;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct FactoryInfo {
11 pub vendor: String,
13 pub url: String,
15 pub email: String,
17 pub flags: i32,
19}
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct ClassInfo {
24 pub name: String,
26 pub category: String,
28 pub class_id: String,
30 pub cardinality: i32,
32 pub version: String,
34}
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct BusInfo {
39 pub name: String,
41 pub bus_type: i32,
43 pub flags: i32,
45 pub channel_count: i32,
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct BusLayout {
52 pub audio_inputs: Vec<BusInfo>,
54 pub audio_outputs: Vec<BusInfo>,
56 pub event_inputs: Vec<BusInfo>,
58 pub event_outputs: Vec<BusInfo>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DetailedPluginInfo {
68 pub info: PluginInfo,
70 pub factory: FactoryInfo,
72 pub classes: Vec<ClassInfo>,
74 pub buses: BusLayout,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PluginReport {
83 pub detailed: DetailedPluginInfo,
85 pub parameters: Vec<crate::parameters::Parameter>,
87}
88
89impl PluginReport {
90 pub fn new(
93 detailed: DetailedPluginInfo,
94 parameters: Vec<crate::parameters::Parameter>,
95 ) -> Self {
96 Self {
97 detailed,
98 parameters,
99 }
100 }
101
102 pub fn to_json(&self) -> serde_json::Result<String> {
104 serde_json::to_string_pretty(self)
105 }
106}
107
108pub 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
141pub 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 plugins.sort();
153 plugins.dedup();
154
155 Ok(plugins)
156}
157
158fn 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 return name_lower.contains("ozone"); }
166 }
167 false
168}
169
170fn 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 if let Some(ext) = path.extension() {
178 if ext == "vst3" {
179 if !is_blacklisted(&path) {
181 plugins.push(path.clone());
182 } else {
183 eprintln!("Skipping blacklisted plugin: {}", path.display());
184 }
185 }
186 }
187
188 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
198pub 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 let module = crate::internal::module_loader::load_module(path)?;
207
208 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 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 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 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 uid = class_info
256 .cid
257 .iter()
258 .map(|b| format!("{:02X}", b))
259 .collect::<String>();
260
261 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 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 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 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 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 component.terminate();
308 }
309
310 break;
311 }
312 }
313 }
314
315 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
346pub 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 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 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 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(), });
400 }
401 }
402
403 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 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
459pub fn get_vst3_binary_path(bundle_path: &Path) -> Result<PathBuf> {
461 if bundle_path.is_file() {
463 return Ok(bundle_path.to_path_buf());
464 }
465
466 #[cfg(target_os = "macos")]
468 {
469 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 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 if bundle_path.is_dir() {
497 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 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 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}