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:
parent
059c733b78
commit
1a8f81e4c0
|
@ -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 <package> <build_arguments>` command that automatically
|
||||
detects which plugin targets your plugin exposes and creates the correct
|
||||
|
|
19
src/midi.rs
19
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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<P: Vst3Plugin> {
|
|||
/// 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<NoteExpressionController>,
|
||||
/// 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)>>>,
|
||||
/// 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<Vec<ProcessEvent>>,
|
||||
/// 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<P: Vst3Plugin> WrapperInner<P> {
|
||||
|
@ -188,11 +200,9 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
|||
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<P: Vst3Plugin> WrapperInner<P> {
|
|||
}
|
||||
|
||||
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()),
|
||||
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,
|
||||
|
||||
|
|
|
@ -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)* $(, )?) => {
|
||||
|
|
|
@ -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<P: Vst3Plugin> {
|
||||
inner: Arc<WrapperInner<P>>,
|
||||
}
|
||||
|
@ -302,7 +304,12 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
}
|
||||
|
||||
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<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
|
||||
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<P: Vst3Plugin> IEditController for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
|
||||
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<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
|||
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<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> {
|
||||
unsafe fn get_unit_count(&self) -> i32 {
|
||||
self.inner.param_units.len() as i32
|
||||
|
|
Loading…
Reference in a new issue