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