use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MidiChannel {
Ch1,
Ch2,
Ch3,
Ch4,
Ch5,
Ch6,
Ch7,
Ch8,
Ch9,
Ch10,
Ch11,
Ch12,
Ch13,
Ch14,
Ch15,
Ch16,
}
impl MidiChannel {
pub fn as_index(&self) -> u8 {
match self {
MidiChannel::Ch1 => 0,
MidiChannel::Ch2 => 1,
MidiChannel::Ch3 => 2,
MidiChannel::Ch4 => 3,
MidiChannel::Ch5 => 4,
MidiChannel::Ch6 => 5,
MidiChannel::Ch7 => 6,
MidiChannel::Ch8 => 7,
MidiChannel::Ch9 => 8,
MidiChannel::Ch10 => 9,
MidiChannel::Ch11 => 10,
MidiChannel::Ch12 => 11,
MidiChannel::Ch13 => 12,
MidiChannel::Ch14 => 13,
MidiChannel::Ch15 => 14,
MidiChannel::Ch16 => 15,
}
}
pub fn from_index(index: u8) -> Option<Self> {
match index {
0 => Some(MidiChannel::Ch1),
1 => Some(MidiChannel::Ch2),
2 => Some(MidiChannel::Ch3),
3 => Some(MidiChannel::Ch4),
4 => Some(MidiChannel::Ch5),
5 => Some(MidiChannel::Ch6),
6 => Some(MidiChannel::Ch7),
7 => Some(MidiChannel::Ch8),
8 => Some(MidiChannel::Ch9),
9 => Some(MidiChannel::Ch10),
10 => Some(MidiChannel::Ch11),
11 => Some(MidiChannel::Ch12),
12 => Some(MidiChannel::Ch13),
13 => Some(MidiChannel::Ch14),
14 => Some(MidiChannel::Ch15),
15 => Some(MidiChannel::Ch16),
_ => None,
}
}
}
impl fmt::Display for MidiChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Ch{}", self.as_index() + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MidiEvent {
NoteOn {
channel: MidiChannel,
note: u8,
velocity: u8,
},
NoteOff {
channel: MidiChannel,
note: u8,
velocity: u8,
},
ControlChange {
channel: MidiChannel,
controller: u8,
value: u8,
},
ProgramChange {
channel: MidiChannel,
program: u8,
},
PitchBend {
channel: MidiChannel,
value: u16,
},
ChannelAftertouch {
channel: MidiChannel,
pressure: u8,
},
PolyAftertouch {
channel: MidiChannel,
note: u8,
pressure: u8,
},
}
pub const MAX_EVENT_PAYLOAD_BYTES: usize = 1024 * 1024;
pub const MAX_EVENT_TEXT_UNITS: usize = 16 * 1024;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PluginEvent {
pub bus_index: i32,
pub sample_offset: i32,
pub ppq_position: f64,
pub flags: u16,
pub data: PluginEventData,
}
pub type OutputEvent = PluginEvent;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum PluginEventData {
NoteOn {
channel: i16,
pitch: i16,
tuning: f32,
velocity: f32,
length: i32,
note_id: i32,
},
NoteOff {
channel: i16,
pitch: i16,
velocity: f32,
note_id: i32,
tuning: f32,
},
Data { data_type: u32, bytes: Vec<u8> },
PolyPressure {
channel: i16,
pitch: i16,
pressure: f32,
note_id: i32,
},
NoteExpressionValue {
type_id: u32,
note_id: i32,
value: f64,
},
NoteExpressionText {
type_id: u32,
note_id: i32,
text: Vec<u16>,
},
NoteExpressionIntValue {
type_id: u32,
note_id: i32,
value: u64,
},
Chord {
root: i16,
bass_note: i16,
mask: i16,
text: Vec<u16>,
},
Scale {
root: i16,
mask: i16,
text: Vec<u16>,
},
LegacyMidiCcOut {
control_number: u8,
channel: i8,
value: u8,
value2: u8,
},
}
impl PluginEvent {
pub fn sysex(bytes: Vec<u8>) -> Self {
Self {
bus_index: 0,
sample_offset: 0,
ppq_position: 0.0,
flags: 0,
data: PluginEventData::Data {
data_type: 0,
bytes,
},
}
}
pub fn at(mut self, sample_offset: i32) -> Self {
self.sample_offset = sample_offset;
self
}
pub fn to_midi(&self) -> Option<MidiEvent> {
let byte = |value: f32| (value * 127.0).round().clamp(0.0, 127.0) as u8;
match &self.data {
PluginEventData::NoteOn {
channel,
pitch,
velocity,
..
} => Some(MidiEvent::NoteOn {
channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
velocity: byte(*velocity),
}),
PluginEventData::NoteOff {
channel,
pitch,
velocity,
..
} => Some(MidiEvent::NoteOff {
channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
velocity: byte(*velocity),
}),
PluginEventData::PolyPressure {
channel,
pitch,
pressure,
..
} => Some(MidiEvent::PolyAftertouch {
channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
pressure: byte(*pressure),
}),
PluginEventData::LegacyMidiCcOut {
control_number,
channel,
value,
value2,
} => {
let channel = MidiChannel::from_index(u8::try_from(*channel).ok()?)?;
match u32::from(*control_number) {
129 => Some(MidiEvent::PitchBend {
channel,
value: (u16::from(*value2 & 0x7f) << 7) | u16::from(*value & 0x7f),
}),
128 => Some(MidiEvent::ChannelAftertouch {
channel,
pressure: *value & 0x7f,
}),
130 => Some(MidiEvent::ProgramChange {
channel,
program: *value & 0x7f,
}),
cc if cc < 128 => Some(MidiEvent::ControlChange {
channel,
controller: cc as u8,
value: *value & 0x7f,
}),
_ => None,
}
}
_ => None,
}
}
pub(crate) fn payload_bytes(&self) -> usize {
match &self.data {
PluginEventData::Data { bytes, .. } => bytes.len(),
PluginEventData::NoteExpressionText { text, .. }
| PluginEventData::Chord { text, .. }
| PluginEventData::Scale { text, .. } => text.len().saturating_mul(2),
_ => 0,
}
}
}
impl From<MidiEvent> for PluginEvent {
fn from(event: MidiEvent) -> Self {
let data = match event {
MidiEvent::NoteOn {
channel,
note,
velocity,
} => PluginEventData::NoteOn {
channel: i16::from(channel.as_index()),
pitch: i16::from(note),
tuning: 0.0,
velocity: f32::from(velocity) / 127.0,
length: 0,
note_id: -1,
},
MidiEvent::NoteOff {
channel,
note,
velocity,
} => PluginEventData::NoteOff {
channel: i16::from(channel.as_index()),
pitch: i16::from(note),
velocity: f32::from(velocity) / 127.0,
note_id: -1,
tuning: 0.0,
},
MidiEvent::ControlChange {
channel,
controller,
value,
} => PluginEventData::LegacyMidiCcOut {
control_number: controller,
channel: channel.as_index() as i8,
value,
value2: 0,
},
MidiEvent::ProgramChange { channel, program } => PluginEventData::LegacyMidiCcOut {
control_number: 130,
channel: channel.as_index() as i8,
value: program,
value2: 0,
},
MidiEvent::PitchBend { channel, value } => PluginEventData::LegacyMidiCcOut {
control_number: 129,
channel: channel.as_index() as i8,
value: (value & 0x7f) as u8,
value2: ((value >> 7) & 0x7f) as u8,
},
MidiEvent::ChannelAftertouch { channel, pressure } => {
PluginEventData::LegacyMidiCcOut {
control_number: 128,
channel: channel.as_index() as i8,
value: pressure,
value2: 0,
}
}
MidiEvent::PolyAftertouch {
channel,
note,
pressure,
} => PluginEventData::PolyPressure {
channel: i16::from(channel.as_index()),
pitch: i16::from(note),
pressure: f32::from(pressure) / 127.0,
note_id: -1,
},
};
Self {
bus_index: 0,
sample_offset: 0,
ppq_position: 0.0,
flags: 0,
data,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NoteId(pub(crate) i32);
impl NoteId {
pub fn raw(self) -> i32 {
self.0
}
pub fn from_raw(raw: i32) -> Self {
NoteId(raw)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NoteExpressionType {
Volume,
Pan,
Tuning,
Vibrato,
Expression,
Brightness,
Custom(u32),
}
impl NoteExpressionType {
pub(crate) fn type_id(self) -> u32 {
match self {
NoteExpressionType::Volume => 0,
NoteExpressionType::Pan => 1,
NoteExpressionType::Tuning => 2,
NoteExpressionType::Vibrato => 3,
NoteExpressionType::Expression => 4,
NoteExpressionType::Brightness => 5,
NoteExpressionType::Custom(id) => id,
}
}
pub(crate) fn from_type_id(id: u32) -> Self {
match id {
0 => NoteExpressionType::Volume,
1 => NoteExpressionType::Pan,
2 => NoteExpressionType::Tuning,
3 => NoteExpressionType::Vibrato,
4 => NoteExpressionType::Expression,
5 => NoteExpressionType::Brightness,
other => NoteExpressionType::Custom(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NoteExpressionInfo {
pub kind: NoteExpressionType,
pub title: String,
pub short_title: String,
pub units: String,
pub default_value: f64,
pub min: f64,
pub max: f64,
pub step_count: i32,
pub is_bipolar: bool,
pub is_one_shot: bool,
pub is_absolute: bool,
}
impl MidiEvent {
pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
let status = *bytes.first()?;
if !(0x80..0xF0).contains(&status) {
return None;
}
let channel = MidiChannel::from_index(status & 0x0F)?;
let d1 = || bytes.get(1).map(|b| b & 0x7F);
let d2 = || bytes.get(2).map(|b| b & 0x7F);
match status & 0xF0 {
0x90 => {
let note = d1()?;
let velocity = d2()?;
Some(if velocity == 0 {
MidiEvent::NoteOff {
channel,
note,
velocity: 0,
}
} else {
MidiEvent::NoteOn {
channel,
note,
velocity,
}
})
}
0x80 => Some(MidiEvent::NoteOff {
channel,
note: d1()?,
velocity: d2()?,
}),
0xB0 => Some(MidiEvent::ControlChange {
channel,
controller: d1()?,
value: d2()?,
}),
0xA0 => Some(MidiEvent::PolyAftertouch {
channel,
note: d1()?,
pressure: d2()?,
}),
0xD0 => Some(MidiEvent::ChannelAftertouch {
channel,
pressure: d1()?,
}),
0xE0 => {
let value = (d2()? as u16) << 7 | d1()? as u16;
Some(MidiEvent::PitchBend { channel, value })
}
0xC0 => Some(MidiEvent::ProgramChange {
channel,
program: d1()?,
}),
_ => None,
}
}
}
pub mod cc {
pub const BANK_SELECT_MSB: u8 = 0;
pub const MODULATION: u8 = 1;
pub const BREATH: u8 = 2;
pub const FOOT: u8 = 4;
pub const PORTAMENTO_TIME: u8 = 5;
pub const DATA_ENTRY_MSB: u8 = 6;
pub const VOLUME: u8 = 7;
pub const BALANCE: u8 = 8;
pub const PAN: u8 = 10;
pub const EXPRESSION: u8 = 11;
pub const SUSTAIN: u8 = 64;
pub const PORTAMENTO: u8 = 65;
pub const SOSTENUTO: u8 = 66;
pub const SOFT_PEDAL: u8 = 67;
pub const LEGATO: u8 = 68;
pub const HOLD_2: u8 = 69;
pub const SOUND_CONTROLLER_1: u8 = 70;
pub const SOUND_CONTROLLER_2: u8 = 71;
pub const SOUND_CONTROLLER_3: u8 = 72;
pub const SOUND_CONTROLLER_4: u8 = 73;
pub const SOUND_CONTROLLER_5: u8 = 74;
pub const SOUND_CONTROLLER_6: u8 = 75;
pub const SOUND_CONTROLLER_7: u8 = 76;
pub const SOUND_CONTROLLER_8: u8 = 77;
pub const SOUND_CONTROLLER_9: u8 = 78;
pub const SOUND_CONTROLLER_10: u8 = 79;
pub const GENERAL_PURPOSE_1: u8 = 80;
pub const GENERAL_PURPOSE_2: u8 = 81;
pub const GENERAL_PURPOSE_3: u8 = 82;
pub const GENERAL_PURPOSE_4: u8 = 83;
pub const PORTAMENTO_CONTROL: u8 = 84;
pub const REVERB_DEPTH: u8 = 91;
pub const TREMOLO_DEPTH: u8 = 92;
pub const CHORUS_DEPTH: u8 = 93;
pub const CELESTE_DEPTH: u8 = 94;
pub const PHASER_DEPTH: u8 = 95;
pub const DATA_INCREMENT: u8 = 96;
pub const DATA_DECREMENT: u8 = 97;
pub const NRPN_LSB: u8 = 98;
pub const NRPN_MSB: u8 = 99;
pub const RPN_LSB: u8 = 100;
pub const RPN_MSB: u8 = 101;
pub const ALL_SOUNDS_OFF: u8 = 120;
pub const RESET_ALL_CONTROLLERS: u8 = 121;
pub const LOCAL_CONTROL: u8 = 122;
pub const ALL_NOTES_OFF: u8 = 123;
pub const OMNI_MODE_OFF: u8 = 124;
pub const OMNI_MODE_ON: u8 = 125;
pub const MONO_MODE_ON: u8 = 126;
pub const POLY_MODE_ON: u8 = 127;
}
pub fn note_to_name(note: u8) -> String {
if note > 127 {
return format!("Invalid({note})");
}
let note_names = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let octave = (note as i32 / 12) - 2;
let note_in_octave = note % 12;
format!("{}{}", note_names[note_in_octave as usize], octave)
}
pub fn name_to_note(name: &str) -> Option<u8> {
let name = name.trim().to_uppercase();
let mut chars = name.chars();
let letter = chars.next()?;
if !letter.is_ascii_alphabetic() {
return None;
}
let rest = chars.as_str();
let (accidental, octave_str) = match rest.chars().next() {
Some('#') => (Some('#'), &rest[1..]),
Some('B') => (Some('B'), &rest[1..]),
_ => (None, rest),
};
let octave: i32 = octave_str.parse().ok()?;
let semitone = match (letter, accidental) {
('C', None) => 0,
('C', Some('#')) | ('D', Some('B')) => 1,
('D', None) => 2,
('D', Some('#')) | ('E', Some('B')) => 3,
('E', None) => 4,
('F', None) => 5,
('F', Some('#')) | ('G', Some('B')) => 6,
('G', None) => 7,
('G', Some('#')) | ('A', Some('B')) => 8,
('A', None) => 9,
('A', Some('#')) | ('B', Some('B')) => 10,
('B', None) => 11,
_ => return None,
};
let midi_note = octave
.checked_add(2)
.and_then(|o| o.checked_mul(12))
.and_then(|base| base.checked_add(semitone))?;
if (0..=127).contains(&midi_note) {
Some(midi_note as u8)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn note_expression_type_ids_round_trip() {
for kind in [
NoteExpressionType::Volume,
NoteExpressionType::Pan,
NoteExpressionType::Tuning,
NoteExpressionType::Vibrato,
NoteExpressionType::Expression,
NoteExpressionType::Brightness,
NoteExpressionType::Custom(100_001),
] {
assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
}
assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
assert_eq!(
NoteExpressionType::from_type_id(5),
NoteExpressionType::Brightness
);
}
#[test]
fn from_midi_bytes_maps_channel_voice_messages() {
assert_eq!(
MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
Some(MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 100
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
Some(MidiEvent::NoteOff {
channel: MidiChannel::Ch1,
note: 60,
velocity: 0
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
Some(MidiEvent::NoteOff {
channel: MidiChannel::Ch10,
note: 64,
velocity: 40
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
Some(MidiEvent::ControlChange {
channel: MidiChannel::Ch1,
controller: 1,
value: 64
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0xD0, 90]),
Some(MidiEvent::ChannelAftertouch {
channel: MidiChannel::Ch1,
pressure: 90
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
Some(MidiEvent::PolyAftertouch {
channel: MidiChannel::Ch1,
note: 60,
pressure: 70
})
);
}
#[test]
fn from_midi_bytes_pitch_bend_is_14_bit() {
assert_eq!(
MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
Some(MidiEvent::PitchBend {
channel: MidiChannel::Ch1,
value: 8192
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
Some(MidiEvent::PitchBend {
channel: MidiChannel::Ch1,
value: 16383
})
);
}
#[test]
fn from_midi_bytes_rejects_unsupported_and_junk() {
assert_eq!(MidiEvent::from_midi_bytes(&[]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); }
#[test]
fn from_midi_bytes_maps_program_change() {
assert_eq!(
MidiEvent::from_midi_bytes(&[0xC0, 5]),
Some(MidiEvent::ProgramChange {
channel: MidiChannel::Ch1,
program: 5
})
);
assert_eq!(
MidiEvent::from_midi_bytes(&[0xC9, 0xFF]),
Some(MidiEvent::ProgramChange {
channel: MidiChannel::Ch10,
program: 127
})
);
assert_eq!(MidiEvent::from_midi_bytes(&[0xC0]), None);
}
#[test]
fn test_midi_conversions() {
assert_eq!(name_to_note("C3"), Some(60));
assert_eq!(name_to_note("C2"), Some(48));
assert_eq!(name_to_note("A3"), Some(69)); assert_eq!(name_to_note("C-2"), Some(0));
assert_eq!(name_to_note("G8"), Some(127));
assert_eq!(note_to_name(60), "C3");
assert_eq!(note_to_name(48), "C2");
assert_eq!(note_to_name(69), "A3");
assert_eq!(note_to_name(0), "C-2");
assert_eq!(note_to_name(127), "G8");
assert_eq!(name_to_note("C#3"), Some(61));
assert_eq!(name_to_note("Db3"), Some(61));
assert_eq!(name_to_note("F#3"), Some(66));
}
#[test]
fn name_to_note_rejects_junk_without_panicking() {
for junk in [
"éB3", "ÉB3", "日本語", "", "3", "H3", "C", "C#", "Cb3", "C##3", "CB3", "C99", "C-99", "#3", "C2147483647",
"C-2147483648",
"Bb2147483647",
"C#2147483647",
] {
assert_eq!(name_to_note(junk), None, "expected None for {junk:?}");
}
}
#[test]
fn name_to_note_distinguishes_b_natural_from_flats() {
assert_eq!(name_to_note("B3"), Some(71));
assert_eq!(name_to_note("Bb3"), Some(70));
assert_eq!(name_to_note("bb3"), Some(70)); assert_eq!(name_to_note("Eb3"), Some(63));
assert_eq!(name_to_note("Ab3"), Some(68));
for n in 0..=127u8 {
assert_eq!(name_to_note(¬e_to_name(n)), Some(n), "round-trip {n}");
}
}
#[test]
fn note_to_name_marks_out_of_domain_notes_instead_of_fabricating_one() {
for n in 128..=255u8 {
let name = note_to_name(n);
assert_eq!(name, format!("Invalid({n})"));
assert_eq!(
name_to_note(&name),
None,
"{name} must not parse back as a note"
);
}
for n in 0..=127u8 {
let name = note_to_name(n);
assert!(!name.starts_with("Invalid"), "note {n} rendered as {name}");
assert_eq!(name_to_note(&name), Some(n), "round-trip {n}");
}
}
#[test]
fn test_midi_channel() {
assert_eq!(MidiChannel::Ch1.as_index(), 0);
assert_eq!(MidiChannel::Ch16.as_index(), 15);
assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
assert_eq!(MidiChannel::from_index(16), None);
}
}