Skip to main content

vst3_host/
plugin.rs

1//! VST3 plugin wrapper with safe API
2
3use crate::{
4    audio::{AudioBuffers, AudioLevels},
5    error::{Error, Result},
6    midi::{MidiChannel, MidiEvent},
7    parameters::{Parameter, ParameterUpdate},
8};
9use std::sync::{Arc, Mutex};
10
11/// Information about a VST3 plugin
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct PluginInfo {
14    /// Full path to the VST3 bundle/file
15    pub path: std::path::PathBuf,
16    /// Plugin name
17    pub name: String,
18    /// Vendor/manufacturer name
19    pub vendor: String,
20    /// Plugin version
21    pub version: String,
22    /// Plugin category (e.g., "Fx", "Instrument")
23    pub category: String,
24    /// Unique plugin ID
25    pub uid: String,
26    /// Number of audio input buses
27    pub audio_inputs: u32,
28    /// Number of audio output buses
29    pub audio_outputs: u32,
30    /// Whether the plugin accepts MIDI input
31    pub has_midi_input: bool,
32    /// Whether the plugin produces MIDI output
33    pub has_midi_output: bool,
34    /// Whether the plugin has a GUI
35    pub has_gui: bool,
36}
37
38/// A saved plugin preset: the plugin's identity plus its opaque state blob.
39///
40/// Written/read by [`Plugin::save_preset`] / [`Plugin::load_preset`]. The `uid` lets a
41/// loader reject a preset that belongs to a different plugin (whose state bytes would be
42/// meaningless or harmful).
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
44pub struct PluginPreset {
45    /// The originating plugin's unique class id ([`PluginInfo::uid`]).
46    pub uid: String,
47    /// The originating plugin's display name (for friendly mismatch messages).
48    pub plugin_name: String,
49    /// The plugin's opaque serialized state (from [`Plugin::save_state`]).
50    pub state: Vec<u8>,
51}
52
53/// A plugin unit (from `IUnitInfo`) and its program list, if any.
54///
55/// Units form a hierarchy (via [`parent_id`](Self::parent_id)); a unit may carry a named
56/// program list (e.g. a synth's factory patches). Query with [`Plugin::get_units`].
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct PluginUnit {
59    /// Unit id (unique within the plugin; the root unit is conventionally `0`).
60    pub id: i32,
61    /// Parent unit id, or `-1` for the root.
62    pub parent_id: i32,
63    /// Unit display name.
64    pub name: String,
65    /// Program names in this unit's program list (empty if the unit has none).
66    pub programs: Vec<String>,
67}
68
69/// How the plugin should run: real-time (live playback) or offline (faster-than-real-time
70/// bounce/render). Maps to VST3 `kRealtime` / `kOffline`; plugins may switch quality or
71/// look-ahead accordingly. Defaults to [`ProcessMode::Realtime`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
73pub enum ProcessMode {
74    /// Real-time / live processing (the default; `kRealtime`).
75    #[default]
76    Realtime,
77    /// Offline / non-real-time processing such as a render or bounce (`kOffline`).
78    Offline,
79}
80
81/// VST3 plugin instance
82#[allow(clippy::type_complexity)] // callback fields are Box<dyn Fn...>; intrinsic to the API
83pub struct Plugin {
84    // Internal state is hidden from public API
85    pub(crate) info: PluginInfo,
86    pub(crate) is_processing: bool,
87    /// Configured sample rate (exposed via [`Plugin::sample_rate`]).
88    pub(crate) sample_rate: f64,
89    /// Configured max block size (exposed via [`Plugin::block_size`]).
90    pub(crate) block_size: usize,
91    pub(crate) audio_levels: Arc<Mutex<AudioLevels>>,
92    pub(crate) parameter_change_callback: Option<Box<dyn Fn(u32, f64) + Send + 'static>>,
93    pub(crate) audio_callback: Option<Box<dyn Fn(&AudioLevels) + Send + 'static>>,
94
95    // These will be populated by the actual implementation
96    pub(crate) internal: Option<Box<dyn PluginInternal>>,
97}
98
99// Internal trait for hiding implementation details
100pub(crate) trait PluginInternal: Send {
101    fn set_parameter(&mut self, id: u32, value: f64) -> Result<()>;
102    /// Schedule a parameter change at a sample offset within the next process block.
103    /// Defaults to a block-start change (ignores the offset) for implementations that don't
104    /// support sample-accurate scheduling (e.g. process isolation).
105    fn set_parameter_at(&mut self, id: u32, value: f64, _sample_offset: i32) -> Result<()> {
106        self.set_parameter(id, value)
107    }
108    fn get_parameter(&self, id: u32) -> Result<f64>;
109    fn get_all_parameters(&self) -> Result<Vec<Parameter>>;
110    fn format_parameter(&self, id: u32, normalized: f64) -> Result<String>;
111    fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()>;
112    /// Re-run `setupProcessing` for a new sample rate / block size. Defaults to unsupported
113    /// (e.g. process isolation, where reconfigure isn't marshalled across the boundary).
114    fn reconfigure(&mut self, _sample_rate: f64, _block_size: usize) -> Result<()> {
115        Err(Error::Other(
116            "runtime reconfigure is not supported for this plugin".to_string(),
117        ))
118    }
119    /// Switch the plugin's process mode (real-time vs offline), re-running `setupProcessing`.
120    /// Defaults to unsupported (e.g. process isolation, where it isn't marshalled).
121    fn set_process_mode(&mut self, _mode: crate::plugin::ProcessMode) -> Result<()> {
122        Err(Error::Other(
123            "process mode switching is not supported for this plugin".to_string(),
124        ))
125    }
126    /// Query each audio bus's current speaker arrangement. Defaults to unsupported.
127    fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
128        Err(Error::Other(
129            "bus arrangement query is not supported for this plugin".to_string(),
130        ))
131    }
132    /// Request specific speaker arrangements for the audio buses (re-runs `setupProcessing`).
133    /// Defaults to unsupported (e.g. process isolation, where it isn't marshalled).
134    fn set_bus_arrangements(
135        &mut self,
136        _inputs: &[crate::audio::SpeakerArrangement],
137        _outputs: &[crate::audio::SpeakerArrangement],
138    ) -> Result<()> {
139        Err(Error::Other(
140            "bus arrangement negotiation is not supported for this plugin".to_string(),
141        ))
142    }
143    fn send_midi_event(&mut self, event: MidiEvent) -> Result<()>;
144    /// Schedule a MIDI event at a sample offset within the next process block.
145    /// Defaults to a block-start event (ignores the offset) for implementations that don't
146    /// support sample-accurate scheduling (e.g. process isolation).
147    fn send_midi_event_at(&mut self, event: MidiEvent, _sample_offset: i32) -> Result<()> {
148        self.send_midi_event(event)
149    }
150    /// Start a note and return a per-voice [`NoteId`] for targeting note-expression. Default:
151    /// unsupported (process isolation does not yet marshal note expression, so it errors).
152    fn note_on(
153        &mut self,
154        _channel: MidiChannel,
155        _note: u8,
156        _velocity: u8,
157        _sample_offset: i32,
158    ) -> Result<crate::midi::NoteId> {
159        Err(Error::Other(
160            "per-note expression is not supported for this plugin".to_string(),
161        ))
162    }
163    /// Release a note started with [`Self::note_on`]. Default: unsupported.
164    fn note_off(&mut self, _id: crate::midi::NoteId, _sample_offset: i32) -> Result<()> {
165        Err(Error::Other(
166            "per-note expression is not supported for this plugin".to_string(),
167        ))
168    }
169    /// Send a per-note expression value (normalized 0..1) for a voice. Default: unsupported.
170    fn send_note_expression(
171        &mut self,
172        _id: crate::midi::NoteId,
173        _kind: crate::midi::NoteExpressionType,
174        _value: f64,
175        _sample_offset: i32,
176    ) -> Result<()> {
177        Err(Error::Other(
178            "per-note expression is not supported for this plugin".to_string(),
179        ))
180    }
181    /// Enumerate the per-note expressions the plugin advertises (`INoteExpressionController`).
182    /// Defaults to empty.
183    fn note_expressions(
184        &self,
185        _bus: i32,
186        _channel: i16,
187    ) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
188        Ok(Vec::new())
189    }
190    fn start_processing(&mut self) -> Result<()>;
191    fn stop_processing(&mut self) -> Result<()>;
192    fn has_editor(&self) -> bool;
193    fn open_editor(&mut self, parent: *mut std::ffi::c_void) -> Result<()>;
194    fn close_editor(&mut self) -> Result<()>;
195    fn get_editor_size(&self) -> Result<(i32, i32)>;
196    fn get_parameter_changes(&self) -> Vec<(u32, f64)>;
197    /// Take the MIDI events the plugin has emitted since the last call. Defaults to empty
198    /// for implementations that don't capture output MIDI (e.g. process isolation).
199    fn take_output_events(&self) -> Vec<MidiEvent> {
200        Vec::new()
201    }
202    /// Enumerate the plugin's units and their program lists (`IUnitInfo`). Defaults to empty
203    /// for implementations that don't query it yet (e.g. process isolation).
204    fn get_units(&self) -> Result<Vec<PluginUnit>> {
205        Ok(Vec::new())
206    }
207    /// Processing latency in samples (`IAudioProcessor::getLatencySamples`). Defaults to 0.
208    fn latency_samples(&self) -> u32 {
209        0
210    }
211    /// Tail length in samples (`IAudioProcessor::getTailSamples`). Defaults to 0.
212    fn tail_samples(&self) -> u32 {
213        0
214    }
215    /// Resolve a MIDI controller `(bus, channel, cc)` to a parameter id via `IMidiMapping`.
216    /// Defaults to `None` (plugin doesn't implement the interface, or no mapping / isolation).
217    fn midi_cc_to_parameter(&self, _bus: i32, _channel: i16, _cc: u16) -> Option<u32> {
218        None
219    }
220    /// Serialize the plugin's current state to an opaque byte blob.
221    fn save_state(&self) -> Result<Vec<u8>> {
222        Err(Error::Other(
223            "state save/restore is not supported".to_string(),
224        ))
225    }
226    /// Restore the plugin's state from a blob previously returned by [`Self::save_state`].
227    fn load_state(&mut self, _data: &[u8]) -> Result<()> {
228        Err(Error::Other(
229            "state save/restore is not supported".to_string(),
230        ))
231    }
232    /// OS process id of the isolated helper, if this plugin runs out-of-process.
233    fn helper_pid(&self) -> Option<u32> {
234        None
235    }
236    /// Number of times this plugin has been recovered (respawned + reloaded). Defaults to 0
237    /// for non-isolated plugins.
238    fn recovery_count(&self) -> u64 {
239        0
240    }
241    /// Recover from a crashed isolated helper by respawning and reloading. Only meaningful
242    /// for process-isolated plugins.
243    fn recover(&mut self) -> Result<()> {
244        Err(Error::Other(
245            "recovery is only supported for process-isolated plugins".to_string(),
246        ))
247    }
248    /// The size the plugin's editor has requested (via `IPlugFrame`) since the last poll.
249    fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
250        None
251    }
252    /// Total output audio channels across the plugin's output buses. Defaults to 2.
253    fn output_channel_count(&self) -> usize {
254        2
255    }
256}
257
258impl Plugin {
259    /// Get plugin information
260    pub fn info(&self) -> &PluginInfo {
261        &self.info
262    }
263
264    /// The sample rate (Hz) this plugin was configured with at load.
265    pub fn sample_rate(&self) -> f64 {
266        self.sample_rate
267    }
268
269    /// The maximum block size (frames per `process_audio` call) configured at load.
270    pub fn block_size(&self) -> usize {
271        self.block_size
272    }
273
274    /// Reconfigure the plugin for a new sample rate and/or maximum block size, re-running the
275    /// plugin's `setupProcessing` and rebuilding its audio buffers.
276    ///
277    /// Use this when the audio device's sample rate changes mid-session instead of reloading.
278    /// The plugin must **not** be processing: call [`Self::stop_processing`] first, reconfigure,
279    /// then [`Self::start_processing`] again. Returns an error if called while processing, on an
280    /// invalid sample rate / zero block size, or under process isolation (not yet marshalled).
281    pub fn reconfigure(&mut self, sample_rate: f64, block_size: usize) -> Result<()> {
282        if self.is_processing {
283            return Err(Error::Other(
284                "cannot reconfigure while processing; call stop_processing() first".to_string(),
285            ));
286        }
287        if !(sample_rate.is_finite() && sample_rate > 0.0) {
288            return Err(Error::InvalidParameter(format!(
289                "sample rate must be finite and positive, got {sample_rate}"
290            )));
291        }
292        if block_size == 0 {
293            return Err(Error::InvalidParameter(
294                "block size must be greater than 0".to_string(),
295            ));
296        }
297
298        self.internal
299            .as_mut()
300            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
301            .reconfigure(sample_rate, block_size)?;
302
303        self.sample_rate = sample_rate;
304        self.block_size = block_size;
305        Ok(())
306    }
307
308    /// Switch the plugin between real-time and offline processing, re-running the plugin's
309    /// `setupProcessing` so it can adjust quality / look-ahead for a faster-than-real-time
310    /// bounce.
311    ///
312    /// Like [`Self::reconfigure`], the plugin must **not** be processing: call
313    /// [`Self::stop_processing`] first. Returns an error if called while processing, or under
314    /// process isolation (not marshalled across the boundary).
315    pub fn set_process_mode(&mut self, mode: ProcessMode) -> Result<()> {
316        if self.is_processing {
317            return Err(Error::Other(
318                "cannot set process mode while processing; call stop_processing() first"
319                    .to_string(),
320            ));
321        }
322        self.internal
323            .as_mut()
324            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
325            .set_process_mode(mode)
326    }
327
328    /// Query the current speaker arrangement of each audio input/output bus.
329    pub fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
330        self.internal
331            .as_ref()
332            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
333            .bus_arrangements()
334    }
335
336    /// Request specific speaker arrangements for the audio buses (e.g. force stereo, or a
337    /// surround layout). The slices give one [`SpeakerArrangement`](crate::audio::SpeakerArrangement)
338    /// per input bus and per output bus, in bus-index order.
339    ///
340    /// Re-runs the plugin's `setupProcessing`, so the plugin must **not** be processing (call
341    /// [`Self::stop_processing`] first). A plugin may decline a requested layout and keep its
342    /// own; re-query with [`Self::bus_arrangements`] to see what was actually applied. Errors
343    /// while processing or under process isolation (not marshalled).
344    pub fn set_bus_arrangements(
345        &mut self,
346        inputs: &[crate::audio::SpeakerArrangement],
347        outputs: &[crate::audio::SpeakerArrangement],
348    ) -> Result<()> {
349        if self.is_processing {
350            return Err(Error::Other(
351                "cannot set bus arrangements while processing; call stop_processing() first"
352                    .to_string(),
353            ));
354        }
355        self.internal
356            .as_mut()
357            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
358            .set_bus_arrangements(inputs, outputs)
359    }
360
361    /// Get all parameters
362    pub fn get_parameters(&self) -> Result<Vec<Parameter>> {
363        self.internal
364            .as_ref()
365            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
366            .get_all_parameters()
367    }
368
369    /// Set a parameter value by ID
370    pub fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
371        if !(0.0..=1.0).contains(&value) {
372            return Err(Error::InvalidParameter(format!(
373                "Value {} is out of range [0.0, 1.0]",
374                value
375            )));
376        }
377
378        self.internal
379            .as_mut()
380            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
381            .set_parameter(id, value)?;
382
383        // Trigger callback if set
384        if let Some(ref callback) = self.parameter_change_callback {
385            callback(id, value);
386        }
387
388        Ok(())
389    }
390
391    /// Set a parameter value at a specific sample offset within the next process block.
392    ///
393    /// This is the sample-accurate building block for automation: call it once per
394    /// sub-block point (e.g. from [`ParameterAutomation::points_for_block`]) and the plugin
395    /// receives the changes at their offsets in the next `process_audio`. Like
396    /// [`Self::set_parameter`], `value` is normalized `0.0..=1.0`.
397    ///
398    /// `sample_offset` is clamped to the block. Under process isolation the offset **is** now
399    /// carried across the boundary and applied by the helper's in-process plugin.
400    ///
401    /// [`ParameterAutomation::points_for_block`]: crate::parameters::ParameterAutomation::points_for_block
402    pub fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
403        if !(0.0..=1.0).contains(&value) {
404            return Err(Error::InvalidParameter(format!(
405                "Value {} is out of range [0.0, 1.0]",
406                value
407            )));
408        }
409        self.internal
410            .as_mut()
411            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
412            .set_parameter_at(id, value, sample_offset)
413    }
414
415    /// Enumerate the plugin's units and their program lists (`IUnitInfo`).
416    ///
417    /// Returns an empty list for plugins that don't implement `IUnitInfo`, and (for now) for
418    /// plugins running under process isolation. The root unit (id `0`) is typically present.
419    pub fn get_units(&self) -> Result<Vec<PluginUnit>> {
420        self.internal
421            .as_ref()
422            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
423            .get_units()
424    }
425
426    /// The plugin's reported processing latency in samples (e.g. from look-ahead or
427    /// oversampling), via `IAudioProcessor::getLatencySamples`. Use it to delay-compensate
428    /// when aligning the plugin's output with other signals. `0` if it reports none, or for
429    /// plugins running under process isolation (not bridged).
430    pub fn latency_samples(&self) -> u32 {
431        self.internal
432            .as_ref()
433            .map(|i| i.latency_samples())
434            .unwrap_or(0)
435    }
436
437    /// The plugin's reported tail length in samples (how long it keeps producing output
438    /// after input stops — e.g. reverb/delay), via `IAudioProcessor::getTailSamples`. `0`
439    /// means no tail; `u32::MAX` means an infinite tail. `0` for isolated plugins (not
440    /// bridged).
441    pub fn tail_samples(&self) -> u32 {
442        self.internal
443            .as_ref()
444            .map(|i| i.tail_samples())
445            .unwrap_or(0)
446    }
447
448    /// Resolve a MIDI controller to the parameter it's mapped to, via the plugin's
449    /// `IMidiMapping` (`getMidiControllerAssignment`).
450    ///
451    /// `bus` is the event input bus index (usually `0`), `channel` the 0-based MIDI channel,
452    /// and `cc` the MIDI controller number (`0–127`, or the VST3 specials such as `128`
453    /// aftertouch / `129` pitch-bend). Returns the parameter id the controller drives, or
454    /// `None` if the plugin doesn't implement `IMidiMapping`, the controller is unmapped, or
455    /// the plugin is process-isolated (not bridged).
456    pub fn midi_cc_to_parameter(&self, bus: i32, channel: i16, cc: u16) -> Option<u32> {
457        // VST3 controller numbers are 0..130 (0–127 MIDI CCs + the specials up to pitch-bend).
458        // Reject out-of-range values rather than forwarding a meaningless controller number.
459        if cc > 129 {
460            return None;
461        }
462        self.internal
463            .as_ref()?
464            .midi_cc_to_parameter(bus, channel, cc)
465    }
466
467    /// Get a parameter value by ID
468    pub fn get_parameter(&self, id: u32) -> Result<f64> {
469        self.internal
470            .as_ref()
471            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
472            .get_parameter(id)
473    }
474
475    /// Format a parameter value as the plugin itself would display it.
476    ///
477    /// VST3 keeps all parameter values normalized (0.0–1.0) and delegates
478    /// human-readable formatting to the plugin's controller. This asks the plugin to
479    /// render `normalized` for parameter `id`, returning exactly what its own UI would
480    /// show — e.g. `"440.00 Hz"`, `"-6.0 dB"`, `"Sine"`. Prefer this over
481    /// [`Parameter::format_value`], which can only approximate without the plugin's
482    /// internal mapping.
483    pub fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
484        self.internal
485            .as_ref()
486            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
487            .format_parameter(id, normalized)
488    }
489
490    /// Set a parameter by name
491    pub fn set_parameter_by_name(&mut self, name: &str, value: f64) -> Result<()> {
492        let params = self.get_parameters()?;
493        let param = params
494            .iter()
495            .find(|p| p.name == name)
496            .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))?;
497
498        self.set_parameter(param.id, value)
499    }
500
501    /// Find a parameter by name
502    pub fn find_parameter(&self, name: &str) -> Result<Parameter> {
503        let params = self.get_parameters()?;
504        params
505            .into_iter()
506            .find(|p| p.name == name)
507            .ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))
508    }
509
510    /// Send a MIDI note on event
511    pub fn send_midi_note(&mut self, note: u8, velocity: u8, channel: MidiChannel) -> Result<()> {
512        if note > 127 {
513            return Err(Error::MidiError(format!("Invalid note number: {}", note)));
514        }
515        if velocity > 127 {
516            return Err(Error::MidiError(format!("Invalid velocity: {}", velocity)));
517        }
518
519        let event = MidiEvent::NoteOn {
520            channel,
521            note,
522            velocity,
523        };
524        self.send_midi_event(event)
525    }
526
527    /// Send a MIDI note off event
528    pub fn send_midi_note_off(&mut self, note: u8, channel: MidiChannel) -> Result<()> {
529        if note > 127 {
530            return Err(Error::MidiError(format!("Invalid note number: {}", note)));
531        }
532
533        let event = MidiEvent::NoteOff {
534            channel,
535            note,
536            velocity: 0,
537        };
538        self.send_midi_event(event)
539    }
540
541    /// Send a MIDI control change event
542    pub fn send_midi_cc(&mut self, controller: u8, value: u8, channel: MidiChannel) -> Result<()> {
543        if controller > 127 {
544            return Err(Error::MidiError(format!(
545                "Invalid controller number: {}",
546                controller
547            )));
548        }
549        if value > 127 {
550            return Err(Error::MidiError(format!("Invalid CC value: {}", value)));
551        }
552
553        let event = MidiEvent::ControlChange {
554            channel,
555            controller,
556            value,
557        };
558        self.send_midi_event(event)
559    }
560
561    /// Send a generic MIDI event
562    pub fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
563        self.internal
564            .as_mut()
565            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
566            .send_midi_event(event)
567    }
568
569    /// Schedule a MIDI event at a sample offset within the **next** [`process_audio`] block.
570    ///
571    /// Use this for sample-accurate sequencing: an event sent with `sample_offset = N` takes
572    /// effect `N` frames into the next processed block, rather than at its start. Keep the
573    /// offset within the upcoming block's frame count ([`Plugin::block_size`] is the maximum);
574    /// a negative offset is treated as 0, and an offset past the block end is plugin-defined.
575    ///
576    /// Under process isolation the offset is not marshalled across the boundary — the event is
577    /// delivered at block start (offset 0), same as [`Self::send_midi_event`].
578    ///
579    /// [`process_audio`]: Self::process_audio
580    pub fn send_midi_event_at(&mut self, event: MidiEvent, sample_offset: i32) -> Result<()> {
581        self.internal
582            .as_mut()
583            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
584            .send_midi_event_at(event, sample_offset)
585    }
586
587    /// Start a note and get a per-voice [`NoteId`](crate::midi::NoteId) handle for sending
588    /// per-note (MPE-style) expression to that exact voice via
589    /// [`send_note_expression`](Self::send_note_expression).
590    ///
591    /// Unlike [`send_midi_note`](Self::send_midi_note) (which uses a shared note id and can't be
592    /// individually expressed), this allocates a unique voice id. Pair it with
593    /// [`note_off`](Self::note_off). Per-note expression is in-process only — it errors under
594    /// process isolation (not yet marshalled across the boundary).
595    pub fn note_on(
596        &mut self,
597        channel: MidiChannel,
598        note: u8,
599        velocity: u8,
600    ) -> Result<crate::midi::NoteId> {
601        self.note_on_at(channel, note, velocity, 0)
602    }
603
604    /// [`note_on`](Self::note_on) scheduled at a sample offset within the next block.
605    pub fn note_on_at(
606        &mut self,
607        channel: MidiChannel,
608        note: u8,
609        velocity: u8,
610        sample_offset: i32,
611    ) -> Result<crate::midi::NoteId> {
612        self.internal
613            .as_mut()
614            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
615            .note_on(channel, note, velocity, sample_offset)
616    }
617
618    /// Release a note started with [`note_on`](Self::note_on).
619    pub fn note_off(&mut self, id: crate::midi::NoteId) -> Result<()> {
620        self.note_off_at(id, 0)
621    }
622
623    /// [`note_off`](Self::note_off) scheduled at a sample offset within the next block.
624    pub fn note_off_at(&mut self, id: crate::midi::NoteId, sample_offset: i32) -> Result<()> {
625        self.internal
626            .as_mut()
627            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
628            .note_off(id, sample_offset)
629    }
630
631    /// Send a per-note expression value for a voice (normalized `0.0..=1.0`; bipolar dimensions
632    /// like [`Tuning`](crate::midi::NoteExpressionType::Tuning) center at `0.5`). The plugin
633    /// must implement `INoteExpressionController` and the dimension must be one it advertises
634    /// (see [`note_expressions`](Self::note_expressions)).
635    pub fn send_note_expression(
636        &mut self,
637        id: crate::midi::NoteId,
638        kind: crate::midi::NoteExpressionType,
639        value: f64,
640    ) -> Result<()> {
641        self.send_note_expression_at(id, kind, value, 0)
642    }
643
644    /// [`send_note_expression`](Self::send_note_expression) scheduled at a sample offset.
645    pub fn send_note_expression_at(
646        &mut self,
647        id: crate::midi::NoteId,
648        kind: crate::midi::NoteExpressionType,
649        value: f64,
650        sample_offset: i32,
651    ) -> Result<()> {
652        if !(0.0..=1.0).contains(&value) {
653            return Err(Error::InvalidParameter(format!(
654                "note-expression value {value} out of range [0.0, 1.0]"
655            )));
656        }
657        self.internal
658            .as_mut()
659            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
660            .send_note_expression(id, kind, value, sample_offset)
661    }
662
663    /// Enumerate the per-note expression dimensions the plugin advertises for the given event
664    /// bus / channel (defaults: bus 0, channel 0), via `INoteExpressionController`. Empty if the
665    /// plugin doesn't implement it.
666    pub fn note_expressions(&self) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
667        self.internal
668            .as_ref()
669            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
670            .note_expressions(0, 0)
671    }
672
673    /// Start audio processing
674    pub fn start_processing(&mut self) -> Result<()> {
675        if self.is_processing {
676            return Ok(());
677        }
678
679        self.internal
680            .as_mut()
681            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
682            .start_processing()?;
683
684        self.is_processing = true;
685        Ok(())
686    }
687
688    /// Stop audio processing
689    pub fn stop_processing(&mut self) -> Result<()> {
690        if !self.is_processing {
691            return Ok(());
692        }
693
694        self.internal
695            .as_mut()
696            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
697            .stop_processing()?;
698
699        self.is_processing = false;
700        Ok(())
701    }
702
703    /// Process audio buffers
704    pub fn process_audio(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
705        if !self.is_processing {
706            return Err(Error::Other("Plugin is not processing".to_string()));
707        }
708
709        self.internal
710            .as_mut()
711            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
712            .process(buffers)?;
713
714        // Update audio levels
715        if let Ok(mut levels) = self.audio_levels.lock() {
716            levels.update_from_buffers(&buffers.outputs);
717
718            // Trigger audio callback if set
719            if let Some(ref callback) = self.audio_callback {
720                callback(&levels);
721            }
722        }
723
724        Ok(())
725    }
726
727    /// Get current output levels.
728    ///
729    /// Recovers automatically if the audio thread panicked while holding the lock
730    /// (poisoned mutex) rather than propagating the panic to the caller — metering
731    /// must never take down a UI thread polling it.
732    pub fn get_output_levels(&self) -> AudioLevels {
733        self.audio_levels
734            .lock()
735            .unwrap_or_else(|poisoned| poisoned.into_inner())
736            .clone()
737    }
738
739    /// Check if the plugin is currently processing
740    pub fn is_processing(&self) -> bool {
741        self.is_processing
742    }
743
744    /// Set a callback for parameter changes
745    pub fn on_parameter_change<F>(&mut self, callback: F)
746    where
747        F: Fn(u32, f64) + Send + 'static,
748    {
749        self.parameter_change_callback = Some(Box::new(callback));
750    }
751
752    /// Set a callback for audio processing (called after each process cycle)
753    pub fn on_audio_process<F>(&mut self, callback: F)
754    where
755        F: Fn(&AudioLevels) + Send + 'static,
756    {
757        self.audio_callback = Some(Box::new(callback));
758    }
759
760    /// Check if the plugin has an editor GUI
761    pub fn has_editor(&self) -> bool {
762        self.internal
763            .as_ref()
764            .map(|i| i.has_editor())
765            .unwrap_or(false)
766    }
767
768    /// Open the plugin editor window
769    pub fn open_editor(&mut self, parent: WindowHandle) -> Result<()> {
770        self.internal
771            .as_mut()
772            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
773            .open_editor(parent.0)
774    }
775
776    /// Close the plugin editor window
777    pub fn close_editor(&mut self) -> Result<()> {
778        self.internal
779            .as_mut()
780            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
781            .close_editor()
782    }
783
784    /// Get the preferred editor size
785    pub fn get_editor_size(&self) -> Result<(i32, i32)> {
786        self.internal
787            .as_ref()
788            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
789            .get_editor_size()
790    }
791
792    /// Create a batch parameter update
793    pub fn update_parameters<F>(&mut self, f: F) -> Result<()>
794    where
795        F: FnOnce(&mut ParameterUpdate) -> Result<()>,
796    {
797        let mut update = ParameterUpdate::new(self);
798        f(&mut update)?;
799        update.apply()
800    }
801
802    /// Send MIDI panic (all notes off, all sounds off, reset controllers)
803    pub fn midi_panic(&mut self) -> Result<()> {
804        for i in 0..16 {
805            if let Some(channel) = MidiChannel::from_index(i) {
806                // All Notes Off
807                self.send_midi_cc(123, 0, channel)?;
808                // All Sounds Off
809                self.send_midi_cc(120, 0, channel)?;
810                // Reset All Controllers
811                self.send_midi_cc(121, 0, channel)?;
812            }
813        }
814        Ok(())
815    }
816
817    /// Get parameter changes from plugin GUI
818    /// Returns a vector of (parameter_id, normalized_value) pairs
819    /// This should be called regularly to pick up parameter changes made through the plugin's GUI
820    pub fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
821        self.internal
822            .as_ref()
823            .map(|i| i.get_parameter_changes())
824            .unwrap_or_default()
825    }
826
827    /// Take the MIDI events the plugin has emitted (e.g. from an arpeggiator or MPE
828    /// controller) since the last call, draining the internal buffer.
829    ///
830    /// Output MIDI is captured while the plugin processes audio, so poll this regularly
831    /// (e.g. each UI frame) while the plugin is playing; an empty vector means the plugin
832    /// emitted nothing. This works for process-isolated plugins too — emitted events are
833    /// marshalled back alongside each processed block.
834    ///
835    /// The buffer is capped at 4096 events: if you never poll while a chatty plugin keeps
836    /// emitting, the oldest events are dropped (silently) to bound memory.
837    pub fn take_output_midi(&self) -> Vec<MidiEvent> {
838        self.internal
839            .as_ref()
840            .map(|i| i.take_output_events())
841            .unwrap_or_default()
842    }
843
844    /// Save the plugin's current state (parameters, internal settings, loaded preset) to
845    /// an opaque byte blob.
846    ///
847    /// The bytes are the plugin's own serialized state — treat them as opaque and pair them
848    /// with the plugin's identity ([`PluginInfo::uid`]); they only mean something to the
849    /// same plugin. Persist them to restore a patch later with [`Self::load_state`], or to
850    /// snapshot a session. Call this on the main thread (see the
851    /// [threading model](https://docs.rs/vst3-host)).
852    ///
853    /// Works both in-process and across process isolation (the state blob is marshalled over
854    /// the IPC boundary). Returns an error for plugins that don't implement state saving.
855    pub fn save_state(&self) -> Result<Vec<u8>> {
856        self.internal
857            .as_ref()
858            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
859            .save_state()
860    }
861
862    /// Restore plugin state from a blob produced by [`Self::save_state`] on the *same*
863    /// plugin. Applies to both the processor and the controller, so parameter values and
864    /// the editor reflect the restored state.
865    ///
866    /// Passing bytes from a different plugin has undefined results (the plugin decides what
867    /// to do with bytes it doesn't recognize). Call this on the main thread.
868    pub fn load_state(&mut self, data: &[u8]) -> Result<()> {
869        self.internal
870            .as_mut()
871            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
872            .load_state(data)
873    }
874
875    /// Save this plugin's state to a file as a [`PluginPreset`] (JSON: the plugin's `uid`
876    /// and name plus the opaque state blob). The embedded `uid` lets [`Self::load_preset`]
877    /// reject a preset saved from a different plugin.
878    pub fn save_preset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
879        let info = self.info();
880        let preset = PluginPreset {
881            uid: info.uid.clone(),
882            plugin_name: info.name.clone(),
883            state: self.save_state()?,
884        };
885        let json = serde_json::to_vec_pretty(&preset)
886            .map_err(|e| Error::Other(format!("serialize preset: {e}")))?;
887        std::fs::write(path, json).map_err(|e| Error::Other(format!("write preset: {e}")))?;
888        Ok(())
889    }
890
891    /// Load a [`PluginPreset`] file written by [`Self::save_preset`] and apply its state.
892    /// Returns an error if the preset's `uid` doesn't match this plugin (loading another
893    /// plugin's state is undefined).
894    pub fn load_preset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
895        let bytes = std::fs::read(path).map_err(|e| Error::Other(format!("read preset: {e}")))?;
896        let preset: PluginPreset = serde_json::from_slice(&bytes)
897            .map_err(|e| Error::Other(format!("parse preset: {e}")))?;
898        if preset.uid != self.info().uid {
899            return Err(Error::Other(format!(
900                "preset is for a different plugin ({}, expected {})",
901                preset.plugin_name,
902                self.info().name
903            )));
904        }
905        self.load_state(&preset.state)
906    }
907
908    /// Save this plugin's state to a standard Steinberg `.vstpreset` file.
909    ///
910    /// Unlike [`Self::save_preset`] (a JSON wrapper specific to this library), the
911    /// `.vstpreset` container is the interchange format shared by VST3 hosts and plugins, so
912    /// the file can be read by other hosts (and by the plugin's own preset browser). It wraps
913    /// the same opaque bytes from [`Self::save_state`] in a single `"Comp"` (component state)
914    /// chunk, tagged with this plugin's class id ([`PluginInfo::uid`]) so a loader can reject
915    /// presets from a different plugin. Call this on the main thread.
916    pub fn save_vstpreset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
917        let state = self.save_state()?;
918        let bytes = vstpreset::build(&self.info().uid, &state)?;
919        std::fs::write(path, bytes).map_err(|e| Error::Other(format!("write vstpreset: {e}")))?;
920        Ok(())
921    }
922
923    /// Load a Steinberg `.vstpreset` file and apply its component state to this plugin.
924    ///
925    /// Parses the `.vstpreset` container written by [`Self::save_vstpreset`] (or another VST3
926    /// host), extracts the `"Comp"` (component state) chunk and passes it to
927    /// [`Self::load_state`]. Returns an error if the file's magic is invalid, or if its class
928    /// id doesn't match this plugin (loading another plugin's state is undefined). Call this
929    /// on the main thread.
930    pub fn load_vstpreset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
931        let bytes =
932            std::fs::read(path).map_err(|e| Error::Other(format!("read vstpreset: {e}")))?;
933        let parsed = vstpreset::parse(&bytes)?;
934        if parsed.class_id != self.info().uid {
935            return Err(Error::Other(format!(
936                "vstpreset is for a different plugin (class id {}, expected {})",
937                parsed.class_id,
938                self.info().uid
939            )));
940        }
941        self.load_state(&parsed.component_state)
942    }
943
944    /// The OS process id of the isolated helper hosting this plugin, or `None` if it runs
945    /// in-process. Useful for monitoring an isolated plugin's resource use.
946    pub fn isolation_pid(&self) -> Option<u32> {
947        self.internal.as_ref().and_then(|i| i.helper_pid())
948    }
949
950    /// How many times this plugin has been recovered (helper respawned + reloaded), via either
951    /// [`Self::recover`] or automatic recovery ([`Vst3HostBuilder::auto_recover_plugins`]).
952    ///
953    /// A recovery reloads the plugin from defaults — parameter values and loaded state are NOT
954    /// replayed. With auto-recover on, a crash is otherwise invisible (the call returns `Ok`),
955    /// so poll this count to detect that a reset happened and re-apply a saved
956    /// [`save_state`](Self::save_state) snapshot.
957    ///
958    /// [`Vst3HostBuilder::auto_recover_plugins`]: crate::Vst3HostBuilder::auto_recover_plugins
959    pub fn recovery_count(&self) -> u64 {
960        self.internal
961            .as_ref()
962            .map(|i| i.recovery_count())
963            .unwrap_or(0)
964    }
965
966    /// Total number of output audio channels across the plugin's output buses.
967    ///
968    /// Reflects the plugin's actual bus layout (mono / stereo / surround / multi-bus), not a
969    /// stereo assumption — useful for sizing meters or output buffers. Returns 2 if unknown.
970    pub fn output_channel_count(&self) -> usize {
971        self.internal
972            .as_ref()
973            .map(|i| i.output_channel_count())
974            .unwrap_or(2)
975    }
976
977    /// Poll for an editor resize the plugin requested via VST3's `IPlugFrame` since the last
978    /// call, as `(width, height)` in pixels, or `None`.
979    ///
980    /// Plugins with resizable editors call back to ask the host to resize the window hosting
981    /// their view. Poll this on your UI thread (e.g. each frame) while the editor is open and
982    /// resize your editor container to match. Only the in-process editor path reports this.
983    pub fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
984        self.internal
985            .as_ref()
986            .and_then(|i| i.take_editor_resize_request())
987    }
988
989    /// Recover a process-isolated plugin whose helper has crashed.
990    ///
991    /// When an isolated plugin's helper process dies, calls return [`Error::PluginCrashed`]
992    /// and the host itself stays alive. This respawns the helper and reloads the plugin
993    /// from the same path and audio settings, restarting processing if it was running.
994    ///
995    /// **The reloaded plugin starts from its default state** — parameter values and any
996    /// loaded preset are lost. Snapshot with [`Self::save_state`] beforehand and
997    /// [`Self::load_state`] after recovering to preserve them. Returns an error for
998    /// in-process plugins (an in-process crash takes down the whole host) and if the
999    /// reload itself fails.
1000    pub fn recover(&mut self) -> Result<()> {
1001        self.internal
1002            .as_mut()
1003            .ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
1004            .recover()
1005    }
1006}
1007
1008/// Platform-specific window handle
1009pub struct WindowHandle(pub(crate) *mut std::ffi::c_void);
1010
1011impl WindowHandle {
1012    /// Create from a raw window handle
1013    ///
1014    /// # Safety
1015    /// The pointer must be a valid window handle for the platform
1016    pub unsafe fn from_raw(handle: *mut std::ffi::c_void) -> Self {
1017        Self(handle)
1018    }
1019}
1020
1021// Safe Send implementation - the window handle is platform-specific
1022unsafe impl Send for WindowHandle {}
1023
1024#[cfg(target_os = "macos")]
1025impl WindowHandle {
1026    /// Create from an NSView pointer on macOS
1027    pub fn from_nsview(view: *mut std::ffi::c_void) -> Self {
1028        Self(view)
1029    }
1030}
1031
1032#[cfg(target_os = "windows")]
1033impl WindowHandle {
1034    /// Create from an HWND on Windows
1035    pub fn from_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
1036        Self(hwnd)
1037    }
1038}
1039
1040#[cfg(target_os = "linux")]
1041impl WindowHandle {
1042    /// Create from an X11 window id on Linux (for VST3 `X11EmbedWindowID`).
1043    ///
1044    /// The VST3 X11 platform type expects the window id itself as the handle value,
1045    /// not a pointer to it.
1046    pub fn from_x11(window_id: u32) -> Self {
1047        Self(window_id as usize as *mut std::ffi::c_void)
1048    }
1049}
1050
1051/// Build and parse the standard Steinberg `.vstpreset` container format.
1052///
1053/// Layout (all multi-byte integers little-endian, matching the SDK's `PresetFile`):
1054///
1055/// - Header (48 bytes): magic `b"VST3"` (4) + version `i32` = 1 (4) + 32-char ASCII class
1056///   id (the plugin's FUID hex) (32) + `i64` byte offset from the start of the file to the
1057///   chunk list (8).
1058/// - Body: the chunk payloads, written back to back after the header. We write a single
1059///   `"Comp"` (component state) chunk.
1060/// - Chunk list (at the header's list offset): magic `b"List"` (4) + entry count `i32` (4),
1061///   then per entry: 4-byte chunk id + `i64` absolute offset + `i64` size.
1062mod vstpreset {
1063    use crate::error::{Error, Result};
1064
1065    const MAGIC: &[u8; 4] = b"VST3";
1066    const LIST_MAGIC: &[u8; 4] = b"List";
1067    const COMPONENT_CHUNK: &[u8; 4] = b"Comp";
1068    const VERSION: i32 = 1;
1069    const CLASS_ID_LEN: usize = 32;
1070    const HEADER_SIZE: usize = 4 + 4 + CLASS_ID_LEN + 8;
1071
1072    /// A parsed `.vstpreset` container.
1073    pub(super) struct Parsed {
1074        /// The 32-char ASCII class id from the header.
1075        pub class_id: String,
1076        /// The bytes of the `"Comp"` (component state) chunk.
1077        pub component_state: Vec<u8>,
1078    }
1079
1080    /// Build a `.vstpreset` file wrapping `component_state` in a single component chunk,
1081    /// tagged with `class_id` (a 32-char ASCII FUID hex string).
1082    pub(super) fn build(class_id: &str, component_state: &[u8]) -> Result<Vec<u8>> {
1083        let class_bytes = class_id.as_bytes();
1084        if class_bytes.len() != CLASS_ID_LEN || !class_id.is_ascii() {
1085            return Err(Error::Other(format!(
1086                "vstpreset class id must be {CLASS_ID_LEN} ASCII chars, got {:?}",
1087                class_id
1088            )));
1089        }
1090
1091        let comp_offset = HEADER_SIZE as i64;
1092        let comp_size = component_state.len() as i64;
1093        let list_offset = HEADER_SIZE + component_state.len();
1094
1095        let mut out = Vec::with_capacity(list_offset + 8 + 24);
1096        // Header.
1097        out.extend_from_slice(MAGIC);
1098        out.extend_from_slice(&VERSION.to_le_bytes());
1099        out.extend_from_slice(class_bytes);
1100        out.extend_from_slice(&(list_offset as i64).to_le_bytes());
1101        // Body.
1102        out.extend_from_slice(component_state);
1103        // Chunk list.
1104        out.extend_from_slice(LIST_MAGIC);
1105        out.extend_from_slice(&1i32.to_le_bytes());
1106        out.extend_from_slice(COMPONENT_CHUNK);
1107        out.extend_from_slice(&comp_offset.to_le_bytes());
1108        out.extend_from_slice(&comp_size.to_le_bytes());
1109
1110        Ok(out)
1111    }
1112
1113    /// Parse a `.vstpreset` file, extracting the class id and the component-state chunk.
1114    pub(super) fn parse(bytes: &[u8]) -> Result<Parsed> {
1115        if bytes.len() < HEADER_SIZE {
1116            return Err(Error::Other("vstpreset too short for header".to_string()));
1117        }
1118        if &bytes[0..4] != MAGIC {
1119            return Err(Error::Other(format!(
1120                "bad vstpreset magic: expected {:?}, got {:?}",
1121                MAGIC,
1122                &bytes[0..4]
1123            )));
1124        }
1125        let version = read_i32(&bytes[4..8]);
1126        if version != VERSION {
1127            return Err(Error::Other(format!(
1128                "unsupported vstpreset version {version} (expected {VERSION})"
1129            )));
1130        }
1131        let class_id = String::from_utf8(bytes[8..8 + CLASS_ID_LEN].to_vec())
1132            .map_err(|e| Error::Other(format!("vstpreset class id not UTF-8: {e}")))?;
1133        let list_offset = read_i64(&bytes[8 + CLASS_ID_LEN..HEADER_SIZE]);
1134        if list_offset < HEADER_SIZE as i64 || list_offset as usize > bytes.len() {
1135            return Err(Error::Other(format!(
1136                "vstpreset chunk-list offset {list_offset} out of bounds (len {})",
1137                bytes.len()
1138            )));
1139        }
1140        let list = &bytes[list_offset as usize..];
1141        if list.len() < 8 || &list[0..4] != LIST_MAGIC {
1142            return Err(Error::Other(
1143                "vstpreset chunk list missing or malformed".to_string(),
1144            ));
1145        }
1146        let count = read_i32(&list[4..8]);
1147        if count < 0 {
1148            return Err(Error::Other("vstpreset negative entry count".to_string()));
1149        }
1150        let mut cursor = 8;
1151        for _ in 0..count {
1152            if list.len() < cursor + 20 {
1153                return Err(Error::Other(
1154                    "vstpreset chunk-list entry truncated".to_string(),
1155                ));
1156            }
1157            let id = &list[cursor..cursor + 4];
1158            let offset = read_i64(&list[cursor + 4..cursor + 12]);
1159            let size = read_i64(&list[cursor + 12..cursor + 20]);
1160            cursor += 20;
1161            if id == COMPONENT_CHUNK {
1162                if offset < 0 || size < 0 {
1163                    return Err(Error::Other(
1164                        "vstpreset component chunk has negative offset/size".to_string(),
1165                    ));
1166                }
1167                let start = offset as usize;
1168                let end = start
1169                    .checked_add(size as usize)
1170                    .ok_or_else(|| Error::Other("vstpreset chunk size overflow".to_string()))?;
1171                if end > bytes.len() {
1172                    return Err(Error::Other(format!(
1173                        "vstpreset component chunk [{start}..{end}] out of bounds (len {})",
1174                        bytes.len()
1175                    )));
1176                }
1177                return Ok(Parsed {
1178                    class_id,
1179                    component_state: bytes[start..end].to_vec(),
1180                });
1181            }
1182        }
1183        Err(Error::Other(
1184            "vstpreset has no component (\"Comp\") chunk".to_string(),
1185        ))
1186    }
1187
1188    fn read_i32(b: &[u8]) -> i32 {
1189        i32::from_le_bytes([b[0], b[1], b[2], b[3]])
1190    }
1191
1192    fn read_i64(b: &[u8]) -> i64 {
1193        i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
1194    }
1195}
1196
1197#[cfg(test)]
1198mod vstpreset_tests {
1199    use super::vstpreset;
1200
1201    const TEST_CLASS_ID: &str = "0123456789ABCDEF0123456789ABCDEF";
1202
1203    #[test]
1204    fn build_parse_round_trip() {
1205        let state = b"opaque plugin state \x00\x01\x02\xff bytes".to_vec();
1206        let bytes = vstpreset::build(TEST_CLASS_ID, &state).expect("build");
1207
1208        // Sanity-check the header layout.
1209        assert_eq!(&bytes[0..4], b"VST3");
1210        assert_eq!(
1211            i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1212            1
1213        );
1214        assert_eq!(&bytes[8..40], TEST_CLASS_ID.as_bytes());
1215
1216        let parsed = vstpreset::parse(&bytes).expect("parse");
1217        assert_eq!(parsed.class_id, TEST_CLASS_ID);
1218        assert_eq!(parsed.component_state, state);
1219    }
1220
1221    #[test]
1222    fn round_trip_empty_state() {
1223        let bytes = vstpreset::build(TEST_CLASS_ID, &[]).expect("build");
1224        let parsed = vstpreset::parse(&bytes).expect("parse");
1225        assert_eq!(parsed.class_id, TEST_CLASS_ID);
1226        assert!(parsed.component_state.is_empty());
1227    }
1228
1229    #[test]
1230    fn build_rejects_wrong_length_class_id() {
1231        assert!(vstpreset::build("short", b"x").is_err());
1232    }
1233
1234    #[test]
1235    fn parse_rejects_bad_magic() {
1236        let mut bytes = vstpreset::build(TEST_CLASS_ID, b"x").expect("build");
1237        bytes[0] = b'X';
1238        assert!(vstpreset::parse(&bytes).is_err());
1239    }
1240
1241    #[test]
1242    fn parse_rejects_truncated_header() {
1243        assert!(vstpreset::parse(b"VST3").is_err());
1244    }
1245
1246    #[test]
1247    fn parse_rejects_out_of_bounds_list_offset() {
1248        let mut bytes = vstpreset::build(TEST_CLASS_ID, b"hello").expect("build");
1249        // Corrupt the list offset (bytes 40..48) to point past the end.
1250        let bad = (bytes.len() as i64 + 100).to_le_bytes();
1251        bytes[40..48].copy_from_slice(&bad);
1252        assert!(vstpreset::parse(&bytes).is_err());
1253    }
1254}