Skip to main content

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}