1
0
Fork 0

Also implement sample accurate automation for VST3

This commit is contained in:
Robbert van der Helm 2022-03-10 23:39:58 +01:00
parent 1559afe080
commit 5d3527c5c2
4 changed files with 279 additions and 159 deletions

View file

@ -18,7 +18,6 @@ use crate::param::internals::Params;
/// - Sidechain inputs /// - Sidechain inputs
/// - Multiple output busses /// - Multiple output busses
/// - Special handling for offline processing /// - Special handling for offline processing
/// - Sample accurate automation for VST3, this can already be enabled for CLAP
/// - Parameter hierarchies/groups /// - Parameter hierarchies/groups
/// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to /// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to
/// interact with it yet /// interact with it yet
@ -51,8 +50,6 @@ pub trait Plugin: Default + Send + Sync + 'static {
/// parameter values change occur in the middle of the buffer. Depending on the host these /// parameter values change occur in the middle of the buffer. Depending on the host these
/// blocks may be as small as a single sample. Bitwig Studio sends at most one parameter change /// blocks may be as small as a single sample. Bitwig Studio sends at most one parameter change
/// every 64 samples. /// every 64 samples.
///
/// TODO: Implement this for VST3, this currently is only implemetned for CLAP.
const SAMPLE_ACCURATE_AUTOMATION: bool = false; const SAMPLE_ACCURATE_AUTOMATION: bool = false;
/// The plugin's parameters. The host will update the parameter values before calling /// The plugin's parameters. The host will update the parameter values before calling

View file

