vst3_host/host.rs
1//! VST3 host implementation
2
3use 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
11/// Ceiling on the output channel count this host will size meters and buffers from.
12///
13/// Matches the isolation layer's own wire limit. The channel count of an isolated plugin is
14/// whatever the helper says it is, and this boundary sizes an allocation from it directly, so
15/// it defends itself rather than trusting the peer to have clamped first.
16const MAX_OUTPUT_CHANNELS: usize = 256;
17
18/// Turn a reported output channel count into one this host can size buffers from: fall back
19/// to stereo when the plugin reports none, and never exceed [`MAX_OUTPUT_CHANNELS`].
20fn clamp_output_channels(reported: i32) -> usize {
21 match reported {
22 n if n <= 0 => 2,
23 n => (n as usize).min(MAX_OUTPUT_CHANNELS),
24 }
25}
26
27/// VST3 host instance
28pub struct Vst3Host {
29 /// Audio configuration
30 pub(crate) config: AudioConfig,
31 /// Custom plugin scan paths
32 pub(crate) custom_paths: Vec<PathBuf>,
33 /// Whether to use process isolation for plugin loading
34 pub(crate) use_process_isolation: bool,
35 /// Whether to scan default system paths for plugins
36 pub(crate) scan_default_paths: bool,
37 /// Explicit path to the isolation helper binary (overrides the heuristic search).
38 pub(crate) helper_path: Option<PathBuf>,
39 /// How long to wait for an isolated helper response before declaring a timeout.
40 pub(crate) response_timeout: std::time::Duration,
41 /// Whether an isolated plugin auto-respawns + retries on a crash/hang (control plane only).
42 pub(crate) auto_recover_plugins: bool,
43 /// Max respawn+retry cycles per command when auto-recover is on.
44 pub(crate) auto_recover_max_retries: u32,
45 /// Per-plugin timeout for the crash-resistant discovery probe ([`Self::discover_plugins_safe`]).
46 pub(crate) probe_timeout: std::time::Duration,
47}
48
49impl Vst3Host {
50 /// Create a new VST3 host with default settings.
51 ///
52 /// Discovery scans the standard system VST3 directories (consistent with
53 /// [`Vst3Host::default`]). For explicit control use [`Vst3Host::builder`]; the builder
54 /// does **not** scan system paths unless you opt in with
55 /// [`Vst3HostBuilder::scan_default_paths`].
56 pub fn new() -> Result<Self> {
57 Self::builder().scan_default_paths().build()
58 }
59
60 /// Create a new VST3 host builder
61 pub fn builder() -> Vst3HostBuilder {
62 Vst3HostBuilder::default()
63 }
64
65 /// Add a custom path to scan for VST3 plugins
66 pub fn add_scan_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
67 let path = path.as_ref();
68 if !path.exists() {
69 return Err(Error::Other(format!(
70 "Path does not exist: {}",
71 path.display()
72 )));
73 }
74 self.custom_paths.push(path.to_path_buf());
75 Ok(())
76 }
77
78 /// Discover VST3 plugins in configured scan paths.
79 ///
80 /// # This can take your process down
81 ///
82 /// Every candidate is instantiated **in this process** to read its metadata, so a plugin that
83 /// aborts, segfaults, or throws a C++ exception through the Rust frames during its own
84 /// initialisation kills the host — there is nothing this function can catch. That is not
85 /// hypothetical: a licensed Waves plugin in a normal `/Library/Audio/Plug-Ins/VST3` aborts
86 /// with "Rust cannot catch foreign exceptions" during its license check.
87 ///
88 /// Prefer [`Self::discover_plugins_safe`], which introspects each plugin in a short-lived
89 /// child process and reports the casualties as skips instead of dying. Use this one only when
90 /// you control which plugins are present.
91 pub fn discover_plugins(&mut self) -> Result<Vec<PluginInfo>> {
92 let mut all_paths = self.custom_paths.clone();
93
94 // Add system paths if enabled
95 if self.scan_default_paths {
96 all_paths.extend(crate::discovery::scan_standard_paths());
97 }
98
99 // Scan directories for VST3 plugins
100 let plugin_paths = crate::discovery::scan_directories(&all_paths)?;
101
102 // Get plugin info for each found plugin
103 let mut plugins = Vec::new();
104 for path in plugin_paths {
105 match crate::discovery::get_plugin_info(&path) {
106 Ok(info) => plugins.push(info),
107 Err(e) => {
108 log::warn!("Failed to get info for plugin {}: {}", path.display(), e);
109 // Continue with other plugins
110 }
111 }
112 }
113
114 Ok(plugins)
115 }
116
117 /// List VST3 bundle paths in the configured scan locations **without loading them**.
118 ///
119 /// Fast and safe: unlike [`Self::discover_plugins`] (which loads and initializes
120 /// every plugin to read its metadata, and can be slow or crash-prone in-process),
121 /// this only walks the filesystem. Use it when you just need the list of available
122 /// `.vst3` paths (e.g. to populate a picker) and will load on demand.
123 pub fn scan_plugin_paths(&self) -> Vec<std::path::PathBuf> {
124 let mut all_paths = self.custom_paths.clone();
125 if self.scan_default_paths {
126 all_paths.extend(crate::discovery::scan_standard_paths());
127 }
128 crate::discovery::scan_directories(&all_paths).unwrap_or_default()
129 }
130
131 /// Discover VST3 plugins, reporting progress through a callback.
132 ///
133 /// The callback receives [`DiscoveryProgress`] events: one `Started` at the
134 /// beginning, a `Found` or `Error` per candidate, and a final `Completed`.
135 /// Returns the successfully-inspected plugins, same as [`Self::discover_plugins`].
136 pub fn discover_plugins_with_callback<F>(
137 &mut self,
138 mut on_progress: F,
139 ) -> Result<Vec<PluginInfo>>
140 where
141 F: FnMut(DiscoveryProgress),
142 {
143 let mut all_paths = self.custom_paths.clone();
144
145 if self.scan_default_paths {
146 all_paths.extend(crate::discovery::scan_standard_paths());
147 }
148
149 let plugin_paths = crate::discovery::scan_directories(&all_paths)?;
150 let total = plugin_paths.len();
151
152 on_progress(DiscoveryProgress::Started {
153 total_plugins: total,
154 });
155
156 let mut plugins = Vec::new();
157 for (index, path) in plugin_paths.into_iter().enumerate() {
158 match crate::discovery::get_plugin_info(&path) {
159 Ok(info) => {
160 on_progress(DiscoveryProgress::Found {
161 plugin: info.clone(),
162 current: index + 1,
163 total,
164 });
165 plugins.push(info);
166 }
167 Err(e) => {
168 log::warn!("Failed to get info for plugin {}: {}", path.display(), e);
169 on_progress(DiscoveryProgress::Error {
170 path: path.display().to_string(),
171 error: e.to_string(),
172 });
173 }
174 }
175 }
176
177 on_progress(DiscoveryProgress::Completed {
178 total_found: plugins.len(),
179 });
180
181 Ok(plugins)
182 }
183
184 /// Crash-resistantly discover plugins in the configured scan paths.
185 ///
186 /// Unlike [`Self::discover_plugins`] — which instantiates each plugin **in-process**
187 /// to read its metadata, so a single plugin that `abort()`s or makes a pure-virtual
188 /// call during init takes down the whole host — this introspects every plugin in a
189 /// throwaway child process (`vst3-host-probe`). A plugin that crashes kills only that
190 /// child; the scan completes and returns the plugins it could introspect, recording
191 /// the skipped ones (and why) in the returned
192 /// [`SafeDiscoveryReport`](crate::discovery::SafeDiscoveryReport).
193 ///
194 /// Trade-off: this spawns one probe process per plugin, so it is slower than the
195 /// in-process path. Use it to safely scan an untrusted folder; keep
196 /// [`Self::discover_plugins`] for speed when you trust the plugins.
197 ///
198 /// The probe timeout per plugin defaults to
199 /// [`DEFAULT_PROBE_TIMEOUT`](crate::discovery::DEFAULT_PROBE_TIMEOUT); override it with
200 /// [`Vst3HostBuilder::probe_timeout`].
201 ///
202 /// If the `vst3-host-probe` binary cannot be located the scan never runs; the report is
203 /// empty and says why in [`SafeDiscoveryReport::error`](crate::discovery::SafeDiscoveryReport::error).
204 pub fn discover_plugins_safe(&self) -> crate::discovery::SafeDiscoveryReport {
205 let mut all_paths = self.custom_paths.clone();
206 if self.scan_default_paths {
207 all_paths.extend(crate::discovery::scan_standard_paths());
208 }
209 crate::discovery::discover_plugins_safe(&all_paths, self.probe_timeout)
210 }
211
212 /// Load a VST3 plugin
213 pub fn load_plugin<P: AsRef<Path>>(&mut self, path: P) -> Result<Plugin> {
214 let path = path.as_ref();
215
216 if !path.exists() {
217 return Err(Error::PluginNotFound(path.display().to_string()));
218 }
219
220 if self.use_process_isolation {
221 self.load_plugin_isolated(path, None)
222 } else {
223 self.load_plugin_internal(path, None)
224 }
225 }
226
227 /// Load a particular audio class from a VST3 bundle.
228 ///
229 /// `class_id` may be either a current class id exported by the factory or a retired id
230 /// mapped to its replacement by the bundle's validated `moduleinfo.json`. This is useful
231 /// when restoring a session whose plugin id predates a vendor's UID migration.
232 pub fn load_plugin_class<P: AsRef<Path>>(&mut self, path: P, class_id: &str) -> Result<Plugin> {
233 let path = path.as_ref();
234 if !path.exists() {
235 return Err(Error::PluginNotFound(path.display().to_string()));
236 }
237 crate::internal::utils::parse_class_uid(class_id).ok_or_else(|| {
238 Error::PluginLoadFailed(
239 "class id must be exactly 32 hexadecimal characters".to_string(),
240 )
241 })?;
242
243 if self.use_process_isolation {
244 self.load_plugin_isolated(path, Some(class_id))
245 } else {
246 self.load_plugin_internal(path, Some(class_id))
247 }
248 }
249
250 /// Probe whether a plugin loads safely, **without risking the host process** — it is
251 /// loaded in an isolated helper, so a crash is contained. This is the "validate
252 /// plugins" operation a scanner uses to blacklist bad plugins.
253 ///
254 /// Requires the `process-isolation` feature.
255 #[cfg(feature = "process-isolation")]
256 pub fn probe_plugin<P: AsRef<Path>>(&self, path: P) -> ProbeResult {
257 use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
258
259 let path = path.as_ref();
260 if !path.exists() {
261 return ProbeResult::Failed("plugin path does not exist".to_string());
262 }
263 let mut process =
264 match PluginHostProcess::new(self.helper_path.clone(), self.response_timeout) {
265 Ok(p) => p,
266 Err(e) => return ProbeResult::Failed(format!("helper unavailable: {e}")),
267 };
268 match process.send_command(HostCommand::LoadPlugin {
269 path: path.display().to_string(),
270 sample_rate: self.config.sample_rate,
271 block_size: self.config.block_size as u32,
272 tempo: self.config.tempo,
273 time_sig_numerator: self.config.time_sig_numerator,
274 time_sig_denominator: self.config.time_sig_denominator,
275 class_id: None,
276 }) {
277 Ok(HostResponse::PluginInfo { .. }) => ProbeResult::Ok,
278 Ok(HostResponse::Error { message }) => ProbeResult::Failed(message),
279 Ok(_) => ProbeResult::Failed("unexpected response from helper".to_string()),
280 Err(e) if e.to_lowercase().contains("crash") => ProbeResult::Crashed,
281 Err(e) if e.to_lowercase().contains("timed out") => ProbeResult::TimedOut,
282 Err(e) => ProbeResult::Failed(e),
283 }
284 }
285
286 /// Load a plugin in-process
287 fn load_plugin_internal(&mut self, path: &Path, class_id: Option<&str>) -> Result<Plugin> {
288 // Load the plugin implementation directly - it will handle path resolution
289 let mut plugin_impl = match class_id {
290 Some(class_id) => crate::internal::plugin_impl::PluginImpl::load_class(path, class_id)?,
291 None => crate::internal::plugin_impl::PluginImpl::load(path)?,
292 };
293
294 // Apply the builder's audio config (sample rate / block size) so the plugin actually
295 // processes at the requested settings, not the internal defaults.
296 plugin_impl.set_audio_config(self.config.sample_rate, self.config.block_size);
297
298 // Thread the configured transport into the plugin's host ProcessContext so
299 // tempo-synced DSP sees the host tempo / time signature.
300 plugin_impl.set_transport(
301 self.config.tempo,
302 self.config.time_sig_numerator,
303 self.config.time_sig_denominator,
304 );
305
306 // Get the updated info from the plugin implementation (has_gui might have been updated)
307 let updated_info = plugin_impl.info.clone();
308 let compatibility = plugin_impl.compatibility.clone();
309
310 // Size meters to the plugin's real output channel count (bus-aware), not a stereo
311 // assumption; fall back to 2 only when the plugin reports no output channels.
312 let output_channels = match plugin_impl.output_channel_count() {
313 0 => 2,
314 n => n,
315 };
316
317 let plugin = Plugin {
318 info: updated_info,
319 compatibility,
320 is_processing: false,
321 sample_rate: self.config.sample_rate,
322 block_size: self.config.block_size,
323 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
324 parameter_change_callback: None,
325 audio_callback: None,
326 internal: Some(Box::new(plugin_impl)),
327 };
328
329 Ok(plugin)
330 }
331
332 /// Load a plugin in an isolated process
333 fn load_plugin_isolated(&mut self, path: &Path, class_id: Option<&str>) -> Result<Plugin> {
334 use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
335
336 // Create and start the isolated plugin process
337 let mut process =
338 PluginHostProcess::new(self.helper_path.clone(), self.response_timeout)
339 .map_err(|e| Error::Other(format!("Failed to create isolated process: {}", e)))?;
340
341 // Load the plugin in the isolated process
342 let response = process
343 .send_command(HostCommand::LoadPlugin {
344 path: path.display().to_string(),
345 sample_rate: self.config.sample_rate,
346 block_size: self.config.block_size as u32,
347 tempo: self.config.tempo,
348 time_sig_numerator: self.config.time_sig_numerator,
349 time_sig_denominator: self.config.time_sig_denominator,
350 class_id: class_id.map(str::to_owned),
351 })
352 .map_err(|e| Error::Other(format!("Failed to load plugin in isolation: {}", e)))?;
353
354 // Verify the plugin loaded successfully. Metadata comes straight from the helper's
355 // accurate introspection, so the isolated path matches the in-process one.
356 let (loaded_info, compatibility, output_channels) = match response {
357 HostResponse::PluginInfo {
358 vendor,
359 name,
360 version,
361 category,
362 uid,
363 has_gui,
364 audio_inputs,
365 audio_outputs,
366 output_channels,
367 has_midi_input,
368 has_midi_output,
369 compatibility,
370 } => {
371 let info = PluginInfo {
372 path: path.to_path_buf(),
373 name,
374 vendor,
375 version,
376 category,
377 uid,
378 has_gui,
379 audio_inputs: audio_inputs as u32,
380 audio_outputs: audio_outputs as u32,
381 has_midi_input,
382 has_midi_output,
383 };
384 let channels = clamp_output_channels(output_channels);
385 (info, compatibility, channels)
386 }
387 HostResponse::Error { message } => {
388 return Err(Error::Other(format!("Failed to load plugin: {}", message)));
389 }
390 _ => {
391 return Err(Error::Other(
392 "Unexpected response from helper process".to_string(),
393 ));
394 }
395 };
396
397 // Create the isolated plugin implementation
398 let plugin_impl = crate::internal::isolated_plugin_impl::IsolatedPluginImpl::new(
399 process,
400 loaded_info.clone(),
401 self.config.sample_rate,
402 self.config.block_size,
403 self.config.tempo,
404 self.config.time_sig_numerator,
405 self.config.time_sig_denominator,
406 output_channels,
407 self.helper_path.clone(),
408 self.response_timeout,
409 self.auto_recover_plugins,
410 self.auto_recover_max_retries,
411 );
412
413 let plugin = Plugin {
414 info: loaded_info,
415 compatibility,
416 is_processing: false,
417 sample_rate: self.config.sample_rate,
418 block_size: self.config.block_size,
419 audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
420 parameter_change_callback: None,
421 audio_callback: None,
422 internal: Some(Box::new(plugin_impl)),
423 };
424
425 Ok(plugin)
426 }
427
428 /// Get audio configuration
429 pub fn config(&self) -> &AudioConfig {
430 &self.config
431 }
432}
433
434impl Default for Vst3Host {
435 fn default() -> Self {
436 Self {
437 config: AudioConfig::default(),
438 custom_paths: Vec::new(),
439 use_process_isolation: false,
440 scan_default_paths: true,
441 helper_path: None,
442 response_timeout: crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT,
443 auto_recover_plugins: false,
444 auto_recover_max_retries: 1,
445 probe_timeout: crate::discovery::DEFAULT_PROBE_TIMEOUT,
446 }
447 }
448}
449
450/// Builder for VST3 host configuration
451///
452/// All fields default to their type defaults; notably `scan_default_paths` defaults to
453/// `false`, requiring explicit opt-in (unlike `Vst3Host`, which defaults it to `true`).
454#[derive(Default)]
455pub struct Vst3HostBuilder {
456 config: AudioConfig,
457 custom_paths: Vec<PathBuf>,
458 use_process_isolation: bool,
459 scan_default_paths: bool,
460 helper_path: Option<PathBuf>,
461 response_timeout: Option<std::time::Duration>,
462 auto_recover_plugins: bool,
463 auto_recover_max_retries: Option<u32>,
464 probe_timeout: Option<std::time::Duration>,
465}
466
467impl Vst3HostBuilder {
468 /// Set the sample rate
469 pub fn sample_rate(mut self, rate: f64) -> Self {
470 self.config.sample_rate = rate;
471 self
472 }
473
474 /// Set the block size
475 pub fn block_size(mut self, size: usize) -> Self {
476 self.config.block_size = size;
477 self
478 }
479
480 /// Set the number of input channels
481 pub fn input_channels(mut self, channels: usize) -> Self {
482 self.config.input_channels = channels;
483 self
484 }
485
486 /// Set the number of output channels
487 pub fn output_channels(mut self, channels: usize) -> Self {
488 self.config.output_channels = channels;
489 self
490 }
491
492 /// Set the transport tempo (beats per minute) advertised to plugins in the host
493 /// `ProcessContext`. Drives tempo-synced DSP (LFOs, synced delays, arpeggiators).
494 /// Defaults to `120.0`. Non-finite or non-positive values are ignored (a tempo of 0 or
495 /// less would freeze/reverse the derived musical playhead), keeping the previous tempo.
496 pub fn tempo(mut self, bpm: f64) -> Self {
497 if bpm.is_finite() && bpm > 0.0 {
498 self.config.tempo = bpm;
499 }
500 self
501 }
502
503 /// Set the transport time signature advertised to plugins in the host
504 /// `ProcessContext` (`num`/`den`, e.g. `4, 4`). Defaults to `4/4`.
505 ///
506 /// Validated by [`Self::build`] against the same rule every runtime entry point applies
507 /// (see [`Plugin::set_time_signature`](crate::Plugin::set_time_signature)): `num` positive
508 /// and `den` one of `1, 2, 4, 8, 16`.
509 pub fn time_signature(mut self, num: i32, den: i32) -> Self {
510 self.config.time_sig_numerator = num;
511 self.config.time_sig_denominator = den;
512 self
513 }
514
515 /// Enable or disable process isolation for plugin loading
516 pub fn with_process_isolation(mut self, enabled: bool) -> Self {
517 self.use_process_isolation = enabled;
518 self
519 }
520
521 /// Add a custom plugin scan path
522 pub fn add_scan_path<P: AsRef<Path>>(mut self, path: P) -> Self {
523 self.custom_paths.push(path.as_ref().to_path_buf());
524 self
525 }
526
527 /// Enable scanning of default system VST3 paths
528 pub fn scan_default_paths(mut self) -> Self {
529 self.scan_default_paths = true;
530 self
531 }
532
533 /// How long to wait for an isolated helper to respond before treating the plugin as hung
534 /// (and killing the helper). Defaults to 5 seconds. Only affects process-isolated loads.
535 pub fn response_timeout(mut self, timeout: std::time::Duration) -> Self {
536 self.response_timeout = Some(timeout);
537 self
538 }
539
540 /// Override the path to the `vst3-host-helper` binary used for process isolation, instead
541 /// of the default heuristic search. The `VST3_HOST_HELPER_PATH` environment variable does
542 /// the same. Useful when the helper ships in a non-standard location.
543 pub fn helper_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
544 self.helper_path = Some(path.into());
545 self
546 }
547
548 /// Transparently respawn + reload a process-isolated plugin and retry the command when the
549 /// helper crashes or hangs, instead of surfacing `Error::PluginCrashed`/`PluginTimeout` for
550 /// the caller to handle via [`Plugin::recover`](crate::Plugin::recover).
551 ///
552 /// Only affects isolated loads and only the control plane — the audio-thread `process`
553 /// path never recovers inline (a respawn would stall the callback). **Recovery reloads the
554 /// plugin from defaults**: parameter values / state are NOT replayed, so snapshot with
555 /// `save_state`/`load_state` if you need them preserved. Off by default.
556 pub fn auto_recover_plugins(mut self, enabled: bool) -> Self {
557 self.auto_recover_plugins = enabled;
558 self
559 }
560
561 /// Max respawn+retry cycles per command when [`Self::auto_recover_plugins`] is on
562 /// (default 1). `0` disables retries even if auto-recover is enabled.
563 pub fn auto_recover_max_retries(mut self, retries: u32) -> Self {
564 self.auto_recover_max_retries = Some(retries);
565 self
566 }
567
568 /// Per-plugin timeout for the crash-resistant discovery probe used by
569 /// [`Vst3Host::discover_plugins_safe`] (default
570 /// [`DEFAULT_PROBE_TIMEOUT`](crate::discovery::DEFAULT_PROBE_TIMEOUT)). A plugin whose
571 /// probe exceeds this is killed and skipped.
572 pub fn probe_timeout(mut self, timeout: std::time::Duration) -> Self {
573 self.probe_timeout = Some(timeout);
574 self
575 }
576
577 /// Build the configured host.
578 ///
579 /// Rejects a sample rate, block size or time signature the plugin setup can't honour, using
580 /// the same rules as the corresponding runtime entry points
581 /// ([`Plugin::reconfigure`](crate::Plugin::reconfigure),
582 /// [`Plugin::set_time_signature`](crate::Plugin::set_time_signature)) — the configuration
583 /// entry points previously disagreed, so a `block_size(0)` accepted here produced permanent
584 /// silence, a `sample_rate(0.0)` reached `setupProcessing` where plugins computing
585 /// `1.0 / sampleRate` generate NaN coefficients, and a `time_signature(4, 3)` built a host
586 /// whose transport every runtime setter refuses.
587 pub fn build(self) -> Result<Vst3Host> {
588 if !self.config.sample_rate.is_finite() || self.config.sample_rate <= 0.0 {
589 return Err(Error::InvalidParameter(format!(
590 "sample rate must be finite and positive, got {}",
591 self.config.sample_rate
592 )));
593 }
594 if self.config.block_size == 0 || self.config.block_size > i32::MAX as usize {
595 return Err(Error::InvalidParameter(format!(
596 "block size must be in 1..={}, got {}",
597 i32::MAX,
598 self.config.block_size
599 )));
600 }
601 if self.config.time_sig_numerator <= 0 {
602 return Err(Error::InvalidParameter(format!(
603 "time signature numerator must be positive, got {}",
604 self.config.time_sig_numerator
605 )));
606 }
607 if !matches!(self.config.time_sig_denominator, 1 | 2 | 4 | 8 | 16) {
608 return Err(Error::InvalidParameter(format!(
609 "time signature denominator must be one of 1, 2, 4, 8, 16, got {}",
610 self.config.time_sig_denominator
611 )));
612 }
613 Ok(Vst3Host {
614 config: self.config,
615 custom_paths: self.custom_paths,
616 use_process_isolation: self.use_process_isolation,
617 scan_default_paths: self.scan_default_paths,
618 helper_path: self.helper_path,
619 response_timeout: self
620 .response_timeout
621 .unwrap_or(crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT),
622 auto_recover_plugins: self.auto_recover_plugins,
623 auto_recover_max_retries: self.auto_recover_max_retries.unwrap_or(1),
624 probe_timeout: self
625 .probe_timeout
626 .unwrap_or(crate::discovery::DEFAULT_PROBE_TIMEOUT),
627 })
628 }
629}
630
631/// The outcome of [`Vst3Host::probe_plugin`] — whether a plugin can be loaded safely.
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub enum ProbeResult {
634 /// The plugin loaded successfully in an isolated process.
635 Ok,
636 /// The plugin crashed the isolated helper while loading (do not load in-process).
637 Crashed,
638 /// The plugin did not respond within the timeout.
639 TimedOut,
640 /// Loading failed with an error (not a crash) — message included.
641 Failed(String),
642}
643
644/// Plugin discovery progress information
645#[derive(Debug, Clone)]
646pub enum DiscoveryProgress {
647 /// Discovery has started
648 Started {
649 /// Total number of plugins to scan
650 total_plugins: usize,
651 },
652 /// A plugin was found
653 Found {
654 /// The plugin information
655 plugin: PluginInfo,
656 /// Current plugin index
657 current: usize,
658 /// Total number of plugins
659 total: usize,
660 },
661 /// An error occurred while scanning a plugin
662 Error {
663 /// Path that failed
664 path: String,
665 /// Error message
666 error: String,
667 },
668 /// Discovery completed
669 Completed {
670 /// Total number of plugins found
671 total_found: usize,
672 },
673}
674
675#[cfg(feature = "cpal-backend")]
676impl Vst3Host {
677 /// Load a plugin and immediately start playing it through the default audio
678 /// output device, using the host's configured sample rate and block size.
679 ///
680 /// This is the "batteries-included" path: it wires a [`CpalBackend`] to the
681 /// plugin and pumps audio for you. The returned [`AudioHandle`] keeps the stream
682 /// alive — drop it to stop — and lets you keep sending MIDI / changing parameters
683 /// while it plays:
684 ///
685 /// ```no_run
686 /// # use vst3_host::Vst3Host;
687 /// # use vst3_host::midi::MidiChannel;
688 /// # fn main() -> vst3_host::Result<()> {
689 /// let mut host = Vst3Host::new()?;
690 /// let plugin = host.load_plugin("/path/to/synth.vst3")?;
691 /// let audio = host.play(plugin)?;
692 /// audio.lock().send_midi_note(60, 100, MidiChannel::Ch1)?;
693 /// std::thread::sleep(std::time::Duration::from_secs(1));
694 /// # Ok(())
695 /// # }
696 /// ```
697 ///
698 /// [`CpalBackend`]: crate::backends::CpalBackend
699 /// [`AudioHandle`]: crate::AudioHandle
700 pub fn play(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
701 let backend = crate::backends::CpalBackend::new()?;
702 let config = crate::audio::AudioConfig {
703 output_channels: 2,
704 input_channels: 0,
705 ..self.config
706 };
707 crate::playback::play_with_backend(&backend, plugin, config)
708 }
709
710 /// Host a plugin on **live audio input** (effect hosting): capture from the default input
711 /// device, process through the plugin, and play the result on the default output device.
712 ///
713 /// Use this for effect plugins (EQ, reverb, compressor); for instruments use
714 /// [`Self::play`]. Control the plugin via the returned [`AudioHandle`].
715 ///
716 /// [`AudioHandle`]: crate::AudioHandle
717 pub fn play_with_input(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
718 let backend = crate::backends::CpalBackend::new()?;
719 let config = crate::audio::AudioConfig {
720 input_channels: 2,
721 output_channels: 2,
722 ..self.config
723 };
724 crate::playback::play_with_input_backend(&backend, plugin, config)
725 }
726
727 /// Play a plugin through the default device using the **lock-free** real-time path
728 /// (a [`RealtimePluginRunner`]) instead of the mutex-based [`Self::play`].
729 ///
730 /// The audio callback takes no lock; queue MIDI and parameter changes through the
731 /// returned handle's [`RtControl`](crate::RtControl):
732 ///
733 /// ```no_run
734 /// # use vst3_host::{Vst3Host, midi::MidiEvent, midi::MidiChannel};
735 /// # fn main() -> vst3_host::Result<()> {
736 /// let mut host = Vst3Host::new()?;
737 /// let plugin = host.load_plugin("/path/synth.vst3")?;
738 /// let mut audio = host.play_realtime(plugin, 1024)?;
739 /// audio.control().send_midi(MidiEvent::NoteOn { channel: MidiChannel::Ch1, note: 60, velocity: 100 });
740 /// std::thread::sleep(std::time::Duration::from_secs(1));
741 /// # Ok(())
742 /// # }
743 /// ```
744 ///
745 /// [`RealtimePluginRunner`]: crate::RealtimePluginRunner
746 pub fn play_realtime(
747 &self,
748 plugin: Plugin,
749 command_capacity: usize,
750 ) -> Result<crate::playback::RtAudioHandle> {
751 let backend = crate::backends::CpalBackend::new()?;
752 let config = crate::audio::AudioConfig {
753 output_channels: 2,
754 input_channels: 0,
755 ..self.config
756 };
757 crate::playback::play_realtime_with_backend(&backend, plugin, config, command_capacity)
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn transport_defaults_to_120_bpm_4_4() {
767 let host = Vst3HostBuilder::default().build().unwrap();
768 assert_eq!(host.config().tempo, 120.0);
769 assert_eq!(host.config().time_sig_numerator, 4);
770 assert_eq!(host.config().time_sig_denominator, 4);
771 }
772
773 /// The builder used to accept any positive denominator, so a `4/3` host built fine and then
774 /// had a transport that `Plugin::set_time_signature` and `RtControl` both refuse.
775 #[test]
776 fn builder_rejects_time_signatures_the_runtime_rejects() {
777 for (num, den) in [(4, 3), (4, 0), (0, 4), (-1, 4), (4, 5), (4, 32), (4, -4)] {
778 let built = Vst3HostBuilder::default().time_signature(num, den).build();
779 assert!(
780 built.is_err(),
781 "builder accepted {num}/{den}, which every runtime setter rejects"
782 );
783 }
784 // The denominators VST3 transports actually express still build.
785 for den in [1, 2, 4, 8, 16] {
786 assert!(Vst3HostBuilder::default()
787 .time_signature(3, den)
788 .build()
789 .is_ok());
790 }
791 }
792
793 /// The isolated load path sizes an `AudioLevels` allocation from a count the helper reports,
794 /// so this boundary clamps it instead of trusting the peer.
795 #[test]
796 fn output_channel_count_from_a_peer_is_clamped() {
797 assert_eq!(clamp_output_channels(2), 2);
798 assert_eq!(clamp_output_channels(0), 2);
799 assert_eq!(clamp_output_channels(-5), 2);
800 assert_eq!(clamp_output_channels(MAX_OUTPUT_CHANNELS as i32), 256);
801 assert_eq!(clamp_output_channels(i32::MAX), MAX_OUTPUT_CHANNELS);
802 }
803
804 #[test]
805 fn builder_threads_tempo_and_time_signature_into_config() {
806 let host = Vst3HostBuilder::default()
807 .tempo(140.0)
808 .time_signature(7, 8)
809 .build()
810 .unwrap();
811 assert_eq!(host.config().tempo, 140.0);
812 assert_eq!(host.config().time_sig_numerator, 7);
813 assert_eq!(host.config().time_sig_denominator, 8);
814 }
815}