vst3_host/simple.rs
1//! Simplified API for common VST3 hosting tasks.
2//!
3//! This module provides convenience functions that make it easy to get started
4//! with VST3 plugin hosting without needing to understand all the configuration
5//! options and complex APIs.
6//!
7//! ## Quick Examples
8//!
9//! ### Load and play a plugin
10//! ```no_run
11//! use vst3_host::simple;
12//! use vst3_host::midi::MidiChannel;
13//!
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! // Load a plugin with sensible defaults
16//! let mut plugin = simple::load_plugin("/path/to/synth.vst3")?;
17//!
18//! // Start processing audio
19//! plugin.start_processing()?;
20//!
21//! // Play a note
22//! plugin.send_midi_note(60, 127, MidiChannel::Ch1)?; // Middle C
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! ### Discover plugins easily
28//! ```no_run
29//! use vst3_host::simple;
30//!
31//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! // Find all plugins on the system
33//! let plugins = simple::discover_plugins()?;
34//!
35//! for plugin in plugins {
36//! println!("Found: {} by {}", plugin.name, plugin.vendor);
37//! }
38//! # Ok(())
39//! # }
40//! ```
41
42use crate::{
43 audio::AudioBuffers,
44 error::{Error, Result},
45 host::Vst3Host,
46 midi::MidiEvent,
47 plugin::{Plugin, PluginInfo},
48};
49use std::path::Path;
50
51/// Load a VST3 plugin with sensible defaults.
52///
53/// This function creates a host with default audio settings and loads the
54/// specified plugin. It's the quickest way to get started with plugin hosting.
55///
56/// # Default Settings
57/// - Sample rate: 44100 Hz
58/// - Block size: 512 samples
59/// - Input channels: 2 (stereo)
60/// - Output channels: 2 (stereo)
61/// - Process isolation: disabled (in-process). Use [`load_plugin_isolated`] to opt in.
62///
63/// # Arguments
64/// * `path` - Path to the VST3 plugin (.vst3 file or directory)
65///
66/// # Returns
67/// A loaded and configured plugin ready for audio processing.
68///
69/// # Examples
70/// ```no_run
71/// use vst3_host::simple;
72/// use vst3_host::midi::MidiChannel;
73///
74/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
75/// let mut plugin = simple::load_plugin("/Applications/Dexed.vst3")?;
76/// plugin.start_processing()?;
77/// plugin.send_midi_note(60, 100, MidiChannel::Ch1)?;
78/// # Ok(())
79/// # }
80/// ```
81pub fn load_plugin<P: AsRef<Path>>(path: P) -> Result<Plugin> {
82 let mut host = Vst3Host::builder()
83 .sample_rate(44100.0)
84 .block_size(512)
85 .input_channels(2)
86 .output_channels(2)
87 .build()?;
88
89 host.load_plugin(path)
90}
91
92/// Load a plugin with custom audio settings.
93///
94/// This provides a middle ground between the fully automatic `load_plugin()`
95/// and the full control of the host builder pattern.
96///
97/// # Arguments
98/// * `path` - Path to the VST3 plugin
99/// * `sample_rate` - Audio sample rate in Hz (typically 44100 or 48000)
100/// * `block_size` - Audio buffer size (typically 512 or 1024)
101///
102/// # Examples
103/// ```no_run
104/// use vst3_host::simple;
105///
106/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107/// // Load with professional audio settings
108/// let mut plugin = simple::load_plugin_with_settings(
109/// "/path/to/plugin.vst3",
110/// 48000.0, // 48kHz sample rate
111/// 256 // Small buffer for low latency
112/// )?;
113/// # Ok(())
114/// # }
115/// ```
116pub fn load_plugin_with_settings<P: AsRef<Path>>(
117 path: P,
118 sample_rate: f64,
119 block_size: usize,
120) -> Result<Plugin> {
121 let mut host = Vst3Host::builder()
122 .sample_rate(sample_rate)
123 .block_size(block_size)
124 .input_channels(2)
125 .output_channels(2)
126 .build()?;
127
128 host.load_plugin(path)
129}
130
131/// Load a plugin with crash protection enabled.
132///
133/// This loads the plugin in a separate process, which prevents plugin crashes
134/// from affecting your application. Use this for untested or problematic plugins.
135///
136/// # Arguments
137/// * `path` - Path to the VST3 plugin
138///
139/// # Examples
140/// ```no_run
141/// use vst3_host::simple;
142///
143/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
144/// // Load a potentially unstable plugin safely
145/// let mut plugin = simple::load_plugin_isolated("/path/to/sketchy_plugin.vst3")?;
146/// # Ok(())
147/// # }
148/// ```
149pub fn load_plugin_isolated<P: AsRef<Path>>(path: P) -> Result<Plugin> {
150 let mut host = Vst3Host::builder()
151 .sample_rate(44100.0)
152 .block_size(512)
153 .input_channels(2)
154 .output_channels(2)
155 .with_process_isolation(true) // Force process isolation
156 .build()?;
157
158 host.load_plugin(path)
159}
160
161/// Load a plugin and immediately start playing it through the default audio device.
162///
163/// The quickest way to actually hear a synth: load, then `play`. The returned
164/// [`AudioHandle`](crate::AudioHandle) keeps audio running until dropped, and lets
165/// you control the plugin while it plays.
166///
167/// # Examples
168/// ```no_run
169/// use vst3_host::{simple, midi::MidiChannel};
170///
171/// # fn main() -> vst3_host::Result<()> {
172/// let plugin = simple::load_plugin("/path/to/synth.vst3")?;
173/// let audio = simple::play(plugin)?;
174/// audio.lock().send_midi_note(60, 100, MidiChannel::Ch1)?; // middle C
175/// std::thread::sleep(std::time::Duration::from_secs(2));
176/// # Ok(())
177/// # }
178/// ```
179#[cfg(feature = "cpal-backend")]
180pub fn play(plugin: Plugin) -> Result<crate::AudioHandle> {
181 let backend = crate::backends::CpalBackend::new()?;
182 let config = crate::audio::AudioConfig {
183 output_channels: 2,
184 input_channels: 0,
185 ..Default::default()
186 };
187 crate::playback::play_with_backend(&backend, plugin, config)
188}
189
190/// Host an effect plugin on live audio input: capture from the default input device, process
191/// through the plugin, and play the result on the default output device.
192///
193/// The instrument counterpart is [`play`]. Returns an [`AudioHandle`](crate::AudioHandle)
194/// that keeps the streams alive and lets you control the plugin; dropping it stops audio.
195#[cfg(feature = "cpal-backend")]
196pub fn play_with_input(plugin: Plugin) -> Result<crate::AudioHandle> {
197 let backend = crate::backends::CpalBackend::new()?;
198 let config = crate::audio::AudioConfig {
199 input_channels: 2,
200 output_channels: 2,
201 ..Default::default()
202 };
203 crate::playback::play_with_input_backend(&backend, plugin, config)
204}
205
206/// Discover all VST3 plugins in the standard system locations.
207///
208/// Scans the platform's standard VST3 directories and returns metadata for each
209/// plugin found. For progress reporting during a long scan, use
210/// [`Vst3Host::discover_plugins_with_callback`](crate::Vst3Host::discover_plugins_with_callback).
211///
212/// # Examples
213/// ```no_run
214/// use vst3_host::simple;
215///
216/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
217/// for info in simple::discover_plugins()? {
218/// println!("Found: {} by {}", info.name, info.vendor);
219/// }
220/// # Ok(())
221/// # }
222/// ```
223pub fn discover_plugins() -> Result<Vec<PluginInfo>> {
224 let mut host = Vst3Host::builder()
225 .scan_default_paths() // Enable scanning system directories
226 .build()?;
227
228 host.discover_plugins()
229}
230
231/// Discover plugins in a specific directory.
232///
233/// # Arguments
234/// * `path` - Directory to scan for VST3 plugins
235///
236/// # Examples
237/// ```no_run
238/// use vst3_host::simple;
239///
240/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
241/// let plugins = simple::discover_plugins_in("/my/custom/vst3/folder")?;
242/// println!("{} plugins found", plugins.len());
243/// # Ok(())
244/// # }
245/// ```
246pub fn discover_plugins_in<P: AsRef<Path>>(path: P) -> Result<Vec<PluginInfo>> {
247 let mut host = Vst3Host::builder().add_scan_path(path).build()?;
248
249 host.discover_plugins()
250}
251
252/// Read a plugin's metadata by loading it in this process.
253///
254/// # This loads the plugin
255///
256/// Despite reading like a cheap lookup, this performs a full in-process load — the plugin's
257/// own initialization code runs inside your process. A plugin that `abort()`s or makes a
258/// pure-virtual call while initializing (a licensed plugin failing its auth check, say) takes
259/// the host down with it, and no Rust `catch_unwind` can prevent that.
260///
261/// For an untrusted plugin use [`crate::discovery::probe_plugin_info_isolated`], which does the
262/// same introspection in a throwaway child process, or
263/// [`crate::discovery::discover_plugins_safe`] to scan a whole folder that way. Use this
264/// function when you already trust the plugin (or intend to load it regardless).
265///
266/// # Arguments
267/// * `path` - Path to the VST3 plugin
268///
269/// # Returns
270/// Plugin information including name, vendor, version, and capabilities.
271///
272/// # Examples
273/// ```no_run
274/// use vst3_host::simple;
275///
276/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
277/// let info = simple::get_plugin_info("/path/to/plugin.vst3")?;
278/// println!("Plugin: {} v{} by {}", info.name, info.version, info.vendor);
279/// println!("Has GUI: {}", info.has_gui);
280/// println!("Audio I/O: {} in, {} out", info.audio_inputs, info.audio_outputs);
281/// # Ok(())
282/// # }
283/// ```
284pub fn get_plugin_info<P: AsRef<Path>>(path: P) -> Result<PluginInfo> {
285 let path = path.as_ref();
286
287 if !path.exists() {
288 return Err(Error::PluginNotFound(path.display().to_string()));
289 }
290
291 // Create a minimal host just for info gathering
292 let mut host = Vst3Host::builder().build()?;
293
294 // Load plugin to get info, then immediately drop it
295 let plugin = host.load_plugin(path)?;
296 Ok(plugin.info().clone())
297}
298
299/// Check if a plugin path is valid and loadable.
300///
301/// This performs basic validation without actually loading the plugin.
302/// Useful for filtering plugin lists or validating user input.
303///
304/// # Arguments
305/// * `path` - Path to check
306///
307/// # Returns
308/// `true` if the path appears to be a valid VST3 plugin, `false` otherwise.
309///
310/// # Examples
311/// ```no_run
312/// use vst3_host::simple;
313///
314/// if simple::is_valid_plugin("/path/to/plugin.vst3") {
315/// println!("Plugin path looks valid");
316/// } else {
317/// println!("Not a valid VST3 plugin path");
318/// }
319/// ```
320pub fn is_valid_plugin<P: AsRef<Path>>(path: P) -> bool {
321 let path = path.as_ref();
322
323 // Basic checks
324 if !path.exists() {
325 return false;
326 }
327
328 // Check for .vst3 extension
329 if let Some(extension) = path.extension() {
330 if extension.to_string_lossy().to_lowercase() == "vst3" {
331 return true;
332 }
333 }
334
335 false
336}
337
338/// Upper bound on one offline render, in total `f32` samples across all output channels.
339///
340/// The `.wav` container itself tops out at a 4 GiB `u32` chunk size, so a render past this
341/// could not be written back out anyway. Bounding it here turns an allocator abort into an
342/// error a caller can handle.
343const MAX_RENDER_SAMPLES: usize = (u32::MAX / 4) as usize;
344
345/// Frame count for an offline render of `duration_secs`, rejecting the durations that would
346/// otherwise reach `Vec::with_capacity` as a request no allocator can serve.
347///
348/// `duration_secs` comes straight from a caller: `f64::INFINITY` saturates through `as usize`
349/// to `usize::MAX` (a capacity-overflow panic), `f64::NAN` casts to `0` and silently renders
350/// nothing, and any absurd-but-finite value asks for tens of gigabytes.
351fn render_frame_count(duration_secs: f64, sample_rate: f64, out_channels: usize) -> Result<usize> {
352 if !duration_secs.is_finite() || duration_secs < 0.0 {
353 return Err(Error::InvalidParameter(format!(
354 "duration must be finite and non-negative, got {duration_secs}"
355 )));
356 }
357 let frames = (duration_secs * sample_rate).round();
358 if !frames.is_finite() || frames < 0.0 {
359 return Err(Error::InvalidParameter(format!(
360 "duration {duration_secs}s at {sample_rate} Hz is not a renderable frame count"
361 )));
362 }
363 let max_frames = MAX_RENDER_SAMPLES / out_channels.max(1);
364 if frames > max_frames as f64 {
365 return Err(Error::InvalidParameter(format!(
366 "duration {duration_secs}s at {sample_rate} Hz is {frames} frames across \
367 {out_channels} channels, past the {max_frames}-frame render limit"
368 )));
369 }
370 Ok(frames as usize)
371}
372
373/// Render a plugin offline to a 32-bit float WAV file.
374///
375/// Drives `process_audio` faster-than-realtime for `duration_secs` (at the plugin's
376/// configured sample rate and block size), starting/stopping processing for you, and writes
377/// the output to `path`. Any `midi` events are sent at the start — pass a held `NoteOn` to
378/// bounce an instrument, or an empty slice for an effect (feed input via the lower-level
379/// `process_audio` loop if you need to process a signal). No audio hardware is used.
380///
381/// `duration_secs` must be finite, non-negative, and short enough that the rendered audio
382/// fits a `.wav` file; anything else is an [`Error::InvalidParameter`].
383///
384/// ```no_run
385/// use vst3_host::{simple, midi::{MidiEvent, MidiChannel}};
386/// # fn main() -> vst3_host::Result<()> {
387/// let mut plugin = simple::load_plugin("/path/synth.vst3")?;
388/// let note = MidiEvent::NoteOn { channel: MidiChannel::Ch1, note: 60, velocity: 100 };
389/// simple::render_to_wav(&mut plugin, 2.0, &[note], "out.wav")?;
390/// # Ok(())
391/// # }
392/// ```
393pub fn render_to_wav<P: AsRef<Path>>(
394 plugin: &mut Plugin,
395 duration_secs: f64,
396 midi: &[MidiEvent],
397 path: P,
398) -> Result<()> {
399 let sample_rate = plugin.sample_rate();
400 let block = plugin.block_size().max(1);
401 let out_channels = plugin.output_channel_count().max(1);
402 let total_frames = render_frame_count(duration_secs, sample_rate, out_channels)?;
403
404 plugin.start_processing()?;
405 for &event in midi {
406 plugin.send_midi_event(event)?;
407 }
408
409 let mut channels: Vec<Vec<f32>> = vec![Vec::with_capacity(total_frames); out_channels];
410 let mut rendered = 0;
411 while rendered < total_frames {
412 let frames = block.min(total_frames - rendered);
413 let mut buffers = AudioBuffers::new(0, out_channels, frames, sample_rate);
414 plugin.process_audio(&mut buffers)?;
415 for (ch, dst) in channels.iter_mut().enumerate() {
416 if let Some(src) = buffers.outputs.get(ch) {
417 dst.extend_from_slice(&src[..frames.min(src.len())]);
418 }
419 }
420 rendered += frames;
421 }
422 plugin.stop_processing()?;
423
424 crate::audio::write_wav(path, &channels, sample_rate as u32)
425}
426
427/// Offline-render a plugin to a WAV while feeding its audio input from an [`InputSource`]
428/// (a generated test signal or a loaded file) — for auditioning/regression-testing effects
429/// with a known input. Like [`render_to_wav`] but with `input_channels` filled each block,
430/// and with the same limits on `duration_secs`.
431///
432/// [`InputSource`]: crate::audio::InputSource
433pub fn render_to_wav_with_input<P: AsRef<Path>>(
434 plugin: &mut Plugin,
435 duration_secs: f64,
436 midi: &[MidiEvent],
437 source: &mut dyn crate::audio::InputSource,
438 path: P,
439) -> Result<()> {
440 let sample_rate = plugin.sample_rate();
441 let block = plugin.block_size().max(1);
442 let out_channels = plugin.output_channel_count().max(1);
443 let in_channels = plugin.info().audio_inputs.max(1) as usize;
444 let total_frames = render_frame_count(duration_secs, sample_rate, out_channels)?;
445
446 plugin.start_processing()?;
447 for &event in midi {
448 plugin.send_midi_event(event)?;
449 }
450
451 let mut channels: Vec<Vec<f32>> = vec![Vec::with_capacity(total_frames); out_channels];
452 let mut rendered = 0;
453 while rendered < total_frames {
454 let frames = block.min(total_frames - rendered);
455 let mut buffers = AudioBuffers::new(in_channels, out_channels, frames, sample_rate);
456 source.fill(&mut buffers.inputs, frames, sample_rate);
457 plugin.process_audio(&mut buffers)?;
458 for (ch, dst) in channels.iter_mut().enumerate() {
459 if let Some(src) = buffers.outputs.get(ch) {
460 dst.extend_from_slice(&src[..frames.min(src.len())]);
461 }
462 }
463 rendered += frames;
464 }
465 plugin.stop_processing()?;
466
467 crate::audio::write_wav(path, &channels, sample_rate as u32)
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_is_valid_plugin() {
476 // Test non-existent path
477 assert!(!is_valid_plugin("/nonexistent/path.vst3"));
478
479 // Test wrong extension
480 assert!(!is_valid_plugin("plugin.dll"));
481 assert!(!is_valid_plugin("plugin.so"));
482
483 // Would need actual plugin files to test positive cases
484 }
485
486 /// `duration_secs` reaches `Vec::with_capacity` through an `as usize` cast that saturates:
487 /// `INFINITY` became `usize::MAX` (a capacity-overflow panic), `NAN` became `0` (a silent
488 /// empty render), and any absurd finite value asked the allocator for tens of gigabytes.
489 #[test]
490 fn render_frame_count_rejects_unrenderable_durations() {
491 for bad in [
492 f64::INFINITY,
493 f64::NEG_INFINITY,
494 f64::NAN,
495 -1.0,
496 1.0e12,
497 f64::MAX,
498 ] {
499 assert!(
500 render_frame_count(bad, 44_100.0, 2).is_err(),
501 "duration {bad} should be rejected"
502 );
503 }
504 }
505
506 #[test]
507 fn render_frame_count_accepts_ordinary_durations() {
508 assert_eq!(render_frame_count(0.0, 44_100.0, 2).unwrap(), 0);
509 assert_eq!(render_frame_count(2.0, 44_100.0, 2).unwrap(), 88_200);
510 assert_eq!(render_frame_count(0.5, 48_000.0, 6).unwrap(), 24_000);
511 // Right at the limit: the largest render that still fits a .wav.
512 let max_frames = MAX_RENDER_SAMPLES / 2;
513 let secs = max_frames as f64 / 44_100.0;
514 assert!(render_frame_count(secs, 44_100.0, 2).is_ok());
515 }
516
517 #[test]
518 fn test_host_creation() {
519 // Test that we can create hosts with different configurations
520 let host1 = Vst3Host::builder()
521 .sample_rate(44100.0)
522 .block_size(512)
523 .build();
524 assert!(host1.is_ok());
525
526 let host2 = Vst3Host::builder()
527 .sample_rate(48000.0)
528 .block_size(256)
529 .with_process_isolation(true)
530 .build();
531 assert!(host2.is_ok());
532 }
533}