Skip to main content

vst3_host/
plugin.rs

1//! VST3 plugin wrapper with safe API
2
3use crate::{
4    audio::{AudioBuffers, AudioBusLayout, AudioLevels, BusAudioBuffers},
5    error::{Error, Result},
6    midi::{
7        MidiChannel, MidiEvent, PluginEvent, PluginEventData, MAX_EVENT_PAYLOAD_BYTES,
8        MAX_EVENT_TEXT_UNITS,
9    },
10    parameters::{Parameter, ParameterUpdate},
11};
12use crossbeam_queue::ArrayQueue;
13use std::sync::{Arc, Mutex};
14
15/// A `Send` + `Sync` handle for draining the MIDI a plugin emits (arpeggiators, MPE, MIDI
16/// thru, …) without locking the audio thread.
17///
18/// Obtain one from [`Plugin::output_midi_handle`]. The plugin's audio thread pushes emitted
19/// events into a lock-free bounded queue; this handle pops them from any other thread (e.g. a
20/// UI poll loop) with no lock on either side — the lock-free counterpart to the audio-thread
21/// drain in [`Plugin::take_output_midi`]. When the queue is full the oldest event is dropped,
22/// so a host that stops polling can't grow it without bound.
23///
24/// Available for in-process plugins; the process-isolation path returns `None` (output MIDI
25/// crosses the boundary in the IPC responses instead).
26#[derive(Clone)]
27pub struct OutputMidiConsumer {
28    queue: Arc<ArrayQueue<PluginEvent>>,
29}
30
31impl OutputMidiConsumer {
32    pub(crate) fn from_queue(queue: Arc<ArrayQueue<PluginEvent>>) -> Self {
33        Self { queue }
34    }
35
36    /// Pop the oldest emitted event, or `None` if none are queued. Lock-free.
37    pub fn pop(&self) -> Option<MidiEvent> {
38        while let Some(event) = self.queue.pop() {
39            if let Some(midi) = event.to_midi() {
40                return Some(midi);
41            }
42        }
43        None
44    }
45
46    /// Drain all currently queued events in emission order into a `Vec`. Lock-free pops; the
47    /// returned `Vec` allocates on the calling thread (intended for a UI/control thread, not
48    /// the audio thread — use [`pop`](Self::pop) in a loop to stay allocation-free).
49    pub fn drain(&self) -> Vec<MidiEvent> {
50        let mut out = Vec::new();
51        while let Some(event) = self.pop() {
52            out.push(event);
53        }
54        out
55    }
56}
57
58/// A `Send` + `Sync` handle for draining every owned event a plugin emits.
59#[derive(Clone)]
60pub struct OutputEventConsumer {
61    queue: Arc<ArrayQueue<PluginEvent>>,
62}
63
64impl OutputEventConsumer {
65    pub(crate) fn from_queue(queue: Arc<ArrayQueue<PluginEvent>>) -> Self {
66        Self { queue }
67    }
68
69    /// Pop the oldest emitted event, or `None` if none are queued.
70    pub fn pop(&self) -> Option<PluginEvent> {
71        self.queue.pop()
72    }
73
74    /// Drain all currently queued events in emission order.
75    pub fn drain(&self) -> Vec<PluginEvent> {
76        let mut out = Vec::new();
77        while let Some(event) = self.pop() {
78            out.push(event);
79        }
80        out
81    }
82}
83
84/// Information about a VST3 plugin
85#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
86pub struct PluginInfo {
87    /// Full path to the VST3 bundle/file
88    pub path: std::path::PathBuf,
89    /// Plugin name
90    pub name: String,
91    /// Vendor/manufacturer name
92    pub vendor: String,
93    /// Plugin version
94    pub version: String,
95    /// Plugin category (e.g., "Fx", "Instrument")
96    pub category: String,
97    /// Unique plugin ID
98    pub uid: String,
99    /// Number of audio input buses
100    pub audio_inputs: u32,
101    /// Number of audio output buses
102    pub audio_outputs: u32,
103    /// Whether the plugin accepts MIDI input
104    pub has_midi_input: bool,
105    /// Whether the plugin produces MIDI output
106    pub has_midi_output: bool,
107    /// Whether the plugin has a GUI
108    pub has_gui: bool,
109}
110
111/// A saved plugin preset: the plugin's identity plus its opaque state blob.
112///
113/// Written/read by [`Plugin::save_preset`] / [`Plugin::load_preset`]. The `uid` lets a
114/// loader reject a preset that belongs to a different plugin (whose state bytes would be
115/// meaningless or harmful).
116#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
117pub struct PluginPreset {
118    /// The originating plugin's unique class id ([`PluginInfo::uid`]).
119    pub uid: String,
120    /// The originating plugin's display name (for friendly mismatch messages).
121    pub plugin_name: String,
122    /// The plugin's opaque serialized state (from [`Plugin::save_state`]).
123    pub state: Vec<u8>,
124}
125
126/// Why a plugin is being handed a state blob.
127///
128/// VST3 lets a plugin ask *where* the state it is being given came from: the host attaches an
129/// `IStreamAttributes` list to the `IBStream` it passes to `setState`, and the plugin reads the
130/// `PresetAttributes::kStateType` key from it. The SDK ships `Vst::Helpers::isProjectState()`
131/// for exactly this — it answers "yes" only for `StateType::kProject` and "this came from a
132/// preset" for every other value — and plugins use the answer to decide what to restore (a
133/// preset should not, for instance, drag a project's per-instance routing along with it).
134///
135/// Pass this to [`Plugin::load_state_with_context`]. [`Plugin::load_state`] uses
136/// [`StateContext::Project`]; [`Plugin::load_vstpreset`] and [`Plugin::load_preset`] use
137/// [`StateContext::Preset`] with the file they read.
138#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
139pub enum StateContext {
140    /// The blob is part of a host session/project restore.
141    ///
142    /// Tags the stream `StateType::kProject` — "the state is restored from a project loading
143    /// or it is saved in a project".
144    #[default]
145    Project,
146    /// The blob came from a standalone preset file.
147    ///
148    /// Tags the stream `StateType::kTrackPreset`. Of the three values the SDK defines,
149    /// `kProject` is explicitly the project case and `kDefault` is narrower than it looks —
150    /// "the state is restored from a preset (marked as *default*) or the host wants to store a
151    /// default state of the plug-in" — so claiming it for a preset the user picked would tell
152    /// the plugin this is its initialization patch. `kTrackPreset` ("the state is restored from
153    /// a track preset") is the SDK's remaining preset-file value, and it is what makes
154    /// `Vst::Helpers::isProjectState()` answer "came from a preset" rather than the
155    /// "host doesn't implement this" it returns when the attribute is missing entirely.
156    Preset {
157        /// Full path of the file the state was read from, when the caller knows it.
158        ///
159        /// Published as `PresetAttributes::kFilePathStringType` ("full file path string (if
160        /// available) where the preset comes from"); left off the stream when `None`.
161        ///
162        /// Text rather than a `PathBuf` because it is published as a UTF-16 stream attribute
163        /// and crosses the process-isolation boundary as JSON, neither of which can carry a
164        /// non-UTF-8 path. [`StateContext::preset_from_path`] does the lossy conversion once,
165        /// where it is visible.
166        path: Option<String>,
167    },
168}
169
170impl StateContext {
171    /// A preset load whose source file is unknown (state handed over in memory, say).
172    pub fn preset() -> Self {
173        Self::Preset { path: None }
174    }
175
176    /// A preset load from `path`, which the plugin will see as the stream's file path.
177    pub fn preset_from_path(path: impl AsRef<std::path::Path>) -> Self {
178        Self::Preset {
179            path: Some(path.as_ref().to_string_lossy().into_owned()),
180        }
181    }
182
183    /// The source file this state came from, when one is known.
184    pub fn file_path(&self) -> Option<&std::path::Path> {
185        match self {
186            Self::Project => None,
187            Self::Preset { path } => path.as_deref().map(std::path::Path::new),
188        }
189    }
190}
191
192/// The two independent state streams defined by VST3.
193///
194/// This stays private to the crate. Public callers continue to exchange an opaque `Vec<u8>`;
195/// the versioned envelope below lets that API preserve both streams while still accepting the
196/// raw component blobs returned by older releases.
197pub(crate) struct StateSnapshot {
198    pub component: Vec<u8>,
199    pub controller: Option<Vec<u8>>,
200}
201
202const STATE_SNAPSHOT_MAGIC: &[u8; 16] = b"VST3HOST_STATE\0\0";
203const STATE_SNAPSHOT_VERSION: u32 = 1;
204const STATE_SNAPSHOT_HEADER_SIZE: usize = 16 + 4 + 4 + 4;
205const NO_CONTROLLER_STATE: u32 = u32::MAX;
206/// Preserve the pre-envelope state capacity: component and controller payloads may together use
207/// the same 64 MiB that a host-provided `MemoryStream` permits.
208const MAX_STATE_SNAPSHOT_PAYLOAD_BYTES: usize =
209    crate::internal::com_implementations::MAX_STREAM_BYTES;
210pub(crate) const MAX_STATE_SNAPSHOT_BYTES: usize =
211    STATE_SNAPSHOT_HEADER_SIZE + MAX_STATE_SNAPSHOT_PAYLOAD_BYTES;
212
213pub(crate) fn encode_state_snapshot(snapshot: &StateSnapshot) -> Result<Vec<u8>> {
214    let component_len = u32::try_from(snapshot.component.len())
215        .map_err(|_| Error::Other("component state is too large".to_string()))?;
216    let controller_len = match snapshot.controller.as_ref() {
217        Some(state) => u32::try_from(state.len())
218            .map_err(|_| Error::Other("controller state is too large".to_string()))?,
219        None => NO_CONTROLLER_STATE,
220    };
221    let payload_size = snapshot
222        .component
223        .len()
224        .checked_add(snapshot.controller.as_ref().map_or(0, Vec::len))
225        .ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
226    let total = STATE_SNAPSHOT_HEADER_SIZE
227        .checked_add(payload_size)
228        .ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
229    if payload_size > MAX_STATE_SNAPSHOT_PAYLOAD_BYTES {
230        return Err(Error::Other(format!(
231            "combined plugin state payload is too large ({payload_size} bytes, maximum \
232             {MAX_STATE_SNAPSHOT_PAYLOAD_BYTES})"
233        )));
234    }
235
236    let mut out = Vec::with_capacity(total);
237    out.extend_from_slice(STATE_SNAPSHOT_MAGIC);
238    out.extend_from_slice(&STATE_SNAPSHOT_VERSION.to_le_bytes());
239    out.extend_from_slice(&component_len.to_le_bytes());
240    out.extend_from_slice(&controller_len.to_le_bytes());
241    out.extend_from_slice(&snapshot.component);
242    if let Some(controller) = snapshot.controller.as_ref() {
243        out.extend_from_slice(controller);
244    }
245    Ok(out)
246}
247
248pub(crate) fn decode_state_snapshot(data: &[u8]) -> Result<StateSnapshot> {
249    if !data.starts_with(STATE_SNAPSHOT_MAGIC) {
250        if data.len() > MAX_STATE_SNAPSHOT_PAYLOAD_BYTES {
251            return Err(Error::Other(format!(
252                "legacy component state is too large ({} bytes, maximum \
253                 {MAX_STATE_SNAPSHOT_PAYLOAD_BYTES})",
254                data.len()
255            )));
256        }
257        return Ok(StateSnapshot {
258            component: data.to_vec(),
259            controller: None,
260        });
261    }
262    if data.len() < STATE_SNAPSHOT_HEADER_SIZE {
263        return Err(Error::Other(
264            "truncated vst3-host state snapshot header".to_string(),
265        ));
266    }
267    let version = read_snapshot_u32(&data[16..20]);
268    if version != STATE_SNAPSHOT_VERSION {
269        return Err(Error::Other(format!(
270            "unsupported vst3-host state snapshot version {version}"
271        )));
272    }
273    let component_len = read_snapshot_u32(&data[20..24]) as usize;
274    let encoded_controller_len = read_snapshot_u32(&data[24..28]);
275    let controller_len =
276        (encoded_controller_len != NO_CONTROLLER_STATE).then_some(encoded_controller_len as usize);
277    let payload_size = component_len
278        .checked_add(controller_len.unwrap_or(0))
279        .ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
280    let expected = STATE_SNAPSHOT_HEADER_SIZE
281        .checked_add(payload_size)
282        .ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
283    if expected != data.len() || expected > MAX_STATE_SNAPSHOT_BYTES {
284        return Err(Error::Other(format!(
285            "invalid vst3-host state snapshot size (header describes {expected} bytes, got {})",
286            data.len()
287        )));
288    }
289    let component_start = STATE_SNAPSHOT_HEADER_SIZE;
290    let component_end = component_start + component_len;
291    Ok(StateSnapshot {
292        component: data[component_start..component_end].to_vec(),
293        controller: controller_len.map(|len| data[component_end..component_end + len].to_vec()),
294    })
295}
296
297fn read_snapshot_u32(bytes: &[u8]) -> u32 {
298    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
299}
300
301/// A plugin unit (from `IUnitInfo`) and its program list, if any.
302///
303/// Units form a hierarchy (via [`parent_id`](Self::parent_id)); a unit may carry a named
304/// program list (e.g. a synth's factory patches). Query with [`Plugin::get_units`].
305#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
306pub struct PluginUnit {
307    /// Unit id (unique within the plugin; the root unit is conventionally `0`).
308    pub id: i32,
309    /// Parent unit id, or `-1` for the root.
310    pub parent_id: i32,
311    /// Unit display name.
312    pub name: String,
313    /// Program-list id associated with this unit, or `None` when it has no program list.
314    pub program_list_id: Option<i32>,
315    /// Program names in this unit's program list (empty if the unit has none).
316    pub programs: Vec<String>,
317}
318
319/// A plugin-provided name for a MIDI pitch in a particular program.
320#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
321pub struct ProgramPitchName {
322    /// MIDI pitch number (`0..=127`).
323    pub midi_pitch: i16,
324    /// Plugin-provided display name.
325    pub name: String,
326}
327
328/// Host automation mode reported to controllers implementing `IAutomationState`.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
330pub enum AutomationState {
331    /// Automation is disabled.
332    Off,
333    /// Read existing automation.
334    Read,
335    /// Write automation.
336    Write,
337    /// Read and write automation.
338    ReadWrite,
339}
340
341/// What kind of parameter-edit gesture event a plugin's editor reported.
342///
343/// VST3 editors bracket a user gesture with `beginEdit`/`endEdit` (e.g. mouse-down /
344/// mouse-up on a knob) and report the values in between with `performEdit`. Capturing the
345/// brackets — not just the value changes — lets a host distinguish a deliberate, completed
346/// edit from intermediate drag values, coalesce automation into one undo step, or know when a
347/// gesture is in progress.
348#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
349pub enum ParameterEditKind {
350    /// The user started editing this parameter (`IComponentHandler::beginEdit`).
351    BeginGesture,
352    /// The parameter's value changed (`IComponentHandler::performEdit`); carries the new
353    /// normalized value in [`ParameterEdit::value`].
354    ValueChange,
355    /// The user finished editing this parameter (`IComponentHandler::endEdit`).
356    EndGesture,
357}
358
359/// A single parameter-edit gesture event reported by a plugin's own editor.
360///
361/// Drained in order via [`Plugin::take_parameter_edits`]. This is the richer superset of
362/// [`Plugin::get_parameter_changes`]: where that drains only the value changes, this preserves
363/// the begin/change/end ordering so a host can reconstruct each gesture.
364#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
365pub struct ParameterEdit {
366    /// Parameter id the gesture targets.
367    pub id: u32,
368    /// Which gesture phase this event is.
369    pub kind: ParameterEditKind,
370    /// The new normalized value (`0.0..=1.0`), present only for
371    /// [`ParameterEditKind::ValueChange`]; `None` for begin/end brackets.
372    pub value: Option<f64>,
373}
374
375/// A control-plane action requested by a plugin through `IComponentHandler2`.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
377pub enum ProgressKind {
378    /// Deferred restoration of plugin state.
379    AsyncStateRestoration,
380    /// Work performed for the plugin's user interface.
381    UiBackgroundTask,
382    /// A newer SDK progress kind unknown to this host version.
383    Other(u32),
384}
385
386/// An equality-safe normalized progress value.
387///
388/// Construction rejects NaN, infinities, and values outside `0.0..=1.0`, allowing progress
389/// notifications to retain exact equality and lossless process-isolation serialization.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
391pub struct ProgressValue(u64);
392
393impl ProgressValue {
394    /// Construct a valid normalized progress value.
395    pub fn new(value: f64) -> Option<Self> {
396        (value.is_finite() && (0.0..=1.0).contains(&value)).then(|| Self(value.to_bits()))
397    }
398
399    /// Return the normalized floating-point value.
400    pub fn get(self) -> f64 {
401        f64::from_bits(self.0)
402    }
403}
404
405/// One entry in a context menu a plugin asked the host to display.
406///
407/// The [`item_id`](Self::item_id) is assigned by the host for one popup and is the value to pass
408/// to [`Plugin::execute_context_menu_item`]. The plugin's own `tag` is included for diagnostics;
409/// it is not necessarily unique.
410#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
411pub struct ContextMenuItem {
412    /// Host-assigned id, unique within the containing popup.
413    pub item_id: u32,
414    /// Plugin-provided menu label.
415    pub name: String,
416    /// Plugin-provided command tag.
417    pub tag: i32,
418    /// Raw VST3 `IContextMenuItem::Flags` bits.
419    pub flags: i32,
420}
421
422impl ContextMenuItem {
423    /// Whether this entry is a separator.
424    pub fn is_separator(&self) -> bool {
425        self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsSeparator as i32 != 0
426    }
427
428    /// Whether this entry is disabled.
429    pub fn is_disabled(&self) -> bool {
430        self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsDisabled as i32 != 0
431    }
432
433    /// Whether this entry is checked.
434    pub fn is_checked(&self) -> bool {
435        self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsChecked as i32 != 0
436    }
437
438    /// Whether this entry begins a logical group.
439    pub fn is_group_start(&self) -> bool {
440        let group_start = vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsGroupStart as i32;
441        self.flags & group_start == group_start
442    }
443
444    /// Whether this entry ends a logical group.
445    pub fn is_group_end(&self) -> bool {
446        let group_end = vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsGroupEnd as i32;
447        self.flags & group_end == group_end
448    }
449}
450
451/// An owned snapshot sent by a plug-in through VST3's data-exchange API.
452///
453/// The plug-in-owned exchange block is only valid during the controller callback. The host
454/// copies it on a non-realtime thread, so values returned by
455/// [`Plugin::take_data_exchange_blocks`] remain valid after that callback returns.
456#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
457pub struct DataExchangeBlock {
458    /// Host queue identifier assigned when the processor opened the queue.
459    pub queue_id: u32,
460    /// Processor-defined context identifier associated with the queue.
461    pub user_context_id: u32,
462    /// Queue-local block identifier.
463    pub block_id: u32,
464    /// Complete block payload.
465    #[serde(with = "crate::process_isolation::state_codec")]
466    pub data: Vec<u8>,
467}
468
469/// A control-plane action requested by a plugin through its host callbacks.
470///
471/// # Ordering
472///
473/// Notifications keep their order relative to each other, and so do the
474/// [`ParameterEdit`]s from [`Plugin::take_parameter_edits`] — but the two streams are buffered
475/// independently, so their relative order is not preserved. In particular, the
476/// [`GroupEditStarted`](Self::GroupEditStarted) / [`GroupEditFinished`](Self::GroupEditFinished)
477/// bracket cannot be correlated with the edits that fell inside it: treat it as "the plugin is
478/// currently in a grouped gesture", not as a delimiter around specific parameter edits.
479#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
480pub enum HostNotification {
481    /// The plugin changed whether its state needs saving.
482    DirtyChanged(bool),
483    /// The plugin asked the host to open an editor, optionally by view name.
484    OpenEditorRequested {
485        /// Requested view name, or `None` for the default editor.
486        name: Option<String>,
487    },
488    /// The plugin began a grouped edit.
489    GroupEditStarted,
490    /// The plugin finished a grouped edit.
491    GroupEditFinished,
492    /// The plugin selected a different unit in its own UI.
493    UnitSelectionChanged {
494        /// Newly selected unit id.
495        unit_id: i32,
496    },
497    /// A program list, or one program in it, changed in the plugin.
498    ProgramListChanged {
499        /// Program-list id.
500        list_id: i32,
501        /// Changed program, or `None` when the whole list changed.
502        program_index: Option<i32>,
503    },
504    /// The plugin changed its bus/channel-to-unit assignments.
505    UnitByBusChanged,
506    /// The plugin began a bounded progress operation.
507    ProgressStarted {
508        /// Host-assigned progress operation id.
509        id: u64,
510        /// Kind of work the plugin is performing.
511        kind: ProgressKind,
512        /// Optional plugin-provided description.
513        description: Option<String>,
514    },
515    /// The plugin updated an existing progress operation.
516    ProgressUpdated {
517        /// Host-assigned progress operation id.
518        id: u64,
519        /// Normalized progress in the inclusive `0.0..=1.0` range.
520        value: ProgressValue,
521    },
522    /// The plugin finished an existing progress operation.
523    ProgressFinished {
524        /// Host-assigned progress operation id.
525        id: u64,
526    },
527    /// The plugin populated a context menu and asked the host to display it.
528    ///
529    /// After showing the menu, call [`Plugin::execute_context_menu_item`] for the chosen entry,
530    /// or [`Plugin::dismiss_context_menu`] if it was dismissed. Either call releases the
531    /// plugin-owned menu targets retained for this popup.
532    ContextMenuRequested {
533        /// Host-assigned popup id.
534        menu_id: u64,
535        /// Parameter the menu belongs to, or `None` for a view-wide menu.
536        parameter_id: Option<u32>,
537        /// Horizontal popup coordinate in the plugin view.
538        x: i32,
539        /// Vertical popup coordinate in the plugin view.
540        y: i32,
541        /// Menu entries in display order.
542        items: Vec<ContextMenuItem>,
543    },
544}
545
546impl HostNotification {
547    pub(crate) fn invalidates_unit_cache(&self) -> bool {
548        matches!(
549            self,
550            Self::ProgramListChanged { .. } | Self::UnitByBusChanged
551        )
552    }
553}
554
555/// What a plugin asked the host to re-read, reported through `IComponentHandler::restartComponent`
556/// and drained with [`Plugin::take_restart_flags`].
557///
558/// A plugin raises these when something about it changed behind the host's back — a preset load
559/// that renamed its parameters, a mode switch that changed its latency, an oversampling toggle
560/// that changed its bus layout. Poll this alongside [`Plugin::take_parameter_edits`] and respond
561/// directly, or use [`Plugin::service_host_requests`] for lifecycle-sensitive changes:
562///
563/// - [`param_values_changed`](Self::param_values_changed) — re-read values with
564///   [`Plugin::get_parameters`].
565/// - [`param_titles_changed`](Self::param_titles_changed) — re-read the parameter list itself
566///   (ids, names, ranges may all differ).
567/// - [`latency_changed`](Self::latency_changed) — re-read [`Plugin::latency_samples`] and adjust
568///   delay compensation. Requires stopping processing first, per the VST3 spec.
569/// - [`io_changed`](Self::io_changed) — the bus layout changed; re-query
570///   [`Plugin::bus_arrangements`] and reconfigure. Nothing is rebuilt for you.
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
572pub struct RestartFlags(i32);
573
574impl RestartFlags {
575    /// Wrap the raw VST3 `RestartFlags` bitmask.
576    pub(crate) fn from_bits(bits: i32) -> Self {
577        Self(bits)
578    }
579
580    /// The raw VST3 bitmask (`Steinberg::Vst::RestartFlags`), for flags this type doesn't name.
581    pub fn bits(self) -> i32 {
582        self.0
583    }
584
585    /// True when the plugin raised no flags since the last drain.
586    pub fn is_empty(self) -> bool {
587        self.0 == 0
588    }
589
590    fn has(self, flag: i32) -> bool {
591        self.0 & flag != 0
592    }
593
594    /// `kParamValuesChanged`: parameter *values* changed (e.g. a preset was loaded).
595    pub fn param_values_changed(self) -> bool {
596        self.has(vst3::Steinberg::Vst::RestartFlags_::kParamValuesChanged)
597    }
598
599    /// `kReloadComponent`: the component must be recreated by the outer host.
600    pub fn reload_component(self) -> bool {
601        self.has(vst3::Steinberg::Vst::RestartFlags_::kReloadComponent)
602    }
603
604    /// `kParamTitlesChanged`: the parameter list itself changed (ids, names, ranges, count).
605    pub fn param_titles_changed(self) -> bool {
606        self.has(vst3::Steinberg::Vst::RestartFlags_::kParamTitlesChanged)
607    }
608
609    /// `kLatencyChanged`: the plugin's reported latency changed.
610    pub fn latency_changed(self) -> bool {
611        self.has(vst3::Steinberg::Vst::RestartFlags_::kLatencyChanged)
612    }
613
614    /// `kIoChanged`: the plugin's bus configuration changed.
615    pub fn io_changed(self) -> bool {
616        self.has(vst3::Steinberg::Vst::RestartFlags_::kIoChanged)
617    }
618
619    /// `kMidiCCAssignmentChanged`: controller-to-parameter mappings changed.
620    pub fn midi_cc_assignment_changed(self) -> bool {
621        self.has(vst3::Steinberg::Vst::RestartFlags_::kMidiCCAssignmentChanged)
622    }
623
624    /// `kNoteExpressionChanged`: note-expression metadata changed.
625    pub fn note_expression_changed(self) -> bool {
626        self.has(vst3::Steinberg::Vst::RestartFlags_::kNoteExpressionChanged)
627    }
628
629    /// `kIoTitlesChanged`: bus names changed.
630    pub fn io_titles_changed(self) -> bool {
631        self.has(vst3::Steinberg::Vst::RestartFlags_::kIoTitlesChanged)
632    }
633
634    /// `kPrefetchableSupportChanged`: prefetch support changed.
635    pub fn prefetchable_support_changed(self) -> bool {
636        self.has(vst3::Steinberg::Vst::RestartFlags_::kPrefetchableSupportChanged)
637    }
638
639    /// `kRoutingInfoChanged`: routing metadata changed.
640    pub fn routing_info_changed(self) -> bool {
641        self.has(vst3::Steinberg::Vst::RestartFlags_::kRoutingInfoChanged)
642    }
643
644    /// `kKeyswitchChanged`: keyswitch metadata changed.
645    pub fn keyswitch_changed(self) -> bool {
646        self.has(vst3::Steinberg::Vst::RestartFlags_::kKeyswitchChanged)
647    }
648
649    /// `kParamIDMappingChanged`: processor/controller parameter-id mappings changed.
650    pub fn param_id_mapping_changed(self) -> bool {
651        self.has(vst3::Steinberg::Vst::RestartFlags_::kParamIDMappingChanged)
652    }
653}
654
655#[cfg(test)]
656mod restart_flag_tests {
657    use super::RestartFlags;
658    use vst3::Steinberg::Vst::RestartFlags_ as Flags;
659
660    #[test]
661    fn exposes_every_vst3_restart_flag() {
662        let bits = Flags::kReloadComponent
663            | Flags::kIoChanged
664            | Flags::kParamValuesChanged
665            | Flags::kLatencyChanged
666            | Flags::kParamTitlesChanged
667            | Flags::kMidiCCAssignmentChanged
668            | Flags::kNoteExpressionChanged
669            | Flags::kIoTitlesChanged
670            | Flags::kPrefetchableSupportChanged
671            | Flags::kRoutingInfoChanged
672            | Flags::kKeyswitchChanged
673            | Flags::kParamIDMappingChanged;
674        let flags = RestartFlags::from_bits(bits);
675        assert!(flags.reload_component());
676        assert!(flags.io_changed());
677        assert!(flags.param_values_changed());
678        assert!(flags.latency_changed());
679        assert!(flags.param_titles_changed());
680        assert!(flags.midi_cc_assignment_changed());
681        assert!(flags.note_expression_changed());
682        assert!(flags.io_titles_changed());
683        assert!(flags.prefetchable_support_changed());
684        assert!(flags.routing_info_changed());
685        assert!(flags.keyswitch_changed());
686        assert!(flags.param_id_mapping_changed());
687    }
688}
689
690/// How the plugin should run: real-time, read-ahead prefetch, or offline rendering.
691/// Maps to VST3 `kRealtime` / `kPrefetch` / `kOffline`; plugins may switch quality or
692/// look-ahead accordingly. Defaults to [`ProcessMode::Realtime`].
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
694pub enum ProcessMode {
695    /// Real-time / live processing (the default; `kRealtime`).
696    #[default]
697    Realtime,
698    /// Real-time playback with data available ahead of the play cursor (`kPrefetch`).
699    ///
700    /// A plugin implementing VST3 `IPrefetchableSupport` may reject this mode when it reports
701    /// that it is never, or not yet, prefetchable.
702    Prefetch,
703    /// Offline / non-real-time processing such as a render or bounce (`kOffline`).
704    Offline,
705}
706
707/// VST3 plugin instance
708#[allow(clippy::type_complexity)] // callback fields are Box<dyn Fn...>; intrinsic to the API
709pub struct Plugin {
710    // Internal state is hidden from public API
711    pub(crate) info: PluginInfo,
712    pub(crate) compatibility: Vec<crate::discovery::ClassCompatibility>,
713    pub(crate) is_processing: bool,
714    /// Configured sample rate (exposed via [`Plugin::sample_rate`]).
715    pub(crate) sample_rate: f64,
716    /// Configured max block size (exposed via [`Plugin::block_size`]).
717    pub(crate) block_size: usize,
718    pub(crate) audio_levels: Arc<Mutex<AudioLevels>>,
719    pub(crate) parameter_change_callback: Option<Box<dyn Fn(u32, f64) + Send + 'static>>,
720    pub(crate) audio_callback: Option<Box<dyn Fn(&AudioLevels) + Send + 'static>>,
721
722    // These will be populated by the actual implementation
723    pub(crate) internal: Option<Box<dyn PluginInternal>>,
724}
725
726// Internal trait for hiding implementation details
727pub(crate) trait PluginInternal: Send {
728    fn set_parameter(&mut self, id: u32, value: f64) -> Result<()>;
729    /// Schedule a parameter change at a sample offset within the next process block.
730    /// Defaults to a block-start change (ignores the offset) for implementations that don't
731    /// support sample-accurate scheduling.
732    fn set_parameter_at(&mut self, id: u32, value: f64, _sample_offset: i32) -> Result<()> {
733        self.set_parameter(id, value)
734    }
735    /// Queue automation for the processor without touching `IEditController`.
736    ///
737    /// Audio callbacks use this path because controller methods belong to the main-thread
738    /// domain. Implementations without a split component/controller path may fall back to the
739    /// ordinary setter.
740    fn queue_processor_parameter_at(
741        &mut self,
742        id: u32,
743        value: f64,
744        sample_offset: i32,
745    ) -> Result<()> {
746        self.set_parameter_at(id, value, sample_offset)
747    }
748    fn get_parameter(&self, id: u32) -> Result<f64>;
749    fn get_all_parameters(&self) -> Result<Vec<Parameter>>;
750    fn format_parameter(&self, id: u32, normalized: f64) -> Result<String>;
751    fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()>;
752    /// Query the current per-bus channel counts and activation state.
753    fn audio_bus_layout(&self) -> Result<AudioBusLayout> {
754        Err(Error::Other(
755            "bus-aware audio processing is not supported for this plugin".to_string(),
756        ))
757    }
758    /// Process buffers while preserving VST3 bus boundaries.
759    fn process_buses(&mut self, _buffers: &mut BusAudioBuffers) -> Result<()> {
760        Err(Error::Other(
761            "bus-aware audio processing is not supported for this plugin".to_string(),
762        ))
763    }
764    /// Re-run `setupProcessing` for a new sample rate / block size. Defaults to unsupported
765    /// for implementations that don't support it.
766    fn reconfigure(&mut self, _sample_rate: f64, _block_size: usize) -> Result<()> {
767        Err(Error::Other(
768            "runtime reconfigure is not supported for this plugin".to_string(),
769        ))
770    }
771    /// Switch the plugin's process mode (real-time vs offline), re-running `setupProcessing`.
772    /// Defaults to unsupported for implementations that don't support it.
773    fn set_process_mode(&mut self, _mode: crate::plugin::ProcessMode) -> Result<()> {
774        Err(Error::Other(
775            "process mode switching is not supported for this plugin".to_string(),
776        ))
777    }
778    /// Query each audio bus's current speaker arrangement. Defaults to unsupported.
779    fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
780        Err(Error::Other(
781            "bus arrangement query is not supported for this plugin".to_string(),
782        ))
783    }
784    /// Request specific speaker arrangements for the audio buses (re-runs `setupProcessing`).
785    /// Defaults to unsupported for implementations that don't support it.
786    fn set_bus_arrangements(
787        &mut self,
788        _inputs: &[crate::audio::SpeakerArrangement],
789        _outputs: &[crate::audio::SpeakerArrangement],
790    ) -> Result<()> {
791        Err(Error::Other(
792            "bus arrangement negotiation is not supported for this plugin".to_string(),
793        ))
794    }
795    /// Activate or deactivate a single bus (`IComponent::activateBus`). Defaults to
796    /// unsupported.
797    fn set_bus_active(
798        &mut self,
799        _media_type: crate::audio::MediaType,
800        _direction: crate::audio::BusDirection,
801        _bus_index: i32,
802        _active: bool,
803    ) -> Result<()> {
804        Err(Error::Other(
805            "bus activation is not supported for this plugin".to_string(),
806        ))
807    }
808    /// Update the transport tempo (BPM) advertised in the host `ProcessContext`, taking effect
809    /// on the next processed block. The caller validates `bpm > 0`. Defaults to unsupported
810    /// (overridden by the in-process and isolated implementations).
811    fn set_tempo(&mut self, _bpm: f64) -> Result<()> {
812        Err(Error::Other(
813            "runtime transport mutation is not supported for this plugin".to_string(),
814        ))
815    }
816    /// Update the transport time signature advertised in the host `ProcessContext`, taking
817    /// effect on the next processed block. The caller validates the numerator/denominator.
818    /// Defaults to unsupported.
819    fn set_time_signature(&mut self, _numerator: i32, _denominator: i32) -> Result<()> {
820        Err(Error::Other(
821            "runtime transport mutation is not supported for this plugin".to_string(),
822        ))
823    }
824    /// Toggle the transport playing state (`kPlaying`) in the host `ProcessContext`, taking
825    /// effect on the next processed block. Defaults to unsupported.
826    fn set_playing(&mut self, _playing: bool) -> Result<()> {
827        Err(Error::Other(
828            "runtime transport mutation is not supported for this plugin".to_string(),
829        ))
830    }
831    fn send_midi_event(&mut self, event: MidiEvent) -> Result<()>;
832    /// Schedule a MIDI event at a sample offset within the next process block.
833    /// Defaults to a block-start event (ignores the offset) for implementations that don't
834    /// support sample-accurate scheduling.
835    fn send_midi_event_at(&mut self, event: MidiEvent, _sample_offset: i32) -> Result<()> {
836        self.send_midi_event(event)
837    }
838    /// Send a fully owned VST3 event.
839    fn send_plugin_event(&mut self, _event: PluginEvent) -> Result<()> {
840        Err(Error::Other(
841            "owned VST3 events are not supported for this plugin".to_string(),
842        ))
843    }
844    /// Silence all notes currently tracked by the implementation.
845    fn midi_panic(&mut self) -> Result<()> {
846        for i in 0..16 {
847            if let Some(channel) = MidiChannel::from_index(i) {
848                for controller in [123, 120, 121] {
849                    self.send_midi_event(MidiEvent::ControlChange {
850                        channel,
851                        controller,
852                        value: 0,
853                    })?;
854                }
855            }
856        }
857        Ok(())
858    }
859    /// Start a note and return a per-voice [`NoteId`] for targeting note-expression. Default:
860    /// unsupported, for implementations that don't support per-note expression.
861    fn note_on(
862        &mut self,
863        _channel: MidiChannel,
864        _note: u8,
865        _velocity: u8,
866        _sample_offset: i32,
867    ) -> Result<crate::midi::NoteId> {
868        Err(Error::Other(
869            "per-note expression is not supported for this plugin".to_string(),
870        ))
871    }
872    /// Release a note started with [`Self::note_on`]. Default: unsupported.
873    fn note_off(&mut self, _id: crate::midi::NoteId, _sample_offset: i32) -> Result<()> {
874        Err(Error::Other(
875            "per-note expression is not supported for this plugin".to_string(),
876        ))
877    }
878    /// Send a per-note expression value (normalized 0..1) for a voice. Default: unsupported.
879    fn send_note_expression(
880        &mut self,
881        _id: crate::midi::NoteId,
882        _kind: crate::midi::NoteExpressionType,
883        _value: f64,
884        _sample_offset: i32,
885    ) -> Result<()> {
886        Err(Error::Other(
887            "per-note expression is not supported for this plugin".to_string(),
888        ))
889    }
890    /// Enumerate the per-note expressions the plugin advertises (`INoteExpressionController`).
891    /// Defaults to empty.
892    fn note_expressions(
893        &self,
894        _bus: i32,
895        _channel: i16,
896    ) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
897        Ok(Vec::new())
898    }
899    fn start_processing(&mut self) -> Result<()>;
900    fn stop_processing(&mut self) -> Result<()>;
901    fn has_editor(&self) -> bool;
902    fn open_editor(&mut self, parent: *mut std::ffi::c_void) -> Result<()>;
903    fn close_editor(&mut self) -> Result<()>;
904    fn get_editor_size(&self) -> Result<(i32, i32)>;
905    /// Whether the editor accepts host-driven size changes.
906    fn editor_can_resize(&self) -> bool {
907        false
908    }
909    /// Ask the open editor to accept a host-driven size change. The returned size includes any
910    /// constraint adjustment made by the plugin.
911    fn resize_editor(&mut self, _width: i32, _height: i32) -> Result<(i32, i32)> {
912        Err(Error::Other("plugin editor is not resizable".to_string()))
913    }
914    /// Set the editor's logical-to-physical content scale. Returns `false` when the view does not
915    /// implement `IPlugViewContentScaleSupport`.
916    fn set_editor_scale_factor(&mut self, _factor: f32) -> Result<bool> {
917        Ok(false)
918    }
919    /// Service the Linux `IRunLoop` registrations the plugin's editor made
920    /// (fire due timers, dispatch ready file descriptors). No-op by default
921    /// (non-Linux, or process isolation where the editor isn't bridged).
922    fn service_run_loop(&mut self) {}
923    fn get_parameter_changes(&self) -> Vec<(u32, f64)>;
924    /// Drain the ordered parameter-edit gesture log (begin/change/end) the plugin's editor
925    /// reported since the last call. Defaults to empty for implementations that don't capture
926    /// gestures.
927    fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
928        Vec::new()
929    }
930    /// Drain ordered requests reported through `IComponentHandler2`.
931    fn take_host_notifications(&mut self) -> Vec<HostNotification> {
932        Vec::new()
933    }
934    /// Dispatch any main-thread data-exchange blocks to the controller and drain owned host
935    /// snapshots. Background-dispatched queues are copied into the same bounded snapshot sink.
936    fn take_data_exchange_blocks(&mut self) -> Vec<DataExchangeBlock> {
937        Vec::new()
938    }
939    /// Execute one entry from a pending plugin context-menu popup.
940    fn execute_context_menu_item(&mut self, _menu_id: u64, _item_id: u32) -> Result<()> {
941        Err(Error::Other(
942            "plugin context menus are not supported".to_string(),
943        ))
944    }
945    /// Dismiss a pending plugin context-menu popup without choosing an entry.
946    fn dismiss_context_menu(&mut self, _menu_id: u64) -> Result<()> {
947        Err(Error::Other(
948            "plugin context menus are not supported".to_string(),
949        ))
950    }
951    /// Take the `restartComponent` flags the plugin has raised since the last call. Defaults to
952    /// empty for implementations that don't record them.
953    fn take_restart_flags(&mut self) -> RestartFlags {
954        RestartFlags::default()
955    }
956    /// Drain and service restart requests which require a component lifecycle transition.
957    fn service_host_requests(&mut self) -> Result<RestartFlags> {
958        Ok(self.take_restart_flags())
959    }
960    /// Take the MIDI events the plugin has emitted since the last call. Defaults to empty
961    /// for implementations that don't capture output MIDI.
962    fn take_output_events(&self) -> Vec<PluginEvent> {
963        Vec::new()
964    }
965    /// A lock-free handle for draining emitted MIDI from another thread. Defaults to `None`
966    /// for implementations without a shared in-process queue (e.g. process isolation).
967    fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
968        None
969    }
970    fn output_event_handle(&self) -> Option<OutputEventConsumer> {
971        None
972    }
973    /// Enumerate the plugin's units and their program lists (`IUnitInfo`). Defaults to empty
974    /// for implementations that don't query it.
975    fn get_units(&self) -> Result<Vec<PluginUnit>> {
976        Ok(Vec::new())
977    }
978    /// Select a program in a unit's program list. Defaults to unsupported (e.g. plugins
979    /// without `IUnitInfo`); implementations resolve the unit's program-change parameter and
980    /// set it to the index's normalized value.
981    fn select_program(&mut self, _unit_id: i32, _program_index: i32) -> Result<()> {
982        Err(Error::Other(
983            "program selection is not supported for this plugin".to_string(),
984        ))
985    }
986    fn selected_unit(&self) -> Result<Option<i32>> {
987        Ok(None)
988    }
989    fn select_unit(&mut self, _unit_id: i32) -> Result<()> {
990        Err(Error::Other(
991            "unit selection is not supported for this plugin".to_string(),
992        ))
993    }
994    fn program_pitch_names(
995        &self,
996        _program_list_id: i32,
997        _program_index: i32,
998    ) -> Result<Vec<ProgramPitchName>> {
999        Ok(Vec::new())
1000    }
1001    fn get_program_data(
1002        &self,
1003        _program_list_id: i32,
1004        _program_index: i32,
1005    ) -> Result<Option<Vec<u8>>> {
1006        Ok(None)
1007    }
1008    fn set_program_data(
1009        &mut self,
1010        _program_list_id: i32,
1011        _program_index: i32,
1012        _data: &[u8],
1013    ) -> Result<()> {
1014        Err(Error::Other(
1015            "program data is not supported for this plugin".to_string(),
1016        ))
1017    }
1018    fn get_unit_data(&self, _unit_id: i32) -> Result<Option<Vec<u8>>> {
1019        Ok(None)
1020    }
1021    fn set_unit_data(&mut self, _unit_id: i32, _data: &[u8]) -> Result<()> {
1022        Err(Error::Other(
1023            "unit data is not supported for this plugin".to_string(),
1024        ))
1025    }
1026    fn begin_host_edit(&mut self, _parameter_id: u32) -> Result<()> {
1027        Err(Error::Other(
1028            "host edit sessions are not supported for this plugin".to_string(),
1029        ))
1030    }
1031    fn end_host_edit(&mut self, _parameter_id: u32) -> Result<()> {
1032        Err(Error::Other(
1033            "host edit sessions are not supported for this plugin".to_string(),
1034        ))
1035    }
1036    fn send_midi_learn(&mut self, _bus: i32, _channel: i16, _controller: u16) -> Result<()> {
1037        Err(Error::Other(
1038            "MIDI learn is not supported for this plugin".to_string(),
1039        ))
1040    }
1041    fn set_automation_state(&mut self, _state: AutomationState) -> Result<()> {
1042        Err(Error::Other(
1043            "automation state is not supported for this plugin".to_string(),
1044        ))
1045    }
1046    /// Ask the controller to map a parameter id from a plugin class it replaces.
1047    fn remap_parameter_id(&self, _old_plugin_uid: &str, _old_param_id: u32) -> Result<Option<u32>> {
1048        Ok(None)
1049    }
1050    /// Processing latency in samples (`IAudioProcessor::getLatencySamples`). Defaults to 0.
1051    fn latency_samples(&self) -> u32 {
1052        0
1053    }
1054    /// Tail length in samples (`IAudioProcessor::getTailSamples`). Defaults to 0.
1055    fn tail_samples(&self) -> u32 {
1056        0
1057    }
1058    /// Resolve a MIDI controller `(bus, channel, cc)` to a parameter id via `IMidiMapping`.
1059    /// Defaults to `None` (plugin doesn't implement the interface, or no mapping).
1060    fn midi_cc_to_parameter(&self, _bus: i32, _channel: i16, _cc: u16) -> Option<u32> {
1061        None
1062    }
1063    /// Serialize the plugin's current state to an opaque byte blob.
1064    fn save_state(&self) -> Result<Vec<u8>> {
1065        Err(Error::Other(
1066            "state save/restore is not supported".to_string(),
1067        ))
1068    }
1069    /// Restore the plugin's state from a blob previously returned by [`Self::save_state`],
1070    /// telling the plugin what kind of restore this is via the stream's attributes.
1071    ///
1072    /// This is the method implementors override; [`Self::load_state`] is the project-restore
1073    /// shorthand that delegates here.
1074    fn load_state_with_context(&mut self, _data: &[u8], _context: &StateContext) -> Result<()> {
1075        Err(Error::Other(
1076            "state save/restore is not supported".to_string(),
1077        ))
1078    }
1079    /// Restore the plugin's state from a blob previously returned by [`Self::save_state`],
1080    /// as a project/session restore ([`StateContext::Project`]).
1081    fn load_state(&mut self, data: &[u8]) -> Result<()> {
1082        self.load_state_with_context(data, &StateContext::Project)
1083    }
1084    /// OS process id of the isolated helper, if this plugin runs out-of-process.
1085    fn helper_pid(&self) -> Option<u32> {
1086        None
1087    }
1088    /// Number of times this plugin has been recovered (respawned + reloaded). Defaults to 0
1089    /// for non-isolated plugins.
1090    fn recovery_count(&self) -> u64 {
1091        0
1092    }
1093    /// Recover from a crashed isolated helper by respawning and reloading. Only meaningful
1094    /// for process-isolated plugins.
1095    fn recover(&mut self) -> Result<()> {
1096        Err(Error::Other(
1097            "recovery is only supported for process-isolated plugins".to_string(),
1098        ))
1099    }
1100    /// The size the plugin's editor has requested (via `IPlugFrame`) since the last poll.
1101    fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
1102        None
1103    }
1104    /// Total output audio channels across the plugin's output buses. Defaults to 2.
1105    fn output_channel_count(&self) -> usize {
1106        2
1107    }
1108}
1109
1110impl Plugin {
1111    /// Get plugin information
1112    pub fn info(&self) -> &PluginInfo {
1113        &self.info
1114    }
1115
1116    /// Current/retired class-id replacement mappings advertised by this plug-in.
1117    ///
1118    /// These come from `moduleinfo.json` when present, otherwise from the factory's optional
1119    /// `IPluginCompatibility` class.
1120    pub fn class_compatibility(&self) -> &[crate::discovery::ClassCompatibility] {
1121        &self.compatibility
1122    }
1123
1124    /// Retired class ids which this loaded audio class replaces.
1125    pub fn replaced_class_ids(&self) -> &[String] {
1126        self.compatibility
1127            .iter()
1128            .find(|mapping| {
1129                crate::internal::utils::class_uid_matches(&mapping.new_class_id, &self.info.uid)
1130            })
1131            .map_or(&[], |mapping| mapping.old_class_ids.as_slice())
1132    }
1133
1134    /// The sample rate (Hz) this plugin was configured with at load.
1135    pub fn sample_rate(&self) -> f64 {
1136        self.sample_rate
1137    }
1138
1139    /// The maximum block size (frames per `process_audio` call) configured at load.
1140    pub fn block_size(&self) -> usize {
1141        self.block_size
1142    }
1143
1144    /// Reconfigure the plugin for a new sample rate and/or maximum block size, re-running the
1145    /// plugin's `setupProcessing` and rebuilding its audio buffers.
1146    ///
1147    /// Use this when the audio device's sample rate changes mid-session instead of reloading.
1148    /// The plugin must **not** be processing: call [`Self::stop_processing`] first, reconfigure,
1149    /// then [`Self::start_processing`] again. Returns an error if called while processing, or
1150    /// on an invalid sample rate / zero block size. Works both in-process and across process
1151    /// isolation.
1152    pub fn reconfigure(&mut self, sample_rate: f64, block_size: usize) -> Result<()> {
1153        if self.is_processing {
1154            return Err(Error::Other(
1155                "cannot reconfigure while processing; call stop_processing() first".to_string(),
1156            ));
1157        }
1158        if !(sample_rate.is_finite() && sample_rate > 0.0) {
1159            return Err(Error::InvalidParameter(format!(
1160                "sample rate must be finite and positive, got {sample_rate}"
1161            )));
1162        }
1163        if block_size == 0 || block_size > i32::MAX as usize {
1164            return Err(Error::InvalidParameter(format!(
1165                "block size must be in 1..={}, got {block_size}",
1166                i32::MAX
1167            )));
1168        }
1169
1170        self.internal
1171            .as_mut()
1172            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1173            .reconfigure(sample_rate, block_size)?;
1174
1175        self.sample_rate = sample_rate;
1176        self.block_size = block_size;
1177        Ok(())
1178    }
1179
1180    /// Switch the plugin between real-time and offline processing, re-running the plugin's
1181    /// `setupProcessing` so it can adjust quality / look-ahead for a faster-than-real-time
1182    /// bounce.
1183    ///
1184    /// Like [`Self::reconfigure`], the plugin must **not** be processing: call
1185    /// [`Self::stop_processing`] first. Returns an error if called while processing. Works both
1186    /// in-process and across process isolation.
1187    pub fn set_process_mode(&mut self, mode: ProcessMode) -> Result<()> {
1188        if self.is_processing {
1189            return Err(Error::Other(
1190                "cannot set process mode while processing; call stop_processing() first"
1191                    .to_string(),
1192            ));
1193        }
1194        self.internal
1195            .as_mut()
1196            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1197            .set_process_mode(mode)
1198    }
1199
1200    /// Query the current speaker arrangement of each audio input/output bus. Works both
1201    /// in-process and across process isolation.
1202    pub fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
1203        self.internal
1204            .as_ref()
1205            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1206            .bus_arrangements()
1207    }
1208
1209    /// Request specific speaker arrangements for the audio buses (e.g. force stereo, or a
1210    /// surround layout). The slices give one [`SpeakerArrangement`](crate::audio::SpeakerArrangement)
1211    /// per input bus and per output bus, in bus-index order.
1212    ///
1213    /// Re-runs the plugin's `setupProcessing`, so the plugin must **not** be processing (call
1214    /// [`Self::stop_processing`] first). A plugin may decline a requested layout and keep its
1215    /// own; re-query with [`Self::bus_arrangements`] to see what was actually applied. Errors
1216    /// while processing. Works both in-process and across process isolation.
1217    pub fn set_bus_arrangements(
1218        &mut self,
1219        inputs: &[crate::audio::SpeakerArrangement],
1220        outputs: &[crate::audio::SpeakerArrangement],
1221    ) -> Result<()> {
1222        if self.is_processing {
1223            return Err(Error::Other(
1224                "cannot set bus arrangements while processing; call stop_processing() first"
1225                    .to_string(),
1226            ));
1227        }
1228        self.internal
1229            .as_mut()
1230            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1231            .set_bus_arrangements(inputs, outputs)
1232    }
1233
1234    /// Activate or deactivate a single bus on the plugin (`IComponent::activateBus`).
1235    ///
1236    /// Hosts must explicitly activate the buses they intend to use; a plugin's secondary
1237    /// buses (sidechain / aux inputs, extra outputs) commonly start **inactive** and only
1238    /// receive/produce audio once activated. (The load sequence already activates the main
1239    /// audio and event buses, so call this to enable the rest.)
1240    ///
1241    /// `media_type` selects audio vs event buses and `direction` selects input vs output;
1242    /// `bus_index` is the 0-based index within that `(media_type, direction)` group (the
1243    /// same indexing as [`crate::discovery::BusLayout`]). `active` true activates, false
1244    /// deactivates.
1245    ///
1246    /// VST3 requires bus activation to happen while the component is **inactive** — i.e.
1247    /// before processing starts. This therefore returns an error if called while the plugin
1248    /// is processing; call [`Self::stop_processing`] first, activate the bus, then
1249    /// [`Self::start_processing`] again. Returns an error for an out-of-range `bus_index`,
1250    /// and under process isolation activation marshals across the boundary.
1251    pub fn set_bus_active(
1252        &mut self,
1253        media_type: crate::audio::MediaType,
1254        direction: crate::audio::BusDirection,
1255        bus_index: i32,
1256        active: bool,
1257    ) -> Result<()> {
1258        if self.is_processing {
1259            return Err(Error::Other(
1260                "cannot activate a bus while processing; call stop_processing() first".to_string(),
1261            ));
1262        }
1263        self.internal
1264            .as_mut()
1265            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1266            .set_bus_active(media_type, direction, bus_index, active)
1267    }
1268
1269    /// Get all parameters
1270    pub fn get_parameters(&self) -> Result<Vec<Parameter>> {
1271        self.internal
1272            .as_ref()
1273            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1274            .get_all_parameters()
1275    }
1276
1277    /// Set a parameter value by ID
1278    pub fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
1279        if !(0.0..=1.0).contains(&value) {
1280            return Err(Error::InvalidParameter(format!(
1281                "Value {} is out of range [0.0, 1.0]",
1282                value
1283            )));
1284        }
1285
1286        self.internal
1287            .as_mut()
1288            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1289            .set_parameter(id, value)?;
1290
1291        // Trigger callback if set
1292        if let Some(ref callback) = self.parameter_change_callback {
1293            callback(id, value);
1294        }
1295
1296        Ok(())
1297    }
1298
1299    /// Set a parameter value at a specific sample offset within the next process block.
1300    ///
1301    /// This is the sample-accurate building block for automation: call it once per
1302    /// sub-block point (e.g. from [`ParameterAutomation::points_for_block`]) and the plugin
1303    /// receives the changes at their offsets in the next `process_audio`. Like
1304    /// [`Self::set_parameter`], `value` is normalized `0.0..=1.0`.
1305    ///
1306    /// `sample_offset` is clamped to the block. Under process isolation the offset **is** now
1307    /// carried across the boundary and applied by the helper's in-process plugin.
1308    ///
1309    /// [`ParameterAutomation::points_for_block`]: crate::parameters::ParameterAutomation::points_for_block
1310    pub fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
1311        if !(0.0..=1.0).contains(&value) {
1312            return Err(Error::InvalidParameter(format!(
1313                "Value {} is out of range [0.0, 1.0]",
1314                value
1315            )));
1316        }
1317        self.internal
1318            .as_mut()
1319            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1320            .set_parameter_at(id, value, sample_offset)
1321    }
1322
1323    /// Audio-thread automation path: queue a processor point without invoking the controller
1324    /// or user callback. Values are validated on the control thread before commands enter the
1325    /// playback rings; this defensive check keeps direct internal callers honest.
1326    pub(crate) fn queue_processor_parameter_at(
1327        &mut self,
1328        id: u32,
1329        value: f64,
1330        sample_offset: i32,
1331    ) -> Result<()> {
1332        if !value.is_finite() || !(0.0..=1.0).contains(&value) {
1333            return Err(Error::InvalidParameter(format!(
1334                "Value {} is out of range [0.0, 1.0]",
1335                value
1336            )));
1337        }
1338        self.internal
1339            .as_mut()
1340            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1341            .queue_processor_parameter_at(id, value, sample_offset)
1342    }
1343
1344    /// Change the transport tempo (beats per minute) advertised to the plugin in the host
1345    /// `ProcessContext`, taking effect on the **next** processed block — even while the plugin
1346    /// is actively processing. Drives tempo-synced DSP (LFOs, synced delays, arpeggiators).
1347    ///
1348    /// `bpm` must be finite and greater than `0` (a non-positive tempo would freeze or reverse
1349    /// the derived musical playhead). Works both in-process and across process isolation.
1350    pub fn set_tempo(&mut self, bpm: f64) -> Result<()> {
1351        if !(bpm.is_finite() && bpm > 0.0) {
1352            return Err(Error::InvalidParameter(format!(
1353                "tempo must be finite and positive, got {bpm}"
1354            )));
1355        }
1356        self.internal
1357            .as_mut()
1358            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1359            .set_tempo(bpm)
1360    }
1361
1362    /// Change the transport time signature advertised to the plugin in the host
1363    /// `ProcessContext` (`numerator`/`denominator`, e.g. `7, 8`), taking effect on the
1364    /// **next** processed block — even while the plugin is actively processing.
1365    ///
1366    /// `numerator` must be greater than `0` and `denominator` must be a power of two between
1367    /// `1` and `16` (`1`, `2`, `4`, `8`, or `16`) — the standard note values a time signature
1368    /// can denominate. Works both in-process and across process isolation.
1369    pub fn set_time_signature(&mut self, numerator: i32, denominator: i32) -> Result<()> {
1370        if numerator <= 0 {
1371            return Err(Error::InvalidParameter(format!(
1372                "time signature numerator must be positive, got {numerator}"
1373            )));
1374        }
1375        if !matches!(denominator, 1 | 2 | 4 | 8 | 16) {
1376            return Err(Error::InvalidParameter(format!(
1377                "time signature denominator must be one of 1, 2, 4, 8, 16, got {denominator}"
1378            )));
1379        }
1380        self.internal
1381            .as_mut()
1382            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1383            .set_time_signature(numerator, denominator)
1384    }
1385
1386    /// Toggle the transport playing state advertised to the plugin in the host
1387    /// `ProcessContext` (the `kPlaying` flag), taking effect on the **next** processed block —
1388    /// even while the plugin is actively processing.
1389    ///
1390    /// While playing, the host advances the continuous and musical playhead each block; while
1391    /// stopped, the playhead still advances but the plugin sees the transport as not playing
1392    /// (so tempo-synced effects can react to a paused transport). Works both in-process and
1393    /// across process isolation.
1394    pub fn set_playing(&mut self, playing: bool) -> Result<()> {
1395        self.internal
1396            .as_mut()
1397            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1398            .set_playing(playing)
1399    }
1400
1401    /// Enumerate the plugin's units and their program lists (`IUnitInfo`).
1402    ///
1403    /// Returns an empty list for plugins that don't implement `IUnitInfo`. The root unit (id
1404    /// `0`) is typically present. Works both in-process and across process isolation.
1405    pub fn get_units(&self) -> Result<Vec<PluginUnit>> {
1406        self.internal
1407            .as_ref()
1408            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1409            .get_units()
1410    }
1411
1412    /// Select a program (preset) in a unit's program list (`IUnitInfo`).
1413    ///
1414    /// `unit_id` is a [`PluginUnit::id`] from [`get_units`](Self::get_units) (the root unit is
1415    /// `0`); `program_index` is a 0-based index into that unit's [`PluginUnit::programs`].
1416    /// Internally this locates the unit's program-change parameter (the controller parameter
1417    /// tied to the unit with the VST3 `kIsProgramChange` flag) and sets it to the normalized
1418    /// value `program_index / max(1, program_count - 1)`, driving both the controller (for the
1419    /// editor/display) and the processor (for the audio DSP).
1420    ///
1421    /// Returns an error for an unknown unit, a unit with no program list, an out-of-range
1422    /// index, a plugin that doesn't implement `IUnitInfo`, or a plugin running under process
1423    /// isolation only if the helper cannot resolve the unit. Works both in-process and across
1424    /// the isolation boundary.
1425    pub fn select_program(&mut self, unit_id: i32, program_index: i32) -> Result<()> {
1426        self.internal
1427            .as_mut()
1428            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1429            .select_program(unit_id, program_index)
1430    }
1431
1432    /// Return the unit currently selected by the plugin, or `None` without `IUnitInfo`.
1433    pub fn selected_unit(&self) -> Result<Option<i32>> {
1434        self.internal
1435            .as_ref()
1436            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1437            .selected_unit()
1438    }
1439
1440    /// Select a unit through `IUnitInfo::selectUnit`.
1441    pub fn select_unit(&mut self, unit_id: i32) -> Result<()> {
1442        self.internal
1443            .as_mut()
1444            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1445            .select_unit(unit_id)
1446    }
1447
1448    /// Query the plugin's MIDI-pitch names for one program.
1449    pub fn program_pitch_names(
1450        &self,
1451        program_list_id: i32,
1452        program_index: i32,
1453    ) -> Result<Vec<ProgramPitchName>> {
1454        self.internal
1455            .as_ref()
1456            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1457            .program_pitch_names(program_list_id, program_index)
1458    }
1459
1460    /// Read opaque per-program data, or `None` when `IProgramListData` is absent/unsupported.
1461    pub fn get_program_data(
1462        &self,
1463        program_list_id: i32,
1464        program_index: i32,
1465    ) -> Result<Option<Vec<u8>>> {
1466        self.internal
1467            .as_ref()
1468            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1469            .get_program_data(program_list_id, program_index)
1470    }
1471
1472    /// Restore opaque per-program data through `IProgramListData`.
1473    pub fn set_program_data(
1474        &mut self,
1475        program_list_id: i32,
1476        program_index: i32,
1477        data: &[u8],
1478    ) -> Result<()> {
1479        self.internal
1480            .as_mut()
1481            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1482            .set_program_data(program_list_id, program_index, data)
1483    }
1484
1485    /// Read opaque per-unit data, or `None` when `IUnitData` is absent/unsupported.
1486    pub fn get_unit_data(&self, unit_id: i32) -> Result<Option<Vec<u8>>> {
1487        self.internal
1488            .as_ref()
1489            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1490            .get_unit_data(unit_id)
1491    }
1492
1493    /// Restore opaque per-unit data through `IUnitData`.
1494    pub fn set_unit_data(&mut self, unit_id: i32, data: &[u8]) -> Result<()> {
1495        self.internal
1496            .as_mut()
1497            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1498            .set_unit_data(unit_id, data)
1499    }
1500
1501    /// Begin a controller-side host edit session for a parameter.
1502    pub fn begin_host_edit(&mut self, parameter_id: u32) -> Result<()> {
1503        self.internal
1504            .as_mut()
1505            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1506            .begin_host_edit(parameter_id)
1507    }
1508
1509    /// End a controller-side host edit session previously begun with [`Self::begin_host_edit`].
1510    pub fn end_host_edit(&mut self, parameter_id: u32) -> Result<()> {
1511        self.internal
1512            .as_mut()
1513            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1514            .end_host_edit(parameter_id)
1515    }
1516
1517    /// Notify a controller implementing `IMidiLearn` of live MIDI-controller input.
1518    pub fn send_midi_learn(&mut self, bus: i32, channel: i16, controller: u16) -> Result<()> {
1519        self.internal
1520            .as_mut()
1521            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1522            .send_midi_learn(bus, channel, controller)
1523    }
1524
1525    /// Report the host's automation mode to a controller implementing `IAutomationState`.
1526    pub fn set_automation_state(&mut self, state: AutomationState) -> Result<()> {
1527        self.internal
1528            .as_mut()
1529            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1530            .set_automation_state(state)
1531    }
1532
1533    /// Map a parameter id from an older/replaced plugin class through `IRemapParamID`.
1534    ///
1535    /// `old_plugin_uid` must be the canonical separator-free 32-hex-character VST3 class id.
1536    /// Returns `None` when the controller does not implement remapping or has no mapping for
1537    /// this class/id pair. On Windows the canonical id is converted to COM-compatible byte
1538    /// order before the controller is called. Works both in-process and across isolation.
1539    pub fn remap_parameter_id(
1540        &self,
1541        old_plugin_uid: &str,
1542        old_param_id: u32,
1543    ) -> Result<Option<u32>> {
1544        if crate::internal::utils::parse_class_uid(old_plugin_uid).is_none() {
1545            return Err(Error::InvalidParameter(
1546                "plugin UID must contain exactly 32 hexadecimal characters".to_string(),
1547            ));
1548        }
1549        self.internal
1550            .as_ref()
1551            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1552            .remap_parameter_id(old_plugin_uid, old_param_id)
1553    }
1554
1555    /// The plugin's reported processing latency in samples (e.g. from look-ahead or
1556    /// oversampling), via `IAudioProcessor::getLatencySamples`. Use it to delay-compensate
1557    /// when aligning the plugin's output with other signals. `0` if it reports none. Works
1558    /// both in-process and across process isolation.
1559    pub fn latency_samples(&self) -> u32 {
1560        self.internal
1561            .as_ref()
1562            .map(|i| i.latency_samples())
1563            .unwrap_or(0)
1564    }
1565
1566    /// The plugin's reported tail length in samples (how long it keeps producing output
1567    /// after input stops — e.g. reverb/delay), via `IAudioProcessor::getTailSamples`. `0`
1568    /// means no tail; `u32::MAX` means an infinite tail. Works both in-process and across
1569    /// process isolation.
1570    pub fn tail_samples(&self) -> u32 {
1571        self.internal
1572            .as_ref()
1573            .map(|i| i.tail_samples())
1574            .unwrap_or(0)
1575    }
1576
1577    /// Resolve a MIDI controller to the parameter it's mapped to, via the plugin's
1578    /// `IMidiMapping` (`getMidiControllerAssignment`).
1579    ///
1580    /// `bus` is the event input bus index (usually `0`), `channel` the 0-based MIDI channel,
1581    /// and `cc` the MIDI controller number (`0–127`, or the VST3 specials such as `128`
1582    /// aftertouch / `129` pitch-bend). Returns the parameter id the controller drives, or
1583    /// `None` if the plugin doesn't implement `IMidiMapping` or the controller is unmapped.
1584    /// Works both in-process and across process isolation.
1585    pub fn midi_cc_to_parameter(&self, bus: i32, channel: i16, cc: u16) -> Option<u32> {
1586        // VST3 controller numbers are 0..130 (0–127 MIDI CCs + the specials up to pitch-bend).
1587        // Reject out-of-range values rather than forwarding a meaningless controller number.
1588        if cc > 129 {
1589            return None;
1590        }
1591        self.internal
1592            .as_ref()?
1593            .midi_cc_to_parameter(bus, channel, cc)
1594    }
1595
1596    /// Get a parameter value by ID
1597    pub fn get_parameter(&self, id: u32) -> Result<f64> {
1598        self.internal
1599            .as_ref()
1600            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1601            .get_parameter(id)
1602    }
1603
1604    /// Format a parameter value as the plugin itself would display it.
1605    ///
1606    /// VST3 keeps all parameter values normalized (0.0–1.0) and delegates
1607    /// human-readable formatting to the plugin's controller. This asks the plugin to
1608    /// render `normalized` for parameter `id`, returning exactly what its own UI would
1609    /// show — e.g. `"440.00 Hz"`, `"-6.0 dB"`, `"Sine"`. Prefer this over
1610    /// [`Parameter::format_value`], which can only approximate without the plugin's
1611    /// internal mapping.
1612    pub fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
1613        self.internal
1614            .as_ref()
1615            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1616            .format_parameter(id, normalized)
1617    }
1618
1619    /// Set a parameter by name
1620    pub fn set_parameter_by_name(&mut self, name: &str, value: f64) -> Result<()> {
1621        let params = self.get_parameters()?;
1622        let param = params
1623            .iter()
1624            .find(|p| p.name == name)
1625            .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))?;
1626
1627        self.set_parameter(param.id, value)
1628    }
1629
1630    /// Find a parameter by name
1631    pub fn find_parameter(&self, name: &str) -> Result<Parameter> {
1632        let params = self.get_parameters()?;
1633        params
1634            .into_iter()
1635            .find(|p| p.name == name)
1636            .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))
1637    }
1638
1639    /// Send a MIDI note on event
1640    pub fn send_midi_note(&mut self, note: u8, velocity: u8, channel: MidiChannel) -> Result<()> {
1641        validate_note(note)?;
1642        validate_velocity(velocity)?;
1643
1644        let event = MidiEvent::NoteOn {
1645            channel,
1646            note,
1647            velocity,
1648        };
1649        self.send_midi_event(event)
1650    }
1651
1652    /// Send a MIDI note off event
1653    pub fn send_midi_note_off(&mut self, note: u8, channel: MidiChannel) -> Result<()> {
1654        validate_note(note)?;
1655
1656        let event = MidiEvent::NoteOff {
1657            channel,
1658            note,
1659            velocity: 0,
1660        };
1661        self.send_midi_event(event)
1662    }
1663
1664    /// Send a MIDI control change event
1665    pub fn send_midi_cc(&mut self, controller: u8, value: u8, channel: MidiChannel) -> Result<()> {
1666        validate_controller(controller)?;
1667        validate_cc_value(value)?;
1668
1669        let event = MidiEvent::ControlChange {
1670            channel,
1671            controller,
1672            value,
1673        };
1674        self.send_midi_event(event)
1675    }
1676
1677    /// Send a generic MIDI event.
1678    ///
1679    /// Every data field is range-checked against the MIDI spec (`0–127`, or `0–16383` for
1680    /// pitch bend) before the event reaches the plugin, the same way
1681    /// [`send_midi_note`](Self::send_midi_note) and [`send_midi_cc`](Self::send_midi_cc)
1682    /// check theirs.
1683    pub fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
1684        validate_midi_event(&event)?;
1685        self.internal
1686            .as_mut()
1687            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1688            .send_midi_event(event)
1689    }
1690
1691    /// Schedule a MIDI event at a sample offset within the **next** [`process_audio`] block.
1692    ///
1693    /// Use this for sample-accurate sequencing: an event sent with `sample_offset = N` takes
1694    /// effect `N` frames into the next processed block, rather than at its start. Keep the
1695    /// offset within the upcoming block's frame count ([`Plugin::block_size`] is the maximum);
1696    /// a negative offset is treated as 0, and an offset past the block end is plugin-defined.
1697    ///
1698    /// Works both in-process and across process isolation — the offset is carried across the
1699    /// boundary and applied by the helper's in-process plugin. The event's data fields are
1700    /// range-checked exactly as in [`send_midi_event`](Self::send_midi_event).
1701    ///
1702    /// [`process_audio`]: Self::process_audio
1703    pub fn send_midi_event_at(&mut self, event: MidiEvent, sample_offset: i32) -> Result<()> {
1704        validate_midi_event(&event)?;
1705        self.internal
1706            .as_mut()
1707            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1708            .send_midi_event_at(event, sample_offset)
1709    }
1710
1711    /// Send a fully owned VST3 event.
1712    ///
1713    /// This is the lossless event path for SysEx, note-expression text/integer values, chord,
1714    /// and scale events. Pointer-backed data is owned by `event` and kept alive until the plugin
1715    /// has consumed it.
1716    pub fn send_plugin_event(&mut self, event: PluginEvent) -> Result<()> {
1717        validate_plugin_event(&event)?;
1718        self.internal
1719            .as_mut()
1720            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1721            .send_plugin_event(event)
1722    }
1723
1724    /// Send MIDI SysEx bytes at block start.
1725    pub fn send_sysex(&mut self, bytes: Vec<u8>) -> Result<()> {
1726        self.send_plugin_event(PluginEvent::sysex(bytes))
1727    }
1728
1729    /// Send MIDI SysEx bytes at a sample offset within the next process block.
1730    pub fn send_sysex_at(&mut self, bytes: Vec<u8>, sample_offset: i32) -> Result<()> {
1731        self.send_plugin_event(PluginEvent::sysex(bytes).at(sample_offset))
1732    }
1733
1734    /// Start a note and get a per-voice [`NoteId`](crate::midi::NoteId) handle for sending
1735    /// per-note (MPE-style) expression to that exact voice via
1736    /// [`send_note_expression`](Self::send_note_expression).
1737    ///
1738    /// Unlike [`send_midi_note`](Self::send_midi_note) (which uses a shared note id and can't be
1739    /// individually expressed), this allocates a unique voice id. Pair it with
1740    /// [`note_off`](Self::note_off). Per-note expression works both in-process and under
1741    /// process isolation — the calls marshal across the boundary.
1742    ///
1743    /// `note` and `velocity` are range-checked (`0–127`), as in
1744    /// [`send_midi_note`](Self::send_midi_note).
1745    pub fn note_on(
1746        &mut self,
1747        channel: MidiChannel,
1748        note: u8,
1749        velocity: u8,
1750    ) -> Result<crate::midi::NoteId> {
1751        self.note_on_at(channel, note, velocity, 0)
1752    }
1753
1754    /// [`note_on`](Self::note_on) scheduled at a sample offset within the next block.
1755    ///
1756    /// `note` and `velocity` must be `0–127`, as for
1757    /// [`send_midi_note`](Self::send_midi_note).
1758    pub fn note_on_at(
1759        &mut self,
1760        channel: MidiChannel,
1761        note: u8,
1762        velocity: u8,
1763        sample_offset: i32,
1764    ) -> Result<crate::midi::NoteId> {
1765        validate_note(note)?;
1766        validate_velocity(velocity)?;
1767        self.internal
1768            .as_mut()
1769            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1770            .note_on(channel, note, velocity, sample_offset)
1771    }
1772
1773    /// Release a note started with [`note_on`](Self::note_on).
1774    pub fn note_off(&mut self, id: crate::midi::NoteId) -> Result<()> {
1775        self.note_off_at(id, 0)
1776    }
1777
1778    /// [`note_off`](Self::note_off) scheduled at a sample offset within the next block.
1779    pub fn note_off_at(&mut self, id: crate::midi::NoteId, sample_offset: i32) -> Result<()> {
1780        self.internal
1781            .as_mut()
1782            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1783            .note_off(id, sample_offset)
1784    }
1785
1786    /// Send a per-note expression value for a voice (normalized `0.0..=1.0`; bipolar dimensions
1787    /// like [`Tuning`](crate::midi::NoteExpressionType::Tuning) center at `0.5`). The plugin
1788    /// must implement `INoteExpressionController` and the dimension must be one it advertises
1789    /// (see [`note_expressions`](Self::note_expressions)).
1790    pub fn send_note_expression(
1791        &mut self,
1792        id: crate::midi::NoteId,
1793        kind: crate::midi::NoteExpressionType,
1794        value: f64,
1795    ) -> Result<()> {
1796        self.send_note_expression_at(id, kind, value, 0)
1797    }
1798
1799    /// [`send_note_expression`](Self::send_note_expression) scheduled at a sample offset.
1800    pub fn send_note_expression_at(
1801        &mut self,
1802        id: crate::midi::NoteId,
1803        kind: crate::midi::NoteExpressionType,
1804        value: f64,
1805        sample_offset: i32,
1806    ) -> Result<()> {
1807        if !(0.0..=1.0).contains(&value) {
1808            return Err(Error::InvalidParameter(format!(
1809                "note-expression value {value} out of range [0.0, 1.0]"
1810            )));
1811        }
1812        self.internal
1813            .as_mut()
1814            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1815            .send_note_expression(id, kind, value, sample_offset)
1816    }
1817
1818    /// Enumerate the per-note expression dimensions the plugin advertises for the given event
1819    /// bus / channel (defaults: bus 0, channel 0), via `INoteExpressionController`. Empty if the
1820    /// plugin doesn't implement it.
1821    pub fn note_expressions(&self) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
1822        self.internal
1823            .as_ref()
1824            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1825            .note_expressions(0, 0)
1826    }
1827
1828    /// Start audio processing
1829    pub fn start_processing(&mut self) -> Result<()> {
1830        if self.is_processing {
1831            return Ok(());
1832        }
1833
1834        self.internal
1835            .as_mut()
1836            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1837            .start_processing()?;
1838
1839        self.is_processing = true;
1840        Ok(())
1841    }
1842
1843    /// Stop audio processing
1844    pub fn stop_processing(&mut self) -> Result<()> {
1845        if !self.is_processing {
1846            return Ok(());
1847        }
1848
1849        self.internal
1850            .as_mut()
1851            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1852            .stop_processing()?;
1853
1854        self.is_processing = false;
1855        Ok(())
1856    }
1857
1858    /// Process audio buffers.
1859    ///
1860    /// # Thread safety
1861    ///
1862    /// Both playback paths call this from the **audio thread**, so any callback registered
1863    /// with [`Self::on_audio_process`] runs there too — see that method's warning.
1864    pub fn process_audio(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
1865        if !self.is_processing {
1866            return Err(Error::NotProcessing);
1867        }
1868
1869        self.internal
1870            .as_mut()
1871            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1872            .process(buffers)?;
1873
1874        // Update audio levels
1875        if let Ok(mut levels) = self.audio_levels.lock() {
1876            levels.update_from_buffers(&buffers.outputs);
1877
1878            // Trigger audio callback if set
1879            if let Some(ref callback) = self.audio_callback {
1880                callback(&levels);
1881            }
1882        }
1883
1884        Ok(())
1885    }
1886
1887    /// Return every audio bus's current channel count and activation state.
1888    pub fn audio_bus_layout(&self) -> Result<AudioBusLayout> {
1889        self.internal
1890            .as_ref()
1891            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1892            .audio_bus_layout()
1893    }
1894
1895    /// Allocate a bus-aware silent buffer set matching the plug-in's current configuration.
1896    ///
1897    /// Do this on the control thread when configuring an audio stream, then reuse the returned
1898    /// storage for every callback. If bus activation or arrangements change, query/create again.
1899    pub fn create_bus_audio_buffers(&self, block_size: usize) -> Result<BusAudioBuffers> {
1900        if block_size == 0 {
1901            return Err(Error::Other(
1902                "bus audio block size must be greater than zero".to_string(),
1903            ));
1904        }
1905        Ok(BusAudioBuffers::new(
1906            &self.audio_bus_layout()?,
1907            block_size,
1908            self.sample_rate,
1909        ))
1910    }
1911
1912    /// Process audio without flattening VST3 bus boundaries.
1913    ///
1914    /// The buffer set must contain every bus in index order, including inactive buses. Its
1915    /// activation flags and channel counts are validated against the current component state.
1916    /// Reuse a set created by [`Self::create_bus_audio_buffers`] for allocation-free in-process
1917    /// steady-state processing.
1918    pub fn process_bus_audio(&mut self, buffers: &mut BusAudioBuffers) -> Result<()> {
1919        if !self.is_processing {
1920            return Err(Error::NotProcessing);
1921        }
1922        self.internal
1923            .as_mut()
1924            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1925            .process_buses(buffers)?;
1926        if let Ok(mut levels) = self.audio_levels.lock() {
1927            levels.update_from_bus_buffers(&buffers.outputs);
1928            if let Some(ref callback) = self.audio_callback {
1929                callback(&levels);
1930            }
1931        }
1932        Ok(())
1933    }
1934
1935    /// Get current output levels.
1936    ///
1937    /// Recovers automatically if the audio thread panicked while holding the lock
1938    /// (poisoned mutex) rather than propagating the panic to the caller — metering
1939    /// must never take down a UI thread polling it.
1940    pub fn get_output_levels(&self) -> AudioLevels {
1941        self.audio_levels
1942            .lock()
1943            .unwrap_or_else(|poisoned| poisoned.into_inner())
1944            .clone()
1945    }
1946
1947    /// Check if the plugin is currently processing
1948    pub fn is_processing(&self) -> bool {
1949        self.is_processing
1950    }
1951
1952    /// Set a callback invoked whenever [`Self::set_parameter`] succeeds, with the parameter id
1953    /// and its new normalized value.
1954    ///
1955    /// # This callback runs on the caller's thread — including the audio thread
1956    ///
1957    /// It fires inline from `set_parameter`, so it runs on whichever thread made that call.
1958    /// Playback-ring automation uses a processor-only queue and does not invoke this callback
1959    /// (or `IEditController`) on the audio thread.
1960    pub fn on_parameter_change<F>(&mut self, callback: F)
1961    where
1962        F: Fn(u32, f64) + Send + 'static,
1963    {
1964        self.parameter_change_callback = Some(Box::new(callback));
1965    }
1966
1967    /// Set a callback invoked after each [`Self::process_audio`] cycle with the freshly
1968    /// computed output levels.
1969    ///
1970    /// # This callback runs on the AUDIO thread
1971    ///
1972    /// It fires from inside `process_audio`, which both playback paths call on the audio
1973    /// callback thread, while the level mutex is held. Keep the body real-time safe — no
1974    /// allocation, no locks, no I/O, no blocking on a UI thread. For metering in a UI, poll
1975    /// [`Self::get_output_levels`] from the UI thread instead.
1976    pub fn on_audio_process<F>(&mut self, callback: F)
1977    where
1978        F: Fn(&AudioLevels) + Send + 'static,
1979    {
1980        self.audio_callback = Some(Box::new(callback));
1981    }
1982
1983    /// Check if the plugin has an editor GUI
1984    pub fn has_editor(&self) -> bool {
1985        self.internal
1986            .as_ref()
1987            .map(|i| i.has_editor())
1988            .unwrap_or(false)
1989    }
1990
1991    /// Open the plugin editor window
1992    pub fn open_editor(&mut self, parent: WindowHandle) -> Result<()> {
1993        self.internal
1994            .as_mut()
1995            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1996            .open_editor(parent.0)
1997    }
1998
1999    /// Drive the Linux `IRunLoop` services (timers and file-descriptor
2000    /// events) that the plugin's editor registered with the host frame.
2001    /// VSTGUI-based editors paint and respond ONLY when this runs - call it
2002    /// on the UI thread every frame (e.g. 30-60 Hz) while an editor is open.
2003    /// A no-op when nothing is registered, on non-Linux, or under process
2004    /// isolation.
2005    pub fn service_run_loop(&mut self) {
2006        if let Some(internal) = self.internal.as_mut() {
2007            internal.service_run_loop();
2008        }
2009    }
2010
2011    /// Close the plugin editor window
2012    pub fn close_editor(&mut self) -> Result<()> {
2013        self.internal
2014            .as_mut()
2015            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2016            .close_editor()
2017    }
2018
2019    /// Get the preferred editor size
2020    pub fn get_editor_size(&self) -> Result<(i32, i32)> {
2021        self.internal
2022            .as_ref()
2023            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2024            .get_editor_size()
2025    }
2026
2027    /// Whether the plugin editor accepts host-driven resize requests.
2028    ///
2029    /// With an editor open this reads the live view. With no editor open it has to *create* a
2030    /// throwaway view to ask, which costs on the order of milliseconds (~4.6 ms for Dexed) —
2031    /// cache the answer rather than calling it per UI frame.
2032    pub fn editor_can_resize(&self) -> bool {
2033        self.internal
2034            .as_ref()
2035            .is_some_and(|internal| internal.editor_can_resize())
2036    }
2037
2038    /// Resize the open plugin editor, honoring the plugin's size constraints.
2039    ///
2040    /// Returns the size the plugin accepted, which may differ from the requested dimensions.
2041    pub fn resize_editor(&mut self, width: i32, height: i32) -> Result<(i32, i32)> {
2042        self.internal
2043            .as_mut()
2044            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2045            .resize_editor(width, height)
2046    }
2047
2048    /// Communicate the editor's logical-to-physical content scale.
2049    ///
2050    /// Returns `false` when the editor does not implement VST3 content-scale support.
2051    pub fn set_editor_scale_factor(&mut self, factor: f32) -> Result<bool> {
2052        self.internal
2053            .as_mut()
2054            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2055            .set_editor_scale_factor(factor)
2056    }
2057
2058    /// Collect several parameter changes with a [`ParameterUpdate`] and apply them in one call.
2059    ///
2060    /// # This batch is not atomic
2061    ///
2062    /// The queued changes are applied in the order they were `set`, and the first failure
2063    /// stops the batch and is returned — the changes queued *before* it have already been
2064    /// applied to the plugin and are **not** rolled back, and the ones after it were never
2065    /// attempted. The error does not say how far the batch got. If that matters, call
2066    /// [`Self::set_parameter`] per parameter and handle each result, or re-read the values
2067    /// with [`Self::get_parameters`] after an error.
2068    pub fn update_parameters<F>(&mut self, f: F) -> Result<()>
2069    where
2070        F: FnOnce(&mut ParameterUpdate) -> Result<()>,
2071    {
2072        let mut update = ParameterUpdate::new(self);
2073        f(&mut update)?;
2074        update.apply()
2075    }
2076
2077    /// Send MIDI panic (all notes off, all sounds off, reset controllers)
2078    pub fn midi_panic(&mut self) -> Result<()> {
2079        self.internal
2080            .as_mut()
2081            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2082            .midi_panic()
2083    }
2084
2085    /// Drain the parameter values that changed behind the host's back, as
2086    /// `(parameter_id, normalized_value)` pairs.
2087    ///
2088    /// Two sources feed this: edits the plugin's **editor** reported through
2089    /// `IComponentHandler::performEdit`, and points the **processor** wrote into its
2090    /// `outputParameterChanges` queue during `process()` (a compressor's gain-reduction readout,
2091    /// an internal LFO driving a visible control). Call it regularly — every UI frame — to keep
2092    /// the host's own display in step with the plugin.
2093    pub fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
2094        self.internal
2095            .as_ref()
2096            .map(|i| i.get_parameter_changes())
2097            .unwrap_or_default()
2098    }
2099
2100    /// Drain the ordered log of parameter-edit gestures the plugin's editor has reported since
2101    /// the last call.
2102    ///
2103    /// This is the richer superset of [`Self::get_parameter_changes`]: rather than just the
2104    /// value changes, it preserves the begin/change/end ordering of each gesture, so the host
2105    /// can tell a deliberate, completed edit (`BeginGesture` … `ValueChange`* … `EndGesture`)
2106    /// from a stream of intermediate drag values. Poll it regularly (e.g. each UI frame) while
2107    /// the editor is open; an empty vector means nothing was reported. Works across process
2108    /// isolation — gestures are marshalled back from the helper.
2109    ///
2110    /// See [`ParameterEdit`] / [`ParameterEditKind`].
2111    pub fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
2112        self.internal
2113            .as_mut()
2114            .map(|i| i.take_parameter_edits())
2115            .unwrap_or_default()
2116    }
2117
2118    /// Drain ordered requests the plugin reported through `IComponentHandler2`.
2119    pub fn take_host_notifications(&mut self) -> Vec<HostNotification> {
2120        self.internal
2121            .as_mut()
2122            .map(|i| i.take_host_notifications())
2123            .unwrap_or_default()
2124    }
2125
2126    /// Dispatch and drain blocks sent through VST3's `IDataExchangeHandler`.
2127    ///
2128    /// Call this regularly on the plug-in's control/UI thread. Queues whose controller requested
2129    /// background dispatch are delivered automatically, but their owned snapshots are drained
2130    /// here too. Storage is bounded; when the host-side snapshot sink is full, newer snapshots
2131    /// are dropped while controller delivery continues.
2132    pub fn take_data_exchange_blocks(&mut self) -> Vec<DataExchangeBlock> {
2133        self.internal
2134            .as_mut()
2135            .map(|i| i.take_data_exchange_blocks())
2136            .unwrap_or_default()
2137    }
2138
2139    /// Execute a plugin context-menu entry previously received through
2140    /// [`Self::take_host_notifications`].
2141    ///
2142    /// A popup can be completed once. Calling this invokes the plugin-provided
2143    /// `IContextMenuTarget` on the plugin control/UI thread and releases all targets retained for
2144    /// that popup.
2145    pub fn execute_context_menu_item(&mut self, menu_id: u64, item_id: u32) -> Result<()> {
2146        self.internal
2147            .as_mut()
2148            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2149            .execute_context_menu_item(menu_id, item_id)
2150    }
2151
2152    /// Dismiss a pending plugin context menu and release its retained targets.
2153    pub fn dismiss_context_menu(&mut self, menu_id: u64) -> Result<()> {
2154        self.internal
2155            .as_mut()
2156            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2157            .dismiss_context_menu(menu_id)
2158    }
2159
2160    /// Take the flags the plugin raised via `IComponentHandler::restartComponent` since the
2161    /// last call — its way of saying "something about me changed, re-read it".
2162    ///
2163    /// Poll this next to [`Self::take_parameter_edits`] (e.g. each UI frame). See
2164    /// [`RestartFlags`] for what each one asks of the host. Returns an empty set for a plugin
2165    /// that hasn't raised anything. Works across process isolation.
2166    pub fn take_restart_flags(&mut self) -> RestartFlags {
2167        self.internal
2168            .as_mut()
2169            .map(|i| i.take_restart_flags())
2170            .unwrap_or_default()
2171    }
2172
2173    /// Service pending restart requests on the caller's control thread.
2174    ///
2175    /// Latency and I/O requests are applied through the required stop/deactivate/reactivate
2176    /// lifecycle. The returned flags still describe every request; in particular,
2177    /// [`RestartFlags::reload_component`] means the caller must replace this plugin instance.
2178    pub fn service_host_requests(&mut self) -> Result<RestartFlags> {
2179        self.internal
2180            .as_mut()
2181            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2182            .service_host_requests()
2183    }
2184
2185    /// Take the MIDI events the plugin has emitted (e.g. from an arpeggiator or MPE
2186    /// controller) since the last call, draining the internal buffer.
2187    ///
2188    /// Output MIDI is captured while the plugin processes audio, so poll this regularly
2189    /// (e.g. each UI frame) while the plugin is playing; an empty vector means the plugin
2190    /// emitted nothing. This works for process-isolated plugins too — emitted events are
2191    /// marshalled back alongside each processed block.
2192    ///
2193    /// The buffer is capped at 4096 events: if you never poll while a chatty plugin keeps
2194    /// emitting, the oldest events are dropped (silently) to bound memory.
2195    pub fn take_output_midi(&self) -> Vec<MidiEvent> {
2196        self.internal
2197            .as_ref()
2198            .map(|i| {
2199                i.take_output_events()
2200                    .into_iter()
2201                    .filter_map(|event| event.to_midi())
2202                    .collect()
2203            })
2204            .unwrap_or_default()
2205    }
2206
2207    /// Take every event the plugin has emitted, preserving SysEx and all VST3 event variants.
2208    pub fn take_output_events(&self) -> Vec<crate::midi::OutputEvent> {
2209        self.internal
2210            .as_ref()
2211            .map(|i| i.take_output_events())
2212            .unwrap_or_default()
2213    }
2214
2215    /// Get a `Send` handle for draining emitted MIDI from another thread without locking the
2216    /// audio thread (see [`OutputMidiConsumer`]). Returns `None` for an unloaded plugin or the
2217    /// process-isolation path. Useful with [`RealtimePluginRunner`](crate::RealtimePluginRunner):
2218    /// take the handle, move the plugin into the runner, and poll it from your UI thread while
2219    /// the audio thread renders.
2220    pub fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
2221        self.internal.as_ref().and_then(|i| i.output_midi_handle())
2222    }
2223
2224    /// Get a lock-free handle for draining all emitted VST3 events.
2225    pub fn output_event_handle(&self) -> Option<OutputEventConsumer> {
2226        self.internal.as_ref().and_then(|i| i.output_event_handle())
2227    }
2228
2229    /// Save the plugin's current state (parameters, internal settings, loaded preset) to
2230    /// an opaque byte blob.
2231    ///
2232    /// The blob is a versioned envelope holding the two streams VST3 defines — the component's
2233    /// state and, for a plugin whose controller is a separate object, the controller's — not a
2234    /// bare copy of either. Treat it as opaque and pair it with the plugin's identity
2235    /// ([`PluginInfo::uid`]); it only means something to the same plugin, and only
2236    /// [`Self::load_state`] can unpack it. (Blobs written by older releases, which were the raw
2237    /// component stream, still load.) Persist it to restore a patch later, or to snapshot a
2238    /// session. Call this on the main thread (see the
2239    /// [threading model](https://docs.rs/vst3-host)). For a blob other VST3 hosts can read,
2240    /// use [`Self::save_vstpreset`].
2241    ///
2242    /// Works both in-process and across process isolation (the state blob is marshalled over
2243    /// the IPC boundary). Returns an error for plugins that don't implement state saving.
2244    pub fn save_state(&self) -> Result<Vec<u8>> {
2245        self.internal
2246            .as_ref()
2247            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2248            .save_state()
2249    }
2250
2251    /// Restore plugin state from a blob produced by [`Self::save_state`] on the *same*
2252    /// plugin. Applies to both the processor and the controller, so parameter values and
2253    /// the editor reflect the restored state.
2254    ///
2255    /// Passing bytes from a different plugin has undefined results (the plugin decides what
2256    /// to do with bytes it doesn't recognize). Call this on the main thread.
2257    ///
2258    /// The plugin is told this is a project/session restore ([`StateContext::Project`]). Use
2259    /// [`Self::load_state_with_context`] when the bytes came from a preset file instead.
2260    pub fn load_state(&mut self, data: &[u8]) -> Result<()> {
2261        self.internal
2262            .as_mut()
2263            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2264            .load_state(data)
2265    }
2266
2267    /// Restore plugin state as [`Self::load_state`] does, and tell the plugin where the bytes
2268    /// came from.
2269    ///
2270    /// VST3 plugins can read the context off the stream they are given (the SDK's
2271    /// `Vst::Helpers::isProjectState()` does exactly that) and restore differently for a
2272    /// session than for a preset. [`Self::load_vstpreset`] and [`Self::load_preset`] already
2273    /// pass [`StateContext::Preset`] with the file they read; reach for this directly when
2274    /// your host holds preset bytes it loaded some other way.
2275    ///
2276    /// Works both in-process and across process isolation — an isolated plugin's `setState`
2277    /// sees the same attributes.
2278    pub fn load_state_with_context(&mut self, data: &[u8], context: &StateContext) -> Result<()> {
2279        self.internal
2280            .as_mut()
2281            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2282            .load_state_with_context(data, context)
2283    }
2284
2285    /// Save this plugin's state to a file as a [`PluginPreset`] (JSON: the plugin's `uid`
2286    /// and name plus the opaque state blob). The embedded `uid` lets [`Self::load_preset`]
2287    /// reject a preset saved from a different plugin.
2288    pub fn save_preset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
2289        let info = self.info();
2290        let preset = PluginPreset {
2291            uid: info.uid.clone(),
2292            plugin_name: info.name.clone(),
2293            state: self.save_state()?,
2294        };
2295        let json = serde_json::to_vec_pretty(&preset)
2296            .map_err(|e| Error::Other(format!("serialize preset: {e}")))?;
2297        std::fs::write(path, json).map_err(|e| Error::Other(format!("write preset: {e}")))?;
2298        Ok(())
2299    }
2300
2301    /// Load a [`PluginPreset`] file written by [`Self::save_preset`] and apply its state.
2302    /// Returns an error if the preset's `uid` doesn't match this plugin (loading another
2303    /// plugin's state is undefined).
2304    ///
2305    /// The plugin sees this as a preset load ([`StateContext::Preset`]) carrying `path`, not
2306    /// as a session restore.
2307    pub fn load_preset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
2308        let path = path.as_ref();
2309        let bytes = std::fs::read(path).map_err(|e| Error::Other(format!("read preset: {e}")))?;
2310        let preset: PluginPreset = serde_json::from_slice(&bytes)
2311            .map_err(|e| Error::Other(format!("parse preset: {e}")))?;
2312        if !self.accepts_state_class_id(&preset.uid)? {
2313            return Err(Error::Other(format!(
2314                "preset is for a different plugin ({}, expected {})",
2315                preset.plugin_name,
2316                self.info().name
2317            )));
2318        }
2319        self.load_state_with_context(&preset.state, &StateContext::preset_from_path(path))
2320    }
2321
2322    /// Save this plugin's state to a standard Steinberg `.vstpreset` file.
2323    ///
2324    /// Unlike [`Self::save_preset`] (a JSON wrapper specific to this library), the
2325    /// `.vstpreset` container is the interchange format shared by VST3 hosts and plugins, so
2326    /// the file can be read by other hosts (and by the plugin's own preset browser). It wraps
2327    /// the component and optional controller streams from [`Self::save_state`] in `"Comp"` and
2328    /// `"Cont"` chunks, tagged with this plugin's class id ([`PluginInfo::uid`]) so a loader can
2329    /// reject presets from a different plugin. Call this on the main thread.
2330    pub fn save_vstpreset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
2331        let state = decode_state_snapshot(&self.save_state()?)?;
2332        let bytes = vstpreset::build(
2333            &self.info().uid,
2334            &state.component,
2335            state.controller.as_deref(),
2336        )?;
2337        std::fs::write(path, bytes).map_err(|e| Error::Other(format!("write vstpreset: {e}")))?;
2338        Ok(())
2339    }
2340
2341    /// Load a Steinberg `.vstpreset` file and apply its component and controller state.
2342    ///
2343    /// Parses the `.vstpreset` container written by [`Self::save_vstpreset`] (or another VST3
2344    /// host), extracts the `"Comp"` and optional `"Cont"` chunks and passes them to
2345    /// [`Self::load_state`]. Returns an error if the file's magic is invalid, or if its class
2346    /// id doesn't match this plugin (loading another plugin's state is undefined). Call this
2347    /// on the main thread.
2348    ///
2349    /// The plugin is told this is a preset load ([`StateContext::Preset`]) and is given the
2350    /// file's full path, the way a DAW's preset browser would — not the project-restore
2351    /// context [`Self::load_state`] uses.
2352    pub fn load_vstpreset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
2353        let path = path.as_ref();
2354        let bytes =
2355            std::fs::read(path).map_err(|e| Error::Other(format!("read vstpreset: {e}")))?;
2356        let parsed = vstpreset::parse(&bytes)?;
2357        if !self.accepts_state_class_id(&parsed.class_id)? {
2358            return Err(Error::Other(format!(
2359                "vstpreset is for a different plugin (class id {}, expected {})",
2360                parsed.class_id,
2361                self.info().uid
2362            )));
2363        }
2364        let state = encode_state_snapshot(&StateSnapshot {
2365            component: parsed.component_state,
2366            controller: parsed.controller_state,
2367        })?;
2368        self.load_state_with_context(&state, &StateContext::preset_from_path(path))
2369    }
2370
2371    /// Accept the current class id or a retired id which this bundle's moduleinfo declares as
2372    /// replaced by the current class. This is deliberately evaluated from validated bundle
2373    /// metadata rather than trusting a preset to name its own replacement.
2374    fn accepts_state_class_id(&self, candidate: &str) -> Result<bool> {
2375        if crate::internal::utils::class_uid_matches(&self.info().uid, candidate) {
2376            return Ok(true);
2377        }
2378        Ok(self.compatibility.iter().any(|mapping| {
2379            crate::internal::utils::class_uid_matches(&mapping.new_class_id, &self.info().uid)
2380                && mapping
2381                    .old_class_ids
2382                    .iter()
2383                    .any(|old| crate::internal::utils::class_uid_matches(old, candidate))
2384        }))
2385    }
2386
2387    /// The OS process id of the isolated helper hosting this plugin, or `None` if it runs
2388    /// in-process. Useful for monitoring an isolated plugin's resource use.
2389    pub fn isolation_pid(&self) -> Option<u32> {
2390        self.internal.as_ref().and_then(|i| i.helper_pid())
2391    }
2392
2393    /// How many times this plugin has been recovered (helper respawned + reloaded), via either
2394    /// [`Self::recover`] or automatic recovery ([`Vst3HostBuilder::auto_recover_plugins`]).
2395    ///
2396    /// A recovery reloads the plugin from defaults — parameter values and loaded state are NOT
2397    /// replayed. With auto-recover on, a crash is otherwise invisible (the call returns `Ok`),
2398    /// so poll this count to detect that a reset happened and re-apply a saved
2399    /// [`save_state`](Self::save_state) snapshot.
2400    ///
2401    /// [`Vst3HostBuilder::auto_recover_plugins`]: crate::Vst3HostBuilder::auto_recover_plugins
2402    pub fn recovery_count(&self) -> u64 {
2403        self.internal
2404            .as_ref()
2405            .map(|i| i.recovery_count())
2406            .unwrap_or(0)
2407    }
2408
2409    /// Total number of output audio channels across the plugin's output buses.
2410    ///
2411    /// Reflects the plugin's actual bus layout (mono / stereo / surround / multi-bus), not a
2412    /// stereo assumption — useful for sizing meters or output buffers. Returns 2 if unknown.
2413    pub fn output_channel_count(&self) -> usize {
2414        self.internal
2415            .as_ref()
2416            .map(|i| i.output_channel_count())
2417            .unwrap_or(2)
2418    }
2419
2420    /// Poll for an editor resize the plugin requested via VST3's `IPlugFrame` since the last
2421    /// call, as `(width, height)` in pixels, or `None`.
2422    ///
2423    /// Plugins with resizable editors call back to ask the host to resize the window hosting
2424    /// their view. Poll this on your UI thread (e.g. each frame) while the editor is open and
2425    /// resize your editor container to match. Only the in-process editor path reports this.
2426    pub fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
2427        self.internal
2428            .as_ref()
2429            .and_then(|i| i.take_editor_resize_request())
2430    }
2431
2432    /// Recover a process-isolated plugin whose helper has crashed.
2433    ///
2434    /// When an isolated plugin's helper process dies, calls return [`Error::PluginCrashed`]
2435    /// and the host itself stays alive. This respawns the helper and reloads the plugin
2436    /// from the same path and audio settings, restarting processing if it was running.
2437    ///
2438    /// **The reloaded plugin starts from its default state** — parameter values and any
2439    /// loaded preset are lost. Snapshot with [`Self::save_state`] beforehand and
2440    /// [`Self::load_state`] after recovering to preserve them. Returns an error for
2441    /// in-process plugins (an in-process crash takes down the whole host) and if the
2442    /// reload itself fails.
2443    pub fn recover(&mut self) -> Result<()> {
2444        self.internal
2445            .as_mut()
2446            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
2447            .recover()
2448    }
2449}
2450
2451/// Highest value a 7-bit MIDI data byte can carry.
2452const MIDI_DATA_MAX: u8 = 127;
2453
2454/// Highest value a 14-bit MIDI pitch-bend can carry.
2455const MIDI_PITCH_BEND_MAX: u16 = 16383;
2456
2457fn validate_note(note: u8) -> Result<()> {
2458    if note > MIDI_DATA_MAX {
2459        return Err(Error::MidiError(format!("Invalid note number: {}", note)));
2460    }
2461    Ok(())
2462}
2463
2464fn validate_velocity(velocity: u8) -> Result<()> {
2465    if velocity > MIDI_DATA_MAX {
2466        return Err(Error::MidiError(format!("Invalid velocity: {}", velocity)));
2467    }
2468    Ok(())
2469}
2470
2471fn validate_controller(controller: u8) -> Result<()> {
2472    if controller > MIDI_DATA_MAX {
2473        return Err(Error::MidiError(format!(
2474            "Invalid controller number: {}",
2475            controller
2476        )));
2477    }
2478    Ok(())
2479}
2480
2481fn validate_cc_value(value: u8) -> Result<()> {
2482    if value > MIDI_DATA_MAX {
2483        return Err(Error::MidiError(format!("Invalid CC value: {}", value)));
2484    }
2485    Ok(())
2486}
2487
2488fn validate_pressure(pressure: u8) -> Result<()> {
2489    if pressure > MIDI_DATA_MAX {
2490        return Err(Error::MidiError(format!(
2491            "Invalid pressure value: {}",
2492            pressure
2493        )));
2494    }
2495    Ok(())
2496}
2497
2498/// Range-check every data field of a [`MidiEvent`] before it reaches a plugin.
2499///
2500/// `MidiEvent`'s fields are plain `u8`/`u16`, so nothing stops a caller from building an
2501/// out-of-spec event by hand. The conversion below this layer masks nothing: a note of `255`
2502/// becomes a `255` pitch in the VST3 event, a velocity of `255` becomes `2.008` where the
2503/// spec's maximum is `1.0`, and a legacy CC value of `200` becomes a *negative* MIDI byte
2504/// once cast to `c_char`. Reject them here, with the same messages the typed senders use.
2505///
2506/// The match below has no wildcard arm on purpose: `MidiEvent` is `#[non_exhaustive]` only to
2507/// downstream crates, so a variant added here fails to compile until it states its own ranges.
2508fn validate_midi_event(event: &MidiEvent) -> Result<()> {
2509    match *event {
2510        MidiEvent::NoteOn { note, velocity, .. } | MidiEvent::NoteOff { note, velocity, .. } => {
2511            validate_note(note)?;
2512            validate_velocity(velocity)
2513        }
2514        MidiEvent::ControlChange {
2515            controller, value, ..
2516        } => {
2517            validate_controller(controller)?;
2518            validate_cc_value(value)
2519        }
2520        MidiEvent::ProgramChange { program, .. } => {
2521            if program > MIDI_DATA_MAX {
2522                return Err(Error::MidiError(format!(
2523                    "Invalid program number: {}",
2524                    program
2525                )));
2526            }
2527            Ok(())
2528        }
2529        MidiEvent::PitchBend { value, .. } => {
2530            if value > MIDI_PITCH_BEND_MAX {
2531                return Err(Error::MidiError(format!(
2532                    "Invalid pitch bend value: {} (0-{})",
2533                    value, MIDI_PITCH_BEND_MAX
2534                )));
2535            }
2536            Ok(())
2537        }
2538        MidiEvent::ChannelAftertouch { pressure, .. } => validate_pressure(pressure),
2539        MidiEvent::PolyAftertouch { note, pressure, .. } => {
2540            validate_note(note)?;
2541            validate_pressure(pressure)
2542        }
2543    }
2544}
2545
2546fn validate_plugin_event(event: &PluginEvent) -> Result<()> {
2547    if event.bus_index < 0 {
2548        return Err(Error::MidiError(format!(
2549            "Invalid event bus index: {}",
2550            event.bus_index
2551        )));
2552    }
2553    if event.sample_offset < 0 {
2554        return Err(Error::MidiError(format!(
2555            "Invalid event sample offset: {}",
2556            event.sample_offset
2557        )));
2558    }
2559    if !event.ppq_position.is_finite() {
2560        return Err(Error::MidiError(
2561            "Event PPQ position must be finite".to_string(),
2562        ));
2563    }
2564
2565    let validate_channel = |channel: i16| {
2566        if (0..16).contains(&channel) {
2567            Ok(())
2568        } else {
2569            Err(Error::MidiError(format!(
2570                "Invalid VST3 event channel: {channel}"
2571            )))
2572        }
2573    };
2574    let validate_pitch = |pitch: i16| {
2575        if (0..=127).contains(&pitch) {
2576            Ok(())
2577        } else {
2578            Err(Error::MidiError(format!(
2579                "Invalid VST3 event pitch: {pitch}"
2580            )))
2581        }
2582    };
2583    let validate_normalized = |name: &str, value: f64| {
2584        if value.is_finite() && (0.0..=1.0).contains(&value) {
2585            Ok(())
2586        } else {
2587            Err(Error::MidiError(format!(
2588                "{name} must be finite and normalized to [0.0, 1.0]"
2589            )))
2590        }
2591    };
2592
2593    match &event.data {
2594        PluginEventData::NoteOn {
2595            channel,
2596            pitch,
2597            tuning,
2598            velocity,
2599            length,
2600            ..
2601        } => {
2602            validate_channel(*channel)?;
2603            validate_pitch(*pitch)?;
2604            validate_normalized("note velocity", f64::from(*velocity))?;
2605            if !tuning.is_finite() || *length < 0 {
2606                return Err(Error::MidiError(
2607                    "note tuning must be finite and length non-negative".to_string(),
2608                ));
2609            }
2610            Ok(())
2611        }
2612        PluginEventData::NoteOff {
2613            channel,
2614            pitch,
2615            velocity,
2616            tuning,
2617            ..
2618        } => {
2619            validate_channel(*channel)?;
2620            validate_pitch(*pitch)?;
2621            validate_normalized("note-off velocity", f64::from(*velocity))?;
2622            if !tuning.is_finite() {
2623                return Err(Error::MidiError(
2624                    "note-off tuning must be finite".to_string(),
2625                ));
2626            }
2627            Ok(())
2628        }
2629        PluginEventData::Data { data_type, bytes } => {
2630            if *data_type != 0 {
2631                return Err(Error::MidiError(format!(
2632                    "Unsupported VST3 data event type: {data_type}"
2633                )));
2634            }
2635            if bytes.len() > MAX_EVENT_PAYLOAD_BYTES {
2636                return Err(Error::MidiError(format!(
2637                    "Event payload is {} bytes; maximum is {MAX_EVENT_PAYLOAD_BYTES}",
2638                    bytes.len()
2639                )));
2640            }
2641            Ok(())
2642        }
2643        PluginEventData::PolyPressure {
2644            channel,
2645            pitch,
2646            pressure,
2647            ..
2648        } => {
2649            validate_channel(*channel)?;
2650            validate_pitch(*pitch)?;
2651            validate_normalized("poly pressure", f64::from(*pressure))
2652        }
2653        PluginEventData::NoteExpressionValue { value, .. } => {
2654            validate_normalized("note-expression value", *value)
2655        }
2656        PluginEventData::NoteExpressionText { text, .. }
2657        | PluginEventData::Chord { text, .. }
2658        | PluginEventData::Scale { text, .. } => {
2659            if text.len() > MAX_EVENT_TEXT_UNITS {
2660                return Err(Error::MidiError(format!(
2661                    "Event text is {} UTF-16 units; maximum is {MAX_EVENT_TEXT_UNITS}",
2662                    text.len()
2663                )));
2664            }
2665            Ok(())
2666        }
2667        PluginEventData::NoteExpressionIntValue { .. } => Ok(()),
2668        PluginEventData::LegacyMidiCcOut { .. } => Err(Error::MidiError(
2669            "Legacy MIDI CC events are plugin output only".to_string(),
2670        )),
2671    }
2672}
2673
2674/// Platform-specific window handle
2675pub struct WindowHandle(pub(crate) *mut std::ffi::c_void);
2676
2677impl WindowHandle {
2678    /// Create from a raw window handle
2679    ///
2680    /// # Safety
2681    /// The pointer must be a valid window handle for the platform. On Linux this API currently
2682    /// selects VST3's `X11EmbedWindowID` contract; a `wl_surface` is not accepted because VST 3.8
2683    /// Wayland embedding additionally requires host-provided `IWaylandHost`/`IWaylandFrame`
2684    /// services.
2685    pub unsafe fn from_raw(handle: *mut std::ffi::c_void) -> Self {
2686        Self(handle)
2687    }
2688}
2689
2690// Safe Send implementation - the window handle is platform-specific
2691unsafe impl Send for WindowHandle {}
2692
2693#[cfg(target_os = "macos")]
2694impl WindowHandle {
2695    /// Create from an `NSView` pointer on macOS.
2696    ///
2697    /// # Safety
2698    ///
2699    /// `view` must be a live `NSView` that stays alive for as long as the editor is attached
2700    /// to it. [`Plugin::open_editor`] hands the pointer straight to the plugin's
2701    /// `IPlugView::attached`, which dereferences it; nothing on that path can tell a valid
2702    /// view from a dangling or foreign pointer.
2703    pub unsafe fn from_nsview(view: *mut std::ffi::c_void) -> Self {
2704        Self(view)
2705    }
2706}
2707
2708#[cfg(target_os = "windows")]
2709impl WindowHandle {
2710    /// Create from an `HWND` on Windows.
2711    ///
2712    /// # Safety
2713    ///
2714    /// `hwnd` must be a live window handle that stays valid for as long as the editor is
2715    /// attached to it. [`Plugin::open_editor`] hands it straight to the plugin's
2716    /// `IPlugView::attached`, which uses it as a window; nothing on that path validates it.
2717    pub unsafe fn from_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
2718        Self(hwnd)
2719    }
2720}
2721
2722#[cfg(target_os = "linux")]
2723impl WindowHandle {
2724    /// Create from an X11 window id on Linux (for VST3 `X11EmbedWindowID`).
2725    ///
2726    /// The VST3 X11 platform type expects the window id itself as the handle value,
2727    /// not a pointer to it.
2728    pub fn from_x11(window_id: u32) -> Self {
2729        Self(window_id as usize as *mut std::ffi::c_void)
2730    }
2731}
2732
2733/// Build and parse the standard Steinberg `.vstpreset` container format.
2734///
2735/// Layout (all multi-byte integers little-endian, matching the SDK's `PresetFile`):
2736///
2737/// - Header (48 bytes): magic `b"VST3"` (4) + version `i32` = 1 (4) + 32-char ASCII class
2738///   id (the plugin's FUID hex) (32) + `i64` byte offset from the start of the file to the
2739///   chunk list (8).
2740/// - Body: the chunk payloads, written back to back after the header: required `"Comp"`
2741///   component state and optional `"Cont"` controller state.
2742/// - Chunk list (at the header's list offset): magic `b"List"` (4) + entry count `i32` (4),
2743///   then per entry: 4-byte chunk id + `i64` absolute offset + `i64` size.
2744mod vstpreset {
2745    use crate::error::{Error, Result};
2746
2747    const MAGIC: &[u8; 4] = b"VST3";
2748    const LIST_MAGIC: &[u8; 4] = b"List";
2749    const COMPONENT_CHUNK: &[u8; 4] = b"Comp";
2750    const CONTROLLER_CHUNK: &[u8; 4] = b"Cont";
2751    const VERSION: i32 = 1;
2752    const CLASS_ID_LEN: usize = 32;
2753    const HEADER_SIZE: usize = 4 + 4 + CLASS_ID_LEN + 8;
2754    const LIST_HEADER_SIZE: usize = 8;
2755    const ENTRY_SIZE: usize = 20;
2756
2757    /// A parsed `.vstpreset` container.
2758    pub(super) struct Parsed {
2759        /// The 32-char ASCII class id from the header.
2760        pub class_id: String,
2761        /// The bytes of the `"Comp"` (component state) chunk.
2762        pub component_state: Vec<u8>,
2763        /// The bytes of the optional `"Cont"` (controller state) chunk.
2764        pub controller_state: Option<Vec<u8>>,
2765    }
2766
2767    /// Build a `.vstpreset` file containing component and optional controller state.
2768    pub(super) fn build(
2769        class_id: &str,
2770        component_state: &[u8],
2771        controller_state: Option<&[u8]>,
2772    ) -> Result<Vec<u8>> {
2773        let class_bytes = class_id.as_bytes();
2774        if class_bytes.len() != CLASS_ID_LEN
2775            || !class_bytes.iter().all(|byte| byte.is_ascii_hexdigit())
2776        {
2777            return Err(Error::Other(format!(
2778                "vstpreset class id must be {CLASS_ID_LEN} ASCII hex chars, got {:?}",
2779                class_id
2780            )));
2781        }
2782
2783        let comp_offset = HEADER_SIZE as i64;
2784        let comp_size = component_state.len() as i64;
2785        let controller_offset = HEADER_SIZE
2786            .checked_add(component_state.len())
2787            .ok_or_else(|| Error::Other("vstpreset size overflow".to_string()))?;
2788        let list_offset = controller_offset
2789            .checked_add(controller_state.map_or(0, <[u8]>::len))
2790            .ok_or_else(|| Error::Other("vstpreset size overflow".to_string()))?;
2791        let entry_count = if controller_state.is_some() { 2 } else { 1 };
2792
2793        let mut out = Vec::with_capacity(list_offset + LIST_HEADER_SIZE + entry_count * ENTRY_SIZE);
2794        // Header.
2795        out.extend_from_slice(MAGIC);
2796        out.extend_from_slice(&VERSION.to_le_bytes());
2797        out.extend(class_bytes.iter().map(u8::to_ascii_uppercase));
2798        out.extend_from_slice(&(list_offset as i64).to_le_bytes());
2799        // Body.
2800        out.extend_from_slice(component_state);
2801        if let Some(controller) = controller_state {
2802            out.extend_from_slice(controller);
2803        }
2804        // Chunk list.
2805        out.extend_from_slice(LIST_MAGIC);
2806        out.extend_from_slice(&(entry_count as i32).to_le_bytes());
2807        out.extend_from_slice(COMPONENT_CHUNK);
2808        out.extend_from_slice(&comp_offset.to_le_bytes());
2809        out.extend_from_slice(&comp_size.to_le_bytes());
2810        if let Some(controller) = controller_state {
2811            out.extend_from_slice(CONTROLLER_CHUNK);
2812            out.extend_from_slice(&(controller_offset as i64).to_le_bytes());
2813            out.extend_from_slice(&(controller.len() as i64).to_le_bytes());
2814        }
2815
2816        Ok(out)
2817    }
2818
2819    /// Parse a `.vstpreset` file, extracting component and optional controller state.
2820    pub(super) fn parse(bytes: &[u8]) -> Result<Parsed> {
2821        if bytes.len() < HEADER_SIZE {
2822            return Err(Error::Other("vstpreset too short for header".to_string()));
2823        }
2824        if &bytes[0..4] != MAGIC {
2825            return Err(Error::Other(format!(
2826                "bad vstpreset magic: expected {:?}, got {:?}",
2827                MAGIC,
2828                &bytes[0..4]
2829            )));
2830        }
2831        let version = read_i32(&bytes[4..8]);
2832        if version != VERSION {
2833            return Err(Error::Other(format!(
2834                "unsupported vstpreset version {version} (expected {VERSION})"
2835            )));
2836        }
2837        let class_id = String::from_utf8(bytes[8..8 + CLASS_ID_LEN].to_vec())
2838            .map_err(|e| Error::Other(format!("vstpreset class id not UTF-8: {e}")))?;
2839        if !class_id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
2840            return Err(Error::Other(
2841                "vstpreset class id is not 32 ASCII hex characters".to_string(),
2842            ));
2843        }
2844        let list_offset = read_i64(&bytes[8 + CLASS_ID_LEN..HEADER_SIZE]);
2845        if list_offset < HEADER_SIZE as i64 || list_offset as usize > bytes.len() {
2846            return Err(Error::Other(format!(
2847                "vstpreset chunk-list offset {list_offset} out of bounds (len {})",
2848                bytes.len()
2849            )));
2850        }
2851        let list = &bytes[list_offset as usize..];
2852        if list.len() < 8 || &list[0..4] != LIST_MAGIC {
2853            return Err(Error::Other(
2854                "vstpreset chunk list missing or malformed".to_string(),
2855            ));
2856        }
2857        let count = read_i32(&list[4..8]);
2858        if count < 0 {
2859            return Err(Error::Other("vstpreset negative entry count".to_string()));
2860        }
2861        let count = count as usize;
2862        let list_size = LIST_HEADER_SIZE
2863            .checked_add(
2864                count
2865                    .checked_mul(ENTRY_SIZE)
2866                    .ok_or_else(|| Error::Other("vstpreset entry count overflow".to_string()))?,
2867            )
2868            .ok_or_else(|| Error::Other("vstpreset list size overflow".to_string()))?;
2869        if list.len() < list_size {
2870            return Err(Error::Other(
2871                "vstpreset chunk-list entry truncated".to_string(),
2872            ));
2873        }
2874
2875        let body_end = list_offset as usize;
2876        let mut cursor = LIST_HEADER_SIZE;
2877        let mut component_state = None;
2878        let mut controller_state = None;
2879        let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(count);
2880        for _ in 0..count {
2881            let id = &list[cursor..cursor + 4];
2882            let offset = read_i64(&list[cursor + 4..cursor + 12]);
2883            let size = read_i64(&list[cursor + 12..cursor + 20]);
2884            cursor += ENTRY_SIZE;
2885            if offset < HEADER_SIZE as i64 || size < 0 {
2886                return Err(Error::Other(
2887                    "vstpreset chunk has invalid offset/size".to_string(),
2888                ));
2889            }
2890            let start = usize::try_from(offset)
2891                .map_err(|_| Error::Other("vstpreset chunk offset overflow".to_string()))?;
2892            let size = usize::try_from(size)
2893                .map_err(|_| Error::Other("vstpreset chunk size overflow".to_string()))?;
2894            let end = start
2895                .checked_add(size)
2896                .ok_or_else(|| Error::Other("vstpreset chunk size overflow".to_string()))?;
2897            if end > body_end {
2898                return Err(Error::Other(format!(
2899                    "vstpreset chunk [{start}..{end}] is outside the payload body \
2900                     [{HEADER_SIZE}..{body_end}]"
2901                )));
2902            }
2903            if ranges
2904                .iter()
2905                .any(|&(other_start, other_end)| start < other_end && other_start < end)
2906            {
2907                return Err(Error::Other("vstpreset chunk payloads overlap".to_string()));
2908            }
2909            ranges.push((start, end));
2910
2911            match id {
2912                id if id == COMPONENT_CHUNK => {
2913                    if component_state.is_some() {
2914                        return Err(Error::Other(
2915                            "vstpreset has duplicate component chunks".to_string(),
2916                        ));
2917                    }
2918                    component_state = Some(bytes[start..end].to_vec());
2919                }
2920                id if id == CONTROLLER_CHUNK => {
2921                    if controller_state.is_some() {
2922                        return Err(Error::Other(
2923                            "vstpreset has duplicate controller chunks".to_string(),
2924                        ));
2925                    }
2926                    controller_state = Some(bytes[start..end].to_vec());
2927                }
2928                _ => {}
2929            }
2930        }
2931        let component_state = component_state.ok_or_else(|| {
2932            Error::Other("vstpreset has no component (\"Comp\") chunk".to_string())
2933        })?;
2934        Ok(Parsed {
2935            class_id,
2936            component_state,
2937            controller_state,
2938        })
2939    }
2940
2941    fn read_i32(b: &[u8]) -> i32 {
2942        i32::from_le_bytes([b[0], b[1], b[2], b[3]])
2943    }
2944
2945    fn read_i64(b: &[u8]) -> i64 {
2946        i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
2947    }
2948}
2949
2950#[cfg(test)]
2951mod public_surface_tests {
2952    use super::*;
2953
2954    /// A `Plugin` with no backing implementation. Enough to exercise the checks the public
2955    /// surface performs *before* it reaches into `internal`.
2956    fn unloaded_plugin() -> Plugin {
2957        Plugin {
2958            info: PluginInfo {
2959                path: std::path::PathBuf::from("/none.vst3"),
2960                name: "None".to_string(),
2961                vendor: String::new(),
2962                version: String::new(),
2963                category: String::new(),
2964                uid: String::new(),
2965                audio_inputs: 0,
2966                audio_outputs: 2,
2967                has_midi_input: true,
2968                has_midi_output: false,
2969                has_gui: false,
2970            },
2971            compatibility: Vec::new(),
2972            is_processing: false,
2973            sample_rate: 44_100.0,
2974            block_size: 512,
2975            audio_levels: Arc::new(Mutex::new(AudioLevels::new(2))),
2976            parameter_change_callback: None,
2977            audio_callback: None,
2978            internal: None,
2979        }
2980    }
2981
2982    /// The "not processing" rejection happens once per block on the audio thread, so it must
2983    /// be the allocation-free unit variant rather than a freshly formatted `Error::Other`.
2984    #[test]
2985    fn process_audio_while_stopped_is_the_allocation_free_variant() {
2986        let mut plugin = unloaded_plugin();
2987        let mut buffers = AudioBuffers::new(0, 2, 64, 44_100.0);
2988        let err = plugin
2989            .process_audio(&mut buffers)
2990            .expect_err("processing is stopped");
2991        assert!(
2992            matches!(err, Error::NotProcessing),
2993            "expected Error::NotProcessing, got {err:?}"
2994        );
2995    }
2996
2997    /// `send_midi_event`/`send_midi_event_at` used to forward whatever a caller built by hand,
2998    /// so an out-of-range field reached the VST3 conversion unmasked (velocity 255 → 2.008
2999    /// where the spec maximum is 1.0; a legacy CC value of 200 → a negative MIDI byte).
3000    #[test]
3001    fn send_midi_event_rejects_out_of_range_fields() {
3002        let mut plugin = unloaded_plugin();
3003        let bad = [
3004            MidiEvent::NoteOn {
3005                channel: MidiChannel::Ch1,
3006                note: 255,
3007                velocity: 100,
3008            },
3009            MidiEvent::NoteOn {
3010                channel: MidiChannel::Ch1,
3011                note: 60,
3012                velocity: 255,
3013            },
3014            MidiEvent::NoteOff {
3015                channel: MidiChannel::Ch1,
3016                note: 128,
3017                velocity: 0,
3018            },
3019            MidiEvent::ControlChange {
3020                channel: MidiChannel::Ch1,
3021                controller: 200,
3022                value: 0,
3023            },
3024            MidiEvent::ControlChange {
3025                channel: MidiChannel::Ch1,
3026                controller: 1,
3027                value: 200,
3028            },
3029            MidiEvent::ProgramChange {
3030                channel: MidiChannel::Ch1,
3031                program: 200,
3032            },
3033            MidiEvent::PitchBend {
3034                channel: MidiChannel::Ch1,
3035                value: 16_384,
3036            },
3037            MidiEvent::ChannelAftertouch {
3038                channel: MidiChannel::Ch1,
3039                pressure: 200,
3040            },
3041            MidiEvent::PolyAftertouch {
3042                channel: MidiChannel::Ch1,
3043                note: 200,
3044                pressure: 1,
3045            },
3046            MidiEvent::PolyAftertouch {
3047                channel: MidiChannel::Ch1,
3048                note: 60,
3049                pressure: 200,
3050            },
3051        ];
3052        for event in bad {
3053            for err in [
3054                plugin.send_midi_event(event).expect_err("out of range"),
3055                plugin
3056                    .send_midi_event_at(event, 0)
3057                    .expect_err("out of range"),
3058            ] {
3059                assert!(
3060                    matches!(err, Error::MidiError(_)),
3061                    "expected a MidiError for {event:?}, got {err:?}"
3062                );
3063            }
3064        }
3065    }
3066
3067    /// In-range events pass validation and fail later, at the uninitialized plugin — proof the
3068    /// check rejects the field values and not the events themselves.
3069    #[test]
3070    fn send_midi_event_accepts_in_range_fields() {
3071        let mut plugin = unloaded_plugin();
3072        let ok = [
3073            MidiEvent::NoteOn {
3074                channel: MidiChannel::Ch1,
3075                note: 127,
3076                velocity: 127,
3077            },
3078            MidiEvent::ControlChange {
3079                channel: MidiChannel::Ch1,
3080                controller: 127,
3081                value: 127,
3082            },
3083            MidiEvent::ProgramChange {
3084                channel: MidiChannel::Ch1,
3085                program: 127,
3086            },
3087            MidiEvent::PitchBend {
3088                channel: MidiChannel::Ch1,
3089                value: 16_383,
3090            },
3091            MidiEvent::ChannelAftertouch {
3092                channel: MidiChannel::Ch1,
3093                pressure: 127,
3094            },
3095            MidiEvent::PolyAftertouch {
3096                channel: MidiChannel::Ch1,
3097                note: 127,
3098                pressure: 127,
3099            },
3100        ];
3101        for event in ok {
3102            let err = plugin.send_midi_event(event).expect_err("no plugin loaded");
3103            assert!(
3104                matches!(err, Error::Other(_)),
3105                "expected the uninitialized-plugin error for {event:?}, got {err:?}"
3106            );
3107        }
3108    }
3109
3110    /// `note_on`/`note_on_at` mint a per-voice id, and skipped the range check its
3111    /// `send_midi_note` sibling performs.
3112    #[test]
3113    fn note_on_rejects_out_of_range_note_and_velocity() {
3114        let mut plugin = unloaded_plugin();
3115        for (note, velocity) in [(128, 100), (60, 128), (255, 255)] {
3116            let err = plugin
3117                .note_on(MidiChannel::Ch1, note, velocity)
3118                .expect_err("out of range");
3119            assert!(
3120                matches!(err, Error::MidiError(_)),
3121                "expected a MidiError for note {note} velocity {velocity}, got {err:?}"
3122            );
3123            let err = plugin
3124                .note_on_at(MidiChannel::Ch1, note, velocity, 0)
3125                .expect_err("out of range");
3126            assert!(matches!(err, Error::MidiError(_)), "got {err:?}");
3127        }
3128    }
3129}
3130
3131#[cfg(test)]
3132mod output_midi_consumer_tests {
3133    use super::*;
3134
3135    fn note(n: u8) -> MidiEvent {
3136        MidiEvent::NoteOn {
3137            channel: MidiChannel::Ch1,
3138            note: n,
3139            velocity: 100,
3140        }
3141    }
3142
3143    #[test]
3144    fn drains_in_order_and_drops_oldest_when_full() {
3145        let q = Arc::new(ArrayQueue::new(2));
3146        let consumer = OutputMidiConsumer::from_queue(q.clone());
3147
3148        // force_push mirrors what process() does: when full, the oldest is dropped.
3149        q.force_push(note(60).into());
3150        q.force_push(note(61).into());
3151        q.force_push(note(62).into()); // capacity 2 → drops note 60
3152
3153        assert_eq!(consumer.drain(), vec![note(61), note(62)]);
3154        // Drained: now empty.
3155        assert_eq!(consumer.pop(), None);
3156        assert_eq!(consumer.drain(), vec![]);
3157    }
3158
3159    #[test]
3160    fn handle_is_send_and_shares_the_queue_across_threads() {
3161        let q = Arc::new(ArrayQueue::new(8));
3162        let consumer = OutputMidiConsumer::from_queue(q.clone());
3163        // Push from another thread (the audio side is a different thread in practice).
3164        let producer = q.clone();
3165        std::thread::spawn(move || {
3166            producer.force_push(note(64).into());
3167        })
3168        .join()
3169        .unwrap();
3170        assert_eq!(consumer.pop(), Some(note(64)));
3171    }
3172}
3173
3174#[cfg(test)]
3175mod vstpreset_tests {
3176    use super::{
3177        decode_state_snapshot, encode_state_snapshot, vstpreset, StateSnapshot,
3178        STATE_SNAPSHOT_MAGIC,
3179    };
3180
3181    const TEST_CLASS_ID: &str = "0123456789ABCDEF0123456789ABCDEF";
3182
3183    #[test]
3184    fn build_parse_round_trip() {
3185        let state = b"opaque plugin state \x00\x01\x02\xff bytes".to_vec();
3186        let controller = b"controller-only state".to_vec();
3187        let bytes = vstpreset::build(TEST_CLASS_ID, &state, Some(&controller)).expect("build");
3188
3189        // Sanity-check the header layout.
3190        assert_eq!(&bytes[0..4], b"VST3");
3191        assert_eq!(
3192            i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
3193            1
3194        );
3195        assert_eq!(&bytes[8..40], TEST_CLASS_ID.as_bytes());
3196
3197        let parsed = vstpreset::parse(&bytes).expect("parse");
3198        assert_eq!(parsed.class_id, TEST_CLASS_ID);
3199        assert_eq!(parsed.component_state, state);
3200        assert_eq!(parsed.controller_state, Some(controller));
3201    }
3202
3203    #[test]
3204    fn round_trip_empty_state() {
3205        let bytes = vstpreset::build(TEST_CLASS_ID, &[], None).expect("build");
3206        let parsed = vstpreset::parse(&bytes).expect("parse");
3207        assert_eq!(parsed.class_id, TEST_CLASS_ID);
3208        assert!(parsed.component_state.is_empty());
3209        assert!(parsed.controller_state.is_none());
3210    }
3211
3212    #[test]
3213    fn round_trip_empty_controller_state() {
3214        let bytes = vstpreset::build(TEST_CLASS_ID, b"component", Some(&[])).expect("build");
3215        let parsed = vstpreset::parse(&bytes).expect("parse");
3216        assert_eq!(parsed.controller_state, Some(Vec::new()));
3217    }
3218
3219    #[test]
3220    fn build_rejects_wrong_length_class_id() {
3221        assert!(vstpreset::build("short", b"x", None).is_err());
3222        assert!(vstpreset::build("Z123456789ABCDEF0123456789ABCDEF", b"x", None).is_err());
3223    }
3224
3225    #[test]
3226    fn parse_rejects_bad_magic() {
3227        let mut bytes = vstpreset::build(TEST_CLASS_ID, b"x", None).expect("build");
3228        bytes[0] = b'X';
3229        assert!(vstpreset::parse(&bytes).is_err());
3230    }
3231
3232    #[test]
3233    fn parse_rejects_truncated_header() {
3234        assert!(vstpreset::parse(b"VST3").is_err());
3235    }
3236
3237    #[test]
3238    fn parse_rejects_out_of_bounds_list_offset() {
3239        let mut bytes = vstpreset::build(TEST_CLASS_ID, b"hello", None).expect("build");
3240        // Corrupt the list offset (bytes 40..48) to point past the end.
3241        let bad = (bytes.len() as i64 + 100).to_le_bytes();
3242        bytes[40..48].copy_from_slice(&bad);
3243        assert!(vstpreset::parse(&bytes).is_err());
3244    }
3245
3246    #[test]
3247    fn parser_accepts_unknown_chunk_and_controller_before_component() {
3248        let comp = b"component";
3249        let cont = b"controller";
3250        let unknown = b"metadata";
3251        let body_len = comp.len() + cont.len() + unknown.len();
3252        let list_offset = 48 + body_len;
3253        let mut bytes = Vec::new();
3254        bytes.extend_from_slice(b"VST3");
3255        bytes.extend_from_slice(&1i32.to_le_bytes());
3256        bytes.extend_from_slice(TEST_CLASS_ID.as_bytes());
3257        bytes.extend_from_slice(&(list_offset as i64).to_le_bytes());
3258        bytes.extend_from_slice(comp);
3259        bytes.extend_from_slice(cont);
3260        bytes.extend_from_slice(unknown);
3261        bytes.extend_from_slice(b"List");
3262        bytes.extend_from_slice(&3i32.to_le_bytes());
3263        for (id, offset, state) in [
3264            (b"Cont", 48 + comp.len(), cont.as_slice()),
3265            (b"Info", 48 + comp.len() + cont.len(), unknown.as_slice()),
3266            (b"Comp", 48, comp.as_slice()),
3267        ] {
3268            bytes.extend_from_slice(id);
3269            bytes.extend_from_slice(&(offset as i64).to_le_bytes());
3270            bytes.extend_from_slice(&(state.len() as i64).to_le_bytes());
3271        }
3272        let parsed = vstpreset::parse(&bytes).expect("parse");
3273        assert_eq!(parsed.component_state, comp);
3274        assert_eq!(parsed.controller_state.as_deref(), Some(cont.as_slice()));
3275    }
3276
3277    #[test]
3278    fn parser_rejects_duplicate_or_overlapping_chunks() {
3279        let mut duplicate =
3280            vstpreset::build(TEST_CLASS_ID, b"component", Some(b"controller")).expect("build");
3281        let list_offset = i64::from_le_bytes(duplicate[40..48].try_into().unwrap()) as usize;
3282        duplicate[list_offset + 28..list_offset + 32].copy_from_slice(b"Comp");
3283        assert!(vstpreset::parse(&duplicate).is_err());
3284
3285        let mut overlap =
3286            vstpreset::build(TEST_CLASS_ID, b"component", Some(b"controller")).expect("build");
3287        let list_offset = i64::from_le_bytes(overlap[40..48].try_into().unwrap()) as usize;
3288        let comp_offset = i64::from_le_bytes(
3289            overlap[list_offset + 12..list_offset + 20]
3290                .try_into()
3291                .unwrap(),
3292        );
3293        overlap[list_offset + 32..list_offset + 40]
3294            .copy_from_slice(&(comp_offset + 1).to_le_bytes());
3295        assert!(vstpreset::parse(&overlap).is_err());
3296    }
3297
3298    #[test]
3299    fn state_snapshot_round_trip_and_legacy_component_compatibility() {
3300        let snapshot = StateSnapshot {
3301            component: b"component".to_vec(),
3302            controller: Some(b"controller".to_vec()),
3303        };
3304        let bytes = encode_state_snapshot(&snapshot).expect("encode");
3305        assert!(bytes.starts_with(STATE_SNAPSHOT_MAGIC));
3306        let decoded = decode_state_snapshot(&bytes).expect("decode");
3307        assert_eq!(decoded.component, snapshot.component);
3308        assert_eq!(decoded.controller, snapshot.controller);
3309
3310        let legacy = decode_state_snapshot(b"old raw component blob").expect("legacy");
3311        assert_eq!(legacy.component, b"old raw component blob");
3312        assert!(legacy.controller.is_none());
3313    }
3314
3315    #[test]
3316    fn state_snapshot_rejects_bad_lengths_and_versions() {
3317        let snapshot = StateSnapshot {
3318            component: b"component".to_vec(),
3319            controller: Some(b"controller".to_vec()),
3320        };
3321        let mut bytes = encode_state_snapshot(&snapshot).expect("encode");
3322        bytes[16..20].copy_from_slice(&2u32.to_le_bytes());
3323        assert!(decode_state_snapshot(&bytes).is_err());
3324
3325        let mut bytes = encode_state_snapshot(&snapshot).expect("encode");
3326        bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
3327        assert!(decode_state_snapshot(&bytes).is_err());
3328    }
3329}