vst3-host 0.9.0

Safe, simple VST3 plugin hosting with audio playback, MIDI, and crash protection
Documentation
# vst3-host

[![Crates.io](https://img.shields.io/crates/v/vst3-host.svg)](https://crates.io/crates/vst3-host)
[![Docs.rs](https://docs.rs/vst3-host/badge.svg)](https://docs.rs/vst3-host)
[![License](https://img.shields.io/crates/l/vst3-host.svg)](#license)

A safe Rust library for hosting VST3 plugins: discover them, load them, play audio through
them, control parameters, send MIDI, and isolate crashes — without writing any `unsafe`
code yourself. All VST3 COM interaction is contained behind a safe API.

```rust
use vst3_host::{simple, midi::MidiChannel};

fn main() -> vst3_host::Result<()> {
    // Load a synth and start it playing through the default audio device.
    let plugin = simple::load_plugin("/Library/Audio/Plug-Ins/VST3/Dexed.vst3")?;
    let audio = simple::play(plugin)?;

    // Play middle C (MIDI note 60) for one second.
    audio.lock().send_midi_note(60, 100, MidiChannel::Ch1)?;
    std::thread::sleep(std::time::Duration::from_secs(1));
    Ok(())
}
```

## Features

- **Discovery** — scan standard locations; read plugin metadata; export a full report as JSON
  (`PluginReport`).
- **Audio** — a bundled CPAL backend drives a plugin to the default output (or bring your own
  backend), with two paths: the easy mutex-based `play` and a lock-free `play_realtime`
  (`RealtimePluginRunner`) that takes no lock on the audio thread.
- **Parameters** — list, read, set (applied to the audio processor), and format them as the
  plugin itself displays them.
- **MIDI** — send notes, CC, pitch bend, aftertouch; capture MIDI the plugin emits
  (`take_output_midi`). Per-note expression / MPE via `note_on`/`send_note_expression`
  (`NoteId`, `NoteExpressionType`), in-process or isolated.
- **State** — save/restore a plugin's state (`save_state`/`load_state`), in-process or isolated.
- **Crash isolation** — optionally run a plugin in a separate process (`process-isolation`),
  with typed `Error::PluginCrashed` + `Plugin::recover()`.
- **Native editors** — open a plugin's own GUI in a standalone window, or embed it in your
  egui app (`EmbeddedEditor`, macOS). Windows/Linux window code compiles but isn't yet
  runtime-verified.

## Examples

**Discover and inspect installed plugins.** `discover_plugins` scans the standard VST3
locations and returns metadata without loading any DSP:

```rust
use vst3_host::simple;

fn main() -> vst3_host::Result<()> {
    for info in simple::discover_plugins()? {
        println!(
            "{} v{} — {} | {} audio out | MIDI in: {} | GUI: {}",
            info.name, info.version, info.category,
            info.audio_outputs, info.has_midi_input, info.has_gui,
        );
    }
    Ok(())
}
```

**Read, format, and set parameters.** Parameters are normalized to `0.0..=1.0`;
`format_parameter` renders the plugin's own display string (e.g. `"8.2 kHz"`):

```rust
use vst3_host::simple;

fn main() -> vst3_host::Result<()> {
    let mut plugin = simple::load_plugin("/Library/Audio/Plug-Ins/VST3/Dexed.vst3")?;

    for p in plugin.get_parameters()?.iter().take(5) {
        println!("{:>4}  {:<24} {}", p.id, p.name, plugin.format_parameter(p.id, p.value)?);
    }

    // Set the first parameter to 75% and read it back.
    let id = plugin.get_parameters()?[0].id;
    plugin.set_parameter(id, 0.75)?;
    assert_eq!(plugin.get_parameter(id)?, 0.75);
    Ok(())
}
```

**Save and restore a plugin's state** (its own serialized preset/patch blob):

```rust
use vst3_host::simple;

fn main() -> vst3_host::Result<()> {
    let mut plugin = simple::load_plugin("/path/synth.vst3")?;

    let snapshot: Vec<u8> = plugin.save_state()?;   // serialize current state
    plugin.set_parameter(0, 0.1)?;                  // change something...
    plugin.load_state(&snapshot)?;                  // ...and restore it exactly
    Ok(())
}
```

**Contain crashes with process isolation.** Run the plugin in a child process; a crash
surfaces as `Error::PluginCrashed` instead of killing your app, and `recover()` respawns
and reloads it:

```rust
use vst3_host::{Vst3Host, Error, midi::MidiChannel};

fn main() -> vst3_host::Result<()> {
    let mut host = Vst3Host::builder().with_process_isolation(true).build()?;
    let mut plugin = host.load_plugin("/path/sketchy.vst3")?;

    if let Err(Error::PluginCrashed) = plugin.send_midi_note(60, 100, MidiChannel::Ch1) {
        plugin.recover()?; // respawn helper + reload + restart processing
    }
    Ok(())
}
```

**Real-time, lock-free playback.** `play_realtime` hands the plugin to the audio thread and
sends control over a lock-free SPSC ring, so the callback never blocks on your thread:

```rust
use vst3_host::{Vst3Host, midi::{MidiEvent, MidiChannel}};

fn main() -> vst3_host::Result<()> {
    let mut host = Vst3Host::new()?;
    let plugin = host.load_plugin("/path/synth.vst3")?;

    let mut audio = host.play_realtime(plugin, 1024)?; // 1024 = command-queue capacity
    audio.control().send_midi(MidiEvent::NoteOn {
        channel: MidiChannel::Ch1,
        note: 60,
        velocity: 100,
    });
    std::thread::sleep(std::time::Duration::from_secs(1));
    Ok(())
}
```

### Putting it all together

A complete offline workflow: configure an isolated host, discover an instrument, tweak a
parameter, snapshot its state, render a held note while measuring the peak, recover if the
plugin's process crashes, capture any emitted MIDI, then restore the original state.

```rust
use vst3_host::{audio::AudioBuffers, midi::MidiChannel, Error, Vst3Host};

fn main() -> vst3_host::Result<()> {
    // 1. Configure the host: 48 kHz, 512-sample blocks, and run the plugin in its own
    //    process so a crash can't take the whole app down.
    let mut host = Vst3Host::builder()
        .sample_rate(48_000.0)
        .block_size(512)
        .with_process_isolation(true)
        .build()?;

    // 2. Discover what's installed and pick the first instrument.
    let info = host
        .discover_plugins()?
        .into_iter()
        .find(|p| p.has_midi_input && p.audio_outputs > 0)
        .ok_or_else(|| Error::Other("no instrument found".into()))?;
    println!("Loading {} v{} by {}", info.name, info.version, info.vendor);

    // 3. Load it (in the isolated helper process).
    let mut plugin = host.load_plugin(&info.path)?;

    // 4. Tweak the first automatable parameter, shown the way the plugin displays it.
    if let Some(p) = plugin.get_parameters()?.into_iter().find(|p| p.can_automate) {
        plugin.set_parameter(p.id, 0.6)?;
        println!("set {} -> {}", p.name, plugin.format_parameter(p.id, 0.6)?);
    }

    // 5. Snapshot the state so we can restore it later.
    let preset = plugin.save_state()?;

    // 6. Render one second of a held note offline, measuring the peak. If the plugin
    //    crashes its process, recover and reload instead of crashing ourselves.
    plugin.start_processing()?;
    plugin.send_midi_note(60, 100, MidiChannel::Ch1)?;

    let mut buffer = AudioBuffers::new(0, 2, 512, 48_000.0);
    let mut peak = 0.0f32;
    for _ in 0..(48_000 / 512) {
        match plugin.process_audio(&mut buffer) {
            Ok(()) => {
                for ch in &buffer.outputs {
                    peak = peak.max(ch.iter().fold(0.0, |m, &s| m.max(s.abs())));
                }
            }
            Err(Error::PluginCrashed) => {
                eprintln!("plugin crashed - recovering");
                plugin.recover()?;
                plugin.load_state(&preset)?;
                break;
            }
            Err(e) => return Err(e),
        }
    }
    plugin.stop_processing()?;
    println!("rendered peak amplitude: {peak:.3}");

    // 7. Capture any MIDI the plugin emitted (arpeggiators, MPE, ...).
    let emitted = plugin.take_output_midi();
    println!("plugin emitted {} MIDI event(s)", emitted.len());

    // 8. Restore the original state.
    plugin.load_state(&preset)?;
    Ok(())
}
```

## Feature flags

| Flag | Default | Enables |
| --- | --- | --- |
| `cpal-backend` | yes | The bundled CPAL backend and `simple::play` / `Vst3Host::play`. |
| `process-isolation` | yes | Out-of-process hosting + the `vst3-host-helper` binary. |
| `egui-widgets` | no | `EmbeddedEditor` — embed a plugin editor in an egui/eframe window (macOS). |

```toml
[dependencies]
vst3-host = "0.4"
```

## Status & known limitations

The core is working and exercised against real plugins on macOS. What to be aware of:

- The default `play` audio path is correctness-first (it locks on the audio callback). For a
  lock-free path use `play_realtime` / `RealtimePluginRunner`; even that isn't a fully
  RT-audited (zero-allocation) engine yet.
- Process isolation is opt-in (`Vst3Host::builder().with_process_isolation(true)`); crashes
  surface as `Error::PluginCrashed` and `Plugin::recover()` reloads. The editor opens across
  the boundary on macOS (helper-owned window); Windows/Linux not yet.
- `MidiEvent::ProgramChange` is unsupported (VST3 routes programs through `IUnitInfo`).
- Windows/Linux build and test in CI but aren't interactively exercised (no plugin run or
  editor opened) — macOS is the exercised platform.

## Building from source

No VST3 SDK or extra setup is required. The `vst3` dependency (0.3) ships pre-generated
bindings, so building is just:

```bash
cargo build --release
```

The only build-time native dependency is `libclang`, used by `cpal`'s `coreaudio-sys`
(macOS) and `alsa-sys` (Linux); on Linux you also need the ALSA and libxcb dev headers
(`libasound2-dev`, `libxcb1-dev`, `libxcb-util-dev`).

## Documentation

Full guides (tutorials, how-to, reference, explanation) are in the
[`docs/`](https://github.com/HelgeSverre/rust-vst3-host/tree/main/docs) directory of the
repository. API reference is on [docs.rs](https://docs.rs/vst3-host).

## License

MIT — see [LICENSE](LICENSE).