Skip to content

Commit

Permalink
Support note expression + Ableton-style MPE (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
russellmcc authored Oct 17, 2024
1 parent ce626bb commit ba9df65
Show file tree
Hide file tree
Showing 16 changed files with 2,081 additions and 325 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 79 additions & 10 deletions rust/component/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod tests;
enum NoteIDInternals {
NoteIDWithID(i32),
NoteIDFromPitch(u8),
NoteIDFromChannelID(i16),
}

/// Represents an identifier for a note
Expand Down Expand Up @@ -45,23 +46,37 @@ impl NoteID {
internals: NoteIDInternals::NoteIDFromPitch(pitch),
}
}

#[doc(hidden)]
#[must_use]
pub const fn from_channel_for_mpe_quirks(id: i16) -> Self {
Self {
internals: NoteIDInternals::NoteIDFromChannelID(id),
}
}
}

#[doc(hidden)]
#[must_use]
pub fn to_vst_note_id(note_id: NoteID) -> i32 {
match note_id.internals {
NoteIDInternals::NoteIDWithID(id) => id,
NoteIDInternals::NoteIDFromPitch(_) => -1,
NoteIDInternals::NoteIDFromPitch(_) | NoteIDInternals::NoteIDFromChannelID(_) => -1,
}
}

#[doc(hidden)]
#[must_use]
pub fn to_vst_note_channel_for_mpe_quirks(note_id: NoteID) -> i16 {
match note_id.internals {
NoteIDInternals::NoteIDFromChannelID(id) => id,
NoteIDInternals::NoteIDFromPitch(_) | NoteIDInternals::NoteIDWithID(_) => 0,
}
}

/// Contains data common to both `NoteOn` and `NoteOff` events.
#[derive(Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct NoteData {
/// The channel of the note. IDs are only unique within a channel
pub channel: u8,

/// Opaque ID of the note.
pub id: NoteID,

Expand All @@ -75,6 +90,52 @@ pub struct NoteData {
pub tuning: f32,
}

/// A specific type of note expression.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum NoteExpression {
/// Pitch bend note expression.
///
/// This corresponds to the [`crate::synth::PITCH_BEND_PARAMETER`] controller and should
/// change the tuning of the note.
///
/// This is expressed in semitones away from the root note of the note (which may itself
/// be affected by the global [`crate::synth::PITCH_BEND_PARAMETER`] controller).
PitchBend(f32),

/// Vertical movement note expression, meant to control some sort of timbre of the synth.
///
/// This is called "slide" in some DAW UIs.
///
/// This corresponds to the "timbre" controller ([`crate::synth::TIMBRE_PARAMETER`]), and
/// its effects must be combined with the global controller.
///
/// This value varies from 0->1, 0 being the bottommost position,
/// and 1 being the topmost position.
Timbre(f32),

/// Depthwise note expression.
///
/// This is called "Pressure" in some DAW UIs.
///
/// This value varies from 0->1, 0 being neutral, and 1 being the maximum depth.
///
/// This corresponds to the [`crate::synth::AFTERTOUCH_PARAMETER`] controller which
/// affects all notes. The total effect must be a combination of this per-note note
/// expression and the global controller.
Aftertouch(f32),
}

/// Contains data about note expression.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct NoteExpressionData {
/// Opaque ID of the note. This will always refer to a note that is
/// currently "on".
pub id: NoteID,

/// The expression that is being sent.
pub expression: NoteExpression,
}

/// The data associated with an event, independent of the time it occurred.
#[derive(Clone, Debug, PartialEq)]
pub enum Data {
Expand All @@ -93,6 +154,14 @@ pub enum Data {
/// Data associated with the note.
data: NoteData,
},

/// A note expression was sent.
///
/// This will never be sent while a note with the same ID is not playing.
NoteExpression {
/// Data associated with the note expression.
data: NoteExpressionData,
},
}

/// An event that occurred at a specific time within a buffer.
Expand Down Expand Up @@ -131,24 +200,24 @@ fn check_events_invariants<I: Iterator<Item = Event>>(iter: I, buffer_size: usiz
true
}

