From 1a8f81e4c027bffd2f177a451e7bb8893cf1e6e3 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 8 Apr 2022 20:53:32 +0200 Subject: [PATCH] Support MIDI CCs, aftertouch, pitch bend for VST3 This required rewriting the way events and parameter changes are handled for VST3 by putting them all in a single sorted array, because we can now no longer read directly from the host's events list because we also need to mix these new generated MIDI CC events in with it. --- README.md | 4 +- src/midi.rs | 19 ++ src/plugin.rs | 1 - src/wrapper/vst3/inner.rs | 88 +++---- src/wrapper/vst3/util.rs | 14 ++ src/wrapper/vst3/wrapper.rs | 441 +++++++++++++++++++++++------------- 6 files changed, 369 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index 2623533c..78c36f12 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,8 @@ for download links. - A simple and safe API for state saving and restoring from the editor is provided by the framework if you want to do your own internal preset management. -- Basic note/MIDI support for VST3, full support for both expressions and MIDI - CCs for CLAP. Similar support for VST3 is coming. +- Full support for both modern polyphonic note expressions as well as MIDI CCs, + channel pressure, and pitch bend for CLAP and VST3. - A plugin bundler accessible through the `cargo xtask bundle ` command that automatically detects which plugin targets your plugin exposes and creates the correct diff --git a/src/midi.rs b/src/midi.rs index 75a3337a..02fefccf 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -212,4 +212,23 @@ impl NoteEvent { NoteEvent::MidiCC { timing, .. } => *timing, } } + + /// Subtract a sample offset from this event's timing, needed to compensate for the block + /// splitting in the VST3 wrapper implementation because all events have to be read upfront. + pub(crate) fn subtract_timing(&mut self, samples: u32) { + match self { + NoteEvent::NoteOn { timing, .. } => *timing -= samples, + NoteEvent::NoteOff { timing, .. } => *timing -= samples, + NoteEvent::PolyPressure { timing, .. } => *timing -= samples, + NoteEvent::Volume { timing, .. } => *timing -= samples, + NoteEvent::Pan { timing, .. } => *timing -= samples, + NoteEvent::Tuning { timing, .. } => *timing -= samples, + NoteEvent::Vibrato { timing, .. } => *timing -= samples, + NoteEvent::Expression { timing, .. } => *timing -= samples, + NoteEvent::Brightness { timing, .. } => *timing -= samples, + NoteEvent::MidiChannelPressure { timing, .. } => *timing -= samples, + NoteEvent::MidiPitchBend { timing, .. } => *timing -= samples, + NoteEvent::MidiCC { timing, .. } => *timing -= samples, + } + } } diff --git a/src/plugin.rs b/src/plugin.rs index 54567e73..0777681b 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -21,7 +21,6 @@ use crate::param::internals::Params; /// - Sidechain inputs /// - Multiple output busses /// - Special handling for offline processing -/// - MIDI CC and expression handling for VST3 (those things are implemented for CLAP) /// - Outputting MIDI events from the process function (you can output parmaeter changes from an /// editor GUI) #[allow(unused_variables)] diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 32b79bd6..276e334d 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -2,8 +2,7 @@ use atomic_refcell::AtomicRefCell; use crossbeam::atomic::AtomicCell; use crossbeam::channel::{self, SendTimeoutError}; use parking_lot::RwLock; -use std::cmp::Reverse; -use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::mem::MaybeUninit; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; @@ -14,12 +13,12 @@ use vst3_sys::vst::{IComponentHandler, RestartFlags}; use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::note_expressions::NoteExpressionController; use super::param_units::ParamUnits; -use super::util::{ObjectPtr, VstPtr}; +use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START}; use super::view::WrapperView; use crate::buffer::Buffer; use crate::context::Transport; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; -use crate::midi::NoteEvent; +use crate::midi::{MidiConfig, NoteEvent}; use crate::param::internals::{ParamPtr, Params}; use crate::param::ParamFlags; use crate::plugin::{BufferConfig, BusConfig, Editor, ProcessStatus, Vst3Plugin}; @@ -88,12 +87,16 @@ pub(crate) struct WrapperInner { /// the msot recent VST3 note IDs we've seen, and then map those back to MIDI note IDs and /// channels as needed. pub note_expression_controller: 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>>, + /// Unprocessed parameter changes and note events sent by the host during a process call. + /// Parameter changes are sent as separate queues for each parameter, and note events are in + /// another queue on top of that. And if `P::MIDI_INPUT >= MidiConfig::MidiCCs`, then we can + /// also receive MIDI CC messages through special parameter changes. On top of that, we also + /// support sample accurate automation through block splitting if + /// `P::SAMPLE_ACCURATE_AUTOMATION` is set. To account for all of this, we'll read all of the + /// parameter changes and events into a vector at the start of the process call, sort it, and + /// then do the block splitting based on that. Note events need to have their timing adjusted to + /// match the block start, since they're all read upfront. + pub process_events: AtomicRefCell>, /// The plugin is able to restore state through a method on the `GuiContext`. To avoid changing /// parameters mid-processing and running into garbled data if the host also tries to load state /// at the same time the restoring happens at the end of each processing call. If this zero @@ -133,24 +136,33 @@ 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) - } +/// VST3 makes audio processing pretty complicated. In order to support both block splitting for +/// sample accurate automation and MIDI CC handling through parameters we need to put all parameter +/// changes and (translated) note events into a sorted array first. +#[derive(Debug, PartialEq)] +pub enum ProcessEvent { + /// An incoming parameter change sent by the host. This will only be used when sample accurate + /// automation has been enabled, and the parameters are only updated when we process this + /// spooled event at the start of a block. + ParameterChange { + /// The event's sample offset within the buffer. Used for sorting. + timing: u32, + /// The parameter's hash, as used everywhere else. + hash: u32, + /// The normalized values, as provided by the host. + normalized_value: f32, + }, + /// An incoming parameter change sent by the host. This will only be used when sample accurate + /// automation has been enabled, and the parameters are only updated when we process this + /// spooled event at the start of a block. + NoteEvent { + /// The event's sample offset within the buffer. Used for sorting. The timing stored within + /// the note event needs to have the block start index subtraced from it. + timing: u32, + /// The actual note event, make sure to subtract the block start index with + /// [`NoteEvent::subtract_timing()`] before putting this into the input event queue. + event: NoteEvent, + }, } impl WrapperInner

