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    /// 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}