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        // Thread the configured transport into the plugin's host ProcessContext so
218        // tempo-synced DSP sees the host tempo / time signature.
219        plugin_impl.set_transport(
220            self.config.tempo,
221            self.config.time_sig_numerator,
222            self.config.time_sig_denominator,
223        );
224
225        // Get the updated info from the plugin implementation (has_gui might have been updated)
226        let updated_info = plugin_impl.info.clone();
227
228        // Size meters to the plugin's real output channel count (bus-aware), not a stereo
229        // assumption; fall back to 2 only when the plugin reports no output channels.
230        let output_channels = match plugin_impl.output_channel_count() {
231            0 => 2,
232            n => n,
233        };
234
235        let plugin = Plugin {
236            info: updated_info,
237            is_processing: false,
238            sample_rate: self.config.sample_rate,
239            block_size: self.config.block_size,
240            audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
241            parameter_change_callback: None,
242            audio_callback: None,
243            internal: Some(Box::new(plugin_impl)),
244        };
245
246        // Note: We can't track plugins in a Vec since they're not cloneable
247        // This would require a different design (e.g., using handles/IDs)
248
249        Ok(plugin)
250    }
251
252    /// Load a plugin in an isolated process
253    fn load_plugin_isolated(&mut self, path: &Path) -> Result<Plugin> {
254        use crate::process_isolation::{HostCommand, HostResponse, PluginHostProcess};
255
256        // Create and start the isolated plugin process
257        let mut process =
258            PluginHostProcess::new(self.helper_path.clone(), self.response_timeout)
259                .map_err(|e| Error::Other(format!("Failed to create isolated process: {}", e)))?;
260
261        // Load the plugin in the isolated process
262        let response = process
263            .send_command(HostCommand::LoadPlugin {
264                path: path.display().to_string(),
265                sample_rate: self.config.sample_rate,
266                block_size: self.config.block_size as u32,
267                tempo: self.config.tempo,
268                time_sig_numerator: self.config.time_sig_numerator,
269                time_sig_denominator: self.config.time_sig_denominator,
270            })
271            .map_err(|e| Error::Other(format!("Failed to load plugin in isolation: {}", e)))?;
272
273        // Verify the plugin loaded successfully. Metadata comes straight from the helper's
274        // accurate introspection, so the isolated path matches the in-process one.
275        let (loaded_info, output_channels) = match response {
276            HostResponse::PluginInfo {
277                vendor,
278                name,
279                version,
280                category,
281                uid,
282                has_gui,
283                audio_inputs,
284                audio_outputs,
285                output_channels,
286                has_midi_input,
287                has_midi_output,
288            } => {
289                let info = PluginInfo {
290                    path: path.to_path_buf(),
291                    name,
292                    vendor,
293                    version,
294                    category,
295                    uid,
296                    has_gui,
297                    audio_inputs: audio_inputs as u32,
298                    audio_outputs: audio_outputs as u32,
299                    has_midi_input,
300                    has_midi_output,
301                };
302                let channels = if output_channels > 0 {
303                    output_channels as usize
304                } else {
305                    2
306                };
307                (info, channels)
308            }
309            HostResponse::Error { message } => {
310                return Err(Error::Other(format!("Failed to load plugin: {}", message)));
311            }
312            _ => {
313                return Err(Error::Other(
314                    "Unexpected response from helper process".to_string(),
315                ));
316            }
317        };
318
319        // Create the isolated plugin implementation
320        let plugin_impl = crate::internal::isolated_plugin_impl::IsolatedPluginImpl::new(
321            process,
322            loaded_info.clone(),
323            self.config.sample_rate,
324            self.config.block_size,
325            self.config.tempo,
326            self.config.time_sig_numerator,
327            self.config.time_sig_denominator,
328            output_channels,
329            self.helper_path.clone(),
330            self.response_timeout,
331            self.auto_recover_plugins,
332            self.auto_recover_max_retries,
333        );
334
335        let plugin = Plugin {
336            info: loaded_info,
337            is_processing: false,
338            sample_rate: self.config.sample_rate,
339            block_size: self.config.block_size,
340            audio_levels: Arc::new(Mutex::new(crate::audio::AudioLevels::new(output_channels))),
341            parameter_change_callback: None,
342            audio_callback: None,
343            internal: Some(Box::new(plugin_impl)),
344        };
345
346        Ok(plugin)
347    }
348
349    /// Get audio configuration
350    pub fn config(&self) -> &AudioConfig {
351        &self.config
352    }
353}
354
355impl Default for Vst3Host {
356    fn default() -> Self {
357        Self {
358            config: AudioConfig::default(),
359            custom_paths: Vec::new(),
360            use_process_isolation: false,
361            auto_isolate_problematic: false,
362            scan_default_paths: true, // Default to true for backward compatibility
363            helper_path: None,
364            response_timeout: crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT,
365            auto_recover_plugins: false,
366            auto_recover_max_retries: 1,
367        }
368    }
369}
370
371/// Builder for VST3 host configuration
372///
373/// All fields default to their type defaults; notably `scan_default_paths` defaults to
374/// `false`, requiring explicit opt-in (unlike `Vst3Host`, which defaults it to `true`).
375#[derive(Default)]
376pub struct Vst3HostBuilder {
377    config: AudioConfig,
378    custom_paths: Vec<PathBuf>,
379    use_process_isolation: bool,
380    auto_isolate_problematic: bool,
381    scan_default_paths: bool,
382    helper_path: Option<PathBuf>,
383    response_timeout: Option<std::time::Duration>,
384    auto_recover_plugins: bool,
385    auto_recover_max_retries: Option<u32>,
386}
387
388impl Vst3HostBuilder {
389    /// Set the sample rate
390    pub fn sample_rate(mut self, rate: f64) -> Self {
391        self.config.sample_rate = rate;
392        self
393    }
394
395    /// Set the block size
396    pub fn block_size(mut self, size: usize) -> Self {
397        self.config.block_size = size;
398        self
399    }
400
401    /// Set the number of input channels
402    pub fn input_channels(mut self, channels: usize) -> Self {
403        self.config.input_channels = channels;
404        self
405    }
406
407    /// Set the number of output channels
408    pub fn output_channels(mut self, channels: usize) -> Self {
409        self.config.output_channels = channels;
410        self
411    }
412
413    /// Set the transport tempo (beats per minute) advertised to plugins in the host
414    /// `ProcessContext`. Drives tempo-synced DSP (LFOs, synced delays, arpeggiators).
415    /// Defaults to `120.0`. Non-finite or non-positive values are ignored (a tempo of 0 or
416    /// less would freeze/reverse the derived musical playhead), keeping the previous tempo.
417    pub fn tempo(mut self, bpm: f64) -> Self {
418        if bpm.is_finite() && bpm > 0.0 {
419            self.config.tempo = bpm;
420        }
421        self
422    }
423
424    /// Set the transport time signature advertised to plugins in the host
425    /// `ProcessContext` (`num`/`den`, e.g. `4, 4`). Defaults to `4/4`. Non-positive values
426    /// are ignored (a malformed time signature), keeping the previous setting.
427    pub fn time_signature(mut self, num: i32, den: i32) -> Self {
428        if num > 0 && den > 0 {
429            self.config.time_sig_numerator = num;
430            self.config.time_sig_denominator = den;
431        }
432        self
433    }
434
435    /// Enable or disable process isolation for plugin loading
436    pub fn with_process_isolation(mut self, enabled: bool) -> Self {
437        self.use_process_isolation = enabled;
438        self
439    }
440
441    /// Automatically load known crash-prone plugins (e.g. Waves/WaveShell) in an isolated
442    /// process so a crash is contained instead of taking down the host. Plugins that load
443    /// fine in-process are unaffected. Requires the `process-isolation` feature at runtime
444    /// (the helper binary must be present).
445    pub fn auto_isolate_problematic(mut self, enabled: bool) -> Self {
446        self.auto_isolate_problematic = enabled;
447        self
448    }
449
450    /// Add a custom plugin scan path
451    pub fn add_scan_path<P: AsRef<Path>>(mut self, path: P) -> Self {
452        self.custom_paths.push(path.as_ref().to_path_buf());
453        self
454    }
455
456    /// Enable scanning of default system VST3 paths
457    pub fn scan_default_paths(mut self) -> Self {
458        self.scan_default_paths = true;
459        self
460    }
461
462    /// How long to wait for an isolated helper to respond before treating the plugin as hung
463    /// (and killing the helper). Defaults to 5 seconds. Only affects process-isolated loads.
464    pub fn response_timeout(mut self, timeout: std::time::Duration) -> Self {
465        self.response_timeout = Some(timeout);
466        self
467    }
468
469    /// Override the path to the `vst3-host-helper` binary used for process isolation, instead
470    /// of the default heuristic search. The `VST3_HOST_HELPER_PATH` environment variable does
471    /// the same. Useful when the helper ships in a non-standard location.
472    pub fn helper_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
473        self.helper_path = Some(path.into());
474        self
475    }
476
477    /// Transparently respawn + reload a process-isolated plugin and retry the command when the
478    /// helper crashes or hangs, instead of surfacing `Error::PluginCrashed`/`PluginTimeout` for
479    /// the caller to handle via [`Plugin::recover`](crate::Plugin::recover).
480    ///
481    /// Only affects isolated loads and only the control plane — the audio-thread `process`
482    /// path never recovers inline (a respawn would stall the callback). **Recovery reloads the
483    /// plugin from defaults**: parameter values / state are NOT replayed, so snapshot with
484    /// `save_state`/`load_state` if you need them preserved. Off by default.
485    pub fn auto_recover_plugins(mut self, enabled: bool) -> Self {
486        self.auto_recover_plugins = enabled;
487        self
488    }
489
490    /// Max respawn+retry cycles per command when [`Self::auto_recover_plugins`] is on
491    /// (default 1). `0` disables retries even if auto-recover is enabled.
492    pub fn auto_recover_max_retries(mut self, retries: u32) -> Self {
493        self.auto_recover_max_retries = Some(retries);
494        self
495    }
496
497    /// Build the configured host.
498    pub fn build(self) -> Result<Vst3Host> {
499        Ok(Vst3Host {
500            config: self.config,
501            custom_paths: self.custom_paths,
502            use_process_isolation: self.use_process_isolation,
503            auto_isolate_problematic: self.auto_isolate_problematic,
504            scan_default_paths: self.scan_default_paths,
505            helper_path: self.helper_path,
506            response_timeout: self
507                .response_timeout
508                .unwrap_or(crate::process_isolation::DEFAULT_RESPONSE_TIMEOUT),
509            auto_recover_plugins: self.auto_recover_plugins,
510            auto_recover_max_retries: self.auto_recover_max_retries.unwrap_or(1),
511        })
512    }
513}
514
515/// The outcome of [`Vst3Host::probe_plugin`] — whether a plugin can be loaded safely.
516#[derive(Debug, Clone, PartialEq, Eq)]
517pub enum ProbeResult {
518    /// The plugin loaded successfully in an isolated process.
519    Ok,
520    /// The plugin crashed the isolated helper while loading (do not load in-process).
521    Crashed,
522    /// The plugin did not respond within the timeout.
523    TimedOut,
524    /// Loading failed with an error (not a crash) — message included.
525    Failed(String),
526}
527
528/// Plugin discovery progress information
529#[derive(Debug, Clone)]
530pub enum DiscoveryProgress {
531    /// Discovery has started
532    Started {
533        /// Total number of plugins to scan
534        total_plugins: usize,
535    },
536    /// A plugin was found
537    Found {
538        /// The plugin information
539        plugin: PluginInfo,
540        /// Current plugin index
541        current: usize,
542        /// Total number of plugins
543        total: usize,
544    },
545    /// An error occurred while scanning a plugin
546    Error {
547        /// Path that failed
548        path: String,
549        /// Error message
550        error: String,
551    },
552    /// Discovery completed
553    Completed {
554        /// Total number of plugins found
555        total_found: usize,
556    },
557}
558
559#[cfg(feature = "cpal-backend")]
560impl Vst3Host {
561    /// Load a plugin and immediately start playing it through the default audio
562    /// output device, using the host's configured sample rate and block size.
563    ///
564    /// This is the "batteries-included" path: it wires a [`CpalBackend`] to the
565    /// plugin and pumps audio for you. The returned [`AudioHandle`] keeps the stream
566    /// alive — drop it to stop — and lets you keep sending MIDI / changing parameters
567    /// while it plays:
568    ///
569    /// ```no_run
570    /// # use vst3_host::Vst3Host;
571    /// # use vst3_host::midi::MidiChannel;
572    /// # fn main() -> vst3_host::Result<()> {
573    /// let mut host = Vst3Host::new()?;
574    /// let plugin = host.load_plugin("/path/to/synth.vst3")?;
575    /// let audio = host.play(plugin)?;
576    /// audio.lock().send_midi_note(60, 100, MidiChannel::Ch1)?;
577    /// std::thread::sleep(std::time::Duration::from_secs(1));
578    /// # Ok(())
579    /// # }
580    /// ```
581    ///
582    /// [`CpalBackend`]: crate::backends::CpalBackend
583    /// [`AudioHandle`]: crate::AudioHandle
584    pub fn play(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
585        let backend = crate::backends::CpalBackend::new()?;
586        let config = crate::audio::AudioConfig {
587            output_channels: 2,
588            input_channels: 0,
589            ..self.config
590        };
591        crate::playback::play_with_backend(&backend, plugin, config)
592    }
593
594    /// Host a plugin on **live audio input** (effect hosting): capture from the default input
595    /// device, process through the plugin, and play the result on the default output device.
596    ///
597    /// Use this for effect plugins (EQ, reverb, compressor); for instruments use
598    /// [`Self::play`]. Control the plugin via the returned [`AudioHandle`].
599    ///
600    /// [`AudioHandle`]: crate::AudioHandle
601    pub fn play_with_input(&self, plugin: Plugin) -> Result<crate::AudioHandle> {
602        let backend = crate::backends::CpalBackend::new()?;
603        let config = crate::audio::AudioConfig {
604            input_channels: 2,
605            output_channels: 2,
606            ..self.config
607        };
608        crate::playback::play_with_input_backend(&backend, plugin, config)
609    }
610
611    /// Play a plugin through the default device using the **lock-free** real-time path
612    /// (a [`RealtimePluginRunner`]) instead of the mutex-based [`Self::play`].
613    ///
614    /// The audio callback takes no lock; queue MIDI and parameter changes through the
615    /// returned handle's [`RtControl`](crate::RtControl):
616    ///
617    /// ```no_run
618    /// # use vst3_host::{Vst3Host, midi::MidiEvent, midi::MidiChannel};
619    /// # fn main() -> vst3_host::Result<()> {
620    /// let mut host = Vst3Host::new()?;
621    /// let plugin = host.load_plugin("/path/synth.vst3")?;
622    /// let mut audio = host.play_realtime(plugin, 1024)?;
623    /// audio.control().send_midi(MidiEvent::NoteOn { channel: MidiChannel::Ch1, note: 60, velocity: 100 });
624    /// std::thread::sleep(std::time::Duration::from_secs(1));
625    /// # Ok(())
626    /// # }
627    /// ```
628    ///
629    /// [`RealtimePluginRunner`]: crate::RealtimePluginRunner
630    pub fn play_realtime(
631        &self,
632        plugin: Plugin,
633        command_capacity: usize,
634    ) -> Result<crate::playback::RtAudioHandle> {
635        let backend = crate::backends::CpalBackend::new()?;
636        let config = crate::audio::AudioConfig {
637            output_channels: 2,
638            input_channels: 0,
639            ..self.config
640        };
641        crate::playback::play_realtime_with_backend(&backend, plugin, config, command_capacity)
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    #[test]
650    fn transport_defaults_to_120_bpm_4_4() {
651        let host = Vst3HostBuilder::default().build().unwrap();
652        assert_eq!(host.config().tempo, 120.0);
653        assert_eq!(host.config().time_sig_numerator, 4);
654        assert_eq!(host.config().time_sig_denominator, 4);
655    }
656
657    #[test]
658    fn builder_threads_tempo_and_time_signature_into_config() {
659        let host = Vst3HostBuilder::default()
660            .tempo(140.0)
661            .time_signature(7, 8)
662            .build()
663            .unwrap();
664        assert_eq!(host.config().tempo, 140.0);
665        assert_eq!(host.config().time_sig_numerator, 7);
666        assert_eq!(host.config().time_sig_denominator, 8);
667    }
668}