Handle predefined VST3 note expressions
This commit is contained in:
parent
25dd0d9bef
commit
059c733b78
4 changed files with 132 additions and 8 deletions
|
@ -4,6 +4,7 @@ mod util;
|
||||||
mod context;
|
mod context;
|
||||||
mod factory;
|
mod factory;
|
||||||
mod inner;
|
mod inner;
|
||||||
|
mod note_expressions;
|
||||||
mod param_units;
|
mod param_units;
|
||||||
mod view;
|
mod view;
|
||||||
mod wrapper;
|
mod wrapper;
|
||||||
|
|
|
@ -12,6 +12,7 @@ use vst3_sys::base::{kInvalidArgument, kResultOk, tresult};
|
||||||
use vst3_sys::vst::{IComponentHandler, RestartFlags};
|
use vst3_sys::vst::{IComponentHandler, RestartFlags};
|
||||||
|
|
||||||
use super::context::{WrapperGuiContext, WrapperProcessContext};
|
use super::context::{WrapperGuiContext, WrapperProcessContext};
|
||||||
|
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};
|
||||||
use super::view::WrapperView;
|
use super::view::WrapperView;
|
||||||
|
@ -82,6 +83,11 @@ pub(crate) struct WrapperInner<P: Vst3Plugin> {
|
||||||
/// interleave parameter changes and note events, this queue has to be sorted when
|
/// interleave parameter changes and note events, this queue has to be sorted when
|
||||||
/// creating the process context
|
/// creating the process context
|
||||||
pub input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
pub input_events: AtomicRefCell<VecDeque<NoteEvent>>,
|
||||||
|
/// VST3 has several useful predefined note expressions, but for some reason they are the only
|
||||||
|
/// note event type that don't have MIDI note ID and channel fields. So we need to keep track of
|
||||||
|
/// 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)`.
|
/// 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
|
/// 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
|
/// `P::SAMPLE_ACCURATE_AUTOMATION` is set, then all parameter changes will be read into this
|
||||||
|
@ -248,6 +254,7 @@ impl<P: Vst3Plugin> WrapperInner<P> {
|
||||||
current_latency: AtomicU32::new(0),
|
current_latency: AtomicU32::new(0),
|
||||||
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()),
|
||||||
input_param_changes: AtomicRefCell::new(BinaryHeap::with_capacity(
|
input_param_changes: AtomicRefCell::new(BinaryHeap::with_capacity(
|
||||||
if P::SAMPLE_ACCURATE_AUTOMATION {
|
if P::SAMPLE_ACCURATE_AUTOMATION {
|
||||||
4096
|
4096
|
||||||
|
|
101
src/wrapper/vst3/note_expressions.rs
Normal file
101
src/wrapper/vst3/note_expressions.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
//! Special handling for note expressions, because VST3 makes this a lot more complicated than it
|
||||||
|
//! needs to be. We only support the predefined expressions.
|
||||||
|
|
||||||
|
use vst3_sys::vst::{NoteExpressionValueEvent, NoteOnEvent};
|
||||||
|
|
||||||
|
use crate::midi::NoteEvent;
|
||||||
|
|
||||||
|
type MidiNote = u8;
|
||||||
|
type MidiChannel = u8;
|
||||||
|
type NoteId = i32;
|
||||||
|
|
||||||
|
/// The number of notes we'll keep track of for mapping note IDs to channel+note combinations.
|
||||||
|
const NOTE_IDS_LEN: usize = 32;
|
||||||
|
|
||||||
|
/// VST3 has predefined note expressions just like CLAP, but unlike the other note events these
|
||||||
|
/// expressions are identified only with a note ID. To account for that, we'll keep track of the
|
||||||
|
/// most recent note IDs we've encountered so we can later map those IDs back to a note and channel
|
||||||
|
/// combination.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct NoteExpressionController {
|
||||||
|
/// The last 32 note IDs we've seen. We'll do a linear search every time we receive a note
|
||||||
|
/// expression value event to find the matching note and channel.
|
||||||
|
note_ids: [(NoteId, MidiNote, MidiChannel); NOTE_IDS_LEN],
|
||||||
|
/// The index in the `note_ids` ring buffer the next event should be inserted at, wraps back
|
||||||
|
/// around to 0 when reaching the end.
|
||||||
|
note_ids_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteExpressionController {
|
||||||
|
/// Register the note ID from a note on event so it can later be retrieved when handling a note
|
||||||
|
/// expression value event.
|
||||||
|
pub fn register_note(&mut self, event: &NoteOnEvent) {
|
||||||
|
self.note_ids[self.note_ids_idx] = (event.note_id, event.pitch as u8, event.channel as u8);
|
||||||
|
self.note_ids_idx = (self.note_ids_idx + 1) % NOTE_IDS_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate the note expression value event into an internal NIH-plug event, if we handle the
|
||||||
|
/// expression type from the note expression value event. The timing is provided here because we
|
||||||
|
/// may be splitting buffers on inter-buffer parameter changes.
|
||||||
|
pub fn translate_event(
|
||||||
|
&self,
|
||||||
|
timing: u32,
|
||||||
|
event: &NoteExpressionValueEvent,
|
||||||
|
) -> Option<NoteEvent> {
|
||||||
|
let (_, note, channel) = *self
|
||||||
|
.note_ids
|
||||||
|
.iter()
|
||||||
|
.find(|(note_id, _, _)| *note_id == event.note_id)?;
|
||||||
|
|
||||||
|
match event.type_id {
|
||||||
|
// kVolumeTypeID
|
||||||
|
0 => Some(NoteEvent::Volume {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
// Because expression values in VST3 are always in the `[0, 1]` range, they added a
|
||||||
|
// 4x scaling factor here to allow the values to go from -infinity to +12 dB
|
||||||
|
gain: event.value as f32 * 4.0,
|
||||||
|
}),
|
||||||
|
// kPanTypeId
|
||||||
|
1 => Some(NoteEvent::Pan {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
// Our panning expressions are symmetrical around 0
|
||||||
|
pan: (event.value as f32 * 2.0) - 1.0,
|
||||||
|
}),
|
||||||
|
// kTuningTypeID
|
||||||
|
2 => Some(NoteEvent::Tuning {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
// This denormalized to the same [-120, 120] range used by CLAP and our expression
|
||||||
|
// events
|
||||||
|
tuning: 240.0 * (event.value as f32 - 0.5),
|
||||||
|
}),
|
||||||
|
// kVibratoTypeID
|
||||||
|
3 => Some(NoteEvent::Vibrato {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
vibrato: event.value as f32,
|
||||||
|
}),
|
||||||
|
// kExpressionTypeID
|
||||||
|
4 => Some(NoteEvent::Brightness {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
brightness: event.value as f32,
|
||||||
|
}),
|
||||||
|
// kBrightnessTypeID
|
||||||
|
5 => Some(NoteEvent::Expression {
|
||||||
|
timing,
|
||||||
|
channel,
|
||||||
|
note,
|
||||||
|
expression: event.value as f32,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tr
|
||||||
use vst3_sys::base::{IBStream, IPluginBase};
|
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, IAudioProcessor, IComponent, IEditController, IEventList,
|
kNoProgramListId, kRootUnitId, EventTypes, IAudioProcessor, IComponent, IEditController,
|
||||||
IParamValueQueue, IParameterChanges, IUnitInfo, ProgramListInfo, TChar, UnitInfo,
|
IEventList, IParamValueQueue, IParameterChanges, IUnitInfo, ProgramListInfo, TChar, UnitInfo,
|
||||||
};
|
};
|
||||||
use vst3_sys::VST3;
|
use vst3_sys::VST3;
|
||||||
use widestring::U16CStr;
|
use widestring::U16CStr;
|
||||||
|
@ -746,6 +746,8 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
||||||
|
|
||||||
if P::MIDI_INPUT >= MidiConfig::Basic {
|
if P::MIDI_INPUT >= MidiConfig::Basic {
|
||||||
let mut input_events = self.inner.input_events.borrow_mut();
|
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() {
|
if let Some(events) = data.input_events.upgrade() {
|
||||||
let num_events = events.get_event_count();
|
let num_events = events.get_event_count();
|
||||||
|
|
||||||
|
@ -765,16 +767,20 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let timing = event.sample_offset as u32 - block_start as u32;
|
let timing = event.sample_offset as u32 - block_start as u32;
|
||||||
if event.type_ == vst3_sys::vst::EventTypes::kNoteOnEvent as u16 {
|
if event.type_ == EventTypes::kNoteOnEvent as u16 {
|
||||||
let event = event.event.note_on;
|
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 {
|
input_events.push_back(NoteEvent::NoteOn {
|
||||||
timing,
|
timing,
|
||||||
channel: event.channel as u8,
|
channel: event.channel as u8,
|
||||||
note: event.pitch as u8,
|
note: event.pitch as u8,
|
||||||
velocity: event.velocity,
|
velocity: event.velocity,
|
||||||
});
|
});
|
||||||
} else if event.type_ == vst3_sys::vst::EventTypes::kNoteOffEvent as u16
|
} else if event.type_ == EventTypes::kNoteOffEvent as u16 {
|
||||||
{
|
|
||||||
let event = event.event.note_off;
|
let event = event.event.note_off;
|
||||||
input_events.push_back(NoteEvent::NoteOff {
|
input_events.push_back(NoteEvent::NoteOff {
|
||||||
timing,
|
timing,
|
||||||
|
@ -782,9 +788,7 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
||||||
note: event.pitch as u8,
|
note: event.pitch as u8,
|
||||||
velocity: event.velocity,
|
velocity: event.velocity,
|
||||||
});
|
});
|
||||||
} else if event.type_
|
} else if event.type_ == EventTypes::kPolyPressureEvent as u16 {
|
||||||
== vst3_sys::vst::EventTypes::kPolyPressureEvent as u16
|
|
||||||
{
|
|
||||||
let event = event.event.poly_pressure;
|
let event = event.event.poly_pressure;
|
||||||
input_events.push_back(NoteEvent::PolyPressure {
|
input_events.push_back(NoteEvent::PolyPressure {
|
||||||
timing,
|
timing,
|
||||||
|
@ -792,6 +796,17 @@ impl<P: Vst3Plugin> IAudioProcessor for Wrapper<P> {
|
||||||
note: event.pitch as u8,
|
note: event.pitch as u8,
|
||||||
pressure: event.pressure,
|
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
|
// TODO: Add note event controllers to support the same expression types
|
||||||
|
|
Loading…
Add table
Reference in a new issue