impl<I: IntoIterator<Item = Event> + Clone> Events<I> {
impl<I: Iterator<Item = Event> + Clone> Events<I> {
/// Create an `Events` object from the given iterator of events.
///
/// Note that if any of the invariants are missed, this will return `None`.
pub fn new(events: I, buffer_size: usize) -> Option<Self> {
if check_events_invariants(events.clone().into_iter(), buffer_size) {
if check_events_invariants(events.clone(), buffer_size) {
Some(Self { events })
} else {
None
}
}
}

impl<I: IntoIterator<Item = Event>> IntoIterator for Events<I> {
impl<I: Iterator<Item = Event>> IntoIterator for Events<I> {
type Item = Event;
type IntoIter = I::IntoIter;
type IntoIter = I;

fn into_iter(self) -> Self::IntoIter {
self.events.into_iter()
self.events
}
}
1 change: 0 additions & 1 deletion rust/component/src/events/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use super::{Data, Event, Events, NoteData, NoteID};

static EXAMPLE_NOTE: NoteData = NoteData {
channel: 0,
id: NoteID::from_pitch(60),
pitch: 60,
velocity: 1.0,
Expand Down
15 changes: 14 additions & 1 deletion rust/component/src/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ impl Default for Flags {
}
}

/// Reserved unique id prefix for internal parameters. No component
/// should have any parameters with unique ids that start with this prefix.
pub const UNIQUE_ID_INTERNAL_PREFIX: &str = "_conformal_internal_";

macro_rules! unique_id_doc {
() => {
"The unique ID of the parameter.
Expand All @@ -285,7 +289,10 @@ As the name implies, each parameter's id must be unique within
the comonent's parameters.
Note that this ID will not be presented to the user, it is only
used to refer to the parameter in code."
used to refer to the parameter in code.
The ID must not begin with the prefix `_conformal_internal`, as
this is reserved for use by the Conformal library itself."
};
}

Expand Down Expand Up @@ -661,6 +668,7 @@ pub struct PiecewiseLinearCurvePoint {
/// - `sample_offset`s will be monotonically increasing and only one
/// point will appear for each `sample_offset`
/// - All point's `value` will be between the parameter's `min` and `max`
#[derive(Clone)]
pub struct PiecewiseLinearCurve<I> {
points: I,

Expand Down Expand Up @@ -830,6 +838,7 @@ impl<V> ValueAndSampleOffset<V> for TimedValue<V> {
/// - `sample_offset`s will be monotonically increasing and only one
/// point will appear for each `sample_offset`
/// - All point's `value` will be valid
#[derive(Clone)]
pub struct TimedEnumValues<I> {
points: I,
buffer_size: usize,
Expand Down Expand Up @@ -896,6 +905,7 @@ impl<I: IntoIterator<Item = TimedValue<u32>>> IntoIterator for TimedEnumValues<I
/// - The first point's `sample_offset` will be 0
/// - `sample_offset`s will be monotonically increasing and only one
/// point will appear for each `sample_offset`
#[derive(Clone)]
pub struct TimedSwitchValues<I> {
points: I,
buffer_size: usize,
Expand Down Expand Up @@ -949,6 +959,7 @@ impl<I: IntoIterator<Item = TimedValue<bool>>> IntoIterator for TimedSwitchValue
}

/// Represents the state of a numeric value across a buffer
#[derive(Clone)]
pub enum NumericBufferState<I> {
/// The value is constant across the buffer.
Constant(f32),
Expand Down Expand Up @@ -985,6 +996,7 @@ impl<I: IntoIterator<Item = PiecewiseLinearCurvePoint>> NumericBufferState<I> {
///
/// Here we refer to the enum by the _index_ of the value,
/// that is, the index of the value in the `values` array of the parameter.
#[derive(Clone)]
pub enum EnumBufferState<I> {
/// The value is constant across the buffer.
Constant(u32),
Expand Down Expand Up @@ -1019,6 +1031,7 @@ impl<I: IntoIterator<Item = TimedValue<u32>>> EnumBufferState<I> {
}

/// Represents the state of an switched value across a buffer
#[derive(Clone)]
pub enum SwitchBufferState<I> {
/// The value is constant across the buffer.
Constant(bool),
Expand Down
44 changes: 41 additions & 3 deletions rust/component/src/synth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use crate::{
};

/// The parameter ID of the pitch bend parameter. See [`CONTROLLER_PARAMETERS`] for more.
///
/// This is the global version of the [`crate::events::NoteExpression::PitchBend`] note expression event.
/// Notes should be shifted by the value of this controller plus the per-note pitch bend expression.
pub const PITCH_BEND_PARAMETER: &str = "pitch_bend";

/// The parameter ID of the mod wheel parameter. See [`CONTROLLER_PARAMETERS`] for more.
Expand All @@ -20,8 +23,29 @@ pub const EXPRESSION_PARAMETER: &str = "expression_pedal";
pub const SUSTAIN_PARAMETER: &str = "sustain_pedal";

/// The parameter ID of the aftertouch parameter. See [`CONTROLLER_PARAMETERS`] for more.
///
/// Aftertouch is a pressure sensor sent by some controllers.
///
/// This is the global version of the [`crate::events::NoteExpression::Aftertouch`] note expression event.
/// This controller parameter should affect all notes,
/// while the note expression event affects a single note. Note that hosts are free
/// to use a combination of this global controller with per-note controllers. This means
/// plug-ins must combine this global controller with the per-note controller to get the total
/// expression value.
pub const AFTERTOUCH_PARAMETER: &str = "aftertouch";

/// The parameter ID of the timbre parameter. See [`CONTROLLER_PARAMETERS`] for more.
///
/// Generally the timbre controller will be some sort of vertical motion, and
/// is the global version of the [`crate::events::NoteExpression::Timbre`] note expression event.
///
/// This controller parameter should affect all notes,
/// while the note expression event affects a single note. Note that hosts are free
/// to use a combination of this global controller with per-note controllers. This means
/// plug-ins must combine this global controller with the per-note controller to get the total
/// expression value.
pub const TIMBRE_PARAMETER: &str = "timbre";

/// Parameter info for the pitch bend parameter. See [`CONTROLLER_PARAMETERS`] for more.
pub const PITCH_BEND_INFO: InfoRef<'static, &'static str> = InfoRef {
title: "Pitch Bend",
Expand Down Expand Up @@ -83,6 +107,19 @@ pub const AFTERTOUCH_INFO: InfoRef<'static, &'static str> = InfoRef {
},
};

/// Parameter info for the timbre parameter. See [`CONTROLLER_PARAMETERS`] for more.
pub const TIMBRE_INFO: InfoRef<'static, &'static str> = InfoRef {
title: "Timbre",
short_title: "Timbre",
unique_id: TIMBRE_PARAMETER,
flags: Flags { automatable: false },
type_specific: TypeSpecificInfoRef::Numeric {
default: 0.0,
valid_range: 0.0..=1.0,
units: None,
},
};

/// This represents a set of "controller parameters" that are common to
/// all synths.
///
Expand All @@ -92,12 +129,13 @@ pub const AFTERTOUCH_INFO: InfoRef<'static, &'static str> = InfoRef {
///
/// Note that synths will receive these regardless of what they returned
/// from `crate::Component::parameter_infos`.
pub const CONTROLLER_PARAMETERS: [InfoRef<'static, &'static str>; 5] = [
pub const CONTROLLER_PARAMETERS: [InfoRef<'static, &'static str>; 6] = [
PITCH_BEND_INFO,
MOD_WHEEL_INFO,
EXPRESSION_INFO,
SUSTAIN_INFO,
AFTERTOUCH_INFO,
TIMBRE_INFO,
];

/// A trait for synthesizers
Expand All @@ -111,7 +149,7 @@ pub trait Synth: Processor {
/// Note that `parameters` will include [`CONTROLLER_PARAMETERS`] related to controller state
/// (e.g. pitch bend, mod wheel, etc.) above, in addition to all the parameters
/// returned by `crate::Component::parameter_infos`.
fn handle_events<E: IntoIterator<Item = events::Data> + Clone, P: parameters::States>(
fn handle_events<E: Iterator<Item = events::Data> + Clone, P: parameters::States>(
&mut self,
events: E,
parameters: P,
Expand All @@ -138,7 +176,7 @@ pub trait Synth: Processor {
/// Note that it's guaranteed that `output` will be no longer than
/// `environment.max_samples_per_process_call` provided in the call to
/// `crate::Component::create_processor`.
fn process<E: IntoIterator<Item = Event> + Clone, P: BufferStates, O: BufferMut>(
fn process<E: Iterator<Item = Event> + Clone, P: BufferStates, O: BufferMut>(
&mut self,
events: Events<E>,
parameters: P,
Expand Down
3 changes: 2 additions & 1 deletion rust/vst-wrapper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ conformal_component = { version = "0.0.0", path = "../component" }
conformal_ui = { version = "0.0.0", path = "../ui" }
conformal_core = { version = "0.0.0", path = "../core" }
rmp-serde = "1.1.2"
itertools = "0.13.0"

[dev-dependencies]
assert_approx_eq = "1.1.0"
assert_approx_eq = "1.1.0"
Loading

0 comments on commit ba9df65

Please sign in to comment.