@ -926,7 +926,7 @@ impl<P: ClapPlugin> Wrapper<P> {
} }
}); });
// Most hosts process data in place, in which case we don't need to do any copying // Some hosts process data in place, in which case we don't need to do any copying
// ourselves. If the pointers do not alias, then we'll do the copy here and then the // ourselves. If the pointers do not alias, then we'll do the copy here and then the
// plugin can just do normal in place processing. // plugin can just do normal in place processing.
if !process.audio_inputs.is_null() { if !process.audio_inputs.is_null() {

View file

@ -1,7 +1,8 @@
use atomic_refcell::AtomicRefCell; use atomic_refcell::AtomicRefCell;
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::collections::{HashMap, VecDeque}; use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, VecDeque};
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc; use std::sync::Arc;
@ -66,11 +67,16 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// between process calls. This buffer owns the vector, because otherwise it would need to store /// between process calls. This buffer owns the vector, because otherwise it would need to store
/// a mutable reference to the data contained in this mutex. /// a mutable reference to the data contained in this mutex.
pub output_buffer: AtomicRefCell<Buffer<'static>>, pub output_buffer: AtomicRefCell<Buffer<'static>>,
/// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. /// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. If
/// /// `P::SAMPLE_ACCURATE_AUTOMATION`, this is also read in lockstep with the parameter change
/// TODO: Maybe load these lazily at some point instead of needing to spool them all to this /// block splitting.
/// queue first
pub input_events: AtomicRefCell<VecDeque<NoteEvent>>, pub input_events: AtomicRefCell<VecDeque<NoteEvent>>,
/// Unprocessed parameter changes sent by the host as pairs of `(sample_idx_in_buffer, change)`.
/// Needed because VST3 does not have a single queue containing all parameter changes. If
/// `P::SAMPLE_ACCURATE_AUTOMATION` is set, then all parameter changes will be read into this
/// priority queue and the buffer will be processed in small chunks whenever there's a parameter
/// change at a new sample index.
pub input_param_changes: AtomicRefCell<BinaryHeap<Reverse<(usize, ParameterChange)>>>,
/// The keys from `param_map` in a stable order. /// The keys from `param_map` in a stable order.
pub param_hashes: Vec<u32>, pub param_hashes: Vec<u32>,
@ -102,6 +108,26 @@ pub enum Task {
TriggerRestart(i32), TriggerRestart(i32),
} }
/// An incoming parameter change sent by the host. Kept in a queue to support block-based sample
/// accurate automation.
#[derive(Debug, PartialEq, PartialOrd)]
pub struct ParameterChange {
/// The parameter's hash, as used everywhere else.
pub hash: u32,
/// The normalized values, as provided by the host.
pub normalized_value: f32,
}
// Instances needed for the binary heap, we'll just pray the host doesn't send NaN values
impl Eq for ParameterChange {}
#[allow(clippy::derive_ord_xor_partial_ord)]
impl Ord for ParameterChange {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
impl<P: Vst3Plugin> WrapperInner<P> { impl<P: Vst3Plugin> WrapperInner<P> {
#[allow(unused_unsafe)] #[allow(unused_unsafe)]
pub fn new() -> Arc<Self> { pub fn new() -> Arc<Self> {
@ -132,7 +158,14 @@ impl<P: Vst3Plugin> WrapperInner<P> {
last_process_status: AtomicCell::new(ProcessStatus::Normal), last_process_status: AtomicCell::new(ProcessStatus::Normal),
current_latency: AtomicU32::new(0), current_latency: AtomicU32::new(0),
output_buffer: AtomicRefCell::new(Buffer::default()), output_buffer: AtomicRefCell::new(Buffer::default()),
input_events: AtomicRefCell::new(VecDeque::with_capacity(512)), input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)),
input_param_changes: AtomicRefCell::new(BinaryHeap::with_capacity(
if P::SAMPLE_ACCURATE_AUTOMATION {
4096
} else {
0
},
)),
param_hashes: Vec::new(), param_hashes: Vec::new(),
param_by_hash: HashMap::new(), param_by_hash: HashMap::new(),

View file

@ -1,4 +1,4 @@
use std::cmp; use std::cmp::{self, Reverse};
use std::ffi::c_void; use std::ffi::c_void;
use std::mem::{self, MaybeUninit}; use std::mem::{self, MaybeUninit};
use std::ptr; use std::ptr;
@ -21,6 +21,7 @@ use crate::context::Transport;
use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin};
use crate::wrapper::state; use crate::wrapper::state;
use crate::wrapper::util::{process_wrapper, u16strlcpy}; use crate::wrapper::util::{process_wrapper, u16strlcpy};
use crate::wrapper::vst3::inner::ParameterChange;
// Alias needed for the VST3 attribute macro // Alias needed for the VST3 attribute macro
use vst3_sys as vst3_com; use vst3_sys as vst3_com;
@ -645,74 +646,17 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
.load() .load()
.expect("Process call without prior setup call") .expect("Process call without prior setup call")
.sample_rate; .sample_rate;
if let Some(param_changes) = data.input_param_changes.upgrade() {
let num_param_queues = param_changes.get_parameter_count();
for change_queue_idx in 0..num_param_queues {
if let Some(param_change_queue) =
param_changes.get_parameter_data(change_queue_idx).upgrade()
{
let param_hash = param_change_queue.get_parameter_id();
let num_changes = param_change_queue.get_point_count();
// TODO: Handle sample accurate parameter changes, possibly in a similar way
// to the smoothing
let mut sample_offset = 0i32;
let mut value = 0.0f64;
if num_changes > 0
&& param_change_queue.get_point(
num_changes - 1,
&mut sample_offset,
&mut value,
) == kResultOk
{
self.inner.set_normalized_value_by_hash(
param_hash,
value as f32,
Some(sample_rate),
);
}
}
}
}
// And also incoming note events if the plugin accepts MDII
if P::ACCEPTS_MIDI {
let mut input_events = self.inner.input_events.borrow_mut();
if let Some(events) = data.input_events.upgrade() {
let num_events = events.get_event_count();
input_events.clear();
let mut event: MaybeUninit<_> = MaybeUninit::uninit();
for i in 0..num_events {
assert_eq!(events.get_event(i, event.as_mut_ptr()), kResultOk);
let event = event.assume_init();
let timing = event.sample_offset as u32;
if event.type_ == vst3_sys::vst::EventTypes::kNoteOnEvent as u16 {
let event = event.event.note_on;
input_events.push_back(NoteEvent::NoteOn {
timing,
channel: event.channel as u8,
note: event.pitch as u8,
velocity: (event.velocity * 127.0).round() as u8,
});
} else if event.type_ == vst3_sys::vst::EventTypes::kNoteOffEvent as u16 {
let event = event.event.note_off;
input_events.push_back(NoteEvent::NoteOff {
timing,
channel: event.channel as u8,
note: event.pitch as u8,
velocity: (event.velocity * 127.0).round() as u8,
});
}
}
}
}
// It's possible the host only wanted to send new parameter values // It's possible the host only wanted to send new parameter values
// TOOD: Send the output events when doing a flush let is_parameter_flush = data.num_outputs == 0;
if data.num_outputs == 0 { if is_parameter_flush {
nih_log!("VST3 parameter flush"); nih_log!("VST3 parameter flush");
return kResultOk; } else {
check_null_ptr_msg!(
"Process output pointer is null",
data.outputs,
(*data.outputs).buffers,
);
} }
// The setups we suppport are: // The setups we suppport are:
@ -732,57 +676,176 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
); );
nih_debug_assert!(data.num_samples >= 0); nih_debug_assert!(data.num_samples >= 0);
let num_output_channels = (*data.outputs).num_channels as usize; // If `P::SAMPLE_ACCURATE_AUTOMATION` is set, then we'll split up the audio buffer into
check_null_ptr_msg!( // chunks whenever a parameter change occurs. Otherwise all parameter changes are
"Process output pointer is null", // handled right here and now.
data.outputs, let mut input_param_changes = self.inner.input_param_changes.borrow_mut();
(*data.outputs).buffers, input_param_changes.clear();
); if let Some(param_changes) = data.input_param_changes.upgrade() {
let num_param_queues = param_changes.get_parameter_count();
for change_queue_idx in 0..num_param_queues {
if let Some(param_change_queue) =
param_changes.get_parameter_data(change_queue_idx).upgrade()
{
let param_hash = param_change_queue.get_parameter_id();
let num_changes = param_change_queue.get_point_count();
// This vector has been preallocated to contain enough slices as there are output let mut sample_offset = 0i32;
// channels let mut value = 0.0f64;
if num_changes > 0
&& param_change_queue.get_point(
num_changes - 1,
&mut sample_offset,
&mut value,
) == kResultOk
{
if P::SAMPLE_ACCURATE_AUTOMATION {
input_param_changes.push(Reverse((
sample_offset as usize,
ParameterChange {
hash: param_hash,
normalized_value: value as f32,
},
)));
} else {
self.inner.set_normalized_value_by_hash(
param_hash,
value as f32,
Some(sample_rate),
);
}
}
}
}
}
let mut block_start = 0;
let mut block_end = data.num_samples as usize;
let mut event_start_idx = 0;
loop {
// In sample-accurate automation mode we'll handle any parameter changes for the
// current sample, and then process the block between the current sample and the
// sample containing the next parameter change, if any. All timings also need to be
// compensated for this.
if P::SAMPLE_ACCURATE_AUTOMATION {
if input_param_changes.is_empty() {
block_end = data.num_samples as usize;
} else {
while let Some(Reverse((sample_idx, _))) = input_param_changes.peek() {
if *sample_idx != block_start {
block_end = *sample_idx;
break;
}
let Reverse((_, change)) = input_param_changes.pop().unwrap();
self.inner.set_normalized_value_by_hash(
change.hash,
change.normalized_value,
Some(sample_rate),
);
}
}
}
if P::ACCEPTS_MIDI {
let mut input_events = self.inner.input_events.borrow_mut();
if let Some(events) = data.input_events.upgrade() {
let num_events = events.get_event_count();
input_events.clear();
let mut event: MaybeUninit<_> = MaybeUninit::uninit();
for i in event_start_idx..num_events {
assert_eq!(events.get_event(i, event.as_mut_ptr()), kResultOk);
let event = event.assume_init();
// Make sure to only process the events for this block if we're
// splitting the buffer
if P::SAMPLE_ACCURATE_AUTOMATION
&& event.sample_offset as u32 >= block_end as u32
{
event_start_idx = i;
break;
}
let timing = event.sample_offset as u32 - block_start as u32;
if event.type_ == vst3_sys::vst::EventTypes::kNoteOnEvent as u16 {
let event = event.event.note_on;
input_events.push_back(NoteEvent::NoteOn {
timing,
channel: event.channel as u8,
note: event.pitch as u8,
velocity: (event.velocity * 127.0).round() as u8,
});
} else if event.type_ == vst3_sys::vst::EventTypes::kNoteOffEvent as u16
{
let event = event.event.note_off;
input_events.push_back(NoteEvent::NoteOff {
timing,
channel: event.channel as u8,
note: event.pitch as u8,
velocity: (event.velocity * 127.0).round() as u8,
});
}
}
}
}
let result = if is_parameter_flush {
kResultOk
} else {
let num_output_channels = (*data.outputs).num_channels as usize;
// This vector has been preallocated to contain enough slices as there are
// output channels
let mut output_buffer = self.inner.output_buffer.borrow_mut(); let mut output_buffer = self.inner.output_buffer.borrow_mut();
output_buffer.with_raw_vec(|output_slices| { output_buffer.with_raw_vec(|output_slices| {
nih_debug_assert_eq!(num_output_channels, output_slices.len()); nih_debug_assert_eq!(num_output_channels, output_slices.len());
for (output_channel_idx, output_channel_slice) in for (output_channel_idx, output_channel_slice) in
output_slices.iter_mut().enumerate() output_slices.iter_mut().enumerate()
{ {
// If `P::SAMPLE_ACCURATE_AUTOMATION` is set, then we may be iterating over
// the buffer in smaller sections.
// SAFETY: These pointers may not be valid outside of this function even though // SAFETY: These pointers may not be valid outside of this function even though
// their lifetime is equal to this structs. This is still safe because they are // their lifetime is equal to this structs. This is still safe because they are
// only dereferenced here later as part of this process function. // only dereferenced here later as part of this process function.
let channel_ptr =
*((*data.outputs).buffers as *mut *mut f32).add(output_channel_idx);
*output_channel_slice = std::slice::from_raw_parts_mut( *output_channel_slice = std::slice::from_raw_parts_mut(
*((*data.outputs).buffers as *mut *mut f32).add(output_channel_idx), channel_ptr.add(block_start),
data.num_samples as usize, block_end - block_start,
); );
} }
}); });
// Most hosts process data in place, in which case we don't need to do any copying // Some hosts process data in place, in which case we don't need to do any
// ourselves. If the pointers do not alias, then we'll do the copy here and then the // copying ourselves. If the pointers do not alias, then we'll do the copy here
// plugin can just do normal in place processing. // and then the plugin can just do normal in place processing.
if !data.inputs.is_null() { if !data.inputs.is_null() {
let num_input_channels = (*data.inputs).num_channels as usize; let num_input_channels = (*data.inputs).num_channels as usize;
nih_debug_assert!( nih_debug_assert!(
num_input_channels <= num_output_channels, num_input_channels <= num_output_channels,
"Stereo to mono and similar configurations are not supported" "Stereo to mono and similar configurations are not supported"
); );
for input_channel_idx in 0..cmp::min(num_input_channels, num_output_channels) { for input_channel_idx in
0..cmp::min(num_input_channels, num_output_channels)
{
let output_channel_ptr = let output_channel_ptr =
*((*data.outputs).buffers as *mut *mut f32).add(input_channel_idx); *((*data.outputs).buffers as *mut *mut f32).add(input_channel_idx);
let input_channel_ptr = let input_channel_ptr = *((*data.inputs).buffers as *const *const f32)
*((*data.inputs).buffers as *const *const f32).add(input_channel_idx); .add(input_channel_idx);
if input_channel_ptr != output_channel_ptr { if input_channel_ptr != output_channel_ptr {
ptr::copy_nonoverlapping( ptr::copy_nonoverlapping(
input_channel_ptr, input_channel_ptr.add(block_start),
output_channel_ptr, output_channel_ptr.add(block_start),
data.num_samples as usize, block_end - block_start,
); );
} }
} }
} }
// Some of the fields are left empty because VST3 does not provide this information, but // Some of the fields are left empty because VST3 does not provide this
// the methods on [`Transport`] can reconstruct these values from the other fields // information, but the methods on [`Transport`] can reconstruct these values
// from the other fields
let mut transport = Transport::new(sample_rate); let mut transport = Transport::new(sample_rate);
if !data.context.is_null() { if !data.context.is_null() {
let context = &*data.context; let context = &*data.context;
@ -800,11 +863,24 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
transport.time_sig_numerator = Some(context.time_sig_num); transport.time_sig_numerator = Some(context.time_sig_num);
transport.time_sig_denominator = Some(context.time_sig_den); transport.time_sig_denominator = Some(context.time_sig_den);
} }
transport.pos_samples = Some(context.project_time_samples);
// We need to compensate for the block splitting here
transport.pos_samples =
Some(context.project_time_samples + block_start as i64);
if context.state & (1 << 9) != 0 { if context.state & (1 << 9) != 0 {
// kProjectTimeMusicValid // kProjectTimeMusicValid
if P::SAMPLE_ACCURATE_AUTOMATION && (context.state & (1 << 10) != 0) {
// kTempoValid
transport.pos_beats = Some(
context.project_time_music
+ (block_start as f64 / sample_rate as f64 / 60.0
* context.tempo),
);
} else {
transport.pos_beats = Some(context.project_time_music); transport.pos_beats = Some(context.project_time_music);
} }
}
if context.state & (1 << 11) != 0 { if context.state & (1 << 11) != 0 {
// kBarPositionValid // kBarPositionValid
transport.bar_start_pos_beats = Some(context.bar_position_music); transport.bar_start_pos_beats = Some(context.bar_position_music);
@ -818,14 +894,28 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
let mut plugin = self.inner.plugin.write(); let mut plugin = self.inner.plugin.write();
let mut context = self.inner.make_process_context(transport); let mut context = self.inner.make_process_context(transport);
match plugin.process(&mut output_buffer, &mut context) {
let result = plugin.process(&mut output_buffer, &mut context);
self.inner.last_process_status.store(result);
match result {
ProcessStatus::Error(err) => { ProcessStatus::Error(err) => {
nih_debug_assert_failure!("Process error: {}", err); nih_debug_assert_failure!("Process error: {}", err);
kResultFalse return kResultFalse;
} }
_ => kResultOk, _ => kResultOk,
} }
};
// If our block ends at the end of the buffer then that means there are no more
// unprocessed (parameter) events. If there are more events, we'll just keep going
// through this process until we've processed the entire buffer.
if block_end as i32 == data.num_samples {
break result;
} else {
block_start = block_end;
}
}
}) })
} }