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