#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Midi {
pub ticks_per_beat: u16,
pub tracks: Vec<Vec<(u32, Event)>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
NoteOn {
channel: u8,
key: u8,
velocity: u8,
},
NoteOff {
channel: u8,
key: u8,
},
Control {
channel: u8,
controller: u8,
value: u8,
},
Program {
channel: u8,
program: u8,
},
PitchBend {
channel: u8,
value: i16,
},
Tempo(u32),
Marker(String),
}
impl Midi {
pub fn to_mid(&self) -> Vec<u8> {
let mut file = b"MThd\0\0\0\x06\0\x01".to_vec();
file.extend((self.tracks.len() as u16).to_be_bytes());
file.extend(self.ticks_per_beat.to_be_bytes());
for track in &self.tracks {
let mut events: Vec<_> = track.iter().collect();
events.sort_by_key(|(tick, event)| (*tick, event.rank()));
let mut body = Vec::new();
let mut now = 0;
for (tick, event) in events {
variable_length(&mut body, tick - now);
now = *tick;
event.encode(&mut body);
}
body.extend(b"\0\xFF\x2F\0");
file.extend(b"MTrk");
file.extend((body.len() as u32).to_be_bytes());
file.extend(body);
}
file
}
}
impl Event {
fn rank(&self) -> u8 {
match self {
Self::NoteOff { .. } => 0,
Self::NoteOn { .. } => 2,
_ => 1,
}
}
fn encode(&self, out: &mut Vec<u8>) {
let status = |kind: u8, channel: u8| kind | channel & 0x0F;
match *self {
Self::NoteOn {
channel,
key,
velocity,
} => out.extend([status(0x90, channel), key & 0x7F, velocity.clamp(1, 0x7F)]),
Self::NoteOff { channel, key } => out.extend([status(0x80, channel), key & 0x7F, 0]),
Self::Control {
channel,
controller,
value,
} => out.extend([status(0xB0, channel), controller & 0x7F, value & 0x7F]),
Self::Program { channel, program } => {
out.extend([status(0xC0, channel), program & 0x7F])
}
Self::PitchBend { channel, value } => {
let value = (i32::from(value).clamp(-8192, 8191) + 8192) as u16;
out.extend([
status(0xE0, channel),
(value & 0x7F) as u8,
(value >> 7) as u8,
]);
}
Self::Tempo(microseconds) => {
out.extend(b"\xFF\x51\x03");
out.extend(µseconds.min(0xFF_FFFF).to_be_bytes()[1..]);
}
Self::Marker(ref text) => {
out.extend(b"\xFF\x06");
variable_length(out, text.len() as u32);
out.extend(text.as_bytes());
}
}
}
}
fn variable_length(out: &mut Vec<u8>, value: u32) {
let value = value.min(0x0FFF_FFFF);
let mut shift = 21;
while shift > 0 && value >> shift == 0 {
shift -= 7;
}
while shift > 0 {
out.push(0x80 | (value >> shift) as u8 & 0x7F);
shift -= 7;
}
out.push(value as u8 & 0x7F);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_variable_lengths() {
let encode = |value| {
let mut out = Vec::new();
variable_length(&mut out, value);
out
};
assert_eq!(encode(0), [0]);
assert_eq!(encode(0x7F), [0x7F]);
assert_eq!(encode(0x80), [0x81, 0]);
assert_eq!(encode(0x0FFF_FFFF), [0xFF, 0xFF, 0xFF, 0x7F]);
}
#[test]
fn writes_tracks_with_ordered_deltas() {
let note = |key| Event::NoteOn {
channel: 1,
key,
velocity: 100,
};
let midi = Midi {
ticks_per_beat: 48,
tracks: vec![vec![
(48, note(60)),
(0, Event::Tempo(500_000)),
(
48,
Event::NoteOff {
channel: 1,
key: 60,
},
),
(96, Event::Marker("loopEnd".into())),
]],
};
let file = midi.to_mid();
assert_eq!(&file[..14], b"MThd\0\0\0\x06\0\x01\0\x01\0\x30");
assert_eq!(&file[14..18], b"MTrk");
let body = &file[22..];
assert_eq!(
body.len() as u32,
u32::from_be_bytes(file[18..22].try_into().unwrap())
);
assert_eq!(
body,
[
&[0, 0xFF, 0x51, 3, 0x07, 0xA1, 0x20][..],
&[0x30, 0x81, 60, 0],
&[0, 0x91, 60, 100],
&[0x30, 0xFF, 6, 7],
b"loopEnd",
&[0, 0xFF, 0x2F, 0],
]
.concat()
);
}
}