//! `Vst3Plugin`: a loaded VST3 instance behind the safe `soundcraft_dsp::Plugin` trait. use crate::ffi::{AFTERTOUCH, BusBuffers, EVENT_CAP, EditorLink, HostContext, Instance, NoteEvent, PARAM_QUEUE_CAP, PITCH_BEND}; use soundcraft_dsp::{Plugin, PluginEditor, PluginInfo, Transport, Unit}; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use vst3::ComWrapper; /// Largest block we set a plugin up for. const MAX_BLOCK: usize = 1 << 16; /// Most channels a host strip may ask for. const MAX_CHANNELS: usize = 64; /// An `unsynced` slot with no value waiting (a NaN pattern, which `set_param` never stores). const SYNCED: u64 = u64::MAX; /// How a SoundCraft parameter value maps to the VST3 normalized value. #[derive(Debug, Clone, Copy, PartialEq)] enum Mapping { /// Toggle/choice index `0..=steps` ↔ `index / steps`. Stepped(f64), /// Plain value through the controller's `plainParamToNormalized`. Plain, /// Linear over the info range (the controller gave no usable plain range). Linear, } /// A hosted VST3 plugin. Parameter ids are VST3 `ParamID`s as decimal strings ("0", "17", …); /// values are plain (display) values with their names and ranges in `info().params`. pub struct Vst3Plugin { inst: Instance, info: &'static PluginInfo, /// VST3 id of each `info.params` entry (same order). ids: Vec, maps: Vec, values: Vec, /// Normalized values waiting for the next block (coalesced per id; bounded). pending: Vec<(u32, f64)>, /// Host-set normalized values the controller has not seen yet (same order as `ids`; the bits /// of an `f64`, or [`SYNCED`]). `set_param` may run on the audio thread, so it never calls the /// controller; the state reader, which already does, hands them over first. unsynced: Arc<[AtomicU64]>, /// Pending notes, kept sorted by time. Never grows past `EVENT_CAP`. notes: Vec, /// The parameter each MIDI controller drives (see `Instance::midi_mapping`). midi_map: Vec>, /// Controller points for the next block: (time, param id, normalized value). Bounded. controls: Vec<(u32, u32, f64)>, inputs: BusBuffers, outputs: BusBuffers, has_input: bool, has_output: bool, event_input: bool, held: [bool; 128], channels: usize, max_block: usize, latency: usize, tail: usize, position: i64, transport: Option, } impl Vst3Plugin { pub(crate) fn new(inst: Instance, info: &'static PluginInfo) -> Vst3Plugin { let ids: Vec = info.params.iter().map(|p| p.id.parse::().unwrap_or(u32::MAX)).collect(); let maps: Vec = info .params .iter() .zip(&ids) .map(|(p, &id)| match p.unit { Unit::Toggle | Unit::Choice => Mapping::Stepped(f64::from(p.max.max(1.0))), _ => { let lo = inst.to_plain(id, 0.0); let hi = inst.to_plain(id, 1.0); match (lo, hi) { (Some(lo), Some(hi)) if (lo as f32 - p.min).abs() <= 1e-6 * p.min.abs().max(1.0) && (hi as f32 - p.max).abs() <= 1e-6 * p.max.abs().max(1.0) => { Mapping::Plain } _ => Mapping::Linear, } } }) .collect(); let mut p = Vst3Plugin { info, values: Vec::with_capacity(ids.len()), pending: Vec::with_capacity(ids.len().min(PARAM_QUEUE_CAP)), unsynced: ids.iter().map(|_| AtomicU64::new(SYNCED)).collect(), notes: Vec::with_capacity(EVENT_CAP), midi_map: Vec::new(), controls: Vec::with_capacity(EVENT_CAP), inputs: BusBuffers::new(&[], 0), outputs: BusBuffers::new(&[], 0), has_input: false, has_output: false, event_input: false, held: [false; 128], channels: 2, max_block: 0, latency: 0, tail: 0, position: 0, transport: None, ids, maps, inst, }; let values: Vec = (0..p.ids.len()) .map(|i| { let (Some(pi), Some(&id)) = (p.info.params.get(i), p.ids.get(i)) else { return 0.0 }; p.inst.normalized_value(id).and_then(|n| p.plain_from_normalized(i, n)).map_or(pi.default, |v| pi.clamp(v)) }) .collect(); p.values = values; p } fn plain_from_normalized(&self, i: usize, n: f64) -> Option { let pi = self.info.params.get(i)?; let id = *self.ids.get(i)?; let v = match self.maps.get(i)? { Mapping::Stepped(steps) => (n * steps).round(), Mapping::Plain => self.inst.to_plain(id, n)?, Mapping::Linear => f64::from(pi.min) + n * f64::from(pi.max - pi.min), }; v.is_finite().then_some(v as f32) } fn normalized_from_plain(&self, i: usize, v: f32) -> Option { let pi = self.info.params.get(i)?; let id = *self.ids.get(i)?; let span = f64::from(pi.max - pi.min); let linear = if span > 0.0 { (f64::from(v) - f64::from(pi.min)) / span } else { 0.0 }; let n = match self.maps.get(i)? { Mapping::Stepped(steps) if *steps > 0.0 => f64::from(v) / steps, Mapping::Stepped(_) => 0.0, Mapping::Plain => self.inst.to_normalized(id, f64::from(v)).unwrap_or(linear), Mapping::Linear => linear, }; n.is_finite().then(|| n.clamp(0.0, 1.0)) } /// The editor's size in pixels without opening it (creates and releases a view). For /// diagnostics and tests; real plugins expect this on the main thread. pub fn editor_size(&self) -> Result<(u32, u32), String> { self.inst.editor().ok_or_else(|| "this plugin has no edit controller".to_string())?.probe() } /// Re-reads every parameter value from the controller (after a state load) and drops /// changes still queued from before it. fn refresh_values(&mut self) { for i in 0..self.ids.len() { let (Some(pi), Some(&id)) = (self.info.params.get(i), self.ids.get(i)) else { continue }; if let Some(v) = self.inst.normalized_value(id).and_then(|n| self.plain_from_normalized(i, n)) && let Some(slot) = self.values.get_mut(i) { *slot = pi.clamp(v); } } self.pending.clear(); for u in self.unsynced.iter() { u.store(SYNCED, Ordering::Relaxed); } } /// Inserts keeping the queue time-sorted; drops the event when the queue is full (never /// allocates on the audio thread). fn push_note(&mut self, e: NoteEvent) { push_sorted(&mut self.notes, e); } } fn push_sorted(notes: &mut Vec, e: NoteEvent) { if notes.len() >= notes.capacity() { return; } let pos = notes.iter().rposition(|q| q.time() <= e.time()).map_or(0, |i| i + 1); notes.insert(pos, e); } /// Keeps only the pending releases, one per key and moved to the start of the next block, so at /// most 128 remain and `all_notes_off` always has room in the queue. fn keep_releases(notes: &mut Vec) { let mut seen = [false; 128]; notes.retain(|e| match *e { NoteEvent::Off { key, .. } => seen.get_mut(usize::from(key)).is_some_and(|s| !std::mem::replace(s, true)), _ => false, }); for e in notes.iter_mut() { e.set_time(0); } } impl Plugin for Vst3Plugin { fn info(&self) -> &'static PluginInfo { self.info } fn prepare(&mut self, sample_rate: f32, max_block: usize, channels: usize) { let sr = if sample_rate.is_finite() && sample_rate > 0.0 { f64::from(sample_rate) } else { 48_000.0 }; let mb = max_block.clamp(1, MAX_BLOCK); self.channels = channels.clamp(1, MAX_CHANNELS); self.max_block = 0; match self.inst.configure(sr, mb, self.channels == 1) { Ok(layout) => { self.inputs = BusBuffers::new(&layout.inputs, mb); self.outputs = BusBuffers::new(&layout.outputs, mb); self.has_input = layout.inputs.first().is_some_and(|&c| c > 0); self.has_output = layout.outputs.first().is_some_and(|&c| c > 0); self.event_input = layout.event_input; self.max_block = mb; self.latency = self.inst.latency() as usize; self.tail = self.inst.tail() as usize; self.midi_map = self.inst.midi_mapping(); } Err(e) => { log::warn!("{}: {e}", self.info.id); self.latency = 0; } } // Re-send every value so a re-activated plugin matches the session. self.pending.clear(); for i in 0..self.ids.len().min(PARAM_QUEUE_CAP) { if let (Some(&id), Some(n)) = (self.ids.get(i), self.values.get(i).and_then(|&v| self.normalized_from_plain(i, v))) { self.pending.push((id, n)); } } } fn reset(&mut self) { // Keep pending releases and release whatever is still held, so the processor's voices stop // on the next block instead of sounding on while the host forgets them. keep_releases(&mut self.notes); self.controls.clear(); self.all_notes_off(); self.held = [false; 128]; } fn set_param(&mut self, id: &str, value: f32) -> bool { let Some(i) = self.info.params.iter().position(|p| p.id == id) else { return false }; let (Some(pi), Some(&vid)) = (self.info.params.get(i), self.ids.get(i)) else { return false }; let v = pi.clamp(value); if let Some(slot) = self.values.get_mut(i) { *slot = v; } let Some(n) = self.normalized_from_plain(i, v) else { return true }; if let Some(u) = self.unsynced.get(i) { u.store(n.to_bits(), Ordering::Relaxed); } if let Some(e) = self.pending.iter_mut().find(|(pid, _)| *pid == vid) { e.1 = n; } else if self.pending.len() < self.pending.capacity() { self.pending.push((vid, n)); } true } fn param(&self, id: &str) -> Option { let i = self.info.params.iter().position(|p| p.id == id)?; self.values.get(i).copied() } fn process(&mut self, io: &mut [Vec], frames: usize) { if !self.inst.is_active() || self.max_block == 0 { return; } let ch = self.channels.min(io.len()); let frames = io.iter().take(ch).map(Vec::len).fold(frames, usize::min); if frames == 0 || ch == 0 { return; } if self.inst.host().latency_changed.swap(false, Ordering::Relaxed) { self.latency = self.inst.latency() as usize; } let last = u32::try_from(frames - 1).unwrap_or(u32::MAX); for e in &mut self.notes { if e.time() > last { e.set_time(last); } } for c in &mut self.controls { c.0 = c.0.min(last); } let mut start = 0usize; while start < frames { let n = (frames - start).min(self.max_block); let end = u32::try_from(start + n).unwrap_or(u32::MAX); // Feed the main input bus (mono sources fill every bus channel). if self.has_input { for c in 0..self.inputs.channels(0) { let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n)); if let (Some(src), Some(dst)) = (src, self.inputs.channel_mut(0, c).and_then(|d| d.get_mut(..n))) { dst.copy_from_slice(src); } } } let k = self.notes.iter().position(|e| e.time() >= end).unwrap_or(self.notes.len()); let offset = u32::try_from(start).unwrap_or(u32::MAX); let notes = self.notes.get(..k).unwrap_or(&[]); let r = self.inst.process( n, &mut self.inputs, &mut self.outputs, &self.pending, &self.controls, notes, offset, self.position, self.transport.as_ref(), ); self.pending.clear(); self.notes.drain(..k); self.position = self.position.saturating_add(i64::try_from(n).unwrap_or(0)); match r { Ok(()) if self.has_output => { let oc = self.outputs.channels(0); for (c, dst) in io.iter_mut().take(ch).enumerate() { let Some(dst) = dst.get_mut(start..start + n) else { continue }; if ch == 1 && oc >= 2 { if let (Some(l), Some(r)) = (self.outputs.channel(0, 0), self.outputs.channel(0, 1)) { for ((d, a), b) in dst.iter_mut().zip(l).zip(r) { *d = 0.5 * (a + b); } } } else if let Some(src) = self.outputs.channel(0, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) { dst.copy_from_slice(src); } } } Err(e) => log::debug!("{}: {e}", self.info.id), _ => {} } start += n; } self.controls.clear(); } fn latency(&self) -> usize { self.latency } fn set_transport(&mut self, transport: &Transport) { self.transport = Some(*transport); } fn tail_samples(&self) -> usize { self.tail } fn note_on(&mut self, offset: usize, note: u8, velocity: u8) { if note > 127 || !self.event_input { return; } let time = u32::try_from(offset).unwrap_or(u32::MAX); self.push_note(NoteEvent::On { time, key: note, velocity: f32::from(velocity.min(127)) / 127.0 }); if let Some(h) = self.held.get_mut(note as usize) { *h = true; } } fn note_off(&mut self, offset: usize, note: u8) { if note > 127 || !self.event_input { return; } let time = u32::try_from(offset).unwrap_or(u32::MAX); self.push_note(NoteEvent::Off { time, key: note }); if let Some(h) = self.held.get_mut(note as usize) { *h = false; } } fn midi(&mut self, offset: usize, msg: [u8; 3]) { let time = u32::try_from(offset).unwrap_or(u32::MAX); let [_, d1, d2] = msg.map(|b| b & 0x7F); let (ctrl, value) = match msg[0] & 0xF0 { 0xB0 => (usize::from(d1), f64::from(d2) / 127.0), 0xD0 => (AFTERTOUCH, f64::from(d1) / 127.0), 0xE0 => (PITCH_BEND, f64::from(u16::from(d2) << 7 | u16::from(d1)) / 16383.0), 0xA0 if self.event_input => { self.push_note(NoteEvent::Pressure { time, key: d1, pressure: f32::from(d2) / 127.0 }); return; } _ => return, }; if let Some(&Some(id)) = self.midi_map.get(ctrl) && self.controls.len() < self.controls.capacity() { self.controls.push((time, id, value)); } } fn all_notes_off(&mut self) { for k in 0..128u8 { if self.held.get(k as usize).copied().unwrap_or(false) { self.note_off(0, k); } } } fn load_state(&mut self, data: &[u8]) -> bool { let ok = self.inst.load_state(data); if ok { self.refresh_values(); } ok } fn open_editor(&mut self) -> Result<(), String> { self.inst.editor().ok_or_else(|| "this plugin has no editor".to_string())?.open(self.info.name) } fn close_editor(&mut self) { if let Some(e) = self.inst.editor() { e.close(); } } fn editor(&mut self) -> Option> { let link = self.inst.editor()?; let mut units: HashMap = self.inst.params().into_iter().map(|p| (p.id, p.units)).collect(); Some(Box::new(Vst3Editor { link, host: self.inst.host_ref(), info: self.info, index: self.info.params.iter().enumerate().map(|(i, p)| (p.id, i)).collect(), units: self.ids.iter().map(|id| units.remove(id).unwrap_or_default()).collect(), ids: self.ids.clone(), maps: self.maps.clone(), })) } fn state_reader(&mut self) -> Option> { let (link, ids, unsynced) = (self.inst.state_link(), self.ids.clone(), Arc::clone(&self.unsynced)); Some(Box::new(move || { // A parameter only the controller stores (no processor state) would otherwise be // saved with the controller's stale value. for (&id, u) in ids.iter().zip(unsynced.iter()) { let bits = u.swap(SYNCED, Ordering::Relaxed); if bits != SYNCED { link.sync_param(id, f64::from_bits(bits)); } } link.save_state() })) } } /// A VST3 plugin's editor (`IPlugView` in a host window), driven from the main thread. struct Vst3Editor { link: Arc, host: ComWrapper, info: &'static PluginInfo, /// Position of each SoundCraft param id in `info.params` (looked up once per row and frame). index: HashMap<&'static str, usize>, /// The plugin's own unit label of each param (same order as `ids`). units: Vec, ids: Vec, maps: Vec, } impl Vst3Editor { fn plain(&self, i: usize, n: f64) -> Option { let pi = self.info.params.get(i)?; let id = *self.ids.get(i)?; let v = match self.maps.get(i)? { Mapping::Stepped(steps) => (n * steps).round(), Mapping::Plain => self.link.to_plain(id, n)?, Mapping::Linear => f64::from(pi.min) + n * f64::from(pi.max - pi.min), }; v.is_finite().then(|| pi.clamp(v as f32)) } fn normalized(&self, i: usize, v: f32) -> Option { let pi = self.info.params.get(i)?; let id = *self.ids.get(i)?; let span = f64::from(pi.max - pi.min); let linear = if span > 0.0 { (f64::from(v) - f64::from(pi.min)) / span } else { 0.0 }; let n = match self.maps.get(i)? { Mapping::Stepped(steps) if *steps > 0.0 => f64::from(v) / steps, Mapping::Stepped(_) => 0.0, Mapping::Plain => self.link.to_normalized(id, f64::from(v)).unwrap_or(linear), Mapping::Linear => linear, }; n.is_finite().then(|| n.clamp(0.0, 1.0)) } } impl PluginEditor for Vst3Editor { fn open(&mut self) -> Result<(), String> { // Edits reported before the editor opened (e.g. during setup) are stale. let _ = self.host.take_edits(); self.link.open(self.info.name) } fn close(&mut self) { self.link.close(); } fn is_open(&self) -> bool { self.link.is_open() } fn idle(&mut self) -> Vec<(String, f32)> { self.link.idle(); let mut out = Vec::new(); for (vid, n) in self.host.take_edits() { let Some(i) = self.ids.iter().position(|&x| x == vid) else { continue }; if let (Some(p), Some(v)) = (self.info.params.get(i), self.plain(i, n)) { out.push((p.id.to_string(), v)); } } out } fn set_param(&mut self, id: &str, value: f32) { let Some(i) = self.info.params.iter().position(|p| p.id == id) else { return }; if let (Some(&vid), Some(n)) = (self.ids.get(i), self.normalized(i, value)) { self.link.set_normalized(vid, n); } } fn value_text(&mut self, id: &str, value: f32) -> Option { let &i = self.index.get(id)?; let pi = self.info.params.get(i)?; // A plugin whose plain mapping is the identity (JUCE, the SDK's default `Parameter`) leaves // a 0..1 range with the units of its real one: only its own text says what the value is. if !matches!(self.maps.get(i)?, Mapping::Plain) || pi.min != 0.0 || pi.max != 1.0 { return None; } let text = self.link.value_text(*self.ids.get(i)?, self.normalized(i, value)?)?; // VST3 keeps the units apart from the text (JUCE's label, the SDK's `RangeParameter`), // but some plugins put them in both. let units = self.units.get(i).map_or("", |u| u.trim()); if units.is_empty() || text.trim_end().to_lowercase().ends_with(&units.to_lowercase()) { Some(text) } else { Some(format!("{text} {units}")) } } } #[cfg(test)] mod tests { use super::*; /// A queue full of releases still has room to release every held key after a reset. #[test] fn reset_keeps_room_to_release_every_held_key() { let mut notes = Vec::with_capacity(EVENT_CAP); for i in 0..EVENT_CAP { push_sorted(&mut notes, NoteEvent::Off { time: i as u32, key: (i % 4) as u8 }); } keep_releases(&mut notes); for key in 0..128u8 { push_sorted(&mut notes, NoteEvent::Off { time: 0, key }); } for key in 0..128u8 { assert!(notes.iter().any(|e| matches!(*e, NoteEvent::Off { key: k, .. } if k == key)), "key {key} was never released"); } } }