{ @@ -188,11 +200,9 @@ impl WrapperInner

{ param_ids.len(), "The plugin has duplicate parameter IDs, weird things may happen" ); - } - if cfg!(debug_assertions) { let mut bypass_param_exists = false; - for (_, _, ptr, _) in ¶m_id_hashes_ptrs_groups { + for (id, hash, ptr, _) in ¶m_id_hashes_ptrs_groups { let flags = unsafe { ptr.flags() }; let is_bypass = flags.contains(ParamFlags::BYPASS); @@ -203,6 +213,14 @@ impl WrapperInner

{ } bypass_param_exists |= is_bypass; + + if P::MIDI_INPUT >= MidiConfig::MidiCCs + && (VST3_MIDI_PARAMS_START..VST3_MIDI_PARAMS_END).contains(hash) + { + nih_debug_assert_failure!( + "Parameter '{}' collides with an automatically generated MIDI CC parameter, consider giving it a different ID", id + ); + } } } @@ -255,13 +273,7 @@ impl WrapperInner

{ output_buffer: AtomicRefCell::new(Buffer::default()), input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)), note_expression_controller: AtomicRefCell::new(NoteExpressionController::default()), - input_param_changes: AtomicRefCell::new(BinaryHeap::with_capacity( - if P::SAMPLE_ACCURATE_AUTOMATION { - 4096 - } else { - 0 - }, - )), + process_events: AtomicRefCell::new(Vec::with_capacity(4096)), updated_state_sender, updated_state_receiver, diff --git a/src/wrapper/vst3/util.rs b/src/wrapper/vst3/util.rs index 870a4c95..94a12aea 100644 --- a/src/wrapper/vst3/util.rs +++ b/src/wrapper/vst3/util.rs @@ -1,6 +1,20 @@ use std::ops::Deref; use vst3_sys::{interfaces::IUnknown, ComInterface}; +/// When `Plugin::MIDI_INPUT` is set to `MidiConfig::MidiCCs` or higher then we'll register 130*16 +/// additional parameters to handle MIDI CCs, channel pressure, and pitch bend, in that order. +/// vst3-sys doesn't expose these constants. +pub const VST3_MIDI_CCS: u32 = 130; +pub const VST3_MIDI_CHANNELS: u32 = 16; +/// The number of parameters we'll need to register if the plugin accepts MIDI CCs. +pub const VST3_MIDI_NUM_PARAMS: u32 = VST3_MIDI_CCS * VST3_MIDI_CHANNELS; +/// The start of the MIDI CC parameter ranges. We'll print an assertion failure if any of the +/// plugin's parameters overlap with this range. The mapping to a parameter index is +/// `VST3_MIDI_PARAMS_START + (cc_idx + (channel * VST3_MIDI_CCS))`. +pub const VST3_MIDI_PARAMS_START: u32 = VST3_MIDI_PARAMS_END - VST3_MIDI_NUM_PARAMS; +/// The (exlucive) end of the MIDI CC parameter range. Anything above this is reserved by the host. +pub const VST3_MIDI_PARAMS_END: u32 = (1 << 31) + 1; + /// Early exit out of a VST3 function when one of the passed pointers is null macro_rules! check_null_ptr { ($ptr:expr $(, $ptrs:expr)* $(, )?) => { diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 4694686a..04cab378 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -1,4 +1,4 @@ -use std::cmp::{self, Reverse}; +use std::cmp; use std::ffi::c_void; use std::mem::{self, MaybeUninit}; use std::ptr; @@ -9,13 +9,14 @@ use vst3_sys::base::{IBStream, IPluginBase}; use vst3_sys::utils::SharedVstPtr; use vst3_sys::vst::{ kNoProgramListId, kRootUnitId, EventTypes, IAudioProcessor, IComponent, IEditController, - IEventList, IParamValueQueue, IParameterChanges, IUnitInfo, ProgramListInfo, TChar, UnitInfo, + IEventList, IMidiMapping, IParamValueQueue, IParameterChanges, IUnitInfo, ParameterFlags, + ProgramListInfo, TChar, UnitInfo, }; use vst3_sys::VST3; use widestring::U16CStr; use super::inner::WrapperInner; -use super::util::VstPtr; +use super::util::{VstPtr, VST3_MIDI_CCS, VST3_MIDI_NUM_PARAMS, VST3_MIDI_PARAMS_START}; use super::view::WrapperView; use crate::context::Transport; use crate::midi::{MidiConfig, NoteEvent}; @@ -24,12 +25,13 @@ use crate::plugin::{BufferConfig, BusConfig, ProcessStatus, Vst3Plugin}; use crate::util::permit_alloc; use crate::wrapper::state; use crate::wrapper::util::{process_wrapper, u16strlcpy}; -use crate::wrapper::vst3::inner::ParameterChange; +use crate::wrapper::vst3::inner::ProcessEvent; +use crate::wrapper::vst3::util::{VST3_MIDI_CHANNELS, VST3_MIDI_PARAMS_END}; // Alias needed for the VST3 attribute macro use vst3_sys as vst3_com; -#[VST3(implements(IComponent, IEditController, IAudioProcessor, IUnitInfo))] +#[VST3(implements(IComponent, IEditController, IAudioProcessor, IMidiMapping, IUnitInfo))] pub(crate) struct Wrapper { inner: Arc>, } @@ -302,7 +304,12 @@ impl IEditController for Wrapper

