Skip to main content

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}