vst3_host/plugin.rs
1//! VST3 plugin wrapper with safe API
2
3use crate::{
4 audio::{AudioBuffers, AudioLevels},
5 error::{Error, Result},
6 midi::{MidiChannel, MidiEvent},
7 parameters::{Parameter, ParameterUpdate},
8};
9use std::sync::{Arc, Mutex};
10
11/// Information about a VST3 plugin
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct PluginInfo {
14 /// Full path to the VST3 bundle/file
15 pub path: std::path::PathBuf,
16 /// Plugin name
17 pub name: String,
18 /// Vendor/manufacturer name
19 pub vendor: String,
20 /// Plugin version
21 pub version: String,
22 /// Plugin category (e.g., "Fx", "Instrument")
23 pub category: String,
24 /// Unique plugin ID
25 pub uid: String,
26 /// Number of audio input buses
27 pub audio_inputs: u32,
28 /// Number of audio output buses
29 pub audio_outputs: u32,
30 /// Whether the plugin accepts MIDI input
31 pub has_midi_input: bool,
32 /// Whether the plugin produces MIDI output
33 pub has_midi_output: bool,
34 /// Whether the plugin has a GUI
35 pub has_gui: bool,
36}
37
38/// A saved plugin preset: the plugin's identity plus its opaque state blob.
39///
40/// Written/read by [`Plugin::save_preset`] / [`Plugin::load_preset`]. The `uid` lets a
41/// loader reject a preset that belongs to a different plugin (whose state bytes would be
42/// meaningless or harmful).
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct PluginPreset {
45 /// The originating plugin's unique class id ([`PluginInfo::uid`]).
46 pub uid: String,
47 /// The originating plugin's display name (for friendly mismatch messages).
48 pub plugin_name: String,
49 /// The plugin's opaque serialized state (from [`Plugin::save_state`]).
50 pub state: Vec<u8>,
51}
52
53/// A plugin unit (from `IUnitInfo`) and its program list, if any.
54///
55/// Units form a hierarchy (via [`parent_id`](Self::parent_id)); a unit may carry a named
56/// program list (e.g. a synth's factory patches). Query with [`Plugin::get_units`].
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct PluginUnit {
59 /// Unit id (unique within the plugin; the root unit is conventionally `0`).
60 pub id: i32,
61 /// Parent unit id, or `-1` for the root.
62 pub parent_id: i32,
63 /// Unit display name.
64 pub name: String,
65 /// Program names in this unit's program list (empty if the unit has none).
66 pub programs: Vec<String>,
67}
68
69/// How the plugin should run: real-time (live playback) or offline (faster-than-real-time
70/// bounce/render). Maps to VST3 `kRealtime` / `kOffline`; plugins may switch quality or
71/// look-ahead accordingly. Defaults to [`ProcessMode::Realtime`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
73pub enum ProcessMode {
74 /// Real-time / live processing (the default; `kRealtime`).
75 #[default]
76 Realtime,
77 /// Offline / non-real-time processing such as a render or bounce (`kOffline`).
78 Offline,
79}
80
81/// VST3 plugin instance
82#[allow(clippy::type_complexity)] // callback fields are Box<dyn Fn...>; intrinsic to the API
83pub struct Plugin {
84 // Internal state is hidden from public API
85 pub(crate) info: PluginInfo,
86 pub(crate) is_processing: bool,
87 /// Configured sample rate (exposed via [`Plugin::sample_rate`]).
88 pub(crate) sample_rate: f64,
89 /// Configured max block size (exposed via [`Plugin::block_size`]).
90 pub(crate) block_size: usize,
91 pub(crate) audio_levels: Arc<Mutex<AudioLevels>>,
92 pub(crate) parameter_change_callback: Option<Box<dyn Fn(u32, f64) + Send + 'static>>,
93 pub(crate) audio_callback: Option<Box<dyn Fn(&AudioLevels) + Send + 'static>>,
94
95 // These will be populated by the actual implementation
96 pub(crate) internal: Option<Box<dyn PluginInternal>>,
97}
98
99// Internal trait for hiding implementation details
100pub(crate) trait PluginInternal: Send {
101 fn set_parameter(&mut self, id: u32, value: f64) -> Result<()>;
102 /// Schedule a parameter change at a sample offset within the next process block.
103 /// Defaults to a block-start change (ignores the offset) for implementations that don't
104 /// support sample-accurate scheduling (e.g. process isolation).
105 fn set_parameter_at(&mut self, id: u32, value: f64, _sample_offset: i32) -> Result<()> {
106 self.set_parameter(id, value)
107 }
108 fn get_parameter(&self, id: u32) -> Result<f64>;
109 fn get_all_parameters(&self) -> Result<Vec<Parameter>>;
110 fn format_parameter(&self, id: u32, normalized: f64) -> Result<String>;
111 fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()>;
112 /// Re-run `setupProcessing` for a new sample rate / block size. Defaults to unsupported
113 /// (e.g. process isolation, where reconfigure isn't marshalled across the boundary).
114 fn reconfigure(&mut self, _sample_rate: f64, _block_size: usize) -> Result<()> {
115 Err(Error::Other(
116 "runtime reconfigure is not supported for this plugin".to_string(),
117 ))
118 }
119 /// Switch the plugin's process mode (real-time vs offline), re-running `setupProcessing`.
120 /// Defaults to unsupported (e.g. process isolation, where it isn't marshalled).
121 fn set_process_mode(&mut self, _mode: crate::plugin::ProcessMode) -> Result<()> {
122 Err(Error::Other(
123 "process mode switching is not supported for this plugin".to_string(),
124 ))
125 }
126 /// Query each audio bus's current speaker arrangement. Defaults to unsupported.
127 fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
128 Err(Error::Other(
129 "bus arrangement query is not supported for this plugin".to_string(),
130 ))
131 }
132 /// Request specific speaker arrangements for the audio buses (re-runs `setupProcessing`).
133 /// Defaults to unsupported (e.g. process isolation, where it isn't marshalled).
134 fn set_bus_arrangements(
135 &mut self,
136 _inputs: &[crate::audio::SpeakerArrangement],
137 _outputs: &[crate::audio::SpeakerArrangement],
138 ) -> Result<()> {
139 Err(Error::Other(
140 "bus arrangement negotiation is not supported for this plugin".to_string(),
141 ))
142 }
143 fn send_midi_event(&mut self, event: MidiEvent) -> Result<()>;
144 /// Schedule a MIDI event at a sample offset within the next process block.
145 /// Defaults to a block-start event (ignores the offset) for implementations that don't
146 /// support sample-accurate scheduling (e.g. process isolation).
147 fn send_midi_event_at(&mut self, event: MidiEvent, _sample_offset: i32) -> Result<()> {
148 self.send_midi_event(event)
149 }
150 fn start_processing(&mut self) -> Result<()>;
151 fn stop_processing(&mut self) -> Result<()>;
152 fn has_editor(&self) -> bool;
153 fn open_editor(&mut self, parent: *mut std::ffi::c_void) -> Result<()>;
154 fn close_editor(&mut self) -> Result<()>;
155 fn get_editor_size(&self) -> Result<(i32, i32)>;
156 fn get_parameter_changes(&self) -> Vec<(u32, f64)>;
157 /// Take the MIDI events the plugin has emitted since the last call. Defaults to empty
158 /// for implementations that don't capture output MIDI (e.g. process isolation).
159 fn take_output_events(&self) -> Vec<MidiEvent> {
160 Vec::new()
161 }
162 /// Enumerate the plugin's units and their program lists (`IUnitInfo`). Defaults to empty
163 /// for implementations that don't query it yet (e.g. process isolation).
164 fn get_units(&self) -> Result<Vec<PluginUnit>> {
165 Ok(Vec::new())
166 }
167 /// Processing latency in samples (`IAudioProcessor::getLatencySamples`). Defaults to 0.
168 fn latency_samples(&self) -> u32 {
169 0
170 }
171 /// Tail length in samples (`IAudioProcessor::getTailSamples`). Defaults to 0.
172 fn tail_samples(&self) -> u32 {
173 0
174 }
175 /// Resolve a MIDI controller `(bus, channel, cc)` to a parameter id via `IMidiMapping`.
176 /// Defaults to `None` (plugin doesn't implement the interface, or no mapping / isolation).
177 fn midi_cc_to_parameter(&self, _bus: i32, _channel: i16, _cc: u16) -> Option<u32> {
178 None
179 }
180 /// Serialize the plugin's current state to an opaque byte blob.
181 fn save_state(&self) -> Result<Vec<u8>> {
182 Err(Error::Other(
183 "state save/restore is not supported".to_string(),
184 ))
185 }
186 /// Restore the plugin's state from a blob previously returned by [`Self::save_state`].
187 fn load_state(&mut self, _data: &[u8]) -> Result<()> {
188 Err(Error::Other(
189 "state save/restore is not supported".to_string(),
190 ))
191 }
192 /// OS process id of the isolated helper, if this plugin runs out-of-process.
193 fn helper_pid(&self) -> Option<u32> {
194 None
195 }
196 /// Number of times this plugin has been recovered (respawned + reloaded). Defaults to 0
197 /// for non-isolated plugins.
198 fn recovery_count(&self) -> u64 {
199 0
200 }
201 /// Recover from a crashed isolated helper by respawning and reloading. Only meaningful
202 /// for process-isolated plugins.
203 fn recover(&mut self) -> Result<()> {
204 Err(Error::Other(
205 "recovery is only supported for process-isolated plugins".to_string(),
206 ))
207 }
208 /// The size the plugin's editor has requested (via `IPlugFrame`) since the last poll.
209 fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
210 None
211 }
212 /// Total output audio channels across the plugin's output buses. Defaults to 2.
213 fn output_channel_count(&self) -> usize {
214 2
215 }
216}
217
218impl Plugin {
219 /// Get plugin information
220 pub fn info(&self) -> &PluginInfo {
221 &self.info
222 }
223
224 /// The sample rate (Hz) this plugin was configured with at load.
225 pub fn sample_rate(&self) -> f64 {
226 self.sample_rate
227 }
228
229 /// The maximum block size (frames per `process_audio` call) configured at load.
230 pub fn block_size(&self) -> usize {
231 self.block_size
232 }
233
234 /// Reconfigure the plugin for a new sample rate and/or maximum block size, re-running the
235 /// plugin's `setupProcessing` and rebuilding its audio buffers.
236 ///
237 /// Use this when the audio device's sample rate changes mid-session instead of reloading.
238 /// The plugin must **not** be processing: call [`Self::stop_processing`] first, reconfigure,
239 /// then [`Self::start_processing`] again. Returns an error if called while processing, on an
240 /// invalid sample rate / zero block size, or under process isolation (not yet marshalled).
241 pub fn reconfigure(&mut self, sample_rate: f64, block_size: usize) -> Result<()> {
242 if self.is_processing {
243 return Err(Error::Other(
244 "cannot reconfigure while processing; call stop_processing() first".to_string(),
245 ));
246 }
247 if !(sample_rate.is_finite() && sample_rate > 0.0) {
248 return Err(Error::InvalidParameter(format!(
249 "sample rate must be finite and positive, got {sample_rate}"
250 )));
251 }
252 if block_size == 0 {
253 return Err(Error::InvalidParameter(
254 "block size must be greater than 0".to_string(),
255 ));
256 }
257
258 self.internal
259 .as_mut()
260 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
261 .reconfigure(sample_rate, block_size)?;
262
263 self.sample_rate = sample_rate;
264 self.block_size = block_size;
265 Ok(())
266 }
267
268 /// Switch the plugin between real-time and offline processing, re-running the plugin's
269 /// `setupProcessing` so it can adjust quality / look-ahead for a faster-than-real-time
270 /// bounce.
271 ///
272 /// Like [`Self::reconfigure`], the plugin must **not** be processing: call
273 /// [`Self::stop_processing`] first. Returns an error if called while processing, or under
274 /// process isolation (not marshalled across the boundary).
275 pub fn set_process_mode(&mut self, mode: ProcessMode) -> Result<()> {
276 if self.is_processing {
277 return Err(Error::Other(
278 "cannot set process mode while processing; call stop_processing() first"
279 .to_string(),
280 ));
281 }
282 self.internal
283 .as_mut()
284 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
285 .set_process_mode(mode)
286 }
287
288 /// Query the current speaker arrangement of each audio input/output bus.
289 pub fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
290 self.internal
291 .as_ref()
292 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
293 .bus_arrangements()
294 }
295
296 /// Request specific speaker arrangements for the audio buses (e.g. force stereo, or a
297 /// surround layout). The slices give one [`SpeakerArrangement`](crate::audio::SpeakerArrangement)
298 /// per input bus and per output bus, in bus-index order.
299 ///
300 /// Re-runs the plugin's `setupProcessing`, so the plugin must **not** be processing (call
301 /// [`Self::stop_processing`] first). A plugin may decline a requested layout and keep its
302 /// own; re-query with [`Self::bus_arrangements`] to see what was actually applied. Errors
303 /// while processing or under process isolation (not marshalled).
304 pub fn set_bus_arrangements(
305 &mut self,
306 inputs: &[crate::audio::SpeakerArrangement],
307 outputs: &[crate::audio::SpeakerArrangement],
308 ) -> Result<()> {
309 if self.is_processing {
310 return Err(Error::Other(
311 "cannot set bus arrangements while processing; call stop_processing() first"
312 .to_string(),
313 ));
314 }
315 self.internal
316 .as_mut()
317 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
318 .set_bus_arrangements(inputs, outputs)
319 }
320
321 /// Get all parameters
322 pub fn get_parameters(&self) -> Result<Vec<Parameter>> {
323 self.internal
324 .as_ref()
325 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
326 .get_all_parameters()
327 }
328
329 /// Set a parameter value by ID
330 pub fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
331 if !(0.0..=1.0).contains(&value) {
332 return Err(Error::InvalidParameter(format!(
333 "Value {} is out of range [0.0, 1.0]",
334 value
335 )));
336 }
337
338 self.internal
339 .as_mut()
340 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
341 .set_parameter(id, value)?;
342
343 // Trigger callback if set
344 if let Some(ref callback) = self.parameter_change_callback {
345 callback(id, value);
346 }
347
348 Ok(())
349 }
350
351 /// Set a parameter value at a specific sample offset within the next process block.
352 ///
353 /// This is the sample-accurate building block for automation: call it once per
354 /// sub-block point (e.g. from [`ParameterAutomation::points_for_block`]) and the plugin
355 /// receives the changes at their offsets in the next `process_audio`. Like
356 /// [`Self::set_parameter`], `value` is normalized `0.0..=1.0`.
357 ///
358 /// `sample_offset` is clamped to the block. Under process isolation the offset **is** now
359 /// carried across the boundary and applied by the helper's in-process plugin.
360 ///
361 /// [`ParameterAutomation::points_for_block`]: crate::parameters::ParameterAutomation::points_for_block
362 pub fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
363 if !(0.0..=1.0).contains(&value) {
364 return Err(Error::InvalidParameter(format!(
365 "Value {} is out of range [0.0, 1.0]",
366 value
367 )));
368 }
369 self.internal
370 .as_mut()
371 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
372 .set_parameter_at(id, value, sample_offset)
373 }
374
375 /// Enumerate the plugin's units and their program lists (`IUnitInfo`).
376 ///
377 /// Returns an empty list for plugins that don't implement `IUnitInfo`, and (for now) for
378 /// plugins running under process isolation. The root unit (id `0`) is typically present.
379 pub fn get_units(&self) -> Result<Vec<PluginUnit>> {
380 self.internal
381 .as_ref()
382 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
383 .get_units()
384 }
385
386 /// The plugin's reported processing latency in samples (e.g. from look-ahead or
387 /// oversampling), via `IAudioProcessor::getLatencySamples`. Use it to delay-compensate
388 /// when aligning the plugin's output with other signals. `0` if it reports none, or for
389 /// plugins running under process isolation (not bridged).
390 pub fn latency_samples(&self) -> u32 {
391 self.internal
392 .as_ref()
393 .map(|i| i.latency_samples())
394 .unwrap_or(0)
395 }
396
397 /// The plugin's reported tail length in samples (how long it keeps producing output
398 /// after input stops — e.g. reverb/delay), via `IAudioProcessor::getTailSamples`. `0`
399 /// means no tail; `u32::MAX` means an infinite tail. `0` for isolated plugins (not
400 /// bridged).
401 pub fn tail_samples(&self) -> u32 {
402 self.internal
403 .as_ref()
404 .map(|i| i.tail_samples())
405 .unwrap_or(0)
406 }
407
408 /// Resolve a MIDI controller to the parameter it's mapped to, via the plugin's
409 /// `IMidiMapping` (`getMidiControllerAssignment`).
410 ///
411 /// `bus` is the event input bus index (usually `0`), `channel` the 0-based MIDI channel,
412 /// and `cc` the MIDI controller number (`0–127`, or the VST3 specials such as `128`
413 /// aftertouch / `129` pitch-bend). Returns the parameter id the controller drives, or
414 /// `None` if the plugin doesn't implement `IMidiMapping`, the controller is unmapped, or
415 /// the plugin is process-isolated (not bridged).
416 pub fn midi_cc_to_parameter(&self, bus: i32, channel: i16, cc: u16) -> Option<u32> {
417 // VST3 controller numbers are 0..130 (0–127 MIDI CCs + the specials up to pitch-bend).
418 // Reject out-of-range values rather than forwarding a meaningless controller number.
419 if cc > 129 {
420 return None;
421 }
422 self.internal
423 .as_ref()?
424 .midi_cc_to_parameter(bus, channel, cc)
425 }
426
427 /// Get a parameter value by ID
428 pub fn get_parameter(&self, id: u32) -> Result<f64> {
429 self.internal
430 .as_ref()
431 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
432 .get_parameter(id)
433 }
434
435 /// Format a parameter value as the plugin itself would display it.
436 ///
437 /// VST3 keeps all parameter values normalized (0.0–1.0) and delegates
438 /// human-readable formatting to the plugin's controller. This asks the plugin to
439 /// render `normalized` for parameter `id`, returning exactly what its own UI would
440 /// show — e.g. `"440.00 Hz"`, `"-6.0 dB"`, `"Sine"`. Prefer this over
441 /// [`Parameter::format_value`], which can only approximate without the plugin's
442 /// internal mapping.
443 pub fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
444 self.internal
445 .as_ref()
446 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
447 .format_parameter(id, normalized)
448 }
449
450 /// Set a parameter by name
451 pub fn set_parameter_by_name(&mut self, name: &str, value: f64) -> Result<()> {
452 let params = self.get_parameters()?;
453 let param = params
454 .iter()
455 .find(|p| p.name == name)
456 .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))?;
457
458 self.set_parameter(param.id, value)
459 }
460
461 /// Find a parameter by name
462 pub fn find_parameter(&self, name: &str) -> Result<Parameter> {
463 let params = self.get_parameters()?;
464 params
465 .into_iter()
466 .find(|p| p.name == name)
467 .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))
468 }
469
470 /// Send a MIDI note on event
471 pub fn send_midi_note(&mut self, note: u8, velocity: u8, channel: MidiChannel) -> Result<()> {
472 if note > 127 {
473 return Err(Error::MidiError(format!("Invalid note number: {}", note)));
474 }
475 if velocity > 127 {
476 return Err(Error::MidiError(format!("Invalid velocity: {}", velocity)));
477 }
478
479 let event = MidiEvent::NoteOn {
480 channel,
481 note,
482 velocity,
483 };
484 self.send_midi_event(event)
485 }
486
487 /// Send a MIDI note off event
488 pub fn send_midi_note_off(&mut self, note: u8, channel: MidiChannel) -> Result<()> {
489 if note > 127 {
490 return Err(Error::MidiError(format!("Invalid note number: {}", note)));
491 }
492
493 let event = MidiEvent::NoteOff {
494 channel,
495 note,
496 velocity: 0,
497 };
498 self.send_midi_event(event)
499 }
500
501 /// Send a MIDI control change event
502 pub fn send_midi_cc(&mut self, controller: u8, value: u8, channel: MidiChannel) -> Result<()> {
503 if controller > 127 {
504 return Err(Error::MidiError(format!(
505 "Invalid controller number: {}",
506 controller
507 )));
508 }
509 if value > 127 {
510 return Err(Error::MidiError(format!("Invalid CC value: {}", value)));
511 }
512
513 let event = MidiEvent::ControlChange {
514 channel,
515 controller,
516 value,
517 };
518 self.send_midi_event(event)
519 }
520
521 /// Send a generic MIDI event
522 pub fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
523 self.internal
524 .as_mut()
525 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
526 .send_midi_event(event)
527 }
528
529 /// Schedule a MIDI event at a sample offset within the **next** [`process_audio`] block.
530 ///
531 /// Use this for sample-accurate sequencing: an event sent with `sample_offset = N` takes
532 /// effect `N` frames into the next processed block, rather than at its start. Keep the
533 /// offset within the upcoming block's frame count ([`Plugin::block_size`] is the maximum);
534 /// a negative offset is treated as 0, and an offset past the block end is plugin-defined.
535 ///
536 /// Under process isolation the offset is not marshalled across the boundary — the event is
537 /// delivered at block start (offset 0), same as [`Self::send_midi_event`].
538 ///
539 /// [`process_audio`]: Self::process_audio
540 pub fn send_midi_event_at(&mut self, event: MidiEvent, sample_offset: i32) -> Result<()> {
541 self.internal
542 .as_mut()
543 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
544 .send_midi_event_at(event, sample_offset)
545 }
546
547 /// Start audio processing
548 pub fn start_processing(&mut self) -> Result<()> {
549 if self.is_processing {
550 return Ok(());
551 }
552
553 self.internal
554 .as_mut()
555 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
556 .start_processing()?;
557
558 self.is_processing = true;
559 Ok(())
560 }
561
562 /// Stop audio processing
563 pub fn stop_processing(&mut self) -> Result<()> {
564 if !self.is_processing {
565 return Ok(());
566 }
567
568 self.internal
569 .as_mut()
570 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
571 .stop_processing()?;
572
573 self.is_processing = false;
574 Ok(())
575 }
576
577 /// Process audio buffers
578 pub fn process_audio(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
579 if !self.is_processing {
580 return Err(Error::Other("Plugin is not processing".to_string()));
581 }
582
583 self.internal
584 .as_mut()
585 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
586 .process(buffers)?;
587
588 // Update audio levels
589 if let Ok(mut levels) = self.audio_levels.lock() {
590 levels.update_from_buffers(&buffers.outputs);
591
592 // Trigger audio callback if set
593 if let Some(ref callback) = self.audio_callback {
594 callback(&levels);
595 }
596 }
597
598 Ok(())
599 }
600
601 /// Get current output levels.
602 ///
603 /// Recovers automatically if the audio thread panicked while holding the lock
604 /// (poisoned mutex) rather than propagating the panic to the caller — metering
605 /// must never take down a UI thread polling it.
606 pub fn get_output_levels(&self) -> AudioLevels {
607 self.audio_levels
608 .lock()
609 .unwrap_or_else(|poisoned| poisoned.into_inner())
610 .clone()
611 }
612
613 /// Check if the plugin is currently processing
614 pub fn is_processing(&self) -> bool {
615 self.is_processing
616 }
617
618 /// Set a callback for parameter changes
619 pub fn on_parameter_change<F>(&mut self, callback: F)
620 where
621 F: Fn(u32, f64) + Send + 'static,
622 {
623 self.parameter_change_callback = Some(Box::new(callback));
624 }
625
626 /// Set a callback for audio processing (called after each process cycle)
627 pub fn on_audio_process<F>(&mut self, callback: F)
628 where
629 F: Fn(&AudioLevels) + Send + 'static,
630 {
631 self.audio_callback = Some(Box::new(callback));
632 }
633
634 /// Check if the plugin has an editor GUI
635 pub fn has_editor(&self) -> bool {
636 self.internal
637 .as_ref()
638 .map(|i| i.has_editor())
639 .unwrap_or(false)
640 }
641
642 /// Open the plugin editor window
643 pub fn open_editor(&mut self, parent: WindowHandle) -> Result<()> {
644 self.internal
645 .as_mut()
646 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
647 .open_editor(parent.0)
648 }
649
650 /// Close the plugin editor window
651 pub fn close_editor(&mut self) -> Result<()> {
652 self.internal
653 .as_mut()
654 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
655 .close_editor()
656 }
657
658 /// Get the preferred editor size
659 pub fn get_editor_size(&self) -> Result<(i32, i32)> {
660 self.internal
661 .as_ref()
662 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
663 .get_editor_size()
664 }
665
666 /// Create a batch parameter update
667 pub fn update_parameters<F>(&mut self, f: F) -> Result<()>
668 where
669 F: FnOnce(&mut ParameterUpdate) -> Result<()>,
670 {
671 let mut update = ParameterUpdate::new(self);
672 f(&mut update)?;
673 update.apply()
674 }
675
676 /// Send MIDI panic (all notes off, all sounds off, reset controllers)
677 pub fn midi_panic(&mut self) -> Result<()> {
678 for i in 0..16 {
679 if let Some(channel) = MidiChannel::from_index(i) {
680 // All Notes Off
681 self.send_midi_cc(123, 0, channel)?;
682 // All Sounds Off
683 self.send_midi_cc(120, 0, channel)?;
684 // Reset All Controllers
685 self.send_midi_cc(121, 0, channel)?;
686 }
687 }
688 Ok(())
689 }
690
691 /// Get parameter changes from plugin GUI
692 /// Returns a vector of (parameter_id, normalized_value) pairs
693 /// This should be called regularly to pick up parameter changes made through the plugin's GUI
694 pub fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
695 self.internal
696 .as_ref()
697 .map(|i| i.get_parameter_changes())
698 .unwrap_or_default()
699 }
700
701 /// Take the MIDI events the plugin has emitted (e.g. from an arpeggiator or MPE
702 /// controller) since the last call, draining the internal buffer.
703 ///
704 /// Output MIDI is captured while the plugin processes audio, so poll this regularly
705 /// (e.g. each UI frame) while the plugin is playing. Returns an empty vector if the
706 /// plugin emits nothing, or for plugins running under process isolation (output MIDI
707 /// across the boundary is not captured yet).
708 pub fn take_output_midi(&self) -> Vec<MidiEvent> {
709 self.internal
710 .as_ref()
711 .map(|i| i.take_output_events())
712 .unwrap_or_default()
713 }
714
715 /// Save the plugin's current state (parameters, internal settings, loaded preset) to
716 /// an opaque byte blob.
717 ///
718 /// The bytes are the plugin's own serialized state — treat them as opaque and pair them
719 /// with the plugin's identity ([`PluginInfo::uid`]); they only mean something to the
720 /// same plugin. Persist them to restore a patch later with [`Self::load_state`], or to
721 /// snapshot a session. Call this on the main thread (see the
722 /// [threading model](https://docs.rs/vst3-host)).
723 ///
724 /// Works both in-process and across process isolation (the state blob is marshalled over
725 /// the IPC boundary). Returns an error for plugins that don't implement state saving.
726 pub fn save_state(&self) -> Result<Vec<u8>> {
727 self.internal
728 .as_ref()
729 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
730 .save_state()
731 }
732
733 /// Restore plugin state from a blob produced by [`Self::save_state`] on the *same*
734 /// plugin. Applies to both the processor and the controller, so parameter values and
735 /// the editor reflect the restored state.
736 ///
737 /// Passing bytes from a different plugin has undefined results (the plugin decides what
738 /// to do with bytes it doesn't recognize). Call this on the main thread.
739 pub fn load_state(&mut self, data: &[u8]) -> Result<()> {
740 self.internal
741 .as_mut()
742 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
743 .load_state(data)
744 }
745
746 /// Save this plugin's state to a file as a [`PluginPreset`] (JSON: the plugin's `uid`
747 /// and name plus the opaque state blob). The embedded `uid` lets [`Self::load_preset`]
748 /// reject a preset saved from a different plugin.
749 pub fn save_preset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
750 let info = self.info();
751 let preset = PluginPreset {
752 uid: info.uid.clone(),
753 plugin_name: info.name.clone(),
754 state: self.save_state()?,
755 };
756 let json = serde_json::to_vec_pretty(&preset)
757 .map_err(|e| Error::Other(format!("serialize preset: {e}")))?;
758 std::fs::write(path, json).map_err(|e| Error::Other(format!("write preset: {e}")))?;
759 Ok(())
760 }
761
762 /// Load a [`PluginPreset`] file written by [`Self::save_preset`] and apply its state.
763 /// Returns an error if the preset's `uid` doesn't match this plugin (loading another
764 /// plugin's state is undefined).
765 pub fn load_preset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
766 let bytes = std::fs::read(path).map_err(|e| Error::Other(format!("read preset: {e}")))?;
767 let preset: PluginPreset = serde_json::from_slice(&bytes)
768 .map_err(|e| Error::Other(format!("parse preset: {e}")))?;
769 if preset.uid != self.info().uid {
770 return Err(Error::Other(format!(
771 "preset is for a different plugin ({}, expected {})",
772 preset.plugin_name,
773 self.info().name
774 )));
775 }
776 self.load_state(&preset.state)
777 }
778
779 /// Save this plugin's state to a standard Steinberg `.vstpreset` file.
780 ///
781 /// Unlike [`Self::save_preset`] (a JSON wrapper specific to this library), the
782 /// `.vstpreset` container is the interchange format shared by VST3 hosts and plugins, so
783 /// the file can be read by other hosts (and by the plugin's own preset browser). It wraps
784 /// the same opaque bytes from [`Self::save_state`] in a single `"Comp"` (component state)
785 /// chunk, tagged with this plugin's class id ([`PluginInfo::uid`]) so a loader can reject
786 /// presets from a different plugin. Call this on the main thread.
787 pub fn save_vstpreset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
788 let state = self.save_state()?;
789 let bytes = vstpreset::build(&self.info().uid, &state)?;
790 std::fs::write(path, bytes).map_err(|e| Error::Other(format!("write vstpreset: {e}")))?;
791 Ok(())
792 }
793
794 /// Load a Steinberg `.vstpreset` file and apply its component state to this plugin.
795 ///
796 /// Parses the `.vstpreset` container written by [`Self::save_vstpreset`] (or another VST3
797 /// host), extracts the `"Comp"` (component state) chunk and passes it to
798 /// [`Self::load_state`]. Returns an error if the file's magic is invalid, or if its class
799 /// id doesn't match this plugin (loading another plugin's state is undefined). Call this
800 /// on the main thread.
801 pub fn load_vstpreset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
802 let bytes =
803 std::fs::read(path).map_err(|e| Error::Other(format!("read vstpreset: {e}")))?;
804 let parsed = vstpreset::parse(&bytes)?;
805 if parsed.class_id != self.info().uid {
806 return Err(Error::Other(format!(
807 "vstpreset is for a different plugin (class id {}, expected {})",
808 parsed.class_id,
809 self.info().uid
810 )));
811 }
812 self.load_state(&parsed.component_state)
813 }
814
815 /// The OS process id of the isolated helper hosting this plugin, or `None` if it runs
816 /// in-process. Useful for monitoring an isolated plugin's resource use.
817 pub fn isolation_pid(&self) -> Option<u32> {
818 self.internal.as_ref().and_then(|i| i.helper_pid())
819 }
820
821 /// How many times this plugin has been recovered (helper respawned + reloaded), via either
822 /// [`Self::recover`] or automatic recovery ([`Vst3HostBuilder::auto_recover_plugins`]).
823 ///
824 /// A recovery reloads the plugin from defaults — parameter values and loaded state are NOT
825 /// replayed. With auto-recover on, a crash is otherwise invisible (the call returns `Ok`),
826 /// so poll this count to detect that a reset happened and re-apply a saved
827 /// [`save_state`](Self::save_state) snapshot.
828 ///
829 /// [`Vst3HostBuilder::auto_recover_plugins`]: crate::Vst3HostBuilder::auto_recover_plugins
830 pub fn recovery_count(&self) -> u64 {
831 self.internal
832 .as_ref()
833 .map(|i| i.recovery_count())
834 .unwrap_or(0)
835 }
836
837 /// Total number of output audio channels across the plugin's output buses.
838 ///
839 /// Reflects the plugin's actual bus layout (mono / stereo / surround / multi-bus), not a
840 /// stereo assumption — useful for sizing meters or output buffers. Returns 2 if unknown.
841 pub fn output_channel_count(&self) -> usize {
842 self.internal
843 .as_ref()
844 .map(|i| i.output_channel_count())
845 .unwrap_or(2)
846 }
847
848 /// Poll for an editor resize the plugin requested via VST3's `IPlugFrame` since the last
849 /// call, as `(width, height)` in pixels, or `None`.
850 ///
851 /// Plugins with resizable editors call back to ask the host to resize the window hosting
852 /// their view. Poll this on your UI thread (e.g. each frame) while the editor is open and
853 /// resize your editor container to match. Only the in-process editor path reports this.
854 pub fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
855 self.internal
856 .as_ref()
857 .and_then(|i| i.take_editor_resize_request())
858 }
859
860 /// Recover a process-isolated plugin whose helper has crashed.
861 ///
862 /// When an isolated plugin's helper process dies, calls return [`Error::PluginCrashed`]
863 /// and the host itself stays alive. This respawns the helper and reloads the plugin
864 /// from the same path and audio settings, restarting processing if it was running.
865 ///
866 /// **The reloaded plugin starts from its default state** — parameter values and any
867 /// loaded preset are lost. Snapshot with [`Self::save_state`] beforehand and
868 /// [`Self::load_state`] after recovering to preserve them. Returns an error for
869 /// in-process plugins (an in-process crash takes down the whole host) and if the
870 /// reload itself fails.
871 pub fn recover(&mut self) -> Result<()> {
872 self.internal
873 .as_mut()
874 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
875 .recover()
876 }
877}
878
879/// Platform-specific window handle
880pub struct WindowHandle(pub(crate) *mut std::ffi::c_void);
881
882impl WindowHandle {
883 /// Create from a raw window handle
884 ///
885 /// # Safety
886 /// The pointer must be a valid window handle for the platform
887 pub unsafe fn from_raw(handle: *mut std::ffi::c_void) -> Self {
888 Self(handle)
889 }
890}
891
892// Safe Send implementation - the window handle is platform-specific
893unsafe impl Send for WindowHandle {}
894
895#[cfg(target_os = "macos")]
896impl WindowHandle {
897 /// Create from an NSView pointer on macOS
898 pub fn from_nsview(view: *mut std::ffi::c_void) -> Self {
899 Self(view)
900 }
901}
902
903#[cfg(target_os = "windows")]
904impl WindowHandle {
905 /// Create from an HWND on Windows
906 pub fn from_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
907 Self(hwnd)
908 }
909}
910
911#[cfg(target_os = "linux")]
912impl WindowHandle {
913 /// Create from an X11 window id on Linux (for VST3 `X11EmbedWindowID`).
914 ///
915 /// The VST3 X11 platform type expects the window id itself as the handle value,
916 /// not a pointer to it.
917 pub fn from_x11(window_id: u32) -> Self {
918 Self(window_id as usize as *mut std::ffi::c_void)
919 }
920}
921
922/// Build and parse the standard Steinberg `.vstpreset` container format.
923///
924/// Layout (all multi-byte integers little-endian, matching the SDK's `PresetFile`):
925///
926/// - Header (48 bytes): magic `b"VST3"` (4) + version `i32` = 1 (4) + 32-char ASCII class
927/// id (the plugin's FUID hex) (32) + `i64` byte offset from the start of the file to the
928/// chunk list (8).
929/// - Body: the chunk payloads, written back to back after the header. We write a single
930/// `"Comp"` (component state) chunk.
931/// - Chunk list (at the header's list offset): magic `b"List"` (4) + entry count `i32` (4),
932/// then per entry: 4-byte chunk id + `i64` absolute offset + `i64` size.
933mod vstpreset {
934 use crate::error::{Error, Result};
935
936 const MAGIC: &[u8; 4] = b"VST3";
937 const LIST_MAGIC: &[u8; 4] = b"List";
938 const COMPONENT_CHUNK: &[u8; 4] = b"Comp";
939 const VERSION: i32 = 1;
940 const CLASS_ID_LEN: usize = 32;
941 const HEADER_SIZE: usize = 4 + 4 + CLASS_ID_LEN + 8;
942
943 /// A parsed `.vstpreset` container.
944 pub(super) struct Parsed {
945 /// The 32-char ASCII class id from the header.
946 pub class_id: String,
947 /// The bytes of the `"Comp"` (component state) chunk.
948 pub component_state: Vec<u8>,
949 }
950
951 /// Build a `.vstpreset` file wrapping `component_state` in a single component chunk,
952 /// tagged with `class_id` (a 32-char ASCII FUID hex string).
953 pub(super) fn build(class_id: &str, component_state: &[u8]) -> Result<Vec<u8>> {
954 let class_bytes = class_id.as_bytes();
955 if class_bytes.len() != CLASS_ID_LEN || !class_id.is_ascii() {
956 return Err(Error::Other(format!(
957 "vstpreset class id must be {CLASS_ID_LEN} ASCII chars, got {:?}",
958 class_id
959 )));
960 }
961
962 let comp_offset = HEADER_SIZE as i64;
963 let comp_size = component_state.len() as i64;
964 let list_offset = HEADER_SIZE + component_state.len();
965
966 let mut out = Vec::with_capacity(list_offset + 8 + 24);
967 // Header.
968 out.extend_from_slice(MAGIC);
969 out.extend_from_slice(&VERSION.to_le_bytes());
970 out.extend_from_slice(class_bytes);
971 out.extend_from_slice(&(list_offset as i64).to_le_bytes());
972 // Body.
973 out.extend_from_slice(component_state);
974 // Chunk list.
975 out.extend_from_slice(LIST_MAGIC);
976 out.extend_from_slice(&1i32.to_le_bytes());
977 out.extend_from_slice(COMPONENT_CHUNK);
978 out.extend_from_slice(&comp_offset.to_le_bytes());
979 out.extend_from_slice(&comp_size.to_le_bytes());
980
981 Ok(out)
982 }
983
984 /// Parse a `.vstpreset` file, extracting the class id and the component-state chunk.
985 pub(super) fn parse(bytes: &[u8]) -> Result<Parsed> {
986 if bytes.len() < HEADER_SIZE {
987 return Err(Error::Other("vstpreset too short for header".to_string()));
988 }
989 if &bytes[0..4] != MAGIC {
990 return Err(Error::Other(format!(
991 "bad vstpreset magic: expected {:?}, got {:?}",
992 MAGIC,
993 &bytes[0..4]
994 )));
995 }
996 let version = read_i32(&bytes[4..8]);
997 if version != VERSION {
998 return Err(Error::Other(format!(
999 "unsupported vstpreset version {version} (expected {VERSION})"
1000 )));
1001 }
1002 let class_id = String::from_utf8(bytes[8..8 + CLASS_ID_LEN].to_vec())
1003 .map_err(|e| Error::Other(format!("vstpreset class id not UTF-8: {e}")))?;
1004 let list_offset = read_i64(&bytes[8 + CLASS_ID_LEN..HEADER_SIZE]);
1005 if list_offset < HEADER_SIZE as i64 || list_offset as usize > bytes.len() {
1006 return Err(Error::Other(format!(
1007 "vstpreset chunk-list offset {list_offset} out of bounds (len {})",
1008 bytes.len()
1009 )));
1010 }
1011 let list = &bytes[list_offset as usize..];
1012 if list.len() < 8 || &list[0..4] != LIST_MAGIC {
1013 return Err(Error::Other(
1014 "vstpreset chunk list missing or malformed".to_string(),
1015 ));
1016 }
1017 let count = read_i32(&list[4..8]);
1018 if count < 0 {
1019 return Err(Error::Other("vstpreset negative entry count".to_string()));
1020 }
1021 let mut cursor = 8;
1022 for _ in 0..count {
1023 if list.len() < cursor + 20 {
1024 return Err(Error::Other(
1025 "vstpreset chunk-list entry truncated".to_string(),
1026 ));
1027 }
1028 let id = &list[cursor..cursor + 4];
1029 let offset = read_i64(&list[cursor + 4..cursor + 12]);
1030 let size = read_i64(&list[cursor + 12..cursor + 20]);
1031 cursor += 20;
1032 if id == COMPONENT_CHUNK {
1033 if offset < 0 || size < 0 {
1034 return Err(Error::Other(
1035 "vstpreset component chunk has negative offset/size".to_string(),
1036 ));
1037 }
1038 let start = offset as usize;
1039 let end = start
1040 .checked_add(size as usize)
1041 .ok_or_else(|| Error::Other("vstpreset chunk size overflow".to_string()))?;
1042 if end > bytes.len() {
1043 return Err(Error::Other(format!(
1044 "vstpreset component chunk [{start}..{end}] out of bounds (len {})",
1045 bytes.len()
1046 )));
1047 }
1048 return Ok(Parsed {
1049 class_id,
1050 component_state: bytes[start..end].to_vec(),
1051 });
1052 }
1053 }
1054 Err(Error::Other(
1055 "vstpreset has no component (\"Comp\") chunk".to_string(),
1056 ))
1057 }
1058
1059 fn read_i32(b: &[u8]) -> i32 {
1060 i32::from_le_bytes([b[0], b[1], b[2], b[3]])
1061 }
1062
1063 fn read_i64(b: &[u8]) -> i64 {
1064 i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
1065 }
1066}
1067
1068#[cfg(test)]
1069mod vstpreset_tests {
1070 use super::vstpreset;
1071
1072 const TEST_CLASS_ID: &str = "0123456789ABCDEF0123456789ABCDEF";
1073
1074 #[test]
1075 fn build_parse_round_trip() {
1076 let state = b"opaque plugin state \x00\x01\x02\xff bytes".to_vec();
1077 let bytes = vstpreset::build(TEST_CLASS_ID, &state).expect("build");
1078
1079 // Sanity-check the header layout.
1080 assert_eq!(&bytes[0..4], b"VST3");
1081 assert_eq!(
1082 i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1083 1
1084 );
1085 assert_eq!(&bytes[8..40], TEST_CLASS_ID.as_bytes());
1086
1087 let parsed = vstpreset::parse(&bytes).expect("parse");
1088 assert_eq!(parsed.class_id, TEST_CLASS_ID);
1089 assert_eq!(parsed.component_state, state);
1090 }
1091
1092 #[test]
1093 fn round_trip_empty_state() {
1094 let bytes = vstpreset::build(TEST_CLASS_ID, &[]).expect("build");
1095 let parsed = vstpreset::parse(&bytes).expect("parse");
1096 assert_eq!(parsed.class_id, TEST_CLASS_ID);
1097 assert!(parsed.component_state.is_empty());
1098 }
1099
1100 #[test]
1101 fn build_rejects_wrong_length_class_id() {
1102 assert!(vstpreset::build("short", b"x").is_err());
1103 }
1104
1105 #[test]
1106 fn parse_rejects_bad_magic() {
1107 let mut bytes = vstpreset::build(TEST_CLASS_ID, b"x").expect("build");
1108 bytes[0] = b'X';
1109 assert!(vstpreset::parse(&bytes).is_err());
1110 }
1111
1112 #[test]
1113 fn parse_rejects_truncated_header() {
1114 assert!(vstpreset::parse(b"VST3").is_err());
1115 }
1116
1117 #[test]
1118 fn parse_rejects_out_of_bounds_list_offset() {
1119 let mut bytes = vstpreset::build(TEST_CLASS_ID, b"hello").expect("build");
1120 // Corrupt the list offset (bytes 40..48) to point past the end.
1121 let bad = (bytes.len() as i64 + 100).to_le_bytes();
1122 bytes[40..48].copy_from_slice(&bad);
1123 assert!(vstpreset::parse(&bytes).is_err());
1124 }
1125}