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