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