///|
/// Parser state for tracking position in byte stream
priv struct ParserState {
mut pos : Int
data : Bytes
mut running_status : Int // for MIDI running status
}
///|
/// Parse errors
pub suberror ParseError {
InvalidHeader
InvalidTrack
UnexpectedEOF
UnknownEventType
InvalidData(String)
}
///|
/// Create a new parser state
fn new_parser_state(data : Bytes) -> ParserState {
{ pos: 0, data, running_status: -1 }
}
///|
/// Read a single byte from the parser state
fn read_byte(state : ParserState) -> Byte raise ParseError {
if state.pos >= state.data.length() {
raise UnexpectedEOF
}
let byte = state.data[state.pos]
state.pos += 1
byte
}
///|
/// Read multiple bytes from the parser state
fn read_bytes(state : ParserState, count : Int) -> Array[Byte] raise ParseError {
if state.pos + count > state.data.length() {
raise UnexpectedEOF
}
let bytes = Array::new(capacity=count)
for i = 0; i < count; i = i + 1 {
bytes.push(state.data[state.pos + i])
}
state.pos += count
bytes
}
///|
/// Read a 16-bit big-endian integer
fn read_uint16(state : ParserState) -> UInt16 raise ParseError {
let bytes = read_bytes(state, 2)
(bytes[0].to_int().to_uint16() << 8) | bytes[1].to_int().to_uint16()
}
///|
/// Read a 32-bit big-endian integer
fn read_uint32(state : ParserState) -> Int raise ParseError {
let bytes = read_bytes(state, 4)
(bytes[0].to_int() << 24) |
(bytes[1].to_int() << 16) |
(bytes[2].to_int() << 8) |
bytes[3].to_int()
}
///|
/// Read a variable-length integer (MIDI format)
fn read_variable_int(state : ParserState) -> UInt raise ParseError {
let mut value : UInt = 0
while true {
let byte = read_byte(state).to_int()
value = (value << 7) | (byte & 0x7F).reinterpret_as_uint()
if byte < 0x80 {
break value
}
} else {
value
}
}
///|
/// Read a 4-byte chunk header (type + size)
fn read_chunk_header(state : ParserState) -> (String, Int) raise ParseError {
let type_bytes = read_bytes(state, 4)
let size = read_uint32(state)
let type_str = String::from_array(
type_bytes.map(fn(b) { b.to_int().to_char().unwrap() }),
)
(type_str, size)
}
///|
/// Parse MIDI file header
fn parse_header(
state : ParserState,
) -> (UInt16, UInt16, UInt16) raise ParseError {
let (chunk_type, chunk_size) = read_chunk_header(state)
if chunk_type != "MThd" {
raise InvalidHeader
}
if chunk_size != 6 {
raise InvalidHeader
}
let format = read_uint16(state)
let num_tracks = read_uint16(state)
let division = read_uint16(state)
(format, num_tracks, division)
}
///|
/// Parse a single MIDI event
fn parse_event(state : ParserState) -> Event raise ParseError {
let delta = read_variable_int(state)
let mut status_byte = read_byte(state).to_int()
// Handle running status
if status_byte < 0x80 {
if state.running_status == -1 {
raise InvalidData("Running status without previous status")
}
// Put the byte back and use running status
state.pos -= 1
status_byte = state.running_status
// Update running status (except for system messages)
} else if status_byte < 0xF0 {
state.running_status = status_byte
}
let channel = status_byte & 0x0F
let message_type = status_byte & 0xF0
match message_type {
0x80 => {
// Note Off
let note = read_byte(state)
let velocity = read_byte(state)
NoteOff(delta~, channel=channel.to_byte(), note~, velocity~)
}
0x90 => {
// Note On
let note = read_byte(state)
let velocity = read_byte(state)
NoteOn(delta~, channel=channel.to_byte(), note~, velocity~)
}
0xA0 => {
// Polyphonic Aftertouch
let note = read_byte(state)
let value = read_byte(state)
PolyTouch(delta~, channel=channel.to_byte(), note~, value~)
}
0xB0 => {
// Control Change
let controller = read_byte(state)
let value = read_byte(state)
ControlChange(delta~, channel=channel.to_byte(), controller~, value~)
}
0xC0 => {
// Program Change
let program = read_byte(state)
ProgramChange(delta~, channel=channel.to_byte(), program~)
}
0xD0 => {
// Channel Aftertouch
let value = read_byte(state)
Aftertouch(delta~, channel=channel.to_byte(), value~)
}
0xE0 => {
// Pitch Wheel
let lsb = read_byte(state).to_int()
let msb = read_byte(state).to_int()
let pitch = (msb << 7) | (lsb - 8192) // Convert to signed 14-bit
PitchWheel(delta~, channel=channel.to_byte(), pitch~)
}
0xF0 =>
// System messages
match status_byte {
0xF0 => {
// System Exclusive
let length = read_variable_int(state)
let data = read_bytes(state, length.reinterpret_as_int())
// Remove end byte if present
let clean_data = if data.length() > 0 &&
data[data.length() - 1] == 0xF7 {
data[0:data.length() - 1]
} else {
data
}
SysEx(delta~, data=clean_data.to_array())
}
0xF8 => Clock(delta~)
0xFA => Start(delta~)
0xFB => Continue(delta~)
0xFC => Stop(delta~)
0xFE => ActiveSensing(delta~)
0xFF => {
// Meta Event
let meta_type = read_byte(state)
let length = read_variable_int(state)
let data = read_bytes(state, length.reinterpret_as_int())
match meta_type {
0x51 => {
// Tempo: 3 bytes of microseconds per quarter note
if data.length() != 3 {
raise InvalidData("Tempo data must be 3 bytes")
}
let microseconds : UInt = (data[0].to_uint() << 16) |
(data[1].to_uint() << 8) |
data[2].to_uint()
Tempo(delta~, microseconds~)
}
0x58 => {
// Time Signature: 4 bytes
if data.length() != 4 {
raise InvalidData("Time signature data must be 4 bytes")
}
let numerator = data[0]
let denominator_power = data[1]
let denominator = (1 << denominator_power.to_int()).to_byte()
let clocks_per_beat = data[2]
let thirty_seconds_per_quarter = data[3]
TimeSignature(
delta~,
numerator~,
denominator~,
clocks_per_beat~,
thirty_seconds_per_quarter~,
)
}
0x59 => {
// Key Signature: 2 bytes
if data.length() != 2 {
raise InvalidData("Key signature data must be 2 bytes")
}
let sf = data[0]
let mi = data[1]
KeySignature(delta~, sf~, mi~)
}
_ => Meta(delta~, meta_type~, data~)
}
}
_ => raise UnknownEventType
}
_ => raise UnknownEventType
}
}
///|
/// Parse a single track
fn parse_track(state : ParserState) -> Track raise ParseError {
let (chunk_type, chunk_size) = read_chunk_header(state)
if chunk_type != "MTrk" {
raise InvalidTrack
}
let track_start = state.pos
let track_end = track_start + chunk_size
let events = Array::new()
// Reset running status for each track
state.running_status = -1
while state.pos < track_end {
let event = parse_event(state)
events.push(event)
}
if state.pos != track_end {
raise InvalidTrack
}
{ events, }
}
///|
/// Main MIDI file parser
pub fn parse(bytes : Bytes) -> MidiFile raise ParseError {
let state = new_parser_state(bytes)
// Parse header
let (format, num_tracks, division) = parse_header(state)
// Parse tracks
let tracks = Array::new()
for i = 0; i < num_tracks.to_int(); i = i + 1 {
let track = parse_track(state)
tracks.push(track)
}
{ format, division, tracks }
}