From 059c733b784a8e32c50deda8ed9d59c89f5aa9c3 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 8 Apr 2022 17:49:13 +0200 Subject: [PATCH] Handle predefined VST3 note expressions --- src/wrapper/vst3.rs | 1 + src/wrapper/vst3/inner.rs | 7 ++ src/wrapper/vst3/note_expressions.rs | 101 +++++++++++++++++++++++++++ src/wrapper/vst3/wrapper.rs | 31 +++++--- 4 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/wrapper/vst3/note_expressions.rs diff --git a/src/wrapper/vst3.rs b/src/wrapper/vst3.rs index ffa176fd..7bb21207 100644 --- a/src/wrapper/vst3.rs +++ b/src/wrapper/vst3.rs @@ -4,6 +4,7 @@ mod util; mod context; mod factory; mod inner; +mod note_expressions; mod param_units; mod view; mod wrapper; diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index d3bcd25d..32b79bd6 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -12,6 +12,7 @@ use vst3_sys::base::{kInvalidArgument, kResultOk, tresult}; 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::view::WrapperView; @@ -82,6 +83,11 @@ pub(crate) struct WrapperInner { /// interleave parameter changes and note events, this queue has to be sorted when /// creating the process context pub input_events: AtomicRefCell>, + /// 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, /// 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 @@ -248,6 +254,7 @@ impl WrapperInner

{ current_latency: AtomicU32::new(0), 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 diff --git a/src/wrapper/vst3/note_expressions.rs b/src/wrapper/vst3/note_expressions.rs new file mode 100644 index 00000000..b242d7eb --- /dev/null +++ b/src/wrapper/vst3/note_expressions.rs @@ -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 { + 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, + } + } +} diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index f57d2ab0..4694686a 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -8,8 +8,8 @@ use vst3_sys::base::{kInvalidArgument, kNoInterface, kResultFalse, kResultOk, tr use vst3_sys::base::{IBStream, IPluginBase}; use vst3_sys::utils::SharedVstPtr; use vst3_sys::vst::{ - kNoProgramListId, kRootUnitId, IAudioProcessor, IComponent, IEditController, IEventList, - IParamValueQueue, IParameterChanges, IUnitInfo, ProgramListInfo, TChar, UnitInfo, + kNoProgramListId, kRootUnitId, EventTypes, IAudioProcessor, IComponent, IEditController, + IEventList, IParamValueQueue, IParameterChanges, IUnitInfo, ProgramListInfo, TChar, UnitInfo, }; use vst3_sys::VST3; use widestring::U16CStr; @@ -746,6 +746,8 @@ impl IAudioProcessor for Wrapper

{ 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(); @@ -765,16 +767,20 @@ impl IAudioProcessor for Wrapper

{ } 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; + + // 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_ == vst3_sys::vst::EventTypes::kNoteOffEvent as u16 - { + } else if event.type_ == EventTypes::kNoteOffEvent as u16 { let event = event.event.note_off; input_events.push_back(NoteEvent::NoteOff { timing, @@ -782,9 +788,7 @@ impl IAudioProcessor for Wrapper

{ note: event.pitch as u8, velocity: event.velocity, }); - } else if event.type_ - == vst3_sys::vst::EventTypes::kPolyPressureEvent as u16 - { + } else if event.type_ == EventTypes::kPolyPressureEvent as u16 { let event = event.event.poly_pressure; input_events.push_back(NoteEvent::PolyPressure { timing, @@ -792,6 +796,17 @@ impl IAudioProcessor for Wrapper

{ 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