Files
soundcraft/crates/vst3-host/src/plugin.rs
T

561 lines
22 KiB
Rust

//! `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<u32>,
maps: Vec<Mapping>,
values: Vec<f32>,
/// 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<NoteEvent>,
/// The parameter each MIDI controller drives (see `Instance::midi_mapping`).
midi_map: Vec<Option<u32>>,
/// 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<Transport>,
}
impl Vst3Plugin {
pub(crate) fn new(inst: Instance, info: &'static PluginInfo) -> Vst3Plugin {
let ids: Vec<u32> = info.params.iter().map(|p| p.id.parse::<u32>().unwrap_or(u32::MAX)).collect();
let maps: Vec<Mapping> = 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<f32> = (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<f32> {
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<f64> {
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<NoteEvent>, 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<NoteEvent>) {
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<f32> {
let i = self.info.params.iter().position(|p| p.id == id)?;
self.values.get(i).copied()
}
fn process(&mut self, io: &mut [Vec<f32>], 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<Box<dyn PluginEditor>> {
let link = self.inst.editor()?;
let mut units: HashMap<u32, String> = 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<Box<dyn soundcraft_dsp::PluginStateReader>> {
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<EditorLink>,
host: ComWrapper<HostContext>,
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<String>,
ids: Vec<u32>,
maps: Vec<Mapping>,
}
impl Vst3Editor {
fn plain(&self, i: usize, n: f64) -> Option<f32> {
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<f64> {
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<String> {
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");
}
}
}