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 crossbeam_queue::ArrayQueue;
10use std::sync::{Arc, Mutex};
11
12/// A `Send` + `Sync` handle for draining the MIDI a plugin emits (arpeggiators, MPE, MIDI
13/// thru, …) without locking the audio thread.
14///
15/// Obtain one from [`Plugin::output_midi_handle`]. The plugin's audio thread pushes emitted
16/// events into a lock-free bounded queue; this handle pops them from any other thread (e.g. a
17/// UI poll loop) with no lock on either side — the lock-free counterpart to the audio-thread
18/// drain in [`Plugin::take_output_midi`]. When the queue is full the oldest event is dropped,
19/// so a host that stops polling can't grow it without bound.
20///
21/// Available for in-process plugins; the process-isolation path returns `None` (output MIDI
22/// crosses the boundary in the IPC responses instead).
23#[derive(Clone)]
24pub struct OutputMidiConsumer {
25 queue: Arc<ArrayQueue<MidiEvent>>,
26}
27
28impl OutputMidiConsumer {
29 pub(crate) fn from_queue(queue: Arc<ArrayQueue<MidiEvent>>) -> Self {
30 Self { queue }
31 }
32
33 /// Pop the oldest emitted event, or `None` if none are queued. Lock-free.
34 pub fn pop(&self) -> Option<MidiEvent> {
35 self.queue.pop()
36 }
37
38 /// Drain all currently queued events in emission order into a `Vec`. Lock-free pops; the
39 /// returned `Vec` allocates on the calling thread (intended for a UI/control thread, not
40 /// the audio thread — use [`pop`](Self::pop) in a loop to stay allocation-free).
41 pub fn drain(&self) -> Vec<MidiEvent> {
42 let mut out = Vec::new();
43 while let Some(event) = self.queue.pop() {
44 out.push(event);
45 }
46 out
47 }
48}
49
50/// Information about a VST3 plugin
51#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
52pub struct PluginInfo {
53 /// Full path to the VST3 bundle/file
54 pub path: std::path::PathBuf,
55 /// Plugin name
56 pub name: String,
57 /// Vendor/manufacturer name
58 pub vendor: String,
59 /// Plugin version
60 pub version: String,
61 /// Plugin category (e.g., "Fx", "Instrument")
62 pub category: String,
63 /// Unique plugin ID
64 pub uid: String,
65 /// Number of audio input buses
66 pub audio_inputs: u32,
67 /// Number of audio output buses
68 pub audio_outputs: u32,
69 /// Whether the plugin accepts MIDI input
70 pub has_midi_input: bool,
71 /// Whether the plugin produces MIDI output
72 pub has_midi_output: bool,
73 /// Whether the plugin has a GUI
74 pub has_gui: bool,
75}
76
77/// A saved plugin preset: the plugin's identity plus its opaque state blob.
78///
79/// Written/read by [`Plugin::save_preset`] / [`Plugin::load_preset`]. The `uid` lets a
80/// loader reject a preset that belongs to a different plugin (whose state bytes would be
81/// meaningless or harmful).
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
83pub struct PluginPreset {
84 /// The originating plugin's unique class id ([`PluginInfo::uid`]).
85 pub uid: String,
86 /// The originating plugin's display name (for friendly mismatch messages).
87 pub plugin_name: String,
88 /// The plugin's opaque serialized state (from [`Plugin::save_state`]).
89 pub state: Vec<u8>,
90}
91
92/// A plugin unit (from `IUnitInfo`) and its program list, if any.
93///
94/// Units form a hierarchy (via [`parent_id`](Self::parent_id)); a unit may carry a named
95/// program list (e.g. a synth's factory patches). Query with [`Plugin::get_units`].
96#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97pub struct PluginUnit {
98 /// Unit id (unique within the plugin; the root unit is conventionally `0`).
99 pub id: i32,
100 /// Parent unit id, or `-1` for the root.
101 pub parent_id: i32,
102 /// Unit display name.
103 pub name: String,
104 /// Program names in this unit's program list (empty if the unit has none).
105 pub programs: Vec<String>,
106}
107
108/// What kind of parameter-edit gesture event a plugin's editor reported.
109///
110/// VST3 editors bracket a user gesture with `beginEdit`/`endEdit` (e.g. mouse-down /
111/// mouse-up on a knob) and report the values in between with `performEdit`. Capturing the
112/// brackets — not just the value changes — lets a host distinguish a deliberate, completed
113/// edit from intermediate drag values, coalesce automation into one undo step, or know when a
114/// gesture is in progress.
115#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
116pub enum ParameterEditKind {
117 /// The user started editing this parameter (`IComponentHandler::beginEdit`).
118 BeginGesture,
119 /// The parameter's value changed (`IComponentHandler::performEdit`); carries the new
120 /// normalized value in [`ParameterEdit::value`].
121 ValueChange,
122 /// The user finished editing this parameter (`IComponentHandler::endEdit`).
123 EndGesture,
124}
125
126/// A single parameter-edit gesture event reported by a plugin's own editor.
127///
128/// Drained in order via [`Plugin::take_parameter_edits`]. This is the richer superset of
129/// [`Plugin::get_parameter_changes`]: where that drains only the value changes, this preserves
130/// the begin/change/end ordering so a host can reconstruct each gesture.
131#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
132pub struct ParameterEdit {
133 /// Parameter id the gesture targets.
134 pub id: u32,
135 /// Which gesture phase this event is.
136 pub kind: ParameterEditKind,
137 /// The new normalized value (`0.0..=1.0`), present only for
138 /// [`ParameterEditKind::ValueChange`]; `None` for begin/end brackets.
139 pub value: Option<f64>,
140}
141
142/// How the plugin should run: real-time (live playback) or offline (faster-than-real-time
143/// bounce/render). Maps to VST3 `kRealtime` / `kOffline`; plugins may switch quality or
144/// look-ahead accordingly. Defaults to [`ProcessMode::Realtime`].
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
146pub enum ProcessMode {
147 /// Real-time / live processing (the default; `kRealtime`).
148 #[default]
149 Realtime,
150 /// Offline / non-real-time processing such as a render or bounce (`kOffline`).
151 Offline,
152}
153
154/// VST3 plugin instance
155#[allow(clippy::type_complexity)] // callback fields are Box<dyn Fn...>; intrinsic to the API
156pub struct Plugin {
157 // Internal state is hidden from public API
158 pub(crate) info: PluginInfo,
159 pub(crate) is_processing: bool,
160 /// Configured sample rate (exposed via [`Plugin::sample_rate`]).
161 pub(crate) sample_rate: f64,
162 /// Configured max block size (exposed via [`Plugin::block_size`]).
163 pub(crate) block_size: usize,
164 pub(crate) audio_levels: Arc<Mutex<AudioLevels>>,
165 pub(crate) parameter_change_callback: Option<Box<dyn Fn(u32, f64) + Send + 'static>>,
166 pub(crate) audio_callback: Option<Box<dyn Fn(&AudioLevels) + Send + 'static>>,
167
168 // These will be populated by the actual implementation
169 pub(crate) internal: Option<Box<dyn PluginInternal>>,
170}
171
172// Internal trait for hiding implementation details
173pub(crate) trait PluginInternal: Send {
174 fn set_parameter(&mut self, id: u32, value: f64) -> Result<()>;
175 /// Schedule a parameter change at a sample offset within the next process block.
176 /// Defaults to a block-start change (ignores the offset) for implementations that don't
177 /// support sample-accurate scheduling.
178 fn set_parameter_at(&mut self, id: u32, value: f64, _sample_offset: i32) -> Result<()> {
179 self.set_parameter(id, value)
180 }
181 fn get_parameter(&self, id: u32) -> Result<f64>;
182 fn get_all_parameters(&self) -> Result<Vec<Parameter>>;
183 fn format_parameter(&self, id: u32, normalized: f64) -> Result<String>;
184 fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()>;
185 /// Re-run `setupProcessing` for a new sample rate / block size. Defaults to unsupported
186 /// for implementations that don't support it.
187 fn reconfigure(&mut self, _sample_rate: f64, _block_size: usize) -> Result<()> {
188 Err(Error::Other(
189 "runtime reconfigure is not supported for this plugin".to_string(),
190 ))
191 }
192 /// Switch the plugin's process mode (real-time vs offline), re-running `setupProcessing`.
193 /// Defaults to unsupported for implementations that don't support it.
194 fn set_process_mode(&mut self, _mode: crate::plugin::ProcessMode) -> Result<()> {
195 Err(Error::Other(
196 "process mode switching is not supported for this plugin".to_string(),
197 ))
198 }
199 /// Query each audio bus's current speaker arrangement. Defaults to unsupported.
200 fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
201 Err(Error::Other(
202 "bus arrangement query is not supported for this plugin".to_string(),
203 ))
204 }
205 /// Request specific speaker arrangements for the audio buses (re-runs `setupProcessing`).
206 /// Defaults to unsupported for implementations that don't support it.
207 fn set_bus_arrangements(
208 &mut self,
209 _inputs: &[crate::audio::SpeakerArrangement],
210 _outputs: &[crate::audio::SpeakerArrangement],
211 ) -> Result<()> {
212 Err(Error::Other(
213 "bus arrangement negotiation is not supported for this plugin".to_string(),
214 ))
215 }
216 /// Activate or deactivate a single bus (`IComponent::activateBus`). Defaults to
217 /// unsupported.
218 fn set_bus_active(
219 &mut self,
220 _media_type: crate::audio::MediaType,
221 _direction: crate::audio::BusDirection,
222 _bus_index: i32,
223 _active: bool,
224 ) -> Result<()> {
225 Err(Error::Other(
226 "bus activation is not supported for this plugin".to_string(),
227 ))
228 }
229 /// Update the transport tempo (BPM) advertised in the host `ProcessContext`, taking effect
230 /// on the next processed block. The caller validates `bpm > 0`. Defaults to unsupported
231 /// (overridden by the in-process and isolated implementations).
232 fn set_tempo(&mut self, _bpm: f64) -> Result<()> {
233 Err(Error::Other(
234 "runtime transport mutation is not supported for this plugin".to_string(),
235 ))
236 }
237 /// Update the transport time signature advertised in the host `ProcessContext`, taking
238 /// effect on the next processed block. The caller validates the numerator/denominator.
239 /// Defaults to unsupported.
240 fn set_time_signature(&mut self, _numerator: i32, _denominator: i32) -> Result<()> {
241 Err(Error::Other(
242 "runtime transport mutation is not supported for this plugin".to_string(),
243 ))
244 }
245 /// Toggle the transport playing state (`kPlaying`) in the host `ProcessContext`, taking
246 /// effect on the next processed block. Defaults to unsupported.
247 fn set_playing(&mut self, _playing: bool) -> Result<()> {
248 Err(Error::Other(
249 "runtime transport mutation is not supported for this plugin".to_string(),
250 ))
251 }
252 fn send_midi_event(&mut self, event: MidiEvent) -> Result<()>;
253 /// Schedule a MIDI event at a sample offset within the next process block.
254 /// Defaults to a block-start event (ignores the offset) for implementations that don't
255 /// support sample-accurate scheduling.
256 fn send_midi_event_at(&mut self, event: MidiEvent, _sample_offset: i32) -> Result<()> {
257 self.send_midi_event(event)
258 }
259 /// Start a note and return a per-voice [`NoteId`] for targeting note-expression. Default:
260 /// unsupported, for implementations that don't support per-note expression.
261 fn note_on(
262 &mut self,
263 _channel: MidiChannel,
264 _note: u8,
265 _velocity: u8,
266 _sample_offset: i32,
267 ) -> Result<crate::midi::NoteId> {
268 Err(Error::Other(
269 "per-note expression is not supported for this plugin".to_string(),
270 ))
271 }
272 /// Release a note started with [`Self::note_on`]. Default: unsupported.
273 fn note_off(&mut self, _id: crate::midi::NoteId, _sample_offset: i32) -> Result<()> {
274 Err(Error::Other(
275 "per-note expression is not supported for this plugin".to_string(),
276 ))
277 }
278 /// Send a per-note expression value (normalized 0..1) for a voice. Default: unsupported.
279 fn send_note_expression(
280 &mut self,
281 _id: crate::midi::NoteId,
282 _kind: crate::midi::NoteExpressionType,
283 _value: f64,
284 _sample_offset: i32,
285 ) -> Result<()> {
286 Err(Error::Other(
287 "per-note expression is not supported for this plugin".to_string(),
288 ))
289 }
290 /// Enumerate the per-note expressions the plugin advertises (`INoteExpressionController`).
291 /// Defaults to empty.
292 fn note_expressions(
293 &self,
294 _bus: i32,
295 _channel: i16,
296 ) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
297 Ok(Vec::new())
298 }
299 fn start_processing(&mut self) -> Result<()>;
300 fn stop_processing(&mut self) -> Result<()>;
301 fn has_editor(&self) -> bool;
302 fn open_editor(&mut self, parent: *mut std::ffi::c_void) -> Result<()>;
303 fn close_editor(&mut self) -> Result<()>;
304 fn get_editor_size(&self) -> Result<(i32, i32)>;
305 fn get_parameter_changes(&self) -> Vec<(u32, f64)>;
306 /// Drain the ordered parameter-edit gesture log (begin/change/end) the plugin's editor
307 /// reported since the last call. Defaults to empty for implementations that don't capture
308 /// gestures.
309 fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
310 Vec::new()
311 }
312 /// Take the MIDI events the plugin has emitted since the last call. Defaults to empty
313 /// for implementations that don't capture output MIDI.
314 fn take_output_events(&self) -> Vec<MidiEvent> {
315 Vec::new()
316 }
317 /// A lock-free handle for draining emitted MIDI from another thread. Defaults to `None`
318 /// for implementations without a shared in-process queue (e.g. process isolation).
319 fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
320 None
321 }
322 /// Enumerate the plugin's units and their program lists (`IUnitInfo`). Defaults to empty
323 /// for implementations that don't query it.
324 fn get_units(&self) -> Result<Vec<PluginUnit>> {
325 Ok(Vec::new())
326 }
327 /// Select a program in a unit's program list. Defaults to unsupported (e.g. plugins
328 /// without `IUnitInfo`); implementations resolve the unit's program-change parameter and
329 /// set it to the index's normalized value.
330 fn select_program(&mut self, _unit_id: i32, _program_index: i32) -> Result<()> {
331 Err(Error::Other(
332 "program selection is not supported for this plugin".to_string(),
333 ))
334 }
335 /// Processing latency in samples (`IAudioProcessor::getLatencySamples`). Defaults to 0.
336 fn latency_samples(&self) -> u32 {
337 0
338 }
339 /// Tail length in samples (`IAudioProcessor::getTailSamples`). Defaults to 0.
340 fn tail_samples(&self) -> u32 {
341 0
342 }
343 /// Resolve a MIDI controller `(bus, channel, cc)` to a parameter id via `IMidiMapping`.
344 /// Defaults to `None` (plugin doesn't implement the interface, or no mapping).
345 fn midi_cc_to_parameter(&self, _bus: i32, _channel: i16, _cc: u16) -> Option<u32> {
346 None
347 }
348 /// Serialize the plugin's current state to an opaque byte blob.
349 fn save_state(&self) -> Result<Vec<u8>> {
350 Err(Error::Other(
351 "state save/restore is not supported".to_string(),
352 ))
353 }
354 /// Restore the plugin's state from a blob previously returned by [`Self::save_state`].
355 fn load_state(&mut self, _data: &[u8]) -> Result<()> {
356 Err(Error::Other(
357 "state save/restore is not supported".to_string(),
358 ))
359 }
360 /// OS process id of the isolated helper, if this plugin runs out-of-process.
361 fn helper_pid(&self) -> Option<u32> {
362 None
363 }
364 /// Number of times this plugin has been recovered (respawned + reloaded). Defaults to 0
365 /// for non-isolated plugins.
366 fn recovery_count(&self) -> u64 {
367 0
368 }
369 /// Recover from a crashed isolated helper by respawning and reloading. Only meaningful
370 /// for process-isolated plugins.
371 fn recover(&mut self) -> Result<()> {
372 Err(Error::Other(
373 "recovery is only supported for process-isolated plugins".to_string(),
374 ))
375 }
376 /// The size the plugin's editor has requested (via `IPlugFrame`) since the last poll.
377 fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
378 None
379 }
380 /// Total output audio channels across the plugin's output buses. Defaults to 2.
381 fn output_channel_count(&self) -> usize {
382 2
383 }
384}
385
386impl Plugin {
387 /// Get plugin information
388 pub fn info(&self) -> &PluginInfo {
389 &self.info
390 }
391
392 /// The sample rate (Hz) this plugin was configured with at load.
393 pub fn sample_rate(&self) -> f64 {
394 self.sample_rate
395 }
396
397 /// The maximum block size (frames per `process_audio` call) configured at load.
398 pub fn block_size(&self) -> usize {
399 self.block_size
400 }
401
402 /// Reconfigure the plugin for a new sample rate and/or maximum block size, re-running the
403 /// plugin's `setupProcessing` and rebuilding its audio buffers.
404 ///
405 /// Use this when the audio device's sample rate changes mid-session instead of reloading.
406 /// The plugin must **not** be processing: call [`Self::stop_processing`] first, reconfigure,
407 /// then [`Self::start_processing`] again. Returns an error if called while processing, or
408 /// on an invalid sample rate / zero block size. Works both in-process and across process
409 /// isolation.
410 pub fn reconfigure(&mut self, sample_rate: f64, block_size: usize) -> Result<()> {
411 if self.is_processing {
412 return Err(Error::Other(
413 "cannot reconfigure while processing; call stop_processing() first".to_string(),
414 ));
415 }
416 if !(sample_rate.is_finite() && sample_rate > 0.0) {
417 return Err(Error::InvalidParameter(format!(
418 "sample rate must be finite and positive, got {sample_rate}"
419 )));
420 }
421 if block_size == 0 || block_size > i32::MAX as usize {
422 return Err(Error::InvalidParameter(format!(
423 "block size must be in 1..={}, got {block_size}",
424 i32::MAX
425 )));
426 }
427
428 self.internal
429 .as_mut()
430 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
431 .reconfigure(sample_rate, block_size)?;
432
433 self.sample_rate = sample_rate;
434 self.block_size = block_size;
435 Ok(())
436 }
437
438 /// Switch the plugin between real-time and offline processing, re-running the plugin's
439 /// `setupProcessing` so it can adjust quality / look-ahead for a faster-than-real-time
440 /// bounce.
441 ///
442 /// Like [`Self::reconfigure`], the plugin must **not** be processing: call
443 /// [`Self::stop_processing`] first. Returns an error if called while processing. Works both
444 /// in-process and across process isolation.
445 pub fn set_process_mode(&mut self, mode: ProcessMode) -> Result<()> {
446 if self.is_processing {
447 return Err(Error::Other(
448 "cannot set process mode while processing; call stop_processing() first"
449 .to_string(),
450 ));
451 }
452 self.internal
453 .as_mut()
454 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
455 .set_process_mode(mode)
456 }
457
458 /// Query the current speaker arrangement of each audio input/output bus. Works both
459 /// in-process and across process isolation.
460 pub fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
461 self.internal
462 .as_ref()
463 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
464 .bus_arrangements()
465 }
466
467 /// Request specific speaker arrangements for the audio buses (e.g. force stereo, or a
468 /// surround layout). The slices give one [`SpeakerArrangement`](crate::audio::SpeakerArrangement)
469 /// per input bus and per output bus, in bus-index order.
470 ///
471 /// Re-runs the plugin's `setupProcessing`, so the plugin must **not** be processing (call
472 /// [`Self::stop_processing`] first). A plugin may decline a requested layout and keep its
473 /// own; re-query with [`Self::bus_arrangements`] to see what was actually applied. Errors
474 /// while processing. Works both in-process and across process isolation.
475 pub fn set_bus_arrangements(
476 &mut self,
477 inputs: &[crate::audio::SpeakerArrangement],
478 outputs: &[crate::audio::SpeakerArrangement],
479 ) -> Result<()> {
480 if self.is_processing {
481 return Err(Error::Other(
482 "cannot set bus arrangements while processing; call stop_processing() first"
483 .to_string(),
484 ));
485 }
486 self.internal
487 .as_mut()
488 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
489 .set_bus_arrangements(inputs, outputs)
490 }
491
492 /// Activate or deactivate a single bus on the plugin (`IComponent::activateBus`).
493 ///
494 /// Hosts must explicitly activate the buses they intend to use; a plugin's secondary
495 /// buses (sidechain / aux inputs, extra outputs) commonly start **inactive** and only
496 /// receive/produce audio once activated. (The load sequence already activates the main
497 /// audio and event buses, so call this to enable the rest.)
498 ///
499 /// `media_type` selects audio vs event buses and `direction` selects input vs output;
500 /// `bus_index` is the 0-based index within that `(media_type, direction)` group (the
501 /// same indexing as [`crate::discovery::BusLayout`]). `active` true activates, false
502 /// deactivates.
503 ///
504 /// VST3 requires bus activation to happen while the component is **inactive** — i.e.
505 /// before processing starts. This therefore returns an error if called while the plugin
506 /// is processing; call [`Self::stop_processing`] first, activate the bus, then
507 /// [`Self::start_processing`] again. Returns an error for an out-of-range `bus_index`,
508 /// and under process isolation activation marshals across the boundary.
509 pub fn set_bus_active(
510 &mut self,
511 media_type: crate::audio::MediaType,
512 direction: crate::audio::BusDirection,
513 bus_index: i32,
514 active: bool,
515 ) -> Result<()> {
516 if self.is_processing {
517 return Err(Error::Other(
518 "cannot activate a bus while processing; call stop_processing() first".to_string(),
519 ));
520 }
521 self.internal
522 .as_mut()
523 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
524 .set_bus_active(media_type, direction, bus_index, active)
525 }
526
527 /// Get all parameters
528 pub fn get_parameters(&self) -> Result<Vec<Parameter>> {
529 self.internal
530 .as_ref()
531 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
532 .get_all_parameters()
533 }
534
535 /// Set a parameter value by ID
536 pub fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
537 if !(0.0..=1.0).contains(&value) {
538 return Err(Error::InvalidParameter(format!(
539 "Value {} is out of range [0.0, 1.0]",
540 value
541 )));
542 }
543
544 self.internal
545 .as_mut()
546 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
547 .set_parameter(id, value)?;
548
549 // Trigger callback if set
550 if let Some(ref callback) = self.parameter_change_callback {
551 callback(id, value);
552 }
553
554 Ok(())
555 }
556
557 /// Set a parameter value at a specific sample offset within the next process block.
558 ///
559 /// This is the sample-accurate building block for automation: call it once per
560 /// sub-block point (e.g. from [`ParameterAutomation::points_for_block`]) and the plugin
561 /// receives the changes at their offsets in the next `process_audio`. Like
562 /// [`Self::set_parameter`], `value` is normalized `0.0..=1.0`.
563 ///
564 /// `sample_offset` is clamped to the block. Under process isolation the offset **is** now
565 /// carried across the boundary and applied by the helper's in-process plugin.
566 ///
567 /// [`ParameterAutomation::points_for_block`]: crate::parameters::ParameterAutomation::points_for_block
568 pub fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
569 if !(0.0..=1.0).contains(&value) {
570 return Err(Error::InvalidParameter(format!(
571 "Value {} is out of range [0.0, 1.0]",
572 value
573 )));
574 }
575 self.internal
576 .as_mut()
577 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
578 .set_parameter_at(id, value, sample_offset)
579 }
580
581 /// Change the transport tempo (beats per minute) advertised to the plugin in the host
582 /// `ProcessContext`, taking effect on the **next** processed block — even while the plugin
583 /// is actively processing. Drives tempo-synced DSP (LFOs, synced delays, arpeggiators).
584 ///
585 /// `bpm` must be finite and greater than `0` (a non-positive tempo would freeze or reverse
586 /// the derived musical playhead). Works both in-process and across process isolation.
587 pub fn set_tempo(&mut self, bpm: f64) -> Result<()> {
588 if !(bpm.is_finite() && bpm > 0.0) {
589 return Err(Error::InvalidParameter(format!(
590 "tempo must be finite and positive, got {bpm}"
591 )));
592 }
593 self.internal
594 .as_mut()
595 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
596 .set_tempo(bpm)
597 }
598
599 /// Change the transport time signature advertised to the plugin in the host
600 /// `ProcessContext` (`numerator`/`denominator`, e.g. `7, 8`), taking effect on the
601 /// **next** processed block — even while the plugin is actively processing.
602 ///
603 /// `numerator` must be greater than `0` and `denominator` must be a power of two between
604 /// `1` and `16` (`1`, `2`, `4`, `8`, or `16`) — the standard note values a time signature
605 /// can denominate. Works both in-process and across process isolation.
606 pub fn set_time_signature(&mut self, numerator: i32, denominator: i32) -> Result<()> {
607 if numerator <= 0 {
608 return Err(Error::InvalidParameter(format!(
609 "time signature numerator must be positive, got {numerator}"
610 )));
611 }
612 if !matches!(denominator, 1 | 2 | 4 | 8 | 16) {
613 return Err(Error::InvalidParameter(format!(
614 "time signature denominator must be one of 1, 2, 4, 8, 16, got {denominator}"
615 )));
616 }
617 self.internal
618 .as_mut()
619 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
620 .set_time_signature(numerator, denominator)
621 }
622
623 /// Toggle the transport playing state advertised to the plugin in the host
624 /// `ProcessContext` (the `kPlaying` flag), taking effect on the **next** processed block —
625 /// even while the plugin is actively processing.
626 ///
627 /// While playing, the host advances the continuous and musical playhead each block; while
628 /// stopped, the playhead still advances but the plugin sees the transport as not playing
629 /// (so tempo-synced effects can react to a paused transport). Works both in-process and
630 /// across process isolation.
631 pub fn set_playing(&mut self, playing: bool) -> Result<()> {
632 self.internal
633 .as_mut()
634 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
635 .set_playing(playing)
636 }
637
638 /// Enumerate the plugin's units and their program lists (`IUnitInfo`).
639 ///
640 /// Returns an empty list for plugins that don't implement `IUnitInfo`. The root unit (id
641 /// `0`) is typically present. Works both in-process and across process isolation.
642 pub fn get_units(&self) -> Result<Vec<PluginUnit>> {
643 self.internal
644 .as_ref()
645 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
646 .get_units()
647 }
648
649 /// Select a program (preset) in a unit's program list (`IUnitInfo`).
650 ///
651 /// `unit_id` is a [`PluginUnit::id`] from [`get_units`](Self::get_units) (the root unit is
652 /// `0`); `program_index` is a 0-based index into that unit's [`PluginUnit::programs`].
653 /// Internally this locates the unit's program-change parameter (the controller parameter
654 /// tied to the unit with the VST3 `kIsProgramChange` flag) and sets it to the normalized
655 /// value `program_index / max(1, program_count - 1)`, driving both the controller (for the
656 /// editor/display) and the processor (for the audio DSP).
657 ///
658 /// Returns an error for an unknown unit, a unit with no program list, an out-of-range
659 /// index, a plugin that doesn't implement `IUnitInfo`, or a plugin running under process
660 /// isolation only if the helper cannot resolve the unit. Works both in-process and across
661 /// the isolation boundary.
662 pub fn select_program(&mut self, unit_id: i32, program_index: i32) -> Result<()> {
663 self.internal
664 .as_mut()
665 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
666 .select_program(unit_id, program_index)
667 }
668
669 /// The plugin's reported processing latency in samples (e.g. from look-ahead or
670 /// oversampling), via `IAudioProcessor::getLatencySamples`. Use it to delay-compensate
671 /// when aligning the plugin's output with other signals. `0` if it reports none. Works
672 /// both in-process and across process isolation.
673 pub fn latency_samples(&self) -> u32 {
674 self.internal
675 .as_ref()
676 .map(|i| i.latency_samples())
677 .unwrap_or(0)
678 }
679
680 /// The plugin's reported tail length in samples (how long it keeps producing output
681 /// after input stops — e.g. reverb/delay), via `IAudioProcessor::getTailSamples`. `0`
682 /// means no tail; `u32::MAX` means an infinite tail. Works both in-process and across
683 /// process isolation.
684 pub fn tail_samples(&self) -> u32 {
685 self.internal
686 .as_ref()
687 .map(|i| i.tail_samples())
688 .unwrap_or(0)
689 }
690
691 /// Resolve a MIDI controller to the parameter it's mapped to, via the plugin's
692 /// `IMidiMapping` (`getMidiControllerAssignment`).
693 ///
694 /// `bus` is the event input bus index (usually `0`), `channel` the 0-based MIDI channel,
695 /// and `cc` the MIDI controller number (`0–127`, or the VST3 specials such as `128`
696 /// aftertouch / `129` pitch-bend). Returns the parameter id the controller drives, or
697 /// `None` if the plugin doesn't implement `IMidiMapping` or the controller is unmapped.
698 /// Works both in-process and across process isolation.
699 pub fn midi_cc_to_parameter(&self, bus: i32, channel: i16, cc: u16) -> Option<u32> {
700 // VST3 controller numbers are 0..130 (0–127 MIDI CCs + the specials up to pitch-bend).
701 // Reject out-of-range values rather than forwarding a meaningless controller number.
702 if cc > 129 {
703 return None;
704 }
705 self.internal
706 .as_ref()?
707 .midi_cc_to_parameter(bus, channel, cc)
708 }
709
710 /// Get a parameter value by ID
711 pub fn get_parameter(&self, id: u32) -> Result<f64> {
712 self.internal
713 .as_ref()
714 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
715 .get_parameter(id)
716 }
717
718 /// Format a parameter value as the plugin itself would display it.
719 ///
720 /// VST3 keeps all parameter values normalized (0.0–1.0) and delegates
721 /// human-readable formatting to the plugin's controller. This asks the plugin to
722 /// render `normalized` for parameter `id`, returning exactly what its own UI would
723 /// show — e.g. `"440.00 Hz"`, `"-6.0 dB"`, `"Sine"`. Prefer this over
724 /// [`Parameter::format_value`], which can only approximate without the plugin's
725 /// internal mapping.
726 pub fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
727 self.internal
728 .as_ref()
729 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
730 .format_parameter(id, normalized)
731 }
732
733 /// Set a parameter by name
734 pub fn set_parameter_by_name(&mut self, name: &str, value: f64) -> Result<()> {
735 let params = self.get_parameters()?;
736 let param = params
737 .iter()
738 .find(|p| p.name == name)
739 .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))?;
740
741 self.set_parameter(param.id, value)
742 }
743
744 /// Find a parameter by name
745 pub fn find_parameter(&self, name: &str) -> Result<Parameter> {
746 let params = self.get_parameters()?;
747 params
748 .into_iter()
749 .find(|p| p.name == name)
750 .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))
751 }
752
753 /// Send a MIDI note on event
754 pub fn send_midi_note(&mut self, note: u8, velocity: u8, channel: MidiChannel) -> Result<()> {
755 if note > 127 {
756 return Err(Error::MidiError(format!("Invalid note number: {}", note)));
757 }
758 if velocity > 127 {
759 return Err(Error::MidiError(format!("Invalid velocity: {}", velocity)));
760 }
761
762 let event = MidiEvent::NoteOn {
763 channel,
764 note,
765 velocity,
766 };
767 self.send_midi_event(event)
768 }
769
770 /// Send a MIDI note off event
771 pub fn send_midi_note_off(&mut self, note: u8, channel: MidiChannel) -> Result<()> {
772 if note > 127 {
773 return Err(Error::MidiError(format!("Invalid note number: {}", note)));
774 }
775
776 let event = MidiEvent::NoteOff {
777 channel,
778 note,
779 velocity: 0,
780 };
781 self.send_midi_event(event)
782 }
783
784 /// Send a MIDI control change event
785 pub fn send_midi_cc(&mut self, controller: u8, value: u8, channel: MidiChannel) -> Result<()> {
786 if controller > 127 {
787 return Err(Error::MidiError(format!(
788 "Invalid controller number: {}",
789 controller
790 )));
791 }
792 if value > 127 {
793 return Err(Error::MidiError(format!("Invalid CC value: {}", value)));
794 }
795
796 let event = MidiEvent::ControlChange {
797 channel,
798 controller,
799 value,
800 };
801 self.send_midi_event(event)
802 }
803
804 /// Send a generic MIDI event
805 pub fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
806 self.internal
807 .as_mut()
808 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
809 .send_midi_event(event)
810 }
811
812 /// Schedule a MIDI event at a sample offset within the **next** [`process_audio`] block.
813 ///
814 /// Use this for sample-accurate sequencing: an event sent with `sample_offset = N` takes
815 /// effect `N` frames into the next processed block, rather than at its start. Keep the
816 /// offset within the upcoming block's frame count ([`Plugin::block_size`] is the maximum);
817 /// a negative offset is treated as 0, and an offset past the block end is plugin-defined.
818 ///
819 /// Works both in-process and across process isolation — the offset is carried across the
820 /// boundary and applied by the helper's in-process plugin.
821 ///
822 /// [`process_audio`]: Self::process_audio
823 pub fn send_midi_event_at(&mut self, event: MidiEvent, sample_offset: i32) -> Result<()> {
824 self.internal
825 .as_mut()
826 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
827 .send_midi_event_at(event, sample_offset)
828 }
829
830 /// Start a note and get a per-voice [`NoteId`](crate::midi::NoteId) handle for sending
831 /// per-note (MPE-style) expression to that exact voice via
832 /// [`send_note_expression`](Self::send_note_expression).
833 ///
834 /// Unlike [`send_midi_note`](Self::send_midi_note) (which uses a shared note id and can't be
835 /// individually expressed), this allocates a unique voice id. Pair it with
836 /// [`note_off`](Self::note_off). Per-note expression works both in-process and under
837 /// process isolation — the calls marshal across the boundary.
838 pub fn note_on(
839 &mut self,
840 channel: MidiChannel,
841 note: u8,
842 velocity: u8,
843 ) -> Result<crate::midi::NoteId> {
844 self.note_on_at(channel, note, velocity, 0)
845 }
846
847 /// [`note_on`](Self::note_on) scheduled at a sample offset within the next block.
848 pub fn note_on_at(
849 &mut self,
850 channel: MidiChannel,
851 note: u8,
852 velocity: u8,
853 sample_offset: i32,
854 ) -> Result<crate::midi::NoteId> {
855 self.internal
856 .as_mut()
857 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
858 .note_on(channel, note, velocity, sample_offset)
859 }
860
861 /// Release a note started with [`note_on`](Self::note_on).
862 pub fn note_off(&mut self, id: crate::midi::NoteId) -> Result<()> {
863 self.note_off_at(id, 0)
864 }
865
866 /// [`note_off`](Self::note_off) scheduled at a sample offset within the next block.
867 pub fn note_off_at(&mut self, id: crate::midi::NoteId, sample_offset: i32) -> Result<()> {
868 self.internal
869 .as_mut()
870 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
871 .note_off(id, sample_offset)
872 }
873
874 /// Send a per-note expression value for a voice (normalized `0.0..=1.0`; bipolar dimensions
875 /// like [`Tuning`](crate::midi::NoteExpressionType::Tuning) center at `0.5`). The plugin
876 /// must implement `INoteExpressionController` and the dimension must be one it advertises
877 /// (see [`note_expressions`](Self::note_expressions)).
878 pub fn send_note_expression(
879 &mut self,
880 id: crate::midi::NoteId,
881 kind: crate::midi::NoteExpressionType,
882 value: f64,
883 ) -> Result<()> {
884 self.send_note_expression_at(id, kind, value, 0)
885 }
886
887 /// [`send_note_expression`](Self::send_note_expression) scheduled at a sample offset.
888 pub fn send_note_expression_at(
889 &mut self,
890 id: crate::midi::NoteId,
891 kind: crate::midi::NoteExpressionType,
892 value: f64,
893 sample_offset: i32,
894 ) -> Result<()> {
895 if !(0.0..=1.0).contains(&value) {
896 return Err(Error::InvalidParameter(format!(
897 "note-expression value {value} out of range [0.0, 1.0]"
898 )));
899 }
900 self.internal
901 .as_mut()
902 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
903 .send_note_expression(id, kind, value, sample_offset)
904 }
905
906 /// Enumerate the per-note expression dimensions the plugin advertises for the given event
907 /// bus / channel (defaults: bus 0, channel 0), via `INoteExpressionController`. Empty if the
908 /// plugin doesn't implement it.
909 pub fn note_expressions(&self) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
910 self.internal
911 .as_ref()
912 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
913 .note_expressions(0, 0)
914 }
915
916 /// Start audio processing
917 pub fn start_processing(&mut self) -> Result<()> {
918 if self.is_processing {
919 return Ok(());
920 }
921
922 self.internal
923 .as_mut()
924 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
925 .start_processing()?;
926
927 self.is_processing = true;
928 Ok(())
929 }
930
931 /// Stop audio processing
932 pub fn stop_processing(&mut self) -> Result<()> {
933 if !self.is_processing {
934 return Ok(());
935 }
936
937 self.internal
938 .as_mut()
939 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
940 .stop_processing()?;
941
942 self.is_processing = false;
943 Ok(())
944 }
945
946 /// Process audio buffers
947 pub fn process_audio(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
948 if !self.is_processing {
949 return Err(Error::Other("Plugin is not processing".to_string()));
950 }
951
952 self.internal
953 .as_mut()
954 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
955 .process(buffers)?;
956
957 // Update audio levels
958 if let Ok(mut levels) = self.audio_levels.lock() {
959 levels.update_from_buffers(&buffers.outputs);
960
961 // Trigger audio callback if set
962 if let Some(ref callback) = self.audio_callback {
963 callback(&levels);
964 }
965 }
966
967 Ok(())
968 }
969
970 /// Get current output levels.
971 ///
972 /// Recovers automatically if the audio thread panicked while holding the lock
973 /// (poisoned mutex) rather than propagating the panic to the caller — metering
974 /// must never take down a UI thread polling it.
975 pub fn get_output_levels(&self) -> AudioLevels {
976 self.audio_levels
977 .lock()
978 .unwrap_or_else(|poisoned| poisoned.into_inner())
979 .clone()
980 }
981
982 /// Check if the plugin is currently processing
983 pub fn is_processing(&self) -> bool {
984 self.is_processing
985 }
986
987 /// Set a callback for parameter changes
988 pub fn on_parameter_change<F>(&mut self, callback: F)
989 where
990 F: Fn(u32, f64) + Send + 'static,
991 {
992 self.parameter_change_callback = Some(Box::new(callback));
993 }
994
995 /// Set a callback for audio processing (called after each process cycle)
996 pub fn on_audio_process<F>(&mut self, callback: F)
997 where
998 F: Fn(&AudioLevels) + Send + 'static,
999 {
1000 self.audio_callback = Some(Box::new(callback));
1001 }
1002
1003 /// Check if the plugin has an editor GUI
1004 pub fn has_editor(&self) -> bool {
1005 self.internal
1006 .as_ref()
1007 .map(|i| i.has_editor())
1008 .unwrap_or(false)
1009 }
1010
1011 /// Open the plugin editor window
1012 pub fn open_editor(&mut self, parent: WindowHandle) -> Result<()> {
1013 self.internal
1014 .as_mut()
1015 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1016 .open_editor(parent.0)
1017 }
1018
1019 /// Close the plugin editor window
1020 pub fn close_editor(&mut self) -> Result<()> {
1021 self.internal
1022 .as_mut()
1023 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1024 .close_editor()
1025 }
1026
1027 /// Get the preferred editor size
1028 pub fn get_editor_size(&self) -> Result<(i32, i32)> {
1029 self.internal
1030 .as_ref()
1031 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1032 .get_editor_size()
1033 }
1034
1035 /// Create a batch parameter update
1036 pub fn update_parameters<F>(&mut self, f: F) -> Result<()>
1037 where
1038 F: FnOnce(&mut ParameterUpdate) -> Result<()>,
1039 {
1040 let mut update = ParameterUpdate::new(self);
1041 f(&mut update)?;
1042 update.apply()
1043 }
1044
1045 /// Send MIDI panic (all notes off, all sounds off, reset controllers)
1046 pub fn midi_panic(&mut self) -> Result<()> {
1047 for i in 0..16 {
1048 if let Some(channel) = MidiChannel::from_index(i) {
1049 // All Notes Off
1050 self.send_midi_cc(123, 0, channel)?;
1051 // All Sounds Off
1052 self.send_midi_cc(120, 0, channel)?;
1053 // Reset All Controllers
1054 self.send_midi_cc(121, 0, channel)?;
1055 }
1056 }
1057 Ok(())
1058 }
1059
1060 /// Get parameter changes from plugin GUI
1061 /// Returns a vector of (parameter_id, normalized_value) pairs
1062 /// This should be called regularly to pick up parameter changes made through the plugin's GUI
1063 pub fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
1064 self.internal
1065 .as_ref()
1066 .map(|i| i.get_parameter_changes())
1067 .unwrap_or_default()
1068 }
1069
1070 /// Drain the ordered log of parameter-edit gestures the plugin's editor has reported since
1071 /// the last call.
1072 ///
1073 /// This is the richer superset of [`Self::get_parameter_changes`]: rather than just the
1074 /// value changes, it preserves the begin/change/end ordering of each gesture, so the host
1075 /// can tell a deliberate, completed edit (`BeginGesture` … `ValueChange`* … `EndGesture`)
1076 /// from a stream of intermediate drag values. Poll it regularly (e.g. each UI frame) while
1077 /// the editor is open; an empty vector means nothing was reported. Works across process
1078 /// isolation — gestures are marshalled back from the helper.
1079 ///
1080 /// See [`ParameterEdit`] / [`ParameterEditKind`].
1081 pub fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
1082 self.internal
1083 .as_mut()
1084 .map(|i| i.take_parameter_edits())
1085 .unwrap_or_default()
1086 }
1087
1088 /// Take the MIDI events the plugin has emitted (e.g. from an arpeggiator or MPE
1089 /// controller) since the last call, draining the internal buffer.
1090 ///
1091 /// Output MIDI is captured while the plugin processes audio, so poll this regularly
1092 /// (e.g. each UI frame) while the plugin is playing; an empty vector means the plugin
1093 /// emitted nothing. This works for process-isolated plugins too — emitted events are
1094 /// marshalled back alongside each processed block.
1095 ///
1096 /// The buffer is capped at 4096 events: if you never poll while a chatty plugin keeps
1097 /// emitting, the oldest events are dropped (silently) to bound memory.
1098 pub fn take_output_midi(&self) -> Vec<MidiEvent> {
1099 self.internal
1100 .as_ref()
1101 .map(|i| i.take_output_events())
1102 .unwrap_or_default()
1103 }
1104
1105 /// Get a `Send` handle for draining emitted MIDI from another thread without locking the
1106 /// audio thread (see [`OutputMidiConsumer`]). Returns `None` for an unloaded plugin or the
1107 /// process-isolation path. Useful with [`RealtimePluginRunner`](crate::RealtimePluginRunner):
1108 /// take the handle, move the plugin into the runner, and poll it from your UI thread while
1109 /// the audio thread renders.
1110 pub fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
1111 self.internal.as_ref().and_then(|i| i.output_midi_handle())
1112 }
1113
1114 /// Save the plugin's current state (parameters, internal settings, loaded preset) to
1115 /// an opaque byte blob.
1116 ///
1117 /// The bytes are the plugin's own serialized state — treat them as opaque and pair them
1118 /// with the plugin's identity ([`PluginInfo::uid`]); they only mean something to the
1119 /// same plugin. Persist them to restore a patch later with [`Self::load_state`], or to
1120 /// snapshot a session. Call this on the main thread (see the
1121 /// [threading model](https://docs.rs/vst3-host)).
1122 ///
1123 /// Works both in-process and across process isolation (the state blob is marshalled over
1124 /// the IPC boundary). Returns an error for plugins that don't implement state saving.
1125 pub fn save_state(&self) -> Result<Vec<u8>> {
1126 self.internal
1127 .as_ref()
1128 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1129 .save_state()
1130 }
1131
1132 /// Restore plugin state from a blob produced by [`Self::save_state`] on the *same*
1133 /// plugin. Applies to both the processor and the controller, so parameter values and
1134 /// the editor reflect the restored state.
1135 ///
1136 /// Passing bytes from a different plugin has undefined results (the plugin decides what
1137 /// to do with bytes it doesn't recognize). Call this on the main thread.
1138 pub fn load_state(&mut self, data: &[u8]) -> Result<()> {
1139 self.internal
1140 .as_mut()
1141 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1142 .load_state(data)
1143 }
1144
1145 /// Save this plugin's state to a file as a [`PluginPreset`] (JSON: the plugin's `uid`
1146 /// and name plus the opaque state blob). The embedded `uid` lets [`Self::load_preset`]
1147 /// reject a preset saved from a different plugin.
1148 pub fn save_preset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
1149 let info = self.info();
1150 let preset = PluginPreset {
1151 uid: info.uid.clone(),
1152 plugin_name: info.name.clone(),
1153 state: self.save_state()?,
1154 };
1155 let json = serde_json::to_vec_pretty(&preset)
1156 .map_err(|e| Error::Other(format!("serialize preset: {e}")))?;
1157 std::fs::write(path, json).map_err(|e| Error::Other(format!("write preset: {e}")))?;
1158 Ok(())
1159 }
1160
1161 /// Load a [`PluginPreset`] file written by [`Self::save_preset`] and apply its state.
1162 /// Returns an error if the preset's `uid` doesn't match this plugin (loading another
1163 /// plugin's state is undefined).
1164 pub fn load_preset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
1165 let bytes = std::fs::read(path).map_err(|e| Error::Other(format!("read preset: {e}")))?;
1166 let preset: PluginPreset = serde_json::from_slice(&bytes)
1167 .map_err(|e| Error::Other(format!("parse preset: {e}")))?;
1168 if preset.uid != self.info().uid {
1169 return Err(Error::Other(format!(
1170 "preset is for a different plugin ({}, expected {})",
1171 preset.plugin_name,
1172 self.info().name
1173 )));
1174 }
1175 self.load_state(&preset.state)
1176 }
1177
1178 /// Save this plugin's state to a standard Steinberg `.vstpreset` file.
1179 ///
1180 /// Unlike [`Self::save_preset`] (a JSON wrapper specific to this library), the
1181 /// `.vstpreset` container is the interchange format shared by VST3 hosts and plugins, so
1182 /// the file can be read by other hosts (and by the plugin's own preset browser). It wraps
1183 /// the same opaque bytes from [`Self::save_state`] in a single `"Comp"` (component state)
1184 /// chunk, tagged with this plugin's class id ([`PluginInfo::uid`]) so a loader can reject
1185 /// presets from a different plugin. Call this on the main thread.
1186 pub fn save_vstpreset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
1187 let state = self.save_state()?;
1188 let bytes = vstpreset::build(&self.info().uid, &state)?;
1189 std::fs::write(path, bytes).map_err(|e| Error::Other(format!("write vstpreset: {e}")))?;
1190 Ok(())
1191 }
1192
1193 /// Load a Steinberg `.vstpreset` file and apply its component state to this plugin.
1194 ///
1195 /// Parses the `.vstpreset` container written by [`Self::save_vstpreset`] (or another VST3
1196 /// host), extracts the `"Comp"` (component state) chunk and passes it to
1197 /// [`Self::load_state`]. Returns an error if the file's magic is invalid, or if its class
1198 /// id doesn't match this plugin (loading another plugin's state is undefined). Call this
1199 /// on the main thread.
1200 pub fn load_vstpreset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
1201 let bytes =
1202 std::fs::read(path).map_err(|e| Error::Other(format!("read vstpreset: {e}")))?;
1203 let parsed = vstpreset::parse(&bytes)?;
1204 if parsed.class_id != self.info().uid {
1205 return Err(Error::Other(format!(
1206 "vstpreset is for a different plugin (class id {}, expected {})",
1207 parsed.class_id,
1208 self.info().uid
1209 )));
1210 }
1211 self.load_state(&parsed.component_state)
1212 }
1213
1214 /// The OS process id of the isolated helper hosting this plugin, or `None` if it runs
1215 /// in-process. Useful for monitoring an isolated plugin's resource use.
1216 pub fn isolation_pid(&self) -> Option<u32> {
1217 self.internal.as_ref().and_then(|i| i.helper_pid())
1218 }
1219
1220 /// How many times this plugin has been recovered (helper respawned + reloaded), via either
1221 /// [`Self::recover`] or automatic recovery ([`Vst3HostBuilder::auto_recover_plugins`]).
1222 ///
1223 /// A recovery reloads the plugin from defaults — parameter values and loaded state are NOT
1224 /// replayed. With auto-recover on, a crash is otherwise invisible (the call returns `Ok`),
1225 /// so poll this count to detect that a reset happened and re-apply a saved
1226 /// [`save_state`](Self::save_state) snapshot.
1227 ///
1228 /// [`Vst3HostBuilder::auto_recover_plugins`]: crate::Vst3HostBuilder::auto_recover_plugins
1229 pub fn recovery_count(&self) -> u64 {
1230 self.internal
1231 .as_ref()
1232 .map(|i| i.recovery_count())
1233 .unwrap_or(0)
1234 }
1235
1236 /// Total number of output audio channels across the plugin's output buses.
1237 ///
1238 /// Reflects the plugin's actual bus layout (mono / stereo / surround / multi-bus), not a
1239 /// stereo assumption — useful for sizing meters or output buffers. Returns 2 if unknown.
1240 pub fn output_channel_count(&self) -> usize {
1241 self.internal
1242 .as_ref()
1243 .map(|i| i.output_channel_count())
1244 .unwrap_or(2)
1245 }
1246
1247 /// Poll for an editor resize the plugin requested via VST3's `IPlugFrame` since the last
1248 /// call, as `(width, height)` in pixels, or `None`.
1249 ///
1250 /// Plugins with resizable editors call back to ask the host to resize the window hosting
1251 /// their view. Poll this on your UI thread (e.g. each frame) while the editor is open and
1252 /// resize your editor container to match. Only the in-process editor path reports this.
1253 pub fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
1254 self.internal
1255 .as_ref()
1256 .and_then(|i| i.take_editor_resize_request())
1257 }
1258
1259 /// Recover a process-isolated plugin whose helper has crashed.
1260 ///
1261 /// When an isolated plugin's helper process dies, calls return [`Error::PluginCrashed`]
1262 /// and the host itself stays alive. This respawns the helper and reloads the plugin
1263 /// from the same path and audio settings, restarting processing if it was running.
1264 ///
1265 /// **The reloaded plugin starts from its default state** — parameter values and any
1266 /// loaded preset are lost. Snapshot with [`Self::save_state`] beforehand and
1267 /// [`Self::load_state`] after recovering to preserve them. Returns an error for
1268 /// in-process plugins (an in-process crash takes down the whole host) and if the
1269 /// reload itself fails.
1270 pub fn recover(&mut self) -> Result<()> {
1271 self.internal
1272 .as_mut()
1273 .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1274 .recover()
1275 }
1276}
1277
1278/// Platform-specific window handle
1279pub struct WindowHandle(pub(crate) *mut std::ffi::c_void);
1280
1281impl WindowHandle {
1282 /// Create from a raw window handle
1283 ///
1284 /// # Safety
1285 /// The pointer must be a valid window handle for the platform
1286 pub unsafe fn from_raw(handle: *mut std::ffi::c_void) -> Self {
1287 Self(handle)
1288 }
1289}
1290
1291// Safe Send implementation - the window handle is platform-specific
1292unsafe impl Send for WindowHandle {}
1293
1294#[cfg(target_os = "macos")]
1295impl WindowHandle {
1296 /// Create from an NSView pointer on macOS
1297 pub fn from_nsview(view: *mut std::ffi::c_void) -> Self {
1298 Self(view)
1299 }
1300}
1301
1302#[cfg(target_os = "windows")]
1303impl WindowHandle {
1304 /// Create from an HWND on Windows
1305 pub fn from_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
1306 Self(hwnd)
1307 }
1308}
1309
1310#[cfg(target_os = "linux")]
1311impl WindowHandle {
1312 /// Create from an X11 window id on Linux (for VST3 `X11EmbedWindowID`).
1313 ///
1314 /// The VST3 X11 platform type expects the window id itself as the handle value,
1315 /// not a pointer to it.
1316 pub fn from_x11(window_id: u32) -> Self {
1317 Self(window_id as usize as *mut std::ffi::c_void)
1318 }
1319}
1320
1321/// Build and parse the standard Steinberg `.vstpreset` container format.
1322///
1323/// Layout (all multi-byte integers little-endian, matching the SDK's `PresetFile`):
1324///
1325/// - Header (48 bytes): magic `b"VST3"` (4) + version `i32` = 1 (4) + 32-char ASCII class
1326/// id (the plugin's FUID hex) (32) + `i64` byte offset from the start of the file to the
1327/// chunk list (8).
1328/// - Body: the chunk payloads, written back to back after the header. We write a single
1329/// `"Comp"` (component state) chunk.
1330/// - Chunk list (at the header's list offset): magic `b"List"` (4) + entry count `i32` (4),
1331/// then per entry: 4-byte chunk id + `i64` absolute offset + `i64` size.
1332mod vstpreset {
1333 use crate::error::{Error, Result};
1334
1335 const MAGIC: &[u8; 4] = b"VST3";
1336 const LIST_MAGIC: &[u8; 4] = b"List";
1337 const COMPONENT_CHUNK: &[u8; 4] = b"Comp";
1338 const VERSION: i32 = 1;
1339 const CLASS_ID_LEN: usize = 32;
1340 const HEADER_SIZE: usize = 4 + 4 + CLASS_ID_LEN + 8;
1341
1342 /// A parsed `.vstpreset` container.
1343 pub(super) struct Parsed {
1344 /// The 32-char ASCII class id from the header.
1345 pub class_id: String,
1346 /// The bytes of the `"Comp"` (component state) chunk.
1347 pub component_state: Vec<u8>,
1348 }
1349
1350 /// Build a `.vstpreset` file wrapping `component_state` in a single component chunk,
1351 /// tagged with `class_id` (a 32-char ASCII FUID hex string).
1352 pub(super) fn build(class_id: &str, component_state: &[u8]) -> Result<Vec<u8>> {
1353 let class_bytes = class_id.as_bytes();
1354 if class_bytes.len() != CLASS_ID_LEN || !class_id.is_ascii() {
1355 return Err(Error::Other(format!(
1356 "vstpreset class id must be {CLASS_ID_LEN} ASCII chars, got {:?}",
1357 class_id
1358 )));
1359 }
1360
1361 let comp_offset = HEADER_SIZE as i64;
1362 let comp_size = component_state.len() as i64;
1363 let list_offset = HEADER_SIZE + component_state.len();
1364
1365 let mut out = Vec::with_capacity(list_offset + 8 + 24);
1366 // Header.
1367 out.extend_from_slice(MAGIC);
1368 out.extend_from_slice(&VERSION.to_le_bytes());
1369 out.extend_from_slice(class_bytes);
1370 out.extend_from_slice(&(list_offset as i64).to_le_bytes());
1371 // Body.
1372 out.extend_from_slice(component_state);
1373 // Chunk list.
1374 out.extend_from_slice(LIST_MAGIC);
1375 out.extend_from_slice(&1i32.to_le_bytes());
1376 out.extend_from_slice(COMPONENT_CHUNK);
1377 out.extend_from_slice(&comp_offset.to_le_bytes());
1378 out.extend_from_slice(&comp_size.to_le_bytes());
1379
1380 Ok(out)
1381 }
1382
1383 /// Parse a `.vstpreset` file, extracting the class id and the component-state chunk.
1384 pub(super) fn parse(bytes: &[u8]) -> Result<Parsed> {
1385 if bytes.len() < HEADER_SIZE {
1386 return Err(Error::Other("vstpreset too short for header".to_string()));
1387 }
1388 if &bytes[0..4] != MAGIC {
1389 return Err(Error::Other(format!(
1390 "bad vstpreset magic: expected {:?}, got {:?}",
1391 MAGIC,
1392 &bytes[0..4]
1393 )));
1394 }
1395 let version = read_i32(&bytes[4..8]);
1396 if version != VERSION {
1397 return Err(Error::Other(format!(
1398 "unsupported vstpreset version {version} (expected {VERSION})"
1399 )));
1400 }
1401 let class_id = String::from_utf8(bytes[8..8 + CLASS_ID_LEN].to_vec())
1402 .map_err(|e| Error::Other(format!("vstpreset class id not UTF-8: {e}")))?;
1403 let list_offset = read_i64(&bytes[8 + CLASS_ID_LEN..HEADER_SIZE]);
1404 if list_offset < HEADER_SIZE as i64 || list_offset as usize > bytes.len() {
1405 return Err(Error::Other(format!(
1406 "vstpreset chunk-list offset {list_offset} out of bounds (len {})",
1407 bytes.len()
1408 )));
1409 }
1410 let list = &bytes[list_offset as usize..];
1411 if list.len() < 8 || &list[0..4] != LIST_MAGIC {
1412 return Err(Error::Other(
1413 "vstpreset chunk list missing or malformed".to_string(),
1414 ));
1415 }
1416 let count = read_i32(&list[4..8]);
1417 if count < 0 {
1418 return Err(Error::Other("vstpreset negative entry count".to_string()));
1419 }
1420 let mut cursor = 8;
1421 for _ in 0..count {
1422 if list.len() < cursor + 20 {
1423 return Err(Error::Other(
1424 "vstpreset chunk-list entry truncated".to_string(),
1425 ));
1426 }
1427 let id = &list[cursor..cursor + 4];
1428 let offset = read_i64(&list[cursor + 4..cursor + 12]);
1429 let size = read_i64(&list[cursor + 12..cursor + 20]);
1430 cursor += 20;
1431 if id == COMPONENT_CHUNK {
1432 if offset < 0 || size < 0 {
1433 return Err(Error::Other(
1434 "vstpreset component chunk has negative offset/size".to_string(),
1435 ));
1436 }
1437 let start = offset as usize;
1438 let end = start
1439 .checked_add(size as usize)
1440 .ok_or_else(|| Error::Other("vstpreset chunk size overflow".to_string()))?;
1441 if end > bytes.len() {
1442 return Err(Error::Other(format!(
1443 "vstpreset component chunk [{start}..{end}] out of bounds (len {})",
1444 bytes.len()
1445 )));
1446 }
1447 return Ok(Parsed {
1448 class_id,
1449 component_state: bytes[start..end].to_vec(),
1450 });
1451 }
1452 }
1453 Err(Error::Other(
1454 "vstpreset has no component (\"Comp\") chunk".to_string(),
1455 ))
1456 }
1457
1458 fn read_i32(b: &[u8]) -> i32 {
1459 i32::from_le_bytes([b[0], b[1], b[2], b[3]])
1460 }
1461
1462 fn read_i64(b: &[u8]) -> i64 {
1463 i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
1464 }
1465}
1466
1467#[cfg(test)]
1468mod output_midi_consumer_tests {
1469 use super::*;
1470
1471 fn note(n: u8) -> MidiEvent {
1472 MidiEvent::NoteOn {
1473 channel: MidiChannel::Ch1,
1474 note: n,
1475 velocity: 100,
1476 }
1477 }
1478
1479 #[test]
1480 fn drains_in_order_and_drops_oldest_when_full() {
1481 let q = Arc::new(ArrayQueue::new(2));
1482 let consumer = OutputMidiConsumer::from_queue(q.clone());
1483
1484 // force_push mirrors what process() does: when full, the oldest is dropped.
1485 q.force_push(note(60));
1486 q.force_push(note(61));
1487 q.force_push(note(62)); // capacity 2 → drops note 60
1488
1489 assert_eq!(consumer.drain(), vec![note(61), note(62)]);
1490 // Drained: now empty.
1491 assert_eq!(consumer.pop(), None);
1492 assert_eq!(consumer.drain(), vec![]);
1493 }
1494
1495 #[test]
1496 fn handle_is_send_and_shares_the_queue_across_threads() {
1497 let q = Arc::new(ArrayQueue::new(8));
1498 let consumer = OutputMidiConsumer::from_queue(q.clone());
1499 // Push from another thread (the audio side is a different thread in practice).
1500 let producer = q.clone();
1501 std::thread::spawn(move || {
1502 producer.force_push(note(64));
1503 })
1504 .join()
1505 .unwrap();
1506 assert_eq!(consumer.pop(), Some(note(64)));
1507 }
1508}
1509
1510#[cfg(test)]
1511mod vstpreset_tests {
1512 use super::vstpreset;
1513
1514 const TEST_CLASS_ID: &str = "0123456789ABCDEF0123456789ABCDEF";
1515
1516 #[test]
1517 fn build_parse_round_trip() {
1518 let state = b"opaque plugin state \x00\x01\x02\xff bytes".to_vec();
1519 let bytes = vstpreset::build(TEST_CLASS_ID, &state).expect("build");
1520
1521 // Sanity-check the header layout.
1522 assert_eq!(&bytes[0..4], b"VST3");
1523 assert_eq!(
1524 i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1525 1
1526 );
1527 assert_eq!(&bytes[8..40], TEST_CLASS_ID.as_bytes());
1528
1529 let parsed = vstpreset::parse(&bytes).expect("parse");
1530 assert_eq!(parsed.class_id, TEST_CLASS_ID);
1531 assert_eq!(parsed.component_state, state);
1532 }
1533
1534 #[test]
1535 fn round_trip_empty_state() {
1536 let bytes = vstpreset::build(TEST_CLASS_ID, &[]).expect("build");
1537 let parsed = vstpreset::parse(&bytes).expect("parse");
1538 assert_eq!(parsed.class_id, TEST_CLASS_ID);
1539 assert!(parsed.component_state.is_empty());
1540 }
1541
1542 #[test]
1543 fn build_rejects_wrong_length_class_id() {
1544 assert!(vstpreset::build("short", b"x").is_err());
1545 }
1546
1547 #[test]
1548 fn parse_rejects_bad_magic() {
1549 let mut bytes = vstpreset::build(TEST_CLASS_ID, b"x").expect("build");
1550 bytes[0] = b'X';
1551 assert!(vstpreset::parse(&bytes).is_err());
1552 }
1553
1554 #[test]
1555 fn parse_rejects_truncated_header() {
1556 assert!(vstpreset::parse(b"VST3").is_err());
1557 }
1558
1559 #[test]
1560 fn parse_rejects_out_of_bounds_list_offset() {
1561 let mut bytes = vstpreset::build(TEST_CLASS_ID, b"hello").expect("build");
1562 // Corrupt the list offset (bytes 40..48) to point past the end.
1563 let bad = (bytes.len() as i64 + 100).to_le_bytes();
1564 bytes[40..48].copy_from_slice(&bad);
1565 assert!(vstpreset::parse(&bytes).is_err());
1566 }
1567}