use crate::{
audio::{AudioBuffers, AudioBusLayout, AudioLevels, BusAudioBuffers},
error::{Error, Result},
midi::{
MidiChannel, MidiEvent, PluginEvent, PluginEventData, MAX_EVENT_PAYLOAD_BYTES,
MAX_EVENT_TEXT_UNITS,
},
parameters::{Parameter, ParameterUpdate},
};
use crossbeam_queue::ArrayQueue;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct OutputMidiConsumer {
queue: Arc<ArrayQueue<PluginEvent>>,
}
impl OutputMidiConsumer {
pub(crate) fn from_queue(queue: Arc<ArrayQueue<PluginEvent>>) -> Self {
Self { queue }
}
pub fn pop(&self) -> Option<MidiEvent> {
while let Some(event) = self.queue.pop() {
if let Some(midi) = event.to_midi() {
return Some(midi);
}
}
None
}
pub fn drain(&self) -> Vec<MidiEvent> {
let mut out = Vec::new();
while let Some(event) = self.pop() {
out.push(event);
}
out
}
}
#[derive(Clone)]
pub struct OutputEventConsumer {
queue: Arc<ArrayQueue<PluginEvent>>,
}
impl OutputEventConsumer {
pub(crate) fn from_queue(queue: Arc<ArrayQueue<PluginEvent>>) -> Self {
Self { queue }
}
pub fn pop(&self) -> Option<PluginEvent> {
self.queue.pop()
}
pub fn drain(&self) -> Vec<PluginEvent> {
let mut out = Vec::new();
while let Some(event) = self.pop() {
out.push(event);
}
out
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PluginInfo {
pub path: std::path::PathBuf,
pub name: String,
pub vendor: String,
pub version: String,
pub category: String,
pub uid: String,
pub audio_inputs: u32,
pub audio_outputs: u32,
pub has_midi_input: bool,
pub has_midi_output: bool,
pub has_gui: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PluginPreset {
pub uid: String,
pub plugin_name: String,
pub state: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum StateContext {
#[default]
Project,
Preset {
path: Option<String>,
},
}
impl StateContext {
pub fn preset() -> Self {
Self::Preset { path: None }
}
pub fn preset_from_path(path: impl AsRef<std::path::Path>) -> Self {
Self::Preset {
path: Some(path.as_ref().to_string_lossy().into_owned()),
}
}
pub fn file_path(&self) -> Option<&std::path::Path> {
match self {
Self::Project => None,
Self::Preset { path } => path.as_deref().map(std::path::Path::new),
}
}
}
pub(crate) struct StateSnapshot {
pub component: Vec<u8>,
pub controller: Option<Vec<u8>>,
}
const STATE_SNAPSHOT_MAGIC: &[u8; 16] = b"VST3HOST_STATE\0\0";
const STATE_SNAPSHOT_VERSION: u32 = 1;
const STATE_SNAPSHOT_HEADER_SIZE: usize = 16 + 4 + 4 + 4;
const NO_CONTROLLER_STATE: u32 = u32::MAX;
const MAX_STATE_SNAPSHOT_PAYLOAD_BYTES: usize =
crate::internal::com_implementations::MAX_STREAM_BYTES;
pub(crate) const MAX_STATE_SNAPSHOT_BYTES: usize =
STATE_SNAPSHOT_HEADER_SIZE + MAX_STATE_SNAPSHOT_PAYLOAD_BYTES;
pub(crate) fn encode_state_snapshot(snapshot: &StateSnapshot) -> Result<Vec<u8>> {
let component_len = u32::try_from(snapshot.component.len())
.map_err(|_| Error::Other("component state is too large".to_string()))?;
let controller_len = match snapshot.controller.as_ref() {
Some(state) => u32::try_from(state.len())
.map_err(|_| Error::Other("controller state is too large".to_string()))?,
None => NO_CONTROLLER_STATE,
};
let payload_size = snapshot
.component
.len()
.checked_add(snapshot.controller.as_ref().map_or(0, Vec::len))
.ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
let total = STATE_SNAPSHOT_HEADER_SIZE
.checked_add(payload_size)
.ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
if payload_size > MAX_STATE_SNAPSHOT_PAYLOAD_BYTES {
return Err(Error::Other(format!(
"combined plugin state payload is too large ({payload_size} bytes, maximum \
{MAX_STATE_SNAPSHOT_PAYLOAD_BYTES})"
)));
}
let mut out = Vec::with_capacity(total);
out.extend_from_slice(STATE_SNAPSHOT_MAGIC);
out.extend_from_slice(&STATE_SNAPSHOT_VERSION.to_le_bytes());
out.extend_from_slice(&component_len.to_le_bytes());
out.extend_from_slice(&controller_len.to_le_bytes());
out.extend_from_slice(&snapshot.component);
if let Some(controller) = snapshot.controller.as_ref() {
out.extend_from_slice(controller);
}
Ok(out)
}
pub(crate) fn decode_state_snapshot(data: &[u8]) -> Result<StateSnapshot> {
if !data.starts_with(STATE_SNAPSHOT_MAGIC) {
if data.len() > MAX_STATE_SNAPSHOT_PAYLOAD_BYTES {
return Err(Error::Other(format!(
"legacy component state is too large ({} bytes, maximum \
{MAX_STATE_SNAPSHOT_PAYLOAD_BYTES})",
data.len()
)));
}
return Ok(StateSnapshot {
component: data.to_vec(),
controller: None,
});
}
if data.len() < STATE_SNAPSHOT_HEADER_SIZE {
return Err(Error::Other(
"truncated vst3-host state snapshot header".to_string(),
));
}
let version = read_snapshot_u32(&data[16..20]);
if version != STATE_SNAPSHOT_VERSION {
return Err(Error::Other(format!(
"unsupported vst3-host state snapshot version {version}"
)));
}
let component_len = read_snapshot_u32(&data[20..24]) as usize;
let encoded_controller_len = read_snapshot_u32(&data[24..28]);
let controller_len =
(encoded_controller_len != NO_CONTROLLER_STATE).then_some(encoded_controller_len as usize);
let payload_size = component_len
.checked_add(controller_len.unwrap_or(0))
.ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
let expected = STATE_SNAPSHOT_HEADER_SIZE
.checked_add(payload_size)
.ok_or_else(|| Error::Other("plugin state size overflow".to_string()))?;
if expected != data.len() || expected > MAX_STATE_SNAPSHOT_BYTES {
return Err(Error::Other(format!(
"invalid vst3-host state snapshot size (header describes {expected} bytes, got {})",
data.len()
)));
}
let component_start = STATE_SNAPSHOT_HEADER_SIZE;
let component_end = component_start + component_len;
Ok(StateSnapshot {
component: data[component_start..component_end].to_vec(),
controller: controller_len.map(|len| data[component_end..component_end + len].to_vec()),
})
}
fn read_snapshot_u32(bytes: &[u8]) -> u32 {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PluginUnit {
pub id: i32,
pub parent_id: i32,
pub name: String,
pub program_list_id: Option<i32>,
pub programs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgramPitchName {
pub midi_pitch: i16,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AutomationState {
Off,
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ParameterEditKind {
BeginGesture,
ValueChange,
EndGesture,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ParameterEdit {
pub id: u32,
pub kind: ParameterEditKind,
pub value: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ProgressKind {
AsyncStateRestoration,
UiBackgroundTask,
Other(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgressValue(u64);
impl ProgressValue {
pub fn new(value: f64) -> Option<Self> {
(value.is_finite() && (0.0..=1.0).contains(&value)).then(|| Self(value.to_bits()))
}
pub fn get(self) -> f64 {
f64::from_bits(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ContextMenuItem {
pub item_id: u32,
pub name: String,
pub tag: i32,
pub flags: i32,
}
impl ContextMenuItem {
pub fn is_separator(&self) -> bool {
self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsSeparator as i32 != 0
}
pub fn is_disabled(&self) -> bool {
self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsDisabled as i32 != 0
}
pub fn is_checked(&self) -> bool {
self.flags & vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsChecked as i32 != 0
}
pub fn is_group_start(&self) -> bool {
let group_start = vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsGroupStart as i32;
self.flags & group_start == group_start
}
pub fn is_group_end(&self) -> bool {
let group_end = vst3::Steinberg::Vst::IContextMenuItem_::Flags_::kIsGroupEnd as i32;
self.flags & group_end == group_end
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DataExchangeBlock {
pub queue_id: u32,
pub user_context_id: u32,
pub block_id: u32,
#[serde(with = "crate::process_isolation::state_codec")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum HostNotification {
DirtyChanged(bool),
OpenEditorRequested {
name: Option<String>,
},
GroupEditStarted,
GroupEditFinished,
UnitSelectionChanged {
unit_id: i32,
},
ProgramListChanged {
list_id: i32,
program_index: Option<i32>,
},
UnitByBusChanged,
ProgressStarted {
id: u64,
kind: ProgressKind,
description: Option<String>,
},
ProgressUpdated {
id: u64,
value: ProgressValue,
},
ProgressFinished {
id: u64,
},
ContextMenuRequested {
menu_id: u64,
parameter_id: Option<u32>,
x: i32,
y: i32,
items: Vec<ContextMenuItem>,
},
}
impl HostNotification {
pub(crate) fn invalidates_unit_cache(&self) -> bool {
matches!(
self,
Self::ProgramListChanged { .. } | Self::UnitByBusChanged
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RestartFlags(i32);
impl RestartFlags {
pub(crate) fn from_bits(bits: i32) -> Self {
Self(bits)
}
pub fn bits(self) -> i32 {
self.0
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
fn has(self, flag: i32) -> bool {
self.0 & flag != 0
}
pub fn param_values_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kParamValuesChanged)
}
pub fn reload_component(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kReloadComponent)
}
pub fn param_titles_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kParamTitlesChanged)
}
pub fn latency_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kLatencyChanged)
}
pub fn io_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kIoChanged)
}
pub fn midi_cc_assignment_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kMidiCCAssignmentChanged)
}
pub fn note_expression_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kNoteExpressionChanged)
}
pub fn io_titles_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kIoTitlesChanged)
}
pub fn prefetchable_support_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kPrefetchableSupportChanged)
}
pub fn routing_info_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kRoutingInfoChanged)
}
pub fn keyswitch_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kKeyswitchChanged)
}
pub fn param_id_mapping_changed(self) -> bool {
self.has(vst3::Steinberg::Vst::RestartFlags_::kParamIDMappingChanged)
}
}
#[cfg(test)]
mod restart_flag_tests {
use super::RestartFlags;
use vst3::Steinberg::Vst::RestartFlags_ as Flags;
#[test]
fn exposes_every_vst3_restart_flag() {
let bits = Flags::kReloadComponent
| Flags::kIoChanged
| Flags::kParamValuesChanged
| Flags::kLatencyChanged
| Flags::kParamTitlesChanged
| Flags::kMidiCCAssignmentChanged
| Flags::kNoteExpressionChanged
| Flags::kIoTitlesChanged
| Flags::kPrefetchableSupportChanged
| Flags::kRoutingInfoChanged
| Flags::kKeyswitchChanged
| Flags::kParamIDMappingChanged;
let flags = RestartFlags::from_bits(bits);
assert!(flags.reload_component());
assert!(flags.io_changed());
assert!(flags.param_values_changed());
assert!(flags.latency_changed());
assert!(flags.param_titles_changed());
assert!(flags.midi_cc_assignment_changed());
assert!(flags.note_expression_changed());
assert!(flags.io_titles_changed());
assert!(flags.prefetchable_support_changed());
assert!(flags.routing_info_changed());
assert!(flags.keyswitch_changed());
assert!(flags.param_id_mapping_changed());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum ProcessMode {
#[default]
Realtime,
Prefetch,
Offline,
}
#[allow(clippy::type_complexity)] pub struct Plugin {
pub(crate) info: PluginInfo,
pub(crate) compatibility: Vec<crate::discovery::ClassCompatibility>,
pub(crate) is_processing: bool,
pub(crate) sample_rate: f64,
pub(crate) block_size: usize,
pub(crate) audio_levels: Arc<Mutex<AudioLevels>>,
pub(crate) parameter_change_callback: Option<Box<dyn Fn(u32, f64) + Send + 'static>>,
pub(crate) audio_callback: Option<Box<dyn Fn(&AudioLevels) + Send + 'static>>,
pub(crate) internal: Option<Box<dyn PluginInternal>>,
}
pub(crate) trait PluginInternal: Send {
fn set_parameter(&mut self, id: u32, value: f64) -> Result<()>;
fn set_parameter_at(&mut self, id: u32, value: f64, _sample_offset: i32) -> Result<()> {
self.set_parameter(id, value)
}
fn queue_processor_parameter_at(
&mut self,
id: u32,
value: f64,
sample_offset: i32,
) -> Result<()> {
self.set_parameter_at(id, value, sample_offset)
}
fn get_parameter(&self, id: u32) -> Result<f64>;
fn get_all_parameters(&self) -> Result<Vec<Parameter>>;
fn format_parameter(&self, id: u32, normalized: f64) -> Result<String>;
fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()>;
fn audio_bus_layout(&self) -> Result<AudioBusLayout> {
Err(Error::Other(
"bus-aware audio processing is not supported for this plugin".to_string(),
))
}
fn process_buses(&mut self, _buffers: &mut BusAudioBuffers) -> Result<()> {
Err(Error::Other(
"bus-aware audio processing is not supported for this plugin".to_string(),
))
}
fn reconfigure(&mut self, _sample_rate: f64, _block_size: usize) -> Result<()> {
Err(Error::Other(
"runtime reconfigure is not supported for this plugin".to_string(),
))
}
fn set_process_mode(&mut self, _mode: crate::plugin::ProcessMode) -> Result<()> {
Err(Error::Other(
"process mode switching is not supported for this plugin".to_string(),
))
}
fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
Err(Error::Other(
"bus arrangement query is not supported for this plugin".to_string(),
))
}
fn set_bus_arrangements(
&mut self,
_inputs: &[crate::audio::SpeakerArrangement],
_outputs: &[crate::audio::SpeakerArrangement],
) -> Result<()> {
Err(Error::Other(
"bus arrangement negotiation is not supported for this plugin".to_string(),
))
}
fn set_bus_active(
&mut self,
_media_type: crate::audio::MediaType,
_direction: crate::audio::BusDirection,
_bus_index: i32,
_active: bool,
) -> Result<()> {
Err(Error::Other(
"bus activation is not supported for this plugin".to_string(),
))
}
fn set_tempo(&mut self, _bpm: f64) -> Result<()> {
Err(Error::Other(
"runtime transport mutation is not supported for this plugin".to_string(),
))
}
fn set_time_signature(&mut self, _numerator: i32, _denominator: i32) -> Result<()> {
Err(Error::Other(
"runtime transport mutation is not supported for this plugin".to_string(),
))
}
fn set_playing(&mut self, _playing: bool) -> Result<()> {
Err(Error::Other(
"runtime transport mutation is not supported for this plugin".to_string(),
))
}
fn send_midi_event(&mut self, event: MidiEvent) -> Result<()>;
fn send_midi_event_at(&mut self, event: MidiEvent, _sample_offset: i32) -> Result<()> {
self.send_midi_event(event)
}
fn send_plugin_event(&mut self, _event: PluginEvent) -> Result<()> {
Err(Error::Other(
"owned VST3 events are not supported for this plugin".to_string(),
))
}
fn midi_panic(&mut self) -> Result<()> {
for i in 0..16 {
if let Some(channel) = MidiChannel::from_index(i) {
for controller in [123, 120, 121] {
self.send_midi_event(MidiEvent::ControlChange {
channel,
controller,
value: 0,
})?;
}
}
}
Ok(())
}
fn note_on(
&mut self,
_channel: MidiChannel,
_note: u8,
_velocity: u8,
_sample_offset: i32,
) -> Result<crate::midi::NoteId> {
Err(Error::Other(
"per-note expression is not supported for this plugin".to_string(),
))
}
fn note_off(&mut self, _id: crate::midi::NoteId, _sample_offset: i32) -> Result<()> {
Err(Error::Other(
"per-note expression is not supported for this plugin".to_string(),
))
}
fn send_note_expression(
&mut self,
_id: crate::midi::NoteId,
_kind: crate::midi::NoteExpressionType,
_value: f64,
_sample_offset: i32,
) -> Result<()> {
Err(Error::Other(
"per-note expression is not supported for this plugin".to_string(),
))
}
fn note_expressions(
&self,
_bus: i32,
_channel: i16,
) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
Ok(Vec::new())
}
fn start_processing(&mut self) -> Result<()>;
fn stop_processing(&mut self) -> Result<()>;
fn has_editor(&self) -> bool;
fn open_editor(&mut self, parent: *mut std::ffi::c_void) -> Result<()>;
fn close_editor(&mut self) -> Result<()>;
fn get_editor_size(&self) -> Result<(i32, i32)>;
fn editor_can_resize(&self) -> bool {
false
}
fn resize_editor(&mut self, _width: i32, _height: i32) -> Result<(i32, i32)> {
Err(Error::Other("plugin editor is not resizable".to_string()))
}
fn set_editor_scale_factor(&mut self, _factor: f32) -> Result<bool> {
Ok(false)
}
fn service_run_loop(&mut self) {}
fn get_parameter_changes(&self) -> Vec<(u32, f64)>;
fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
Vec::new()
}
fn take_host_notifications(&mut self) -> Vec<HostNotification> {
Vec::new()
}
fn take_data_exchange_blocks(&mut self) -> Vec<DataExchangeBlock> {
Vec::new()
}
fn execute_context_menu_item(&mut self, _menu_id: u64, _item_id: u32) -> Result<()> {
Err(Error::Other(
"plugin context menus are not supported".to_string(),
))
}
fn dismiss_context_menu(&mut self, _menu_id: u64) -> Result<()> {
Err(Error::Other(
"plugin context menus are not supported".to_string(),
))
}
fn take_restart_flags(&mut self) -> RestartFlags {
RestartFlags::default()
}
fn service_host_requests(&mut self) -> Result<RestartFlags> {
Ok(self.take_restart_flags())
}
fn take_output_events(&self) -> Vec<PluginEvent> {
Vec::new()
}
fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
None
}
fn output_event_handle(&self) -> Option<OutputEventConsumer> {
None
}
fn get_units(&self) -> Result<Vec<PluginUnit>> {
Ok(Vec::new())
}
fn select_program(&mut self, _unit_id: i32, _program_index: i32) -> Result<()> {
Err(Error::Other(
"program selection is not supported for this plugin".to_string(),
))
}
fn selected_unit(&self) -> Result<Option<i32>> {
Ok(None)
}
fn select_unit(&mut self, _unit_id: i32) -> Result<()> {
Err(Error::Other(
"unit selection is not supported for this plugin".to_string(),
))
}
fn program_pitch_names(
&self,
_program_list_id: i32,
_program_index: i32,
) -> Result<Vec<ProgramPitchName>> {
Ok(Vec::new())
}
fn get_program_data(
&self,
_program_list_id: i32,
_program_index: i32,
) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn set_program_data(
&mut self,
_program_list_id: i32,
_program_index: i32,
_data: &[u8],
) -> Result<()> {
Err(Error::Other(
"program data is not supported for this plugin".to_string(),
))
}
fn get_unit_data(&self, _unit_id: i32) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn set_unit_data(&mut self, _unit_id: i32, _data: &[u8]) -> Result<()> {
Err(Error::Other(
"unit data is not supported for this plugin".to_string(),
))
}
fn begin_host_edit(&mut self, _parameter_id: u32) -> Result<()> {
Err(Error::Other(
"host edit sessions are not supported for this plugin".to_string(),
))
}
fn end_host_edit(&mut self, _parameter_id: u32) -> Result<()> {
Err(Error::Other(
"host edit sessions are not supported for this plugin".to_string(),
))
}
fn send_midi_learn(&mut self, _bus: i32, _channel: i16, _controller: u16) -> Result<()> {
Err(Error::Other(
"MIDI learn is not supported for this plugin".to_string(),
))
}
fn set_automation_state(&mut self, _state: AutomationState) -> Result<()> {
Err(Error::Other(
"automation state is not supported for this plugin".to_string(),
))
}
fn remap_parameter_id(&self, _old_plugin_uid: &str, _old_param_id: u32) -> Result<Option<u32>> {
Ok(None)
}
fn latency_samples(&self) -> u32 {
0
}
fn tail_samples(&self) -> u32 {
0
}
fn midi_cc_to_parameter(&self, _bus: i32, _channel: i16, _cc: u16) -> Option<u32> {
None
}
fn save_state(&self) -> Result<Vec<u8>> {
Err(Error::Other(
"state save/restore is not supported".to_string(),
))
}
fn load_state_with_context(&mut self, _data: &[u8], _context: &StateContext) -> Result<()> {
Err(Error::Other(
"state save/restore is not supported".to_string(),
))
}
fn load_state(&mut self, data: &[u8]) -> Result<()> {
self.load_state_with_context(data, &StateContext::Project)
}
fn helper_pid(&self) -> Option<u32> {
None
}
fn recovery_count(&self) -> u64 {
0
}
fn recover(&mut self) -> Result<()> {
Err(Error::Other(
"recovery is only supported for process-isolated plugins".to_string(),
))
}
fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
None
}
fn output_channel_count(&self) -> usize {
2
}
}
impl Plugin {
pub fn info(&self) -> &PluginInfo {
&self.info
}
pub fn class_compatibility(&self) -> &[crate::discovery::ClassCompatibility] {
&self.compatibility
}
pub fn replaced_class_ids(&self) -> &[String] {
self.compatibility
.iter()
.find(|mapping| {
crate::internal::utils::class_uid_matches(&mapping.new_class_id, &self.info.uid)
})
.map_or(&[], |mapping| mapping.old_class_ids.as_slice())
}
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
pub fn block_size(&self) -> usize {
self.block_size
}
pub fn reconfigure(&mut self, sample_rate: f64, block_size: usize) -> Result<()> {
if self.is_processing {
return Err(Error::Other(
"cannot reconfigure while processing; call stop_processing() first".to_string(),
));
}
if !(sample_rate.is_finite() && sample_rate > 0.0) {
return Err(Error::InvalidParameter(format!(
"sample rate must be finite and positive, got {sample_rate}"
)));
}
if block_size == 0 || block_size > i32::MAX as usize {
return Err(Error::InvalidParameter(format!(
"block size must be in 1..={}, got {block_size}",
i32::MAX
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.reconfigure(sample_rate, block_size)?;
self.sample_rate = sample_rate;
self.block_size = block_size;
Ok(())
}
pub fn set_process_mode(&mut self, mode: ProcessMode) -> Result<()> {
if self.is_processing {
return Err(Error::Other(
"cannot set process mode while processing; call stop_processing() first"
.to_string(),
));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_process_mode(mode)
}
pub fn bus_arrangements(&self) -> Result<crate::audio::BusArrangements> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.bus_arrangements()
}
pub fn set_bus_arrangements(
&mut self,
inputs: &[crate::audio::SpeakerArrangement],
outputs: &[crate::audio::SpeakerArrangement],
) -> Result<()> {
if self.is_processing {
return Err(Error::Other(
"cannot set bus arrangements while processing; call stop_processing() first"
.to_string(),
));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_bus_arrangements(inputs, outputs)
}
pub fn set_bus_active(
&mut self,
media_type: crate::audio::MediaType,
direction: crate::audio::BusDirection,
bus_index: i32,
active: bool,
) -> Result<()> {
if self.is_processing {
return Err(Error::Other(
"cannot activate a bus while processing; call stop_processing() first".to_string(),
));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_bus_active(media_type, direction, bus_index, active)
}
pub fn get_parameters(&self) -> Result<Vec<Parameter>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_all_parameters()
}
pub fn set_parameter(&mut self, id: u32, value: f64) -> Result<()> {
if !(0.0..=1.0).contains(&value) {
return Err(Error::InvalidParameter(format!(
"Value {} is out of range [0.0, 1.0]",
value
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_parameter(id, value)?;
if let Some(ref callback) = self.parameter_change_callback {
callback(id, value);
}
Ok(())
}
pub fn set_parameter_at(&mut self, id: u32, value: f64, sample_offset: i32) -> Result<()> {
if !(0.0..=1.0).contains(&value) {
return Err(Error::InvalidParameter(format!(
"Value {} is out of range [0.0, 1.0]",
value
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_parameter_at(id, value, sample_offset)
}
pub(crate) fn queue_processor_parameter_at(
&mut self,
id: u32,
value: f64,
sample_offset: i32,
) -> Result<()> {
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
return Err(Error::InvalidParameter(format!(
"Value {} is out of range [0.0, 1.0]",
value
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.queue_processor_parameter_at(id, value, sample_offset)
}
pub fn set_tempo(&mut self, bpm: f64) -> Result<()> {
if !(bpm.is_finite() && bpm > 0.0) {
return Err(Error::InvalidParameter(format!(
"tempo must be finite and positive, got {bpm}"
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_tempo(bpm)
}
pub fn set_time_signature(&mut self, numerator: i32, denominator: i32) -> Result<()> {
if numerator <= 0 {
return Err(Error::InvalidParameter(format!(
"time signature numerator must be positive, got {numerator}"
)));
}
if !matches!(denominator, 1 | 2 | 4 | 8 | 16) {
return Err(Error::InvalidParameter(format!(
"time signature denominator must be one of 1, 2, 4, 8, 16, got {denominator}"
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_time_signature(numerator, denominator)
}
pub fn set_playing(&mut self, playing: bool) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_playing(playing)
}
pub fn get_units(&self) -> Result<Vec<PluginUnit>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_units()
}
pub fn select_program(&mut self, unit_id: i32, program_index: i32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.select_program(unit_id, program_index)
}
pub fn selected_unit(&self) -> Result<Option<i32>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.selected_unit()
}
pub fn select_unit(&mut self, unit_id: i32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.select_unit(unit_id)
}
pub fn program_pitch_names(
&self,
program_list_id: i32,
program_index: i32,
) -> Result<Vec<ProgramPitchName>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.program_pitch_names(program_list_id, program_index)
}
pub fn get_program_data(
&self,
program_list_id: i32,
program_index: i32,
) -> Result<Option<Vec<u8>>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_program_data(program_list_id, program_index)
}
pub fn set_program_data(
&mut self,
program_list_id: i32,
program_index: i32,
data: &[u8],
) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_program_data(program_list_id, program_index, data)
}
pub fn get_unit_data(&self, unit_id: i32) -> Result<Option<Vec<u8>>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_unit_data(unit_id)
}
pub fn set_unit_data(&mut self, unit_id: i32, data: &[u8]) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_unit_data(unit_id, data)
}
pub fn begin_host_edit(&mut self, parameter_id: u32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.begin_host_edit(parameter_id)
}
pub fn end_host_edit(&mut self, parameter_id: u32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.end_host_edit(parameter_id)
}
pub fn send_midi_learn(&mut self, bus: i32, channel: i16, controller: u16) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.send_midi_learn(bus, channel, controller)
}
pub fn set_automation_state(&mut self, state: AutomationState) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_automation_state(state)
}
pub fn remap_parameter_id(
&self,
old_plugin_uid: &str,
old_param_id: u32,
) -> Result<Option<u32>> {
if crate::internal::utils::parse_class_uid(old_plugin_uid).is_none() {
return Err(Error::InvalidParameter(
"plugin UID must contain exactly 32 hexadecimal characters".to_string(),
));
}
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.remap_parameter_id(old_plugin_uid, old_param_id)
}
pub fn latency_samples(&self) -> u32 {
self.internal
.as_ref()
.map(|i| i.latency_samples())
.unwrap_or(0)
}
pub fn tail_samples(&self) -> u32 {
self.internal
.as_ref()
.map(|i| i.tail_samples())
.unwrap_or(0)
}
pub fn midi_cc_to_parameter(&self, bus: i32, channel: i16, cc: u16) -> Option<u32> {
if cc > 129 {
return None;
}
self.internal
.as_ref()?
.midi_cc_to_parameter(bus, channel, cc)
}
pub fn get_parameter(&self, id: u32) -> Result<f64> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_parameter(id)
}
pub fn format_parameter(&self, id: u32, normalized: f64) -> Result<String> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.format_parameter(id, normalized)
}
pub fn set_parameter_by_name(&mut self, name: &str, value: f64) -> Result<()> {
let params = self.get_parameters()?;
let param = params
.iter()
.find(|p| p.name == name)
.ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))?;
self.set_parameter(param.id, value)
}
pub fn find_parameter(&self, name: &str) -> Result<Parameter> {
let params = self.get_parameters()?;
params
.into_iter()
.find(|p| p.name == name)
.ok_or_else(|| Error::InvalidParameter(format!("Parameter '{}' not found", name)))
}
pub fn send_midi_note(&mut self, note: u8, velocity: u8, channel: MidiChannel) -> Result<()> {
validate_note(note)?;
validate_velocity(velocity)?;
let event = MidiEvent::NoteOn {
channel,
note,
velocity,
};
self.send_midi_event(event)
}
pub fn send_midi_note_off(&mut self, note: u8, channel: MidiChannel) -> Result<()> {
validate_note(note)?;
let event = MidiEvent::NoteOff {
channel,
note,
velocity: 0,
};
self.send_midi_event(event)
}
pub fn send_midi_cc(&mut self, controller: u8, value: u8, channel: MidiChannel) -> Result<()> {
validate_controller(controller)?;
validate_cc_value(value)?;
let event = MidiEvent::ControlChange {
channel,
controller,
value,
};
self.send_midi_event(event)
}
pub fn send_midi_event(&mut self, event: MidiEvent) -> Result<()> {
validate_midi_event(&event)?;
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.send_midi_event(event)
}
pub fn send_midi_event_at(&mut self, event: MidiEvent, sample_offset: i32) -> Result<()> {
validate_midi_event(&event)?;
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.send_midi_event_at(event, sample_offset)
}
pub fn send_plugin_event(&mut self, event: PluginEvent) -> Result<()> {
validate_plugin_event(&event)?;
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.send_plugin_event(event)
}
pub fn send_sysex(&mut self, bytes: Vec<u8>) -> Result<()> {
self.send_plugin_event(PluginEvent::sysex(bytes))
}
pub fn send_sysex_at(&mut self, bytes: Vec<u8>, sample_offset: i32) -> Result<()> {
self.send_plugin_event(PluginEvent::sysex(bytes).at(sample_offset))
}
pub fn note_on(
&mut self,
channel: MidiChannel,
note: u8,
velocity: u8,
) -> Result<crate::midi::NoteId> {
self.note_on_at(channel, note, velocity, 0)
}
pub fn note_on_at(
&mut self,
channel: MidiChannel,
note: u8,
velocity: u8,
sample_offset: i32,
) -> Result<crate::midi::NoteId> {
validate_note(note)?;
validate_velocity(velocity)?;
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.note_on(channel, note, velocity, sample_offset)
}
pub fn note_off(&mut self, id: crate::midi::NoteId) -> Result<()> {
self.note_off_at(id, 0)
}
pub fn note_off_at(&mut self, id: crate::midi::NoteId, sample_offset: i32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.note_off(id, sample_offset)
}
pub fn send_note_expression(
&mut self,
id: crate::midi::NoteId,
kind: crate::midi::NoteExpressionType,
value: f64,
) -> Result<()> {
self.send_note_expression_at(id, kind, value, 0)
}
pub fn send_note_expression_at(
&mut self,
id: crate::midi::NoteId,
kind: crate::midi::NoteExpressionType,
value: f64,
sample_offset: i32,
) -> Result<()> {
if !(0.0..=1.0).contains(&value) {
return Err(Error::InvalidParameter(format!(
"note-expression value {value} out of range [0.0, 1.0]"
)));
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.send_note_expression(id, kind, value, sample_offset)
}
pub fn note_expressions(&self) -> Result<Vec<crate::midi::NoteExpressionInfo>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.note_expressions(0, 0)
}
pub fn start_processing(&mut self) -> Result<()> {
if self.is_processing {
return Ok(());
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.start_processing()?;
self.is_processing = true;
Ok(())
}
pub fn stop_processing(&mut self) -> Result<()> {
if !self.is_processing {
return Ok(());
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.stop_processing()?;
self.is_processing = false;
Ok(())
}
pub fn process_audio(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
if !self.is_processing {
return Err(Error::NotProcessing);
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.process(buffers)?;
if let Ok(mut levels) = self.audio_levels.lock() {
levels.update_from_buffers(&buffers.outputs);
if let Some(ref callback) = self.audio_callback {
callback(&levels);
}
}
Ok(())
}
pub fn audio_bus_layout(&self) -> Result<AudioBusLayout> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.audio_bus_layout()
}
pub fn create_bus_audio_buffers(&self, block_size: usize) -> Result<BusAudioBuffers> {
if block_size == 0 {
return Err(Error::Other(
"bus audio block size must be greater than zero".to_string(),
));
}
Ok(BusAudioBuffers::new(
&self.audio_bus_layout()?,
block_size,
self.sample_rate,
))
}
pub fn process_bus_audio(&mut self, buffers: &mut BusAudioBuffers) -> Result<()> {
if !self.is_processing {
return Err(Error::NotProcessing);
}
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.process_buses(buffers)?;
if let Ok(mut levels) = self.audio_levels.lock() {
levels.update_from_bus_buffers(&buffers.outputs);
if let Some(ref callback) = self.audio_callback {
callback(&levels);
}
}
Ok(())
}
pub fn get_output_levels(&self) -> AudioLevels {
self.audio_levels
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn is_processing(&self) -> bool {
self.is_processing
}
pub fn on_parameter_change<F>(&mut self, callback: F)
where
F: Fn(u32, f64) + Send + 'static,
{
self.parameter_change_callback = Some(Box::new(callback));
}
pub fn on_audio_process<F>(&mut self, callback: F)
where
F: Fn(&AudioLevels) + Send + 'static,
{
self.audio_callback = Some(Box::new(callback));
}
pub fn has_editor(&self) -> bool {
self.internal
.as_ref()
.map(|i| i.has_editor())
.unwrap_or(false)
}
pub fn open_editor(&mut self, parent: WindowHandle) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.open_editor(parent.0)
}
pub fn service_run_loop(&mut self) {
if let Some(internal) = self.internal.as_mut() {
internal.service_run_loop();
}
}
pub fn close_editor(&mut self) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.close_editor()
}
pub fn get_editor_size(&self) -> Result<(i32, i32)> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.get_editor_size()
}
pub fn editor_can_resize(&self) -> bool {
self.internal
.as_ref()
.is_some_and(|internal| internal.editor_can_resize())
}
pub fn resize_editor(&mut self, width: i32, height: i32) -> Result<(i32, i32)> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.resize_editor(width, height)
}
pub fn set_editor_scale_factor(&mut self, factor: f32) -> Result<bool> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.set_editor_scale_factor(factor)
}
pub fn update_parameters<F>(&mut self, f: F) -> Result<()>
where
F: FnOnce(&mut ParameterUpdate) -> Result<()>,
{
let mut update = ParameterUpdate::new(self);
f(&mut update)?;
update.apply()
}
pub fn midi_panic(&mut self) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.midi_panic()
}
pub fn get_parameter_changes(&self) -> Vec<(u32, f64)> {
self.internal
.as_ref()
.map(|i| i.get_parameter_changes())
.unwrap_or_default()
}
pub fn take_parameter_edits(&mut self) -> Vec<ParameterEdit> {
self.internal
.as_mut()
.map(|i| i.take_parameter_edits())
.unwrap_or_default()
}
pub fn take_host_notifications(&mut self) -> Vec<HostNotification> {
self.internal
.as_mut()
.map(|i| i.take_host_notifications())
.unwrap_or_default()
}
pub fn take_data_exchange_blocks(&mut self) -> Vec<DataExchangeBlock> {
self.internal
.as_mut()
.map(|i| i.take_data_exchange_blocks())
.unwrap_or_default()
}
pub fn execute_context_menu_item(&mut self, menu_id: u64, item_id: u32) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.execute_context_menu_item(menu_id, item_id)
}
pub fn dismiss_context_menu(&mut self, menu_id: u64) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.dismiss_context_menu(menu_id)
}
pub fn take_restart_flags(&mut self) -> RestartFlags {
self.internal
.as_mut()
.map(|i| i.take_restart_flags())
.unwrap_or_default()
}
pub fn service_host_requests(&mut self) -> Result<RestartFlags> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.service_host_requests()
}
pub fn take_output_midi(&self) -> Vec<MidiEvent> {
self.internal
.as_ref()
.map(|i| {
i.take_output_events()
.into_iter()
.filter_map(|event| event.to_midi())
.collect()
})
.unwrap_or_default()
}
pub fn take_output_events(&self) -> Vec<crate::midi::OutputEvent> {
self.internal
.as_ref()
.map(|i| i.take_output_events())
.unwrap_or_default()
}
pub fn output_midi_handle(&self) -> Option<OutputMidiConsumer> {
self.internal.as_ref().and_then(|i| i.output_midi_handle())
}
pub fn output_event_handle(&self) -> Option<OutputEventConsumer> {
self.internal.as_ref().and_then(|i| i.output_event_handle())
}
pub fn save_state(&self) -> Result<Vec<u8>> {
self.internal
.as_ref()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.save_state()
}
pub fn load_state(&mut self, data: &[u8]) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.load_state(data)
}
pub fn load_state_with_context(&mut self, data: &[u8], context: &StateContext) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.load_state_with_context(data, context)
}
pub fn save_preset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
let info = self.info();
let preset = PluginPreset {
uid: info.uid.clone(),
plugin_name: info.name.clone(),
state: self.save_state()?,
};
let json = serde_json::to_vec_pretty(&preset)
.map_err(|e| Error::Other(format!("serialize preset: {e}")))?;
std::fs::write(path, json).map_err(|e| Error::Other(format!("write preset: {e}")))?;
Ok(())
}
pub fn load_preset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|e| Error::Other(format!("read preset: {e}")))?;
let preset: PluginPreset = serde_json::from_slice(&bytes)
.map_err(|e| Error::Other(format!("parse preset: {e}")))?;
if !self.accepts_state_class_id(&preset.uid)? {
return Err(Error::Other(format!(
"preset is for a different plugin ({}, expected {})",
preset.plugin_name,
self.info().name
)));
}
self.load_state_with_context(&preset.state, &StateContext::preset_from_path(path))
}
pub fn save_vstpreset<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
let state = decode_state_snapshot(&self.save_state()?)?;
let bytes = vstpreset::build(
&self.info().uid,
&state.component,
state.controller.as_deref(),
)?;
std::fs::write(path, bytes).map_err(|e| Error::Other(format!("write vstpreset: {e}")))?;
Ok(())
}
pub fn load_vstpreset<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref();
let bytes =
std::fs::read(path).map_err(|e| Error::Other(format!("read vstpreset: {e}")))?;
let parsed = vstpreset::parse(&bytes)?;
if !self.accepts_state_class_id(&parsed.class_id)? {
return Err(Error::Other(format!(
"vstpreset is for a different plugin (class id {}, expected {})",
parsed.class_id,
self.info().uid
)));
}
let state = encode_state_snapshot(&StateSnapshot {
component: parsed.component_state,
controller: parsed.controller_state,
})?;
self.load_state_with_context(&state, &StateContext::preset_from_path(path))
}
fn accepts_state_class_id(&self, candidate: &str) -> Result<bool> {
if crate::internal::utils::class_uid_matches(&self.info().uid, candidate) {
return Ok(true);
}
Ok(self.compatibility.iter().any(|mapping| {
crate::internal::utils::class_uid_matches(&mapping.new_class_id, &self.info().uid)
&& mapping
.old_class_ids
.iter()
.any(|old| crate::internal::utils::class_uid_matches(old, candidate))
}))
}
pub fn isolation_pid(&self) -> Option<u32> {
self.internal.as_ref().and_then(|i| i.helper_pid())
}
pub fn recovery_count(&self) -> u64 {
self.internal
.as_ref()
.map(|i| i.recovery_count())
.unwrap_or(0)
}
pub fn output_channel_count(&self) -> usize {
self.internal
.as_ref()
.map(|i| i.output_channel_count())
.unwrap_or(2)
}
pub fn take_editor_resize_request(&self) -> Option<(i32, i32)> {
self.internal
.as_ref()
.and_then(|i| i.take_editor_resize_request())
}
pub fn recover(&mut self) -> Result<()> {
self.internal
.as_mut()
.ok_or_else(|| Error::Other("Plugin not initialized".to_string()))?
.recover()
}
}
const MIDI_DATA_MAX: u8 = 127;
const MIDI_PITCH_BEND_MAX: u16 = 16383;
fn validate_note(note: u8) -> Result<()> {
if note > MIDI_DATA_MAX {
return Err(Error::MidiError(format!("Invalid note number: {}", note)));
}
Ok(())
}
fn validate_velocity(velocity: u8) -> Result<()> {
if velocity > MIDI_DATA_MAX {
return Err(Error::MidiError(format!("Invalid velocity: {}", velocity)));
}
Ok(())
}
fn validate_controller(controller: u8) -> Result<()> {
if controller > MIDI_DATA_MAX {
return Err(Error::MidiError(format!(
"Invalid controller number: {}",
controller
)));
}
Ok(())
}
fn validate_cc_value(value: u8) -> Result<()> {
if value > MIDI_DATA_MAX {
return Err(Error::MidiError(format!("Invalid CC value: {}", value)));
}
Ok(())
}
fn validate_pressure(pressure: u8) -> Result<()> {
if pressure > MIDI_DATA_MAX {
return Err(Error::MidiError(format!(
"Invalid pressure value: {}",
pressure
)));
}
Ok(())
}
fn validate_midi_event(event: &MidiEvent) -> Result<()> {
match *event {
MidiEvent::NoteOn { note, velocity, .. } | MidiEvent::NoteOff { note, velocity, .. } => {
validate_note(note)?;
validate_velocity(velocity)
}
MidiEvent::ControlChange {
controller, value, ..
} => {
validate_controller(controller)?;
validate_cc_value(value)
}
MidiEvent::ProgramChange { program, .. } => {
if program > MIDI_DATA_MAX {
return Err(Error::MidiError(format!(
"Invalid program number: {}",
program
)));
}
Ok(())
}
MidiEvent::PitchBend { value, .. } => {
if value > MIDI_PITCH_BEND_MAX {
return Err(Error::MidiError(format!(
"Invalid pitch bend value: {} (0-{})",
value, MIDI_PITCH_BEND_MAX
)));
}
Ok(())
}
MidiEvent::ChannelAftertouch { pressure, .. } => validate_pressure(pressure),
MidiEvent::PolyAftertouch { note, pressure, .. } => {
validate_note(note)?;
validate_pressure(pressure)
}
}
}
fn validate_plugin_event(event: &PluginEvent) -> Result<()> {
if event.bus_index < 0 {
return Err(Error::MidiError(format!(
"Invalid event bus index: {}",
event.bus_index
)));
}
if event.sample_offset < 0 {
return Err(Error::MidiError(format!(
"Invalid event sample offset: {}",
event.sample_offset
)));
}
if !event.ppq_position.is_finite() {
return Err(Error::MidiError(
"Event PPQ position must be finite".to_string(),
));
}
let validate_channel = |channel: i16| {
if (0..16).contains(&channel) {
Ok(())
} else {
Err(Error::MidiError(format!(
"Invalid VST3 event channel: {channel}"
)))
}
};
let validate_pitch = |pitch: i16| {
if (0..=127).contains(&pitch) {
Ok(())
} else {
Err(Error::MidiError(format!(
"Invalid VST3 event pitch: {pitch}"
)))
}
};
let validate_normalized = |name: &str, value: f64| {
if value.is_finite() && (0.0..=1.0).contains(&value) {
Ok(())
} else {
Err(Error::MidiError(format!(
"{name} must be finite and normalized to [0.0, 1.0]"
)))
}
};
match &event.data {
PluginEventData::NoteOn {
channel,
pitch,
tuning,
velocity,
length,
..
} => {
validate_channel(*channel)?;
validate_pitch(*pitch)?;
validate_normalized("note velocity", f64::from(*velocity))?;
if !tuning.is_finite() || *length < 0 {
return Err(Error::MidiError(
"note tuning must be finite and length non-negative".to_string(),
));
}
Ok(())
}
PluginEventData::NoteOff {
channel,
pitch,
velocity,
tuning,
..
} => {
validate_channel(*channel)?;
validate_pitch(*pitch)?;
validate_normalized("note-off velocity", f64::from(*velocity))?;
if !tuning.is_finite() {
return Err(Error::MidiError(
"note-off tuning must be finite".to_string(),
));
}
Ok(())
}
PluginEventData::Data { data_type, bytes } => {
if *data_type != 0 {
return Err(Error::MidiError(format!(
"Unsupported VST3 data event type: {data_type}"
)));
}
if bytes.len() > MAX_EVENT_PAYLOAD_BYTES {
return Err(Error::MidiError(format!(
"Event payload is {} bytes; maximum is {MAX_EVENT_PAYLOAD_BYTES}",
bytes.len()
)));
}
Ok(())
}
PluginEventData::PolyPressure {
channel,
pitch,
pressure,
..
} => {
validate_channel(*channel)?;
validate_pitch(*pitch)?;
validate_normalized("poly pressure", f64::from(*pressure))
}
PluginEventData::NoteExpressionValue { value, .. } => {
validate_normalized("note-expression value", *value)
}
PluginEventData::NoteExpressionText { text, .. }
| PluginEventData::Chord { text, .. }
| PluginEventData::Scale { text, .. } => {
if text.len() > MAX_EVENT_TEXT_UNITS {
return Err(Error::MidiError(format!(
"Event text is {} UTF-16 units; maximum is {MAX_EVENT_TEXT_UNITS}",
text.len()
)));
}
Ok(())
}
PluginEventData::NoteExpressionIntValue { .. } => Ok(()),
PluginEventData::LegacyMidiCcOut { .. } => Err(Error::MidiError(
"Legacy MIDI CC events are plugin output only".to_string(),
)),
}
}
pub struct WindowHandle(pub(crate) *mut std::ffi::c_void);
impl WindowHandle {
pub unsafe fn from_raw(handle: *mut std::ffi::c_void) -> Self {
Self(handle)
}
}
unsafe impl Send for WindowHandle {}
#[cfg(target_os = "macos")]
impl WindowHandle {
pub unsafe fn from_nsview(view: *mut std::ffi::c_void) -> Self {
Self(view)
}
}
#[cfg(target_os = "windows")]
impl WindowHandle {
pub unsafe fn from_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
Self(hwnd)
}
}
#[cfg(target_os = "linux")]
impl WindowHandle {
pub fn from_x11(window_id: u32) -> Self {
Self(window_id as usize as *mut std::ffi::c_void)
}
}
mod vstpreset {
use crate::error::{Error, Result};
const MAGIC: &[u8; 4] = b"VST3";
const LIST_MAGIC: &[u8; 4] = b"List";
const COMPONENT_CHUNK: &[u8; 4] = b"Comp";
const CONTROLLER_CHUNK: &[u8; 4] = b"Cont";
const VERSION: i32 = 1;
const CLASS_ID_LEN: usize = 32;
const HEADER_SIZE: usize = 4 + 4 + CLASS_ID_LEN + 8;
const LIST_HEADER_SIZE: usize = 8;
const ENTRY_SIZE: usize = 20;
pub(super) struct Parsed {
pub class_id: String,
pub component_state: Vec<u8>,
pub controller_state: Option<Vec<u8>>,
}
pub(super) fn build(
class_id: &str,
component_state: &[u8],
controller_state: Option<&[u8]>,
) -> Result<Vec<u8>> {
let class_bytes = class_id.as_bytes();
if class_bytes.len() != CLASS_ID_LEN
|| !class_bytes.iter().all(|byte| byte.is_ascii_hexdigit())
{
return Err(Error::Other(format!(
"vstpreset class id must be {CLASS_ID_LEN} ASCII hex chars, got {:?}",
class_id
)));
}
let comp_offset = HEADER_SIZE as i64;
let comp_size = component_state.len() as i64;
let controller_offset = HEADER_SIZE
.checked_add(component_state.len())
.ok_or_else(|| Error::Other("vstpreset size overflow".to_string()))?;
let list_offset = controller_offset
.checked_add(controller_state.map_or(0, <[u8]>::len))
.ok_or_else(|| Error::Other("vstpreset size overflow".to_string()))?;
let entry_count = if controller_state.is_some() { 2 } else { 1 };
let mut out = Vec::with_capacity(list_offset + LIST_HEADER_SIZE + entry_count * ENTRY_SIZE);
out.extend_from_slice(MAGIC);
out.extend_from_slice(&VERSION.to_le_bytes());
out.extend(class_bytes.iter().map(u8::to_ascii_uppercase));
out.extend_from_slice(&(list_offset as i64).to_le_bytes());
out.extend_from_slice(component_state);
if let Some(controller) = controller_state {
out.extend_from_slice(controller);
}
out.extend_from_slice(LIST_MAGIC);
out.extend_from_slice(&(entry_count as i32).to_le_bytes());
out.extend_from_slice(COMPONENT_CHUNK);
out.extend_from_slice(&comp_offset.to_le_bytes());
out.extend_from_slice(&comp_size.to_le_bytes());
if let Some(controller) = controller_state {
out.extend_from_slice(CONTROLLER_CHUNK);
out.extend_from_slice(&(controller_offset as i64).to_le_bytes());
out.extend_from_slice(&(controller.len() as i64).to_le_bytes());
}
Ok(out)
}
pub(super) fn parse(bytes: &[u8]) -> Result<Parsed> {
if bytes.len() < HEADER_SIZE {
return Err(Error::Other("vstpreset too short for header".to_string()));
}
if &bytes[0..4] != MAGIC {
return Err(Error::Other(format!(
"bad vstpreset magic: expected {:?}, got {:?}",
MAGIC,
&bytes[0..4]
)));
}
let version = read_i32(&bytes[4..8]);
if version != VERSION {
return Err(Error::Other(format!(
"unsupported vstpreset version {version} (expected {VERSION})"
)));
}
let class_id = String::from_utf8(bytes[8..8 + CLASS_ID_LEN].to_vec())
.map_err(|e| Error::Other(format!("vstpreset class id not UTF-8: {e}")))?;
if !class_id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(Error::Other(
"vstpreset class id is not 32 ASCII hex characters".to_string(),
));
}
let list_offset = read_i64(&bytes[8 + CLASS_ID_LEN..HEADER_SIZE]);
if list_offset < HEADER_SIZE as i64 || list_offset as usize > bytes.len() {
return Err(Error::Other(format!(
"vstpreset chunk-list offset {list_offset} out of bounds (len {})",
bytes.len()
)));
}
let list = &bytes[list_offset as usize..];
if list.len() < 8 || &list[0..4] != LIST_MAGIC {
return Err(Error::Other(
"vstpreset chunk list missing or malformed".to_string(),
));
}
let count = read_i32(&list[4..8]);
if count < 0 {
return Err(Error::Other("vstpreset negative entry count".to_string()));
}
let count = count as usize;
let list_size = LIST_HEADER_SIZE
.checked_add(
count
.checked_mul(ENTRY_SIZE)
.ok_or_else(|| Error::Other("vstpreset entry count overflow".to_string()))?,
)
.ok_or_else(|| Error::Other("vstpreset list size overflow".to_string()))?;
if list.len() < list_size {
return Err(Error::Other(
"vstpreset chunk-list entry truncated".to_string(),
));
}
let body_end = list_offset as usize;
let mut cursor = LIST_HEADER_SIZE;
let mut component_state = None;
let mut controller_state = None;
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(count);
for _ in 0..count {
let id = &list[cursor..cursor + 4];
let offset = read_i64(&list[cursor + 4..cursor + 12]);
let size = read_i64(&list[cursor + 12..cursor + 20]);
cursor += ENTRY_SIZE;
if offset < HEADER_SIZE as i64 || size < 0 {
return Err(Error::Other(
"vstpreset chunk has invalid offset/size".to_string(),
));
}
let start = usize::try_from(offset)
.map_err(|_| Error::Other("vstpreset chunk offset overflow".to_string()))?;
let size = usize::try_from(size)
.map_err(|_| Error::Other("vstpreset chunk size overflow".to_string()))?;
let end = start
.checked_add(size)
.ok_or_else(|| Error::Other("vstpreset chunk size overflow".to_string()))?;
if end > body_end {
return Err(Error::Other(format!(
"vstpreset chunk [{start}..{end}] is outside the payload body \
[{HEADER_SIZE}..{body_end}]"
)));
}
if ranges
.iter()
.any(|&(other_start, other_end)| start < other_end && other_start < end)
{
return Err(Error::Other("vstpreset chunk payloads overlap".to_string()));
}
ranges.push((start, end));
match id {
id if id == COMPONENT_CHUNK => {
if component_state.is_some() {
return Err(Error::Other(
"vstpreset has duplicate component chunks".to_string(),
));
}
component_state = Some(bytes[start..end].to_vec());
}
id if id == CONTROLLER_CHUNK => {
if controller_state.is_some() {
return Err(Error::Other(
"vstpreset has duplicate controller chunks".to_string(),
));
}
controller_state = Some(bytes[start..end].to_vec());
}
_ => {}
}
}
let component_state = component_state.ok_or_else(|| {
Error::Other("vstpreset has no component (\"Comp\") chunk".to_string())
})?;
Ok(Parsed {
class_id,
component_state,
controller_state,
})
}
fn read_i32(b: &[u8]) -> i32 {
i32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
fn read_i64(b: &[u8]) -> i64 {
i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
}
}
#[cfg(test)]
mod public_surface_tests {
use super::*;
fn unloaded_plugin() -> Plugin {
Plugin {
info: PluginInfo {
path: std::path::PathBuf::from("/none.vst3"),
name: "None".to_string(),
vendor: String::new(),
version: String::new(),
category: String::new(),
uid: String::new(),
audio_inputs: 0,
audio_outputs: 2,
has_midi_input: true,
has_midi_output: false,
has_gui: false,
},
compatibility: Vec::new(),
is_processing: false,
sample_rate: 44_100.0,
block_size: 512,
audio_levels: Arc::new(Mutex::new(AudioLevels::new(2))),
parameter_change_callback: None,
audio_callback: None,
internal: None,
}
}
#[test]
fn process_audio_while_stopped_is_the_allocation_free_variant() {
let mut plugin = unloaded_plugin();
let mut buffers = AudioBuffers::new(0, 2, 64, 44_100.0);
let err = plugin
.process_audio(&mut buffers)
.expect_err("processing is stopped");
assert!(
matches!(err, Error::NotProcessing),
"expected Error::NotProcessing, got {err:?}"
);
}
#[test]
fn send_midi_event_rejects_out_of_range_fields() {
let mut plugin = unloaded_plugin();
let bad = [
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 255,
velocity: 100,
},
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 255,
},
MidiEvent::NoteOff {
channel: MidiChannel::Ch1,
note: 128,
velocity: 0,
},
MidiEvent::ControlChange {
channel: MidiChannel::Ch1,
controller: 200,
value: 0,
},
MidiEvent::ControlChange {
channel: MidiChannel::Ch1,
controller: 1,
value: 200,
},
MidiEvent::ProgramChange {
channel: MidiChannel::Ch1,
program: 200,
},
MidiEvent::PitchBend {
channel: MidiChannel::Ch1,
value: 16_384,
},
MidiEvent::ChannelAftertouch {
channel: MidiChannel::Ch1,
pressure: 200,
},
MidiEvent::PolyAftertouch {
channel: MidiChannel::Ch1,
note: 200,
pressure: 1,
},
MidiEvent::PolyAftertouch {
channel: MidiChannel::Ch1,
note: 60,
pressure: 200,
},
];
for event in bad {
for err in [
plugin.send_midi_event(event).expect_err("out of range"),
plugin
.send_midi_event_at(event, 0)
.expect_err("out of range"),
] {
assert!(
matches!(err, Error::MidiError(_)),
"expected a MidiError for {event:?}, got {err:?}"
);
}
}
}
#[test]
fn send_midi_event_accepts_in_range_fields() {
let mut plugin = unloaded_plugin();
let ok = [
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 127,
velocity: 127,
},
MidiEvent::ControlChange {
channel: MidiChannel::Ch1,
controller: 127,
value: 127,
},
MidiEvent::ProgramChange {
channel: MidiChannel::Ch1,
program: 127,
},
MidiEvent::PitchBend {
channel: MidiChannel::Ch1,
value: 16_383,
},
MidiEvent::ChannelAftertouch {
channel: MidiChannel::Ch1,
pressure: 127,
},
MidiEvent::PolyAftertouch {
channel: MidiChannel::Ch1,
note: 127,
pressure: 127,
},
];
for event in ok {
let err = plugin.send_midi_event(event).expect_err("no plugin loaded");
assert!(
matches!(err, Error::Other(_)),
"expected the uninitialized-plugin error for {event:?}, got {err:?}"
);
}
}
#[test]
fn note_on_rejects_out_of_range_note_and_velocity() {
let mut plugin = unloaded_plugin();
for (note, velocity) in [(128, 100), (60, 128), (255, 255)] {
let err = plugin
.note_on(MidiChannel::Ch1, note, velocity)
.expect_err("out of range");
assert!(
matches!(err, Error::MidiError(_)),
"expected a MidiError for note {note} velocity {velocity}, got {err:?}"
);
let err = plugin
.note_on_at(MidiChannel::Ch1, note, velocity, 0)
.expect_err("out of range");
assert!(matches!(err, Error::MidiError(_)), "got {err:?}");
}
}
}
#[cfg(test)]
mod output_midi_consumer_tests {
use super::*;
fn note(n: u8) -> MidiEvent {
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: n,
velocity: 100,
}
}
#[test]
fn drains_in_order_and_drops_oldest_when_full() {
let q = Arc::new(ArrayQueue::new(2));
let consumer = OutputMidiConsumer::from_queue(q.clone());
q.force_push(note(60).into());
q.force_push(note(61).into());
q.force_push(note(62).into());
assert_eq!(consumer.drain(), vec![note(61), note(62)]);
assert_eq!(consumer.pop(), None);
assert_eq!(consumer.drain(), vec![]);
}
#[test]
fn handle_is_send_and_shares_the_queue_across_threads() {
let q = Arc::new(ArrayQueue::new(8));
let consumer = OutputMidiConsumer::from_queue(q.clone());
let producer = q.clone();
std::thread::spawn(move || {
producer.force_push(note(64).into());
})
.join()
.unwrap();
assert_eq!(consumer.pop(), Some(note(64)));
}
}
#[cfg(test)]
mod vstpreset_tests {
use super::{
decode_state_snapshot, encode_state_snapshot, vstpreset, StateSnapshot,
STATE_SNAPSHOT_MAGIC,
};
const TEST_CLASS_ID: &str = "0123456789ABCDEF0123456789ABCDEF";
#[test]
fn build_parse_round_trip() {
let state = b"opaque plugin state \x00\x01\x02\xff bytes".to_vec();
let controller = b"controller-only state".to_vec();
let bytes = vstpreset::build(TEST_CLASS_ID, &state, Some(&controller)).expect("build");
assert_eq!(&bytes[0..4], b"VST3");
assert_eq!(
i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
1
);
assert_eq!(&bytes[8..40], TEST_CLASS_ID.as_bytes());
let parsed = vstpreset::parse(&bytes).expect("parse");
assert_eq!(parsed.class_id, TEST_CLASS_ID);
assert_eq!(parsed.component_state, state);
assert_eq!(parsed.controller_state, Some(controller));
}
#[test]
fn round_trip_empty_state() {
let bytes = vstpreset::build(TEST_CLASS_ID, &[], None).expect("build");
let parsed = vstpreset::parse(&bytes).expect("parse");
assert_eq!(parsed.class_id, TEST_CLASS_ID);
assert!(parsed.component_state.is_empty());
assert!(parsed.controller_state.is_none());
}
#[test]
fn round_trip_empty_controller_state() {
let bytes = vstpreset::build(TEST_CLASS_ID, b"component", Some(&[])).expect("build");
let parsed = vstpreset::parse(&bytes).expect("parse");
assert_eq!(parsed.controller_state, Some(Vec::new()));
}
#[test]
fn build_rejects_wrong_length_class_id() {
assert!(vstpreset::build("short", b"x", None).is_err());
assert!(vstpreset::build("Z123456789ABCDEF0123456789ABCDEF", b"x", None).is_err());
}
#[test]
fn parse_rejects_bad_magic() {
let mut bytes = vstpreset::build(TEST_CLASS_ID, b"x", None).expect("build");
bytes[0] = b'X';
assert!(vstpreset::parse(&bytes).is_err());
}
#[test]
fn parse_rejects_truncated_header() {
assert!(vstpreset::parse(b"VST3").is_err());
}
#[test]
fn parse_rejects_out_of_bounds_list_offset() {
let mut bytes = vstpreset::build(TEST_CLASS_ID, b"hello", None).expect("build");
let bad = (bytes.len() as i64 + 100).to_le_bytes();
bytes[40..48].copy_from_slice(&bad);
assert!(vstpreset::parse(&bytes).is_err());
}
#[test]
fn parser_accepts_unknown_chunk_and_controller_before_component() {
let comp = b"component";
let cont = b"controller";
let unknown = b"metadata";
let body_len = comp.len() + cont.len() + unknown.len();
let list_offset = 48 + body_len;
let mut bytes = Vec::new();
bytes.extend_from_slice(b"VST3");
bytes.extend_from_slice(&1i32.to_le_bytes());
bytes.extend_from_slice(TEST_CLASS_ID.as_bytes());
bytes.extend_from_slice(&(list_offset as i64).to_le_bytes());
bytes.extend_from_slice(comp);
bytes.extend_from_slice(cont);
bytes.extend_from_slice(unknown);
bytes.extend_from_slice(b"List");
bytes.extend_from_slice(&3i32.to_le_bytes());
for (id, offset, state) in [
(b"Cont", 48 + comp.len(), cont.as_slice()),
(b"Info", 48 + comp.len() + cont.len(), unknown.as_slice()),
(b"Comp", 48, comp.as_slice()),
] {
bytes.extend_from_slice(id);
bytes.extend_from_slice(&(offset as i64).to_le_bytes());
bytes.extend_from_slice(&(state.len() as i64).to_le_bytes());
}
let parsed = vstpreset::parse(&bytes).expect("parse");
assert_eq!(parsed.component_state, comp);
assert_eq!(parsed.controller_state.as_deref(), Some(cont.as_slice()));
}
#[test]
fn parser_rejects_duplicate_or_overlapping_chunks() {
let mut duplicate =
vstpreset::build(TEST_CLASS_ID, b"component", Some(b"controller")).expect("build");
let list_offset = i64::from_le_bytes(duplicate[40..48].try_into().unwrap()) as usize;
duplicate[list_offset + 28..list_offset + 32].copy_from_slice(b"Comp");
assert!(vstpreset::parse(&duplicate).is_err());
let mut overlap =
vstpreset::build(TEST_CLASS_ID, b"component", Some(b"controller")).expect("build");
let list_offset = i64::from_le_bytes(overlap[40..48].try_into().unwrap()) as usize;
let comp_offset = i64::from_le_bytes(
overlap[list_offset + 12..list_offset + 20]
.try_into()
.unwrap(),
);
overlap[list_offset + 32..list_offset + 40]
.copy_from_slice(&(comp_offset + 1).to_le_bytes());
assert!(vstpreset::parse(&overlap).is_err());
}
#[test]
fn state_snapshot_round_trip_and_legacy_component_compatibility() {
let snapshot = StateSnapshot {
component: b"component".to_vec(),
controller: Some(b"controller".to_vec()),
};
let bytes = encode_state_snapshot(&snapshot).expect("encode");
assert!(bytes.starts_with(STATE_SNAPSHOT_MAGIC));
let decoded = decode_state_snapshot(&bytes).expect("decode");
assert_eq!(decoded.component, snapshot.component);
assert_eq!(decoded.controller, snapshot.controller);
let legacy = decode_state_snapshot(b"old raw component blob").expect("legacy");
assert_eq!(legacy.component, b"old raw component blob");
assert!(legacy.controller.is_none());
}
#[test]
fn state_snapshot_rejects_bad_lengths_and_versions() {
let snapshot = StateSnapshot {
component: b"component".to_vec(),
controller: Some(b"controller".to_vec()),
};
let mut bytes = encode_state_snapshot(&snapshot).expect("encode");
bytes[16..20].copy_from_slice(&2u32.to_le_bytes());
assert!(decode_state_snapshot(&bytes).is_err());
let mut bytes = encode_state_snapshot(&snapshot).expect("encode");
bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(decode_state_snapshot(&bytes).is_err());
}
}