{ } unsafe fn get_parameter_count(&self) -> i32 { - self.inner.param_hashes.len() as i32 + // We need to add a whole bunch of parameters if the plugin accepts MIDI CCs + if P::MIDI_INPUT >= MidiConfig::MidiCCs { + self.inner.param_hashes.len() as i32 + VST3_MIDI_NUM_PARAMS as i32 + } else { + self.inner.param_hashes.len() as i32 + } } unsafe fn get_parameter_info( @@ -316,35 +323,57 @@ impl IEditController for Wrapper

{ return kInvalidArgument; } - let param_hash = &self.inner.param_hashes[param_index as usize]; - let param_unit = &self - .inner - .param_units - .get_vst3_unit_id(*param_hash) - .expect("Inconsistent parameter data"); - let param_ptr = &self.inner.param_by_hash[param_hash]; - let default_value = param_ptr.default_normalized_value(); - let flags = param_ptr.flags(); - let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE); - let is_bypass = flags.contains(ParamFlags::BYPASS); - *info = std::mem::zeroed(); - let info = &mut *info; - info.id = *param_hash; - u16strlcpy(&mut info.title, param_ptr.name()); - u16strlcpy(&mut info.short_title, param_ptr.name()); - u16strlcpy(&mut info.units, param_ptr.unit()); - info.step_count = param_ptr.step_count().unwrap_or(0) as i32; - info.default_normalized_value = default_value as f64; - info.unit_id = *param_unit; - info.flags = if automatable { - vst3_sys::vst::ParameterFlags::kCanAutomate as i32 + + // If the parameter is a generated MIDI CC/channel pressure/pitch bend then it needs to be + // handled separately + let num_actual_params = self.inner.param_hashes.len() as i32; + if P::MIDI_INPUT >= MidiConfig::MidiCCs && param_index >= num_actual_params { + let midi_param_relative_idx = (param_index - num_actual_params) as u32; + // This goes up to 130 for the 128 CCs followed by channel pressure and pitch bend + let midi_cc = midi_param_relative_idx % VST3_MIDI_CCS; + let midi_channel = midi_param_relative_idx / VST3_MIDI_CCS; + let name = match midi_cc { + // kAfterTouch + 128 => format!("MIDI Ch. {} Channel Pressure", midi_channel + 1), + // kPitchBend + 129 => format!("MIDI Ch. {} Pitch Bend", midi_channel + 1), + n => format!("MIDI Ch. {} CC {}", midi_channel + 1, n), + }; + + info.id = VST3_MIDI_PARAMS_START + midi_param_relative_idx; + u16strlcpy(&mut info.title, &name); + u16strlcpy(&mut info.short_title, &name); + info.flags = ParameterFlags::kIsReadOnly as i32 | (1 << 4); // kIsHidden } else { - vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden - }; - if is_bypass { - info.flags |= vst3_sys::vst::ParameterFlags::kIsBypass as i32; + let param_hash = &self.inner.param_hashes[param_index as usize]; + let param_unit = &self + .inner + .param_units + .get_vst3_unit_id(*param_hash) + .expect("Inconsistent parameter data"); + let param_ptr = &self.inner.param_by_hash[param_hash]; + let default_value = param_ptr.default_normalized_value(); + let flags = param_ptr.flags(); + let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE); + let is_bypass = flags.contains(ParamFlags::BYPASS); + + info.id = *param_hash; + u16strlcpy(&mut info.title, param_ptr.name()); + u16strlcpy(&mut info.short_title, param_ptr.name()); + u16strlcpy(&mut info.units, param_ptr.unit()); + info.step_count = param_ptr.step_count().unwrap_or(0) as i32; + info.default_normalized_value = default_value as f64; + info.unit_id = *param_unit; + info.flags = if automatable { + ParameterFlags::kCanAutomate as i32 + } else { + ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden + }; + if is_bypass { + info.flags |= ParameterFlags::kIsBypass as i32; + } } kResultOk @@ -360,6 +389,8 @@ impl IEditController for Wrapper

{ let dest = &mut *(string as *mut [TChar; 128]); + // TODO: We don't implement these methods at all for our generated MIDI CC parameters, + // should be fine right? They should be hidden anyways. match self.inner.param_by_hash.get(&id) { Some(param_ptr) => { u16strlcpy( @@ -403,14 +434,14 @@ impl IEditController for Wrapper

{ unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 { match self.inner.param_by_hash.get(&id) { Some(param_ptr) => param_ptr.preview_plain(value_normalized as f32) as f64, - _ => 0.5, + _ => value_normalized, } } unsafe fn plain_param_to_normalized(&self, id: u32, plain_value: f64) -> f64 { match self.inner.param_by_hash.get(&id) { Some(param_ptr) => param_ptr.preview_normalized(plain_value as f32) as f64, - _ => 0.5, + _ => plain_value, } } @@ -608,6 +639,8 @@ impl IAudioProcessor for Wrapper

{ kResultOk } + // Clippy doesn't understand our `event_start_idx` + #[allow(clippy::mut_range_bound)] unsafe fn process(&self, data: *mut vst3_sys::vst::ProcessData) -> tresult { check_null_ptr!(data); @@ -653,11 +686,17 @@ impl IAudioProcessor for Wrapper

{ nih_debug_assert!(data.num_samples >= 0); // 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(); + // chunks whenever a parameter change occurs. To do that, we'll store all of those + // parameter changes in a vector. Otherwise all parameter changes are handled right here + // and now. We'll also need to store the note events in the same vector because MIDI CC + // messages are sent through parameter changes. This vector gets sorted at the end so we + // can treat it as a sort of queue. + let mut process_events = self.inner.process_events.borrow_mut(); let mut parameter_values_changed = false; - input_param_changes.clear(); + process_events.clear(); + + // First we'll go through the parameter changes. This may also include MIDI CC messages + // if the plugin supports those 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 { @@ -672,68 +711,200 @@ impl IAudioProcessor for Wrapper

{ let mut sample_offset = 0i32; let mut value = 0.0f64; - #[allow(clippy::collapsible_else_if)] - if P::SAMPLE_ACCURATE_AUTOMATION { - for change_idx in 0..num_changes { - if param_change_queue.get_point( - change_idx, - &mut sample_offset, - &mut value, - ) == kResultOk - { - input_param_changes.push(Reverse(( - sample_offset as usize, - ParameterChange { - hash: param_hash, - normalized_value: value as f32, - }, - ))); - } - } - } else { + for change_idx in 0..num_changes { if param_change_queue.get_point( - num_changes - 1, + change_idx, &mut sample_offset, &mut value, ) == kResultOk { - self.inner.set_normalized_value_by_hash( - param_hash, - value as f32, - Some(sample_rate), - ); - parameter_values_changed = true; + let timing = sample_offset as u32; + let value = value as f32; + + // MIDI CC messages, channel pressure, and pitch bend are also sent + // as parameter changes + if P::MIDI_INPUT >= MidiConfig::MidiCCs + && (VST3_MIDI_PARAMS_START..VST3_MIDI_PARAMS_END) + .contains(¶m_hash) + { + let midi_param_relative_idx = + param_hash - VST3_MIDI_PARAMS_START; + // This goes up to 130 for the 128 CCs followed by channel pressure and pitch bend + let midi_cc = (midi_param_relative_idx % VST3_MIDI_CCS) as u8; + let midi_channel = + (midi_param_relative_idx / VST3_MIDI_CCS) as u8; + process_events.push(ProcessEvent::NoteEvent { + timing, + event: match midi_cc { + // kAfterTouch + 128 => NoteEvent::MidiChannelPressure { + timing, + channel: midi_channel, + pressure: value, + }, + // kPitchBend + 129 => NoteEvent::MidiPitchBend { + timing, + channel: midi_channel, + value, + }, + n => NoteEvent::MidiCC { + timing, + channel: midi_channel, + cc: n, + value, + }, + }, + }); + } else if P::SAMPLE_ACCURATE_AUTOMATION { + process_events.push(ProcessEvent::ParameterChange { + timing, + hash: param_hash, + normalized_value: value, + }); + } else { + self.inner.set_normalized_value_by_hash( + param_hash, + value, + Some(sample_rate), + ); + parameter_values_changed = true; + } } } } } } - let mut block_start = 0; - let mut block_end = data.num_samples as usize; + // Then we'll add all of our input events + if P::MIDI_INPUT >= MidiConfig::Basic { + let mut note_expression_controller = + self.inner.note_expression_controller.borrow_mut(); + if let Some(events) = data.input_events.upgrade() { + let num_events = events.get_event_count(); + + let mut event: MaybeUninit<_> = MaybeUninit::uninit(); + for i in 0..num_events { + let result = events.get_event(i, event.as_mut_ptr()); + nih_debug_assert_eq!(result, kResultOk); + + let event = event.assume_init(); + let timing = event.sample_offset as u32; + if event.type_ == EventTypes::kNoteOnEvent as u16 { + let event = event.event.note_on; + + // We need to keep track of note IDs to be able to handle not + // expression value events + note_expression_controller.register_note(&event); + + process_events.push(ProcessEvent::NoteEvent { + timing, + event: NoteEvent::NoteOn { + timing, + channel: event.channel as u8, + note: event.pitch as u8, + velocity: event.velocity, + }, + }); + } else if event.type_ == EventTypes::kNoteOffEvent as u16 { + let event = event.event.note_off; + process_events.push(ProcessEvent::NoteEvent { + timing, + event: NoteEvent::NoteOff { + timing, + channel: event.channel as u8, + note: event.pitch as u8, + velocity: event.velocity, + }, + }); + } else if event.type_ == EventTypes::kPolyPressureEvent as u16 { + let event = event.event.poly_pressure; + process_events.push(ProcessEvent::NoteEvent { + timing, + event: NoteEvent::PolyPressure { + timing, + channel: event.channel as u8, + note: event.pitch as u8, + pressure: event.pressure, + }, + }); + } else if event.type_ == EventTypes::kNoteExpressionValueEvent as u16 { + let event = event.event.note_expression_value; + match note_expression_controller.translate_event(timing, &event) { + Some(translated_event) => { + process_events.push(ProcessEvent::NoteEvent { + timing, + event: translated_event, + }) + } + None => nih_debug_assert_failure!( + "Unhandled note expression type: {}", + event.type_id + ), + } + } + } + } + } + + // And then we'll make sure everything is in the right order + // NOTE: It's important that this sort is stable, because parameter changes need to be + // processed before note events. Otherwise you'll get out of bounds note events + // with block splitting when the note event occurs at one index after the end (or + // on the exlusive end index) of the block. + process_events.sort_by_key(|event| match event { + ProcessEvent::ParameterChange { timing, .. } => *timing, + ProcessEvent::NoteEvent { timing, .. } => *timing, + }); + + let mut block_start = 0usize; + let mut block_end; let mut event_start_idx = 0; let result = 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; - } + // In sample-accurate automation mode we'll handle all parameter changes from the + // sorted process event array until we run into 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. As + // mentioend above, for this to work correctly parameter changes need to be ordered + // before note events at the same index. + // The extra scope is here to make sure we release the borrow on input_events + { + let mut input_events = self.inner.input_events.borrow_mut(); + input_events.clear(); - let Reverse((_, change)) = input_param_changes.pop().unwrap(); - self.inner.set_normalized_value_by_hash( - change.hash, - change.normalized_value, - Some(sample_rate), - ); - parameter_values_changed = true; + block_end = data.num_samples as usize; + for event_idx in event_start_idx..process_events.len() { + match process_events[event_idx] { + ProcessEvent::ParameterChange { + timing, + hash, + normalized_value, + } => { + // If this parameter change happens after the start of this block, then + // we'll split the block here and handle this parmaeter change after + // we've processed this block + if timing != block_start as u32 { + event_start_idx = event_idx; + block_end = timing as usize; + break; + } + + self.inner.set_normalized_value_by_hash( + hash, + normalized_value, + Some(sample_rate), + ); + parameter_values_changed = true; + } + ProcessEvent::NoteEvent { + timing: _, + mut event, + } => { + // We need to make sure to compensate the event for any block splitting, + // since we had to create the event object beforehand + event.subtract_timing(block_start as u32); + input_events.push_back(event); + } } } } @@ -744,77 +915,6 @@ impl IAudioProcessor for Wrapper

{ parameter_values_changed = false; } - if P::MIDI_INPUT >= MidiConfig::Basic { - let mut input_events = self.inner.input_events.borrow_mut(); - let mut note_expression_controller = - self.inner.note_expression_controller.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_ == EventTypes::kNoteOnEvent as u16 { - let event = event.event.note_on; - - // We need to keep track of note IDs to be able to handle not - // expression value events - note_expression_controller.register_note(&event); - - input_events.push_back(NoteEvent::NoteOn { - timing, - channel: event.channel as u8, - note: event.pitch as u8, - velocity: event.velocity, - }); - } else if event.type_ == 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, - }); - } else if event.type_ == EventTypes::kPolyPressureEvent as u16 { - let event = event.event.poly_pressure; - input_events.push_back(NoteEvent::PolyPressure { - timing, - channel: event.channel as u8, - note: event.pitch as u8, - pressure: event.pressure, - }); - } else if event.type_ == EventTypes::kNoteExpressionValueEvent as u16 { - let event = event.event.note_expression_value; - match note_expression_controller.translate_event(timing, &event) { - Some(translated_event) => { - input_events.push_back(translated_event) - } - None => nih_debug_assert_failure!( - "Unhandled note expression type: {}", - event.type_id - ), - } - } - - // TODO: Add note event controllers to support the same expression types - // we're supporting for CLAP - } - } - } - let result = if is_parameter_flush { kResultOk } else { @@ -989,6 +1089,33 @@ impl IAudioProcessor for Wrapper

{ } } +impl IMidiMapping for Wrapper

{ + unsafe fn get_midi_controller_assignment( + &self, + bus_index: i32, + channel: i16, + midi_cc_number: vst3_com::vst::CtrlNumber, + param_id: *mut vst3_com::vst::ParamID, + ) -> tresult { + if P::MIDI_INPUT < MidiConfig::MidiCCs + || bus_index != 0 + || !(0..VST3_MIDI_CHANNELS as i16).contains(&channel) + || !(0..VST3_MIDI_CCS as i16).contains(&midi_cc_number) + { + return kResultFalse; + } + + check_null_ptr!(param_id); + + // We reserve a contiguous parameter range right at the end of the allowed parameter indices + // for these MIDI CC parameters + *param_id = + VST3_MIDI_PARAMS_START + midi_cc_number as u32 + (channel as u32 * VST3_MIDI_CCS); + + kResultOk + } +} + impl IUnitInfo for Wrapper

{ unsafe fn get_unit_count(&self) -> i32 { self.inner.param_units.len() as i32