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
|
- 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
|
||||||
|
|
19
src/midi.rs
19
src/midi.rs
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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)]
|
||||||
/// The parameter's hash, as used everywhere else.
|
pub enum ProcessEvent {
|
||||||
pub hash: u32,
|
/// An incoming parameter change sent by the host. This will only be used when sample accurate
|
||||||
/// The normalized values, as provided by the host.
|
/// automation has been enabled, and the parameters are only updated when we process this
|
||||||
pub normalized_value: f32,
|
/// spooled event at the start of a block.
|
||||||
}
|
ParameterChange {
|
||||||
|
/// The event's sample offset within the buffer. Used for sorting.
|
||||||
// Instances needed for the binary heap, we'll just pray the host doesn't send NaN values
|
timing: u32,
|
||||||
impl Eq for ParameterChange {}
|
/// The parameter's hash, as used everywhere else.
|
||||||
|
hash: u32,
|
||||||
#[allow(clippy::derive_ord_xor_partial_ord)]
|
/// The normalized values, as provided by the host.
|
||||||
impl Ord for ParameterChange {
|
normalized_value: f32,
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
},
|
||||||
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
|
/// 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> {
|
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 ¶m_id_hashes_ptrs_groups {
|
for (id, hash, ptr, _) in ¶m_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,
|
||||||
|
|
||||||
|
|
|
@ -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)* $(, )?) => {
|
||||||
|
|
|
@ -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,7 +304,12 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn get_parameter_count(&self) -> i32 {
|
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(
|
unsafe fn get_parameter_info(
|
||||||
|
@ -316,35 +323,57 @@ impl<P: Vst3Plugin> IEditController for Wrapper<P> {
|
||||||
return kInvalidArgument;
|
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();
|
*info = std::mem::zeroed();
|
||||||
|
|
||||||
let info = &mut *info;
|
let info = &mut *info;
|
||||||
info.id = *param_hash;
|
|
||||||
u16strlcpy(&mut info.title, param_ptr.name());
|
// If the parameter is a generated MIDI CC/channel pressure/pitch bend then it needs to be
|
||||||
u16strlcpy(&mut info.short_title, param_ptr.name());
|
// handled separately
|
||||||
u16strlcpy(&mut info.units, param_ptr.unit());
|
let num_actual_params = self.inner.param_hashes.len() as i32;
|
||||||
info.step_count = param_ptr.step_count().unwrap_or(0) as i32;
|
if P::MIDI_INPUT >= MidiConfig::MidiCCs && param_index >= num_actual_params {
|
||||||
info.default_normalized_value = default_value as f64;
|
let midi_param_relative_idx = (param_index - num_actual_params) as u32;
|
||||||
info.unit_id = *param_unit;
|
// This goes up to 130 for the 128 CCs followed by channel pressure and pitch bend
|
||||||
info.flags = if automatable {
|
let midi_cc = midi_param_relative_idx % VST3_MIDI_CCS;
|
||||||
vst3_sys::vst::ParameterFlags::kCanAutomate as i32
|
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 {
|
} else {
|
||||||
vst3_sys::vst::ParameterFlags::kIsReadOnly as i32 | (1 << 4) // kIsHidden
|
let param_hash = &self.inner.param_hashes[param_index as usize];
|
||||||
};
|
let param_unit = &self
|
||||||
if is_bypass {
|
.inner
|
||||||
info.flags |= vst3_sys::vst::ParameterFlags::kIsBypass as i32;
|
.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
|
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,68 +711,200 @@ 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)]
|
for change_idx in 0..num_changes {
|
||||||
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 {
|
|
||||||
if param_change_queue.get_point(
|
if param_change_queue.get_point(
|
||||||
num_changes - 1,
|
change_idx,
|
||||||
&mut sample_offset,
|
&mut sample_offset,
|
||||||
&mut value,
|
&mut value,
|
||||||
) == kResultOk
|
) == kResultOk
|
||||||
{
|
{
|
||||||
self.inner.set_normalized_value_by_hash(
|
let timing = sample_offset as u32;
|
||||||
param_hash,
|
let value = value as f32;
|
||||||
value as f32,
|
|
||||||
Some(sample_rate),
|
// MIDI CC messages, channel pressure, and pitch bend are also sent
|
||||||
);
|
// as parameter changes
|
||||||
parameter_values_changed = true;
|
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;
|
// 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.
|
||||||
block_end = data.num_samples as usize;
|
// The extra scope is here to make sure we release the borrow on input_events
|
||||||
} else {
|
{
|
||||||
while let Some(Reverse((sample_idx, _))) = input_param_changes.peek() {
|
let mut input_events = self.inner.input_events.borrow_mut();
|
||||||
if *sample_idx != block_start {
|
input_events.clear();
|
||||||
block_end = *sample_idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Reverse((_, change)) = input_param_changes.pop().unwrap();
|
block_end = data.num_samples as usize;
|
||||||
self.inner.set_normalized_value_by_hash(
|
for event_idx in event_start_idx..process_events.len() {
|
||||||
change.hash,
|
match process_events[event_idx] {
|
||||||
change.normalized_value,
|
ProcessEvent::ParameterChange {
|
||||||
Some(sample_rate),
|
timing,
|
||||||
);
|
hash,
|
||||||
parameter_values_changed = true;
|
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;
|
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
|
||||||
|
|
Loading…
Reference in a new issue