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_audio_config(self.config.sample_rate, self.config.block_size);
220
221 plugin_impl.set_transport(
224 self.config.tempo,
225 self.config.time_sig_numerator,
226 self.config.time_sig_denominator,
227 );
228
229 let updated_info = plugin_impl.info.clone();
231
232 let output_channels = match plugin_impl.output_channel_count() {
235 0 => 2,
236 n => n,
237 };
238
239 let plugin = Plugin {
240 info: updated_info,
241 is_processing: false,
242 sample_rate: self.config.sample_rate,
243 block_size: self.config.block_size,
244 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
245 parameter_change_callback: None,
246 audio_callback: None,
247 internal: Some(Box::new(plugin_impl)),
248 };
249
250 Ok(plugin)
254 }
255
256 fn load_plugin_isolated(&mut self, path: &Path) -> Result<Plugin> {
258 use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
259
260 let mut process =
262 PluginHostProcess::new(self.helper_path.clone(), self.response_timeout)
263 .map_err(|e| Error::Other(format!("Failed to create isolated process: {}", e)))?;
264
265 let response = process
267 .send_command(HostCommand::LoadPlugin {
268 path: path.display().to_string(),
269 sample_rate: self.config.sample_rate,
270 block_size: self.config.block_size as u32,
271 tempo: self.config.tempo,
272 time_sig_numerator: self.config.time_sig_numerator,
273 time_sig_denominator: self.config.time_sig_denominator,
274 })
275 .map_err(|e| Error::Other(format!("Failed to load plugin in isolation: {}", e)))?;
276
277 let (loaded_info, output_channels) = match response {
280 HostResponse::PluginInfo {
281 vendor,
282 name,
283 version,
284 category,
285 uid,
286 has_gui,
287 audio_inputs,
288 audio_outputs,
289 output_channels,
290 has_midi_input,
291 has_midi_output,
292 } => {
293 let info = PluginInfo {
294 path: path.to_path_buf(),
295 name,
296 vendor,
297 version,
298 category,
299 uid,
300 has_gui,
301 audio_inputs: audio_inputs as u32,
302 audio_outputs: audio_outputs as u32,
303 has_midi_input,
304 has_midi_output,
305 };
306 let channels = if output_channels > 0 {
307 output_channels as usize
308 } else {
309 2
310 };
311 (info, channels)
312 }
313 HostResponse::Error { message } => {
314 return Err(Error::Other(format!("Failed to load plugin: {}", message)));
315 }
316 _ => {
317 return Err(Error::Other(
318 "Unexpected response from helper process".to_string(),
319 ));
320 }
321 };
322
323 let plugin_impl = crate::internal::isolated_plugin_impl::IsolatedPluginImpl::new(
325 process,
326 loaded_info.clone(),
327 self.config.sample_rate,
328 self.config.block_size,
329 self.config.tempo,
330 self.config.time_sig_numerator,
331 self.config.time_sig_denominator,
332 output_channels,
333 self.helper_path.clone(),
334 self.response_timeout,
335 self.auto_recover_plugins,
336 self.auto_recover_max_retries,
337 );
338
339 let plugin = Plugin {
340 info: loaded_info,
341 is_processing: false,
342 sample_rate: self.config.sample_rate,
343 block_size: self.config.block_size,
344 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
345 parameter_change_callback: None,
346 audio_callback: None,
347 internal: Some(Box::new(plugin_impl)),
348 };
349
350 Ok(plugin)
351 }
352
353 pub fn config(&self) -> &AudioConfig {
355 &self.config
356 }
357}
358
359impl Default for Vst3Host {
360 fn default() -> Self {
361 Self {
362 config: AudioConfig::default(),
363 custom_paths: Vec::new(),
364 use_process_isolation: false,
365 auto_isolate_problematic: false,
366 scan_default_paths: true, helper_path: None,
368 response_timeout: crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT,
369 auto_recover_plugins: false,
370 auto_recover_max_retries: 1,
371 }
372 }
373}
374
375#[derive(Default)]
380pub struct Vst3HostBuilder {
381 config: AudioConfig,
382 custom_paths: Vec<PathBuf>,
383 use_process_isolation: bool,
384 auto_isolate_problematic: bool,
385 scan_default_paths: bool,
386 helper_path: Option<PathBuf>,
387 response_timeout: Option<std::time::Duration>,
388 auto_recover_plugins: bool,
389 auto_recover_max_retries: Option<u32>,
390}
391
392impl Vst3HostBuilder {
393 pub fn sample_rate(mut self, rate: f64) -> Self {
395 self.config.sample_rate = rate;
396 self
397 }
398
399 pub fn block_size(mut self, size: usize) -> Self {
401 self.config.block_size = size;
402 self
403 }
404
405 pub fn input_channels(mut self, channels: usize) -> Self {
407 self.config.input_channels = channels;
408 self
409 }
410
411 pub fn output_channels(mut self, channels: usize) -> Self {
413 self.config.output_channels = channels;
414 self
415 }
416
417 pub fn tempo(mut self, bpm: f64) -> Self {
422 if bpm.is_finite() && bpm > 0.0 {
423 self.config.tempo = bpm;
424 }
425 self
426 }
427
428 pub fn time_signature(mut self, num: i32, den: i32) -> Self {
432 if num > 0 && den > 0 {
433 self.config.time_sig_numerator = num;
434 self.config.time_sig_denominator = den;
435 }
436 self
437 }
438
439 pub fn with_process_isolation(mut self, enabled: bool) -> Self {
441 self.use_process_isolation = enabled;
442 self
443 }
444
445 pub fn auto_isolate_problematic(mut self, enabled: bool) -> Self {
450 self.auto_isolate_problematic = enabled;
451 self
452 }
453
454 pub fn add_scan_path<P: AsRef<Path>>(mut self, path: P) -> Self {
456 self.custom_paths.push(path.as_ref().to_path_buf());
457 self
458 }
459
460 pub fn scan_default_paths(mut self) -> Self {
462 self.scan_default_paths = true;
463 self
464 }
465
466 pub fn response_timeout(mut self, timeout: std::time::Duration) -> Self {
469 self.response_timeout = Some(timeout);
470 self
471 }
472
473 pub fn helper_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
477 self.helper_path = Some(path.into());
478 self
479 }
480
481 pub fn auto_recover_plugins(mut self, enabled: bool) -> Self {
490 self.auto_recover_plugins = enabled;
491 self
492 }
493
494 pub fn auto_recover_max_retries(mut self, retries: u32) -> Self {
497 self.auto_recover_max_retries = Some(retries);
498 self
499 }
500
501 pub fn build(self) -> Result<Vst3Host> {
503 Ok(Vst3Host {
504 config: self.config,
505 custom_paths: self.custom_paths,
506 use_process_isolation: self.use_process_isolation,
507 auto_isolate_problematic: self.auto_isolate_problematic,
508 scan_default_paths: self.scan_default_paths,
509 helper_path: self.helper_path,
510 response_timeout: self
511 .response_timeout
512 .unwrap_or(crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT),
513 auto_recover_plugins: self.auto_recover_plugins,
514 auto_recover_max_retries: self.auto_recover_max_retries.unwrap_or(1),
515 })
516 }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
521pub enum ProbeResult {
522 Ok,
524 Crashed,
526 TimedOut,
528 Failed(String),
530}
531
532#[derive(Debug, Clone)]
534pub enum DiscoveryProgress {
535 Started {
537 total_plugins: usize,
539 },
540 Found {
542 plugin: PluginInfo,
544 current: usize,
546 total: usize,
548 },
549 Error {
551 path: String,
553 error: String,
555 },
556 Completed {
558 total_found: usize,
560 },
561}
562
563#[cfg(feature = "cpal-backend")]
564impl Vst3Host {
565 pub fn play(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
589 let backend = crate::backends::CpalBackend::new()?;
590 let config = crate::audio::AudioConfig {
591 output_channels: 2,
592 input_channels: 0,
593 ..self.config
594 };
595 crate::playback::play_with_backend(&backend, plugin, config)
596 }
597
598 pub fn play_with_input(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
606 let backend = crate::backends::CpalBackend::new()?;
607 let config = crate::audio::AudioConfig {
608 input_channels: 2,
609 output_channels: 2,
610 ..self.config
611 };
612 crate::playback::play_with_input_backend(&backend, plugin, config)
613 }
614
615 pub fn play_realtime(
635 &self,
636 plugin: Plugin,
637 command_capacity: usize,
638 ) -> Result<crate::playback::RtAudioHandle> {
639 let backend = crate::backends::CpalBackend::new()?;
640 let config = crate::audio::AudioConfig {
641 output_channels: 2,
642 input_channels: 0,
643 ..self.config
644 };
645 crate::playback::play_realtime_with_backend(&backend, plugin, config, command_capacity)
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn transport_defaults_to_120_bpm_4_4() {
655 let host = Vst3HostBuilder::default().build().unwrap();
656 assert_eq!(host.config().tempo, 120.0);
657 assert_eq!(host.config().time_sig_numerator, 4);
658 assert_eq!(host.config().time_sig_denominator, 4);
659 }
660
661 #[test]
662 fn builder_threads_tempo_and_time_signature_into_config() {
663 let host = Vst3HostBuilder::default()
664 .tempo(140.0)
665 .time_signature(7, 8)
666 .build()
667 .unwrap();
668 assert_eq!(host.config().tempo, 140.0);
669 assert_eq!(host.config().time_sig_numerator, 7);
670 assert_eq!(host.config().time_sig_denominator, 8);
671 }
672}