1use crate::{
4 audio::AudioConfig,
5 error::{Error, Result},
6 plugin::{Plugin, PluginInfo, PluginInternal},
7};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11pub struct Vst3Host {
13 pub(crate) config: AudioConfig,
15 pub(crate) custom_paths: Vec<PathBuf>,
17 pub(crate) use_process_isolation: bool,
19 pub(crate) auto_isolate_problematic: bool,
22 pub(crate) scan_default_paths: bool,
24 pub(crate) helper_path: Option<PathBuf>,
26 pub(crate) response_timeout: std::time::Duration,
28 pub(crate) auto_recover_plugins: bool,
30 pub(crate) auto_recover_max_retries: u32,
32}
33
34impl Vst3Host {
35 pub fn new() -> Result<Self> {
42 Self::builder().scan_default_paths().build()
43 }
44
45 pub fn builder() -> Vst3HostBuilder {
47 Vst3HostBuilder::default()
48 }
49
50 pub fn add_scan_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
52 let path = path.as_ref();
53 if !path.exists() {
54 return Err(Error::Other(format!(
55 "Path does not exist: {}",
56 path.display()
57 )));
58 }
59 self.custom_paths.push(path.to_path_buf());
60 Ok(())
61 }
62
63 pub fn discover_plugins(&mut self) -> Result<Vec<PluginInfo>> {
65 let mut all_paths = self.custom_paths.clone();
66
67 if self.scan_default_paths {
69 all_paths.extend(crate::discovery::scan_standard_paths());
70 }
71
72 let plugin_paths = crate::discovery::scan_directories(&all_paths)?;
74
75 let mut plugins = Vec::new();
77 for path in plugin_paths {
78 match crate::discovery::get_plugin_info(&path) {
79 Ok(info) => plugins.push(info),
80 Err(e) => {
81 log::warn!("Failed to get info for plugin {}: {}", path.display(), e);
82 }
84 }
85 }
86
87 Ok(plugins)
88 }
89
90 pub fn scan_plugin_paths(&self) -> Vec<std::path::PathBuf> {
97 let mut all_paths = self.custom_paths.clone();
98 if self.scan_default_paths {
99 all_paths.extend(crate::discovery::scan_standard_paths());
100 }
101 crate::discovery::scan_directories(&all_paths).unwrap_or_default()
102 }
103
104 pub fn discover_plugins_with_callback<F>(
110 &mut self,
111 mut on_progress: F,
112 ) -> Result<Vec<PluginInfo>>
113 where
114 F: FnMut(DiscoveryProgress),
115 {
116 let mut all_paths = self.custom_paths.clone();
117
118 if self.scan_default_paths {
119 all_paths.extend(crate::discovery::scan_standard_paths());
120 }
121
122 let plugin_paths = crate::discovery::scan_directories(&all_paths)?;
123 let total = plugin_paths.len();
124
125 on_progress(DiscoveryProgress::Started {
126 total_plugins: total,
127 });
128
129 let mut plugins = Vec::new();
130 for (index, path) in plugin_paths.into_iter().enumerate() {
131 match crate::discovery::get_plugin_info(&path) {
132 Ok(info) => {
133 on_progress(DiscoveryProgress::Found {
134 plugin: info.clone(),
135 current: index + 1,
136 total,
137 });
138 plugins.push(info);
139 }
140 Err(e) => {
141 log::warn!("Failed to get info for plugin {}: {}", path.display(), e);
142 on_progress(DiscoveryProgress::Error {
143 path: path.display().to_string(),
144 error: e.to_string(),
145 });
146 }
147 }
148 }
149
150 on_progress(DiscoveryProgress::Completed {
151 total_found: plugins.len(),
152 });
153
154 Ok(plugins)
155 }
156
157 pub fn load_plugin<P: AsRef<Path>>(&mut self, path: P) -> Result<Plugin> {
159 let path = path.as_ref();
160
161 if !path.exists() {
162 return Err(Error::PluginNotFound(path.display().to_string()));
163 }
164
165 let isolate = self.use_process_isolation
168 || (self.auto_isolate_problematic
169 && crate::internal::module_loader::has_objc_conflicts(path));
170 if isolate {
171 self.load_plugin_isolated(path)
172 } else {
173 self.load_plugin_internal(path)
174 }
175 }
176
177 #[cfg(feature = "process-isolation")]
183 pub fn probe_plugin<P: AsRef<Path>>(&self, path: P) -> ProbeResult {
184 use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
185
186 let path = path.as_ref();
187 if !path.exists() {
188 return ProbeResult::Failed("plugin path does not exist".to_string());
189 }
190 let mut process =
191 match PluginHostProcess::new(self.helper_path.clone(), self.response_timeout) {
192 Ok(p) => p,
193 Err(e) => return ProbeResult::Failed(format!("helper unavailable: {e}")),
194 };
195 match process.send_command(HostCommand::LoadPlugin {
196 path: path.display().to_string(),
197 sample_rate: self.config.sample_rate,
198 block_size: self.config.block_size as u32,
199 tempo: self.config.tempo,
200 time_sig_numerator: self.config.time_sig_numerator,
201 time_sig_denominator: self.config.time_sig_denominator,
202 }) {
203 Ok(HostResponse::PluginInfo { .. }) => ProbeResult::Ok,
204 Ok(HostResponse::Error { message }) => ProbeResult::Failed(message),
205 Ok(_) => ProbeResult::Failed("unexpected response from helper".to_string()),
206 Err(e) if e.to_lowercase().contains("crash") => ProbeResult::Crashed,
207 Err(e) if e.to_lowercase().contains("timed out") => ProbeResult::TimedOut,
208 Err(e) => ProbeResult::Failed(e),
209 }
210 }
211
212 fn load_plugin_internal(&mut self, path: &Path) -> Result<Plugin> {
214 let mut plugin_impl = crate::internal::plugin_impl::PluginImpl::load(path)?;
216
217 plugin_impl.set_transport(
220 self.config.tempo,
221 self.config.time_sig_numerator,
222 self.config.time_sig_denominator,
223 );
224
225 let updated_info = plugin_impl.info.clone();
227
228 let output_channels = match plugin_impl.output_channel_count() {
231 0 => 2,
232 n => n,
233 };
234
235 let plugin = Plugin {
236 info: updated_info,
237 is_processing: false,
238 sample_rate: self.config.sample_rate,
239 block_size: self.config.block_size,
240 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
241 parameter_change_callback: None,
242 audio_callback: None,
243 internal: Some(Box::new(plugin_impl)),
244 };
245
246 Ok(plugin)
250 }
251
252 fn load_plugin_isolated(&mut self, path: &Path) -> Result<Plugin> {
254 use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
255
256 let mut process =
258 PluginHostProcess::new(self.helper_path.clone(), self.response_timeout)
259 .map_err(|e| Error::Other(format!("Failed to create isolated process: {}", e)))?;
260
261 let response = process
263 .send_command(HostCommand::LoadPlugin {
264 path: path.display().to_string(),
265 sample_rate: self.config.sample_rate,
266 block_size: self.config.block_size as u32,
267 tempo: self.config.tempo,
268 time_sig_numerator: self.config.time_sig_numerator,
269 time_sig_denominator: self.config.time_sig_denominator,
270 })
271 .map_err(|e| Error::Other(format!("Failed to load plugin in isolation: {}", e)))?;
272
273 let (loaded_info, output_channels) = match response {
276 HostResponse::PluginInfo {
277 vendor,
278 name,
279 version,
280 category,
281 uid,
282 has_gui,
283 audio_inputs,
284 audio_outputs,
285 output_channels,
286 has_midi_input,
287 has_midi_output,
288 } => {
289 let info = PluginInfo {
290 path: path.to_path_buf(),
291 name,
292 vendor,
293 version,
294 category,
295 uid,
296 has_gui,
297 audio_inputs: audio_inputs as u32,
298 audio_outputs: audio_outputs as u32,
299 has_midi_input,
300 has_midi_output,
301 };
302 let channels = if output_channels > 0 {
303 output_channels as usize
304 } else {
305 2
306 };
307 (info, channels)
308 }
309 HostResponse::Error { message } => {
310 return Err(Error::Other(format!("Failed to load plugin: {}", message)));
311 }
312 _ => {
313 return Err(Error::Other(
314 "Unexpected response from helper process".to_string(),
315 ));
316 }
317 };
318
319 let plugin_impl = crate::internal::isolated_plugin_impl::IsolatedPluginImpl::new(
321 process,
322 loaded_info.clone(),
323 self.config.sample_rate,
324 self.config.block_size,
325 self.config.tempo,
326 self.config.time_sig_numerator,
327 self.config.time_sig_denominator,
328 output_channels,
329 self.helper_path.clone(),
330 self.response_timeout,
331 self.auto_recover_plugins,
332 self.auto_recover_max_retries,
333 );
334
335 let plugin = Plugin {
336 info: loaded_info,
337 is_processing: false,
338 sample_rate: self.config.sample_rate,
339 block_size: self.config.block_size,
340 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
341 parameter_change_callback: None,
342 audio_callback: None,
343 internal: Some(Box::new(plugin_impl)),
344 };
345
346 Ok(plugin)
347 }
348
349 pub fn config(&self) -> &AudioConfig {
351 &self.config
352 }
353}
354
355impl Default for Vst3Host {
356 fn default() -> Self {
357 Self {
358 config: AudioConfig::default(),
359 custom_paths: Vec::new(),
360 use_process_isolation: false,
361 auto_isolate_problematic: false,
362 scan_default_paths: true, helper_path: None,
364 response_timeout: crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT,
365 auto_recover_plugins: false,
366 auto_recover_max_retries: 1,
367 }
368 }
369}
370
371#[derive(Default)]
376pub struct Vst3HostBuilder {
377 config: AudioConfig,
378 custom_paths: Vec<PathBuf>,
379 use_process_isolation: bool,
380 auto_isolate_problematic: bool,
381 scan_default_paths: bool,
382 helper_path: Option<PathBuf>,
383 response_timeout: Option<std::time::Duration>,
384 auto_recover_plugins: bool,
385 auto_recover_max_retries: Option<u32>,
386}
387
388impl Vst3HostBuilder {
389 pub fn sample_rate(mut self, rate: f64) -> Self {
391 self.config.sample_rate = rate;
392 self
393 }
394
395 pub fn block_size(mut self, size: usize) -> Self {
397 self.config.block_size = size;
398 self
399 }
400
401 pub fn input_channels(mut self, channels: usize) -> Self {
403 self.config.input_channels = channels;
404 self
405 }
406
407 pub fn output_channels(mut self, channels: usize) -> Self {
409 self.config.output_channels = channels;
410 self
411 }
412
413 pub fn tempo(mut self, bpm: f64) -> Self {
418 if bpm.is_finite() && bpm > 0.0 {
419 self.config.tempo = bpm;
420 }
421 self
422 }
423
424 pub fn time_signature(mut self, num: i32, den: i32) -> Self {
428 if num > 0 && den > 0 {
429 self.config.time_sig_numerator = num;
430 self.config.time_sig_denominator = den;
431 }
432 self
433 }
434
435 pub fn with_process_isolation(mut self, enabled: bool) -> Self {
437 self.use_process_isolation = enabled;
438 self
439 }
440
441 pub fn auto_isolate_problematic(mut self, enabled: bool) -> Self {
446 self.auto_isolate_problematic = enabled;
447 self
448 }
449
450 pub fn add_scan_path<P: AsRef<Path>>(mut self, path: P) -> Self {
452 self.custom_paths.push(path.as_ref().to_path_buf());
453 self
454 }
455
456 pub fn scan_default_paths(mut self) -> Self {
458 self.scan_default_paths = true;
459 self
460 }
461
462 pub fn response_timeout(mut self, timeout: std::time::Duration) -> Self {
465 self.response_timeout = Some(timeout);
466 self
467 }
468
469 pub fn helper_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
473 self.helper_path = Some(path.into());
474 self
475 }
476
477 pub fn auto_recover_plugins(mut self, enabled: bool) -> Self {
486 self.auto_recover_plugins = enabled;
487 self
488 }
489
490 pub fn auto_recover_max_retries(mut self, retries: u32) -> Self {
493 self.auto_recover_max_retries = Some(retries);
494 self
495 }
496
497 pub fn build(self) -> Result<Vst3Host> {
499 Ok(Vst3Host {
500 config: self.config,
501 custom_paths: self.custom_paths,
502 use_process_isolation: self.use_process_isolation,
503 auto_isolate_problematic: self.auto_isolate_problematic,
504 scan_default_paths: self.scan_default_paths,
505 helper_path: self.helper_path,
506 response_timeout: self
507 .response_timeout
508 .unwrap_or(crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT),
509 auto_recover_plugins: self.auto_recover_plugins,
510 auto_recover_max_retries: self.auto_recover_max_retries.unwrap_or(1),
511 })
512 }
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
517pub enum ProbeResult {
518 Ok,
520 Crashed,
522 TimedOut,
524 Failed(String),
526}
527
528#[derive(Debug, Clone)]
530pub enum DiscoveryProgress {
531 Started {
533 total_plugins: usize,
535 },
536 Found {
538 plugin: PluginInfo,
540 current: usize,
542 total: usize,
544 },
545 Error {
547 path: String,
549 error: String,
551 },
552 Completed {
554 total_found: usize,
556 },
557}
558
559#[cfg(feature = "cpal-backend")]
560impl Vst3Host {
561 pub fn play(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
585 let backend = crate::backends::CpalBackend::new()?;
586 let config = crate::audio::AudioConfig {
587 output_channels: 2,
588 input_channels: 0,
589 ..self.config
590 };
591 crate::playback::play_with_backend(&backend, plugin, config)
592 }
593
594 pub fn play_with_input(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
602 let backend = crate::backends::CpalBackend::new()?;
603 let config = crate::audio::AudioConfig {
604 input_channels: 2,
605 output_channels: 2,
606 ..self.config
607 };
608 crate::playback::play_with_input_backend(&backend, plugin, config)
609 }
610
611 pub fn play_realtime(
631 &self,
632 plugin: Plugin,
633 command_capacity: usize,
634 ) -> Result<crate::playback::RtAudioHandle> {
635 let backend = crate::backends::CpalBackend::new()?;
636 let config = crate::audio::AudioConfig {
637 output_channels: 2,
638 input_channels: 0,
639 ..self.config
640 };
641 crate::playback::play_realtime_with_backend(&backend, plugin, config, command_capacity)
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 #[test]
650 fn transport_defaults_to_120_bpm_4_4() {
651 let host = Vst3HostBuilder::default().build().unwrap();
652 assert_eq!(host.config().tempo, 120.0);
653 assert_eq!(host.config().time_sig_numerator, 4);
654 assert_eq!(host.config().time_sig_denominator, 4);
655 }
656
657 #[test]
658 fn builder_threads_tempo_and_time_signature_into_config() {
659 let host = Vst3HostBuilder::default()
660 .tempo(140.0)
661 .time_signature(7, 8)
662 .build()
663 .unwrap();
664 assert_eq!(host.config().tempo, 140.0);
665 assert_eq!(host.config().time_sig_numerator, 7);
666 assert_eq!(host.config().time_sig_denominator, 8);
667 }
668}