use crate::event::{MidiEvent, MidiEventKind};
use std::io::{Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MidiFormat {
SingleTrack = 0,
MultiTrack = 1,
MultiTrackAsync = 2,
}
#[derive(Debug, Clone, Copy)]
pub enum Division {
TicksPerQuarterNote(u16),
SmpteFrames { fps: u8, ticks_per_frame: u8 },
}
#[derive(Debug, Clone)]
pub struct TimedMidiEvent {
pub delta_time: u32,
pub event: MidiEvent,
}
#[derive(Debug, Clone)]
pub struct MidiTrack {
pub name: Option<String>,
pub events: Vec<TimedMidiEvent>,
}
#[derive(Debug, Clone)]
pub struct MidiFile {
pub format: MidiFormat,
pub division: Division,
pub tracks: Vec<MidiTrack>,
}
impl MidiFile {
pub fn new(format: MidiFormat, division: Division) -> Self {
Self {
format,
division,
tracks: Vec::new(),
}
}
pub fn add_track(&mut self, track: MidiTrack) {
self.tracks.push(track);
}
pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
let mut reader = data;
let mut chunk_type = [0u8; 4];
reader
.read_exact(&mut chunk_type)
.map_err(|e| format!("Failed to read chunk type: {}", e))?;
if &chunk_type != b"MThd" {
return Err(format!(
"Invalid MIDI file: expected MThd, got {:?}",
chunk_type
));
}
let header_length = read_u32(&mut reader)?;
if header_length != 6 {
return Err(format!("Invalid header length: {}", header_length));
}
let format_value = read_u16(&mut reader)?;
let format = match format_value {
0 => MidiFormat::SingleTrack,
1 => MidiFormat::MultiTrack,
2 => MidiFormat::MultiTrackAsync,
_ => return Err(format!("Unsupported MIDI format: {}", format_value)),
};
let num_tracks = read_u16(&mut reader)?;
let division_value = read_u16(&mut reader)?;
let division = if division_value & 0x8000 == 0 {
Division::TicksPerQuarterNote(division_value)
} else {
let fps = ((division_value >> 8) & 0x7F) as u8;
let ticks_per_frame = (division_value & 0xFF) as u8;
Division::SmpteFrames {
fps,
ticks_per_frame,
}
};
let mut tracks = Vec::new();
for _ in 0..num_tracks {
let track = read_track(&mut reader)?;
tracks.push(track);
}
Ok(Self {
format,
division,
tracks,
})
}
pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
let mut data = Vec::new();
data.write_all(b"MThd")
.map_err(|e| format!("Write error: {}", e))?;
write_u32(&mut data, 6)?;
let format_value = self.format as u16;
write_u16(&mut data, format_value)?;
write_u16(&mut data, self.tracks.len() as u16)?;
let division_value = match self.division {
Division::TicksPerQuarterNote(ticks) => ticks,
Division::SmpteFrames {
fps,
ticks_per_frame,
} => 0x8000 | ((fps as u16) << 8) | (ticks_per_frame as u16),
};
write_u16(&mut data, division_value)?;
for track in &self.tracks {
write_track(&mut data, track)?;
}
Ok(data)
}
}
impl MidiTrack {
pub fn new() -> Self {
Self {
name: None,
events: Vec::new(),
}
}
pub fn with_name(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
events: Vec::new(),
}
}
pub fn add_event(&mut self, delta_time: u32, event: MidiEvent) {
self.events.push(TimedMidiEvent { delta_time, event });
}
}
impl Default for MidiTrack {
fn default() -> Self {
Self::new()
}
}
fn read_u16(reader: &mut &[u8]) -> Result<u16, String> {
let mut buf = [0u8; 2];
reader
.read_exact(&mut buf)
.map_err(|e| format!("Read error: {}", e))?;
Ok(u16::from_be_bytes(buf))
}
fn read_u32(reader: &mut &[u8]) -> Result<u32, String> {
let mut buf = [0u8; 4];
reader
.read_exact(&mut buf)
.map_err(|e| format!("Read error: {}", e))?;
Ok(u32::from_be_bytes(buf))
}
fn write_u16(writer: &mut Vec<u8>, value: u16) -> Result<(), String> {
writer
.write_all(&value.to_be_bytes())
.map_err(|e| format!("Write error: {}", e))
}
fn write_u32(writer: &mut Vec<u8>, value: u32) -> Result<(), String> {
writer
.write_all(&value.to_be_bytes())
.map_err(|e| format!("Write error: {}", e))
}
fn read_variable_length(reader: &mut &[u8]) -> Result<u32, String> {
let mut value = 0u32;
for _ in 0..4 {
let mut byte = [0u8];
reader
.read_exact(&mut byte)
.map_err(|e| format!("Read error: {}", e))?;
value = (value << 7) | ((byte[0] & 0x7F) as u32);
if byte[0] & 0x80 == 0 {
return Ok(value);
}
}
Err("Variable length value too long".to_string())
}
fn write_variable_length(writer: &mut Vec<u8>, mut value: u32) -> Result<(), String> {
let mut bytes = Vec::new();
bytes.push((value & 0x7F) as u8);
value >>= 7;
while value > 0 {
bytes.push(((value & 0x7F) | 0x80) as u8);
value >>= 7;
}
for &byte in bytes.iter().rev() {
writer
.write_all(&[byte])
.map_err(|e| format!("Write error: {}", e))?;
}
Ok(())
}
fn read_track(reader: &mut &[u8]) -> Result<MidiTrack, String> {
let mut chunk_type = [0u8; 4];
reader
.read_exact(&mut chunk_type)
.map_err(|e| format!("Read error: {}", e))?;
if &chunk_type != b"MTrk" {
return Err(format!(
"Invalid track chunk: expected MTrk, got {:?}",
chunk_type
));
}
let track_length = read_u32(reader)?;
let track_data = reader[..track_length as usize].to_vec();
*reader = &reader[track_length as usize..];
let mut track = MidiTrack::new();
let mut track_reader = &track_data[..];
let mut running_status = 0u8;
while !track_reader.is_empty() {
let delta_time = read_variable_length(&mut track_reader)?;
let mut status_byte = [0u8];
track_reader
.read_exact(&mut status_byte)
.map_err(|e| format!("Read error: {}", e))?;
let status = status_byte[0];
let (actual_status, data_start) = if status & 0x80 == 0 {
(running_status, status)
} else {
running_status = status;
(status, 0)
};
let event = match actual_status & 0xF0 {
0x90 => {
let note = if data_start != 0 {
data_start
} else {
let mut byte = [0u8];
track_reader
.read_exact(&mut byte)
.map_err(|e| format!("Read error: {}", e))?;
byte[0]
};
let mut velocity = [0u8];
track_reader
.read_exact(&mut velocity)
.map_err(|e| format!("Read error: {}", e))?;
let channel = (actual_status & 0x0F) + 1;
MidiEvent {
timestamp: 0,
channel,
kind: MidiEventKind::NoteOn {
note,
velocity: velocity[0],
},
}
}
0x80 => {
let note = if data_start != 0 {
data_start
} else {
let mut byte = [0u8];
track_reader
.read_exact(&mut byte)
.map_err(|e| format!("Read error: {}", e))?;
byte[0]
};
let mut velocity = [0u8];
track_reader
.read_exact(&mut velocity)
.map_err(|e| format!("Read error: {}", e))?;
let channel = (actual_status & 0x0F) + 1;
MidiEvent {
timestamp: 0,
channel,
kind: MidiEventKind::NoteOff {
note,
velocity: velocity[0],
},
}
}
0xFF => {
let mut meta_type = [0u8];
track_reader
.read_exact(&mut meta_type)
.map_err(|e| format!("Read error: {}", e))?;
let length = read_variable_length(&mut track_reader)?;
if length as usize <= track_reader.len() {
track_reader = &track_reader[length as usize..];
}
continue;
}
_ => {
continue;
}
};
track.add_event(delta_time, event);
}
Ok(track)
}
fn write_track(writer: &mut Vec<u8>, track: &MidiTrack) -> Result<(), String> {
writer
.write_all(b"MTrk")
.map_err(|e| format!("Write error: {}", e))?;
let length_pos = writer.len();
write_u32(writer, 0)?;
let track_start = writer.len();
for timed_event in &track.events {
write_variable_length(writer, timed_event.delta_time)?;
match &timed_event.event.kind {
MidiEventKind::NoteOn { note, velocity } => {
let status = 0x90 | ((timed_event.event.channel - 1) & 0x0F);
writer
.write_all(&[status, *note, *velocity])
.map_err(|e| format!("Write error: {}", e))?;
}
MidiEventKind::NoteOff { note, velocity } => {
let status = 0x80 | ((timed_event.event.channel - 1) & 0x0F);
writer
.write_all(&[status, *note, *velocity])
.map_err(|e| format!("Write error: {}", e))?;
}
_ => {
}
}
}
writer
.write_all(&[0xFF, 0x2F, 0x00])
.map_err(|e| format!("Write error: {}", e))?;
let track_length = (writer.len() - track_start) as u32;
let length_bytes = track_length.to_be_bytes();
writer[length_pos..length_pos + 4].copy_from_slice(&length_bytes);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_midi_file_creation() {
let mut file = MidiFile::new(MidiFormat::SingleTrack, Division::TicksPerQuarterNote(480));
let mut track = MidiTrack::with_name("Test Track");
track.add_event(
0,
MidiEvent {
timestamp: 0,
channel: 1,
kind: MidiEventKind::NoteOn {
note: 60,
velocity: 100,
},
},
);
track.add_event(
480,
MidiEvent {
timestamp: 0,
channel: 1,
kind: MidiEventKind::NoteOff {
note: 60,
velocity: 0,
},
},
);
file.add_track(track);
assert_eq!(file.tracks.len(), 1);
assert_eq!(file.tracks[0].events.len(), 2);
}
#[test]
#[ignore] fn test_midi_file_write_read() {
let mut file = MidiFile::new(MidiFormat::SingleTrack, Division::TicksPerQuarterNote(480));
let mut track = MidiTrack::new();
track.add_event(
0,
MidiEvent {
timestamp: 0,
channel: 1,
kind: MidiEventKind::NoteOn {
note: 60,
velocity: 100,
},
},
);
track.add_event(
480,
MidiEvent {
timestamp: 0,
channel: 1,
kind: MidiEventKind::NoteOff {
note: 60,
velocity: 0,
},
},
);
file.add_track(track);
let bytes = file.to_bytes().unwrap();
let loaded = MidiFile::from_bytes(&bytes).unwrap();
assert_eq!(loaded.tracks.len(), 1);
assert!(loaded.tracks[0].events.len() >= 2); }
#[test]
fn test_variable_length_encoding() {
let mut data = Vec::new();
write_variable_length(&mut data, 0).unwrap();
assert_eq!(data, vec![0x00]);
data.clear();
write_variable_length(&mut data, 127).unwrap();
assert_eq!(data, vec![0x7F]);
data.clear();
write_variable_length(&mut data, 128).unwrap();
assert_eq!(data, vec![0x81, 0x00]);
data.clear();
write_variable_length(&mut data, 16383).unwrap();
assert_eq!(data, vec![0xFF, 0x7F]);
}
}