1
0
Fork 0

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.
This commit is contained in:
Robbert van der Helm 2022-04-08 20:53:32 +02:00
parent 059c733b78
commit 1a8f81e4c0
6 changed files with 369 additions and 198 deletions

View file

@ -90,8 +90,8 @@ for download links.
- A simple and safe API for state saving and restoring from the editor is - 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 provided by the framework if you want to do your own internal preset
management. management.
- Basic note/MIDI support for VST3, full support for both expressions and MIDI - Full support for both modern polyphonic note expressions as well as MIDI CCs,
CCs for CLAP. Similar support for VST3 is coming. channel pressure, and pitch bend for CLAP and VST3.
- A plugin bundler accessible through the - A plugin bundler accessible through the
`cargo xtask bundle <package> <build_arguments>` command that automatically `cargo xtask bundle <package> <build_arguments>` command that automatically
detects which plugin targets your plugin exposes and creates the correct detects which plugin targets your plugin exposes and creates the correct

View file

@ -212,4 +212,23 @@ impl NoteEvent {
NoteEvent::MidiCC { timing, .. } => *timing, 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,
}
}
} }

View file

@ -21,7 +21,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
/// - 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 /// - Outputting MIDI events from the process function (you can output parmaeter changes from an
/// editor GUI) /// editor GUI)
#[allow(unused_variables)] #[allow(unused_variables)]

View file

@ -2,8 +2,7 @@ use atomic_refcell::AtomicRefCell;
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use crossbeam::channel::{self, SendTimeoutError}; use crossbeam::channel::{self, SendTimeoutError};
use parking_lot::RwLock; use parking_lot::RwLock;
use std::cmp::Reverse; use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::{BinaryHeap, HashMap, HashSet, 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;
@ -14,12 +13,12 @@ use vst3_sys::vst::{IComponentHandler, RestartFlags};
use super::context::{WrapperGuiContext, WrapperProcessContext}; use super::context::{WrapperGuiContext, WrapperProcessContext};
use super::note_expressions::NoteExpressionController; use super::note_expressions::NoteExpressionController;
use super::param_units::ParamUnits; 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 super::view::WrapperView;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::context::Transport; use crate::context::Transport;
use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; 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::internals::{ParamPtr, Params};
use crate::param::ParamFlags; use crate::param::ParamFlags;
use crate::plugin::{BufferConfig, BusConfig, Editor, ProcessStatus, Vst3Plugin}; use crate::plugin::{BufferConfig, BusConfig, Editor, ProcessStatus, Vst3Plugin};
@ -88,12 +87,16 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
/// the msot recent VST3 note IDs we've seen, and then map those back to MIDI note IDs and /// the msot recent VST3 note IDs we've seen, and then map those back to MIDI note IDs and
/// channels as needed. /// channels as needed.
pub note_expression_controller: AtomicRefCell<NoteExpressionController>, pub note_expression_controller: AtomicRefCell<NoteExpressionController>,
/// Unprocessed parameter changes sent by the host as pairs of `(sample_idx_in_buffer, change)`. /// Unprocessed parameter changes and note events sent by the host during a process call.
/// Needed because VST3 does not have a single queue containing all parameter changes. If /// Parameter changes are sent as separate queues for each parameter, and note events are in
/// `P::SAMPLE_ACCURATE_AUTOMATION` is set, then all parameter changes will be read into this /// another queue on top of that. And if `P::MIDI_INPUT >= MidiConfig::MidiCCs`, then we can
/// priority queue and the buffer will be processed in small chunks whenever there's a parameter /// also receive MIDI CC messages through special parameter changes. On top of that, we also
/// change at a new sample index. /// support sample accurate automation through block splitting if
pub input_param_changes: AtomicRefCell<BinaryHeap<Reverse<(usize, ParameterChange)>>>, /// `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<Vec<ProcessEvent>>,
/// The plugin is able to restore state through a method on the `GuiContext`. To avoid changing /// 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 /// 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 /// 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), TriggerRestart(i32),
} }
/// An incoming parameter change sent by the host. Kept in a queue to support block-based sample /// VST3 makes audio processing pretty complicated. In order to support both block splitting for
/// accurate automation. /// sample accurate automation and MIDI CC handling through parameters we need to put all parameter
#[derive(Debug, PartialEq, PartialOrd)] /// changes and (translated) note events into a sorted array first.
pub struct ParameterChange { #[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. /// The parameter's hash, as used everywhere else.
pub hash: u32, hash: u32,
/// The normalized values, as provided by the host. /// The normalized values, as provided by the host.
pub normalized_value: f32, normalized_value: f32,
} },
/// An incoming parameter change sent by the host. This will only be used when sample accurate
// Instances needed for the binary heap, we'll just pray the host doesn't send NaN values /// automation has been enabled, and the parameters are only updated when we process this
impl Eq for ParameterChange {} /// spooled event at the start of a block.
NoteEvent {
#[allow(clippy::derive_ord_xor_partial_ord)] /// The event's sample offset within the buffer. Used for sorting. The timing stored within
impl Ord for ParameterChange { /// the note event needs to have the block start index subtraced from it.
fn cmp(&self, other: &Self) -> std::cmp::Ordering { timing: u32,
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) /// 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<P: Vst3Plugin> WrapperInner<P> { impl<P: Vst3Plugin> WrapperInner<P> {
@ -188,11 +200,9 @@ impl<P: Vst3Plugin> WrapperInner<P> {
param_ids.len(), param_ids.len(),
"The plugin has duplicate parameter IDs, weird things may happen" "The plugin has duplicate parameter IDs, weird things may happen"
); );
}
if cfg!(debug_assertions) {
let mut bypass_param_exists = false; let mut bypass_param_exists = false;
for (_, _, ptr, _) in &param_id_hashes_ptrs_groups { for (id, hash, ptr, _) in &param_id_hashes_ptrs_groups {
let flags = unsafe { ptr.flags() }; let flags = unsafe { ptr.flags() };
let is_bypass = flags.contains(ParamFlags::BYPASS); let is_bypass = flags.contains(ParamFlags::BYPASS);
@ -203,6 +213,14 @@ impl<P: Vst3Plugin> WrapperInner<P> {
} }
bypass_param_exists |= is_bypass; 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<P: Vst3Plugin> WrapperInner<P> {
output_buffer: AtomicRefCell::new(Buffer::default()), output_buffer: AtomicRefCell::new(Buffer::default()),
input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)), input_events: AtomicRefCell::new(VecDeque::with_capacity(1024)),
note_expression_controller: AtomicRefCell::new(NoteExpressionController::default()), note_expression_controller: AtomicRefCell::new(NoteExpressionController::default()),
input_param_changes: AtomicRefCell::new(BinaryHeap::with_capacity( process_events: AtomicRefCell::new(Vec::with_capacity(4096)),
if P::SAMPLE_ACCURATE_AUTOMATION {
4096
} else {
0
},
)),
updated_state_sender, updated_state_sender,
updated_state_receiver, updated_state_receiver,

View file

@ -1,6 +1,20 @@
use std::ops::Deref; use std::ops::Deref;
use vst3_sys::{interfaces::IUnknown, ComInterface}; 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 /// Early exit out of a VST3 function when one of the passed pointers is null
macro_rules! check_null_ptr { macro_rules! check_null_ptr {
($ptr:expr $(, $ptrs:expr)* $(, )?) => { ($ptr:expr $(, $ptrs:expr)* $(, )?) => {

View file

@ -1,4 +1,4 @@
use std::cmp::{self, Reverse}; use std::cmp;
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;
@ -9,13 +9,14 @@ use vst3_sys::base::{IBStream, IPluginBase};
use vst3_sys::utils::SharedVstPtr; use vst3_sys::utils::SharedVstPtr;
use vst3_sys::vst::{ use vst3_sys::vst::{
kNoProgramListId, kRootUnitId, EventTypes, IAudioProcessor, IComponent, IEditController, 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 vst3_sys::VST3;
use widestring::U16CStr; use widestring::U16CStr;
use super::inner::WrapperInner; 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 super::view::WrapperView;
use crate::context::Transport; use crate::context::Transport;
use crate::midi::{MidiConfig, NoteEvent}; use crate::midi::{MidiConfig, NoteEvent};
@ -24,12 +25,13 @@ use crate::plugin::{BufferConfig, BusConfig, ProcessStatus, Vst3Plugin};
use crate::util::permit_alloc; use crate::util::permit_alloc;
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; 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 // Alias needed for the VST3 attribute macro
use vst3_sys as vst3_com; use vst3_sys as vst3_com;
#[VST3(implements(IComponent, IEditController, IAudioProcessor, IUnitInfo))] #[VST3(implements(IComponent, IEditController, IAudioProcessor, IMidiMapping, IUnitInfo))]
pub(crate) struct Wrapper<P: Vst3Plugin> { pub(crate) struct Wrapper<P: Vst3Plugin> {
inner: Arc<WrapperInner<P>>, inner: Arc<WrapperInner<P>>,
} }
@ -302,8 +304,13 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
} }
unsafe fn get_parameter_count(&self) -> i32 { unsafe fn get_parameter_count(&self) -> 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 self.inner.param_hashes.len() as i32
} }
}
unsafe fn get_parameter_info( unsafe fn get_parameter_info(
&self, &self,
@ -316,6 +323,30 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
return kInvalidArgument; return kInvalidArgument;
} }
*info = std::mem::zeroed();
let info = &mut *info;
// 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 {
let param_hash = &self.inner.param_hashes[param_index as usize]; let param_hash = &self.inner.param_hashes[param_index as usize];
let param_unit = &self let param_unit = &self
.inner .inner
@ -328,9 +359,6 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE); let automatable = !flags.contains(ParamFlags::NON_AUTOMATABLE);
let is_bypass = flags.contains(ParamFlags::BYPASS); let is_bypass = flags.contains(ParamFlags::BYPASS);
*info = std::mem::zeroed();
let info = &mut *info;
info.id = *param_hash; info.id = *param_hash;
u16strlcpy(&mut info.title, param_ptr.name()); u16strlcpy(&mut info.title, param_ptr.name());
u16strlcpy(&mut info.short_title, param_ptr.name()); u16strlcpy(&mut info.short_title, param_ptr.name());
@ -339,12 +367,13 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
info.default_normalized_value = default_value as f64; info.default_normalized_value = default_value as f64;
info.unit_id = *param_unit; info.unit_id = *param_unit;
info.flags = if automatable { info.flags = if automatable {
vst3_sys::vst::ParameterFlags::kCanAutomate as i32 ParameterFlags::kCanAutomate as i32
} else { } else {
vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden
}; };
if is_bypass { if is_bypass {
info.flags |= vst3_sys::vst::ParameterFlags::kIsBypass as i32; info.flags |= ParameterFlags::kIsBypass as i32;
}
} }
kResultOk kResultOk
@ -360,6 +389,8 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
let dest = &mut *(string as *mut [TChar; 128]); 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) { match self.inner.param_by_hash.get(&id) {
Some(param_ptr) => { Some(param_ptr) => {
u16strlcpy( u16strlcpy(
@ -403,14 +434,14 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 { unsafe fn normalized_param_to_plain(&self, id: u32, value_normalized: f64) -> f64 {
match self.inner.param_by_hash.get(&id) { match self.inner.param_by_hash.get(&id) {
Some(param_ptr) => param_ptr.preview_plain(value_normalized as f32) as f64, 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 { unsafe fn plain_param_to_normalized(&self, id: u32, plain_value: f64) -> f64 {
match self.inner.param_by_hash.get(&id) { match self.inner.param_by_hash.get(&id) {
Some(param_ptr) => param_ptr.preview_normalized(plain_value as f32) as f64, Some(param_ptr) => param_ptr.preview_normalized(plain_value as f32) as f64,
_ => 0.5, _ => plain_value,
} }
} }
@ -608,6 +639,8 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
kResultOk 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 { unsafe fn process(&self, data: *mut vst3_sys::vst::ProcessData) -> tresult {
check_null_ptr!(data); check_null_ptr!(data);
@ -653,11 +686,17 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
nih_debug_assert!(data.num_samples >= 0); nih_debug_assert!(data.num_samples >= 0);
// If `P::SAMPLE_ACCURATE_AUTOMATION` is set, then we'll split up the audio buffer into // 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 // chunks whenever a parameter change occurs. To do that, we'll store all of those
// handled right here and now. // parameter changes in a vector. Otherwise all parameter changes are handled right here
let mut input_param_changes = self.inner.input_param_changes.borrow_mut(); // 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; 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() { if let Some(param_changes) = data.input_param_changes.upgrade() {
let num_param_queues = param_changes.get_parameter_count(); let num_param_queues = param_changes.get_parameter_count();
for change_queue_idx in 0..num_param_queues { for change_queue_idx in 0..num_param_queues {
@ -672,8 +711,6 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
let mut sample_offset = 0i32; let mut sample_offset = 0i32;
let mut value = 0.0f64; let mut value = 0.0f64;
#[allow(clippy::collapsible_else_if)]
if P::SAMPLE_ACCURATE_AUTOMATION {
for change_idx in 0..num_changes { for change_idx in 0..num_changes {
if param_change_queue.get_point( if param_change_queue.get_point(
change_idx, change_idx,
@ -681,25 +718,54 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
&mut value, &mut value,
) == kResultOk ) == kResultOk
{ {
input_param_changes.push(Reverse(( let timing = sample_offset as u32;
sample_offset as usize, let value = value as f32;
ParameterChange {
hash: param_hash, // MIDI CC messages, channel pressure, and pitch bend are also sent
normalized_value: value as f32, // as parameter changes
}, if P::MIDI_INPUT >= MidiConfig::MidiCCs
))); && (VST3_MIDI_PARAMS_START..VST3_MIDI_PARAMS_END)
} .contains(&param_hash)
}
} else {
if param_change_queue.get_point(
num_changes - 1,
&mut sample_offset,
&mut value,
) == kResultOk
{ {
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( self.inner.set_normalized_value_by_hash(
param_hash, param_hash,
value as f32, value,
Some(sample_rate), Some(sample_rate),
); );
parameter_values_changed = true; parameter_values_changed = true;
@ -708,33 +774,138 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
} }
} }
} }
}
let mut block_start = 0; // Then we'll add all of our input events
let mut block_end = data.num_samples as usize; 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 mut event_start_idx = 0;
let result = loop { let result = loop {
// In sample-accurate automation mode we'll handle any parameter changes for the // In sample-accurate automation mode we'll handle all parameter changes from the
// current sample, and then process the block between the current sample and the // sorted process event array until we run into for the current sample, and then
// sample containing the next parameter change, if any. All timings also need to be // process the block between the current sample and the sample containing the next
// compensated for this. // parameter change, if any. All timings also need to be compensated for this. As
if P::SAMPLE_ACCURATE_AUTOMATION { // mentioend above, for this to work correctly parameter changes need to be ordered
if input_param_changes.is_empty() { // 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();
block_end = data.num_samples as usize; block_end = data.num_samples as usize;
} else { for event_idx in event_start_idx..process_events.len() {
while let Some(Reverse((sample_idx, _))) = input_param_changes.peek() { match process_events[event_idx] {
if *sample_idx != block_start { ProcessEvent::ParameterChange {
block_end = *sample_idx; 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; break;
} }
let Reverse((_, change)) = input_param_changes.pop().unwrap();
self.inner.set_normalized_value_by_hash( self.inner.set_normalized_value_by_hash(
change.hash, hash,
change.normalized_value, normalized_value,
Some(sample_rate), Some(sample_rate),
); );
parameter_values_changed = true; 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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
parameter_values_changed = false; 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 { let result = if is_parameter_flush {
kResultOk kResultOk
} else { } else {
@ -989,6 +1089,33 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
} }
} }
impl<P: Vst3Plugin> IMidiMapping for Wrapper<P> {
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<P: Vst3Plugin> IUnitInfo for Wrapper<P> { impl<P: Vst3Plugin> IUnitInfo for Wrapper<P> {
unsafe fn get_unit_count(&self) -> i32 { unsafe fn get_unit_count(&self) -> i32 {
self.inner.param_units.len() as i32 self.inner.param_units.len() as i32