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}