Skip to main content

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}