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