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,
},
}
#[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 })
}
_ => 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 {
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 (note_part, octave_str) = if name.contains('#') {
let parts: Vec<&str> = name.split('#').collect();
if parts.len() != 2 {
return None;
}
(format!("{}#", parts[0]), parts[1])
} else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
(format!("{}B", &name[0..1]), &name[2..])
} else {
let mut chars = name.chars();
let note = chars.next()?.to_string();
let octave = chars.as_str();
(note, octave)
};
let octave: i32 = octave_str.parse().ok()?;
let semitone = match note_part.as_str() {
"C" => 0,
"C#" | "DB" => 1,
"D" => 2,
"D#" | "EB" => 3,
"E" => 4,
"F" => 5,
"F#" | "GB" => 6,
"G" => 7,
"G#" | "AB" => 8,
"A" => 9,
"A#" | "BB" => 10,
"B" => 11,
_ => return None,
};
let midi_note = (octave + 2) * 12 + 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(&[0xC0, 5]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), 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 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);
}
}