From 5d3527c5c2fd309002760ccb51ffe1e3ee916ec6 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 10 Mar 2022 23:39:58 +0100 Subject: [PATCH] Also implement sample accurate automation for VST3 --- src/plugin.rs | 3 - src/wrapper/clap/wrapper.rs | 2 +- src/wrapper/vst3/inner.rs | 45 ++++- src/wrapper/vst3/wrapper.rs | 388 ++++++++++++++++++++++-------------- 4 files changed, 279 insertions(+), 159 deletions(-) diff --git a/src/plugin.rs b/src/plugin.rs index 4b883b08..30112efb 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -18,7 +18,6 @@ use crate::param::internals::Params; /// - Sidechain inputs /// - Multiple output busses /// - Special handling for offline processing -/// - Sample accurate automation for VST3, this can already be enabled for CLAP /// - Parameter hierarchies/groups /// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to /// 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 /// blocks may be as small as a single sample. Bitwig Studio sends at most one parameter change /// every 64 samples. - /// - /// TODO: Implement this for VST3, this currently is only implemetned for CLAP. const SAMPLE_ACCURATE_AUTOMATION: bool = false; /// The plugin's parameters. The host will update the parameter values before calling diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index ecff8969..31c45c65 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -926,7 +926,7 @@ impl Wrapper

{ } }); - // 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 // plugin can just do normal in place processing. if !process.audio_inputs.is_null() { diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 118c4046..e0148a0e 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -1,7 +1,8 @@ use atomic_refcell::AtomicRefCell; use crossbeam::atomic::AtomicCell; 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::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; @@ -66,11 +67,16 @@ pub(crate) struct WrapperInner { /// 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. pub output_buffer: AtomicRefCell>, - /// The incoming events for the plugin, if `P::ACCEPTS_MIDI` is set. - /// - /// TODO: Maybe load these lazily at some point instead of needing to spool them all to this - /// queue first + /// 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 + /// block splitting. pub input_events: AtomicRefCell>, + /// 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>>, /// The keys from `param_map` in a stable order. pub param_hashes: Vec, @@ -102,6 +108,26 @@ pub enum Task { 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 WrapperInner

{ #[allow(unused_unsafe)] pub fn new() -> Arc { @@ -132,7 +158,14 @@ impl WrapperInner

{ last_process_status: AtomicCell::new(ProcessStatus::Normal), current_latency: AtomicU32::new(0), 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_by_hash: HashMap::new(), diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 430efd7e..55172a96 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -1,4 +1,4 @@ -use std::cmp; +use std::cmp::{self, Reverse}; use std::ffi::c_void; use std::mem::{self, MaybeUninit}; use std::ptr; @@ -21,6 +21,7 @@ use crate::context::Transport; use crate::plugin::{BufferConfig, BusConfig, NoteEvent, ProcessStatus, Vst3Plugin}; use crate::wrapper::state; use crate::wrapper::util::{process_wrapper, u16strlcpy}; +use crate::wrapper::vst3::inner::ParameterChange; // Alias needed for the VST3 attribute macro use vst3_sys as vst3_com; @@ -645,74 +646,17 @@ impl IAudioProcessor for Wrapper

{ .load() .expect("Process call without prior setup call") .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 - // TOOD: Send the output events when doing a flush - if data.num_outputs == 0 { + let is_parameter_flush = data.num_outputs == 0; + if is_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: @@ -732,99 +676,245 @@ impl IAudioProcessor for Wrapper

{ ); nih_debug_assert!(data.num_samples >= 0); - let num_output_channels = (*data.outputs).num_channels as usize; - check_null_ptr_msg!( - "Process output pointer is null", - data.outputs, - (*data.outputs).buffers, - ); + // If `P::SAMPLE_ACCURATE_AUTOMATION` is set, then we'll split up the audio buffer into + // chunks whenever a parameter change occurs. Otherwise all parameter changes are + // handled right here and now. + let mut input_param_changes = self.inner.input_param_changes.borrow_mut(); + 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 - // channels - let mut output_buffer = self.inner.output_buffer.borrow_mut(); - output_buffer.with_raw_vec(|output_slices| { - nih_debug_assert_eq!(num_output_channels, output_slices.len()); - for (output_channel_idx, output_channel_slice) in - output_slices.iter_mut().enumerate() - { - // 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 - // only dereferenced here later as part of this process function. - *output_channel_slice = std::slice::from_raw_parts_mut( - *((*data.outputs).buffers as *mut *mut f32).add(output_channel_idx), - data.num_samples as usize, - ); - } - }); - - // Most 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 - // plugin can just do normal in place processing. - if !data.inputs.is_null() { - let num_input_channels = (*data.inputs).num_channels as usize; - nih_debug_assert!( - num_input_channels <= num_output_channels, - "Stereo to mono and similar configurations are not supported" - ); - for input_channel_idx in 0..cmp::min(num_input_channels, num_output_channels) { - let output_channel_ptr = - *((*data.outputs).buffers as *mut *mut f32).add(input_channel_idx); - let input_channel_ptr = - *((*data.inputs).buffers as *const *const f32).add(input_channel_idx); - if input_channel_ptr != output_channel_ptr { - ptr::copy_nonoverlapping( - input_channel_ptr, - output_channel_ptr, - data.num_samples as usize, - ); + 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 + { + 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), + ); + } + } } } } - // Some of the fields are left empty because VST3 does not provide this information, but - // the methods on [`Transport`] can reconstruct these values from the other fields - let mut transport = Transport::new(sample_rate); - if !data.context.is_null() { - let context = &*data.context; + 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; + } - // These constants are missing from vst3-sys, see: - // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/structSteinberg_1_1Vst_1_1ProcessContext.html - transport.playing = context.state & (1 << 1) != 0; // kPlaying - transport.recording = context.state & (1 << 3) != 0; // kRecording - if context.state & (1 << 10) != 0 { - // kTempoValid - transport.tempo = Some(context.tempo); + let Reverse((_, change)) = input_param_changes.pop().unwrap(); + self.inner.set_normalized_value_by_hash( + change.hash, + change.normalized_value, + Some(sample_rate), + ); + } + } } - if context.state & (1 << 13) != 0 { - // kTimeSigValid - transport.time_sig_numerator = Some(context.time_sig_num); - transport.time_sig_denominator = Some(context.time_sig_den); - } - transport.pos_samples = Some(context.project_time_samples); - if context.state & (1 << 9) != 0 { - // kProjectTimeMusicValid - transport.pos_beats = Some(context.project_time_music); - } - if context.state & (1 << 11) != 0 { - // kBarPositionValid - transport.bar_start_pos_beats = Some(context.bar_position_music); - } - if context.state & (1 << 2) != 0 && context.state & (1 << 12) != 0 { - // kCycleActive && kCycleValid - transport.loop_range_beats = - Some((context.cycle_start_music, context.cycle_end_music)); - } - } - let mut plugin = self.inner.plugin.write(); - let mut context = self.inner.make_process_context(transport); - match plugin.process(&mut output_buffer, &mut context) { - ProcessStatus::Error(err) => { - nih_debug_assert_failure!("Process error: {}", err); + 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(); - kResultFalse + 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(); + output_buffer.with_raw_vec(|output_slices| { + nih_debug_assert_eq!(num_output_channels, output_slices.len()); + for (output_channel_idx, output_channel_slice) in + 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 + // their lifetime is equal to this structs. This is still safe because they are + // 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( + channel_ptr.add(block_start), + block_end - block_start, + ); + } + }); + + // 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 plugin can just do normal in place processing. + if !data.inputs.is_null() { + let num_input_channels = (*data.inputs).num_channels as usize; + nih_debug_assert!( + num_input_channels <= num_output_channels, + "Stereo to mono and similar configurations are not supported" + ); + for input_channel_idx in + 0..cmp::min(num_input_channels, num_output_channels) + { + let output_channel_ptr = + *((*data.outputs).buffers as *mut *mut f32).add(input_channel_idx); + let input_channel_ptr = *((*data.inputs).buffers as *const *const f32) + .add(input_channel_idx); + if input_channel_ptr != output_channel_ptr { + ptr::copy_nonoverlapping( + input_channel_ptr.add(block_start), + output_channel_ptr.add(block_start), + block_end - block_start, + ); + } + } + } + + // Some of the fields are left empty because VST3 does not provide this + // information, but the methods on [`Transport`] can reconstruct these values + // from the other fields + let mut transport = Transport::new(sample_rate); + if !data.context.is_null() { + let context = &*data.context; + + // These constants are missing from vst3-sys, see: + // https://steinbergmedia.github.io/vst3_doc/vstinterfaces/structSteinberg_1_1Vst_1_1ProcessContext.html + transport.playing = context.state & (1 << 1) != 0; // kPlaying + transport.recording = context.state & (1 << 3) != 0; // kRecording + if context.state & (1 << 10) != 0 { + // kTempoValid + transport.tempo = Some(context.tempo); + } + if context.state & (1 << 13) != 0 { + // kTimeSigValid + transport.time_sig_numerator = Some(context.time_sig_num); + transport.time_sig_denominator = Some(context.time_sig_den); + } + + // 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 { + // 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); + } + } + + if context.state & (1 << 11) != 0 { + // kBarPositionValid + transport.bar_start_pos_beats = Some(context.bar_position_music); + } + if context.state & (1 << 2) != 0 && context.state & (1 << 12) != 0 { + // kCycleActive && kCycleValid + transport.loop_range_beats = + Some((context.cycle_start_music, context.cycle_end_music)); + } + } + + let mut plugin = self.inner.plugin.write(); + let mut context = self.inner.make_process_context(transport); + + let result = plugin.process(&mut output_buffer, &mut context); + self.inner.last_process_status.store(result); + match result { + ProcessStatus::Error(err) => { + nih_debug_assert_failure!("Process error: {}", err); + + return kResultFalse; + } + _ => 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; } - _ => kResultOk, } }) }