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}