///|
/// Writer state for building byte sequences
priv struct WriterState {
data : Array[Byte]
}
///|
/// Create a new writer state
fn new_writer_state() -> WriterState {
{ data: Array::new() }
}
///|
/// Write a single byte
fn write_byte(state : WriterState, byte : Byte) -> Unit {
state.data.push(byte)
}
///|
/// Write multiple bytes
fn write_bytes(state : WriterState, bytes : Array[Byte]) -> Unit {
for byte in bytes {
state.data.push(byte)
}
}
///|
/// Write a 16-bit big-endian integer
fn write_uint16(state : WriterState, value : UInt16) -> Unit {
write_byte(state, (value >> 8).to_byte())
write_byte(state, value.to_byte())
}
///|
/// Write a 32-bit big-endian integer
fn write_uint32(state : WriterState, value : Int) -> Unit {
write_byte(state, (value >> 24).to_byte())
write_byte(state, (value >> 16).to_byte())
write_byte(state, (value >> 8).to_byte())
write_byte(state, value.to_byte())
}
///|
/// Write a variable-length integer (MIDI format)
fn write_variable_int(state : WriterState, value : UInt) -> Unit {
let mut val = value
let bytes = Array::new()
// Extract 7-bit groups from right to left
bytes.push((val & 0x7F).to_byte())
val = val >> 7
while val > 0 {
bytes.push(((val & 0x7F) | 0x80).to_byte())
val = val >> 7
}
// Write bytes in reverse order (big-endian)
for i = bytes.length() - 1; i >= 0; i = i - 1 {
write_byte(state, bytes[i])
}
}
///|
/// Write a 4-byte chunk header (type + size)
fn write_chunk_header(
state : WriterState,
chunk_type : String,
size : Int,
) -> Unit {
// Write chunk type (4 ASCII characters)
for char in chunk_type {
write_byte(state, char.to_int().to_byte())
}
// Write size
write_uint32(state, size)
}
///|
/// Serialize a MIDI event to bytes
fn serialize_event(
state : WriterState,
event : Event,
use_running_status : Bool,
last_status : Byte,
) -> Byte {
write_variable_int(state, event.delta())
match event {
NoteOff(channel~, note~, velocity~, ..) => {
let status = 0x80 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, note)
write_byte(state, velocity)
status.to_byte()
}
NoteOn(channel~, note~, velocity~, ..) => {
let status = 0x90 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, note)
write_byte(state, velocity)
status.to_byte()
}
PolyTouch(channel~, note~, value~, ..) => {
let status = 0xA0 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, note)
write_byte(state, value)
status.to_byte()
}
ControlChange(channel~, controller~, value~, ..) => {
let status = 0xB0 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, controller)
write_byte(state, value)
status.to_byte()
}
ProgramChange(channel~, program~, ..) => {
let status = 0xC0 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, program)
status.to_byte()
}
Aftertouch(channel~, value~, ..) => {
let status = 0xD0 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
write_byte(state, value)
status.to_byte()
}
PitchWheel(channel~, pitch~, ..) => {
let status = 0xE0 | channel.to_int()
if not(use_running_status) || status.to_byte() != last_status {
write_byte(state, status.to_byte())
}
let adjusted_pitch = pitch + 8192
write_byte(state, (adjusted_pitch & 0x7F).to_byte()) // LSB
write_byte(state, ((adjusted_pitch >> 7) & 0x7F).to_byte()) // MSB
status.to_byte()
}
Tempo(delta~, microseconds~) => {
write_byte(state, b'\xFF')
write_byte(state, b'\x51')
write_byte(state, b'\x03')
write_byte(state, ((microseconds >> 16) & 0xFF).to_byte())
write_byte(state, ((microseconds >> 8) & 0xFF).to_byte())
write_byte(state, (microseconds & 0xFF).to_byte())
b'\xFF'
}
TimeSignature(
delta~,
numerator~,
denominator~,
clocks_per_beat~,
thirty_seconds_per_quarter~
) =>
// TODO
b'\xFF'
KeySignature(delta~, sf~, mi~) =>
// TODO
b'\xFF'
SysEx(data~, ..) => {
write_byte(state, b'\xF0')
write_variable_int(state, (data.length() + 1).reinterpret_as_uint()) // +1 for end byte
write_bytes(state, data)
write_byte(state, b'\xF7') // End of SysEx
b'\xF0'
}
Clock(_) => {
write_byte(state, b'\xF8')
b'\xF8'
}
Start(_) => {
write_byte(state, b'\xFA')
b'\xFA'
}
Continue(_) => {
write_byte(state, b'\xFB')
b'\xFB'
}
Stop(_) => {
write_byte(state, b'\xFC')
b'\xFC'
}
ActiveSensing(_) => {
write_byte(state, b'\xFE')
b'\xFE'
}
Reset(_) => {
write_byte(state, b'\xFF')
b'\xFF'
}
Meta(meta_type~, data~, ..) => {
write_byte(state, b'\xFF')
write_byte(state, meta_type)
write_variable_int(state, data.length().reinterpret_as_uint())
write_bytes(state, data)
b'\xFF'
}
}
}
///|
/// Helper function to get delta time from event
fn Event::delta(self : Event) -> UInt {
match self {
NoteOff(delta~, ..)
| NoteOn(delta~, ..)
| PolyTouch(delta~, ..)
| ControlChange(delta~, ..)
| ProgramChange(delta~, ..)
| Aftertouch(delta~, ..)
| PitchWheel(delta~, ..)
| Tempo(delta~, ..)
| TimeSignature(delta~, ..)
| KeySignature(delta~, ..)
| SysEx(delta~, ..)
| Clock(delta~)
| Start(delta~)
| Continue(delta~)
| Stop(delta~)
| ActiveSensing(delta~)
| Reset(delta~)
| Meta(delta~, ..) => delta
}
}
///|
/// Serialize a track to bytes
fn serialize_track(state : WriterState, track : Track) -> Unit {
let track_state = new_writer_state()
let mut last_status : Byte = 0x00
// Serialize all events in the track
for event in track.events {
last_status = serialize_event(track_state, event, true, last_status)
}
// Write track header
write_chunk_header(state, "MTrk", track_state.data.length())
// Write track data
write_bytes(state, track_state.data)
}
///|
/// Main MIDI file serializer
pub fn serialize(midi_file : MidiFile) -> Bytes {
let state = new_writer_state()
// Write header chunk
write_chunk_header(state, "MThd", 6)
write_uint16(state, midi_file.format)
write_uint16(state, midi_file.tracks.length().to_uint16())
write_uint16(state, midi_file.division)
// Write all tracks
for track in midi_file.tracks {
serialize_track(state, track)
}
Bytes::from_array(state.data)
}