diff --git a/src/midi.cairo b/src/midi.cairo index 1a6dcac..bd824c0 100644 --- a/src/midi.cairo +++ b/src/midi.cairo @@ -6,3 +6,4 @@ mod modes; mod pitch; mod velocitycurve; mod euclidean; +mod output; diff --git a/src/midi/output.cairo b/src/midi/output.cairo new file mode 100644 index 0000000..f214b5a --- /dev/null +++ b/src/midi/output.cairo @@ -0,0 +1,207 @@ +use core::array::ArrayTrait; +use core::array::SpanTrait; +use core::traits::TryInto; +use core::option::OptionTrait; +use core::num::traits::Bounded; +use core::byte_array::ByteArrayTrait; +use koji::midi::types::{ + Midi, Message, NoteOn, NoteOff, SetTempo, TimeSignature, ControlChange, + PitchWheel, AfterTouch, PolyTouch, ProgramChange, SystemExclusive +}; + +#[derive(Drop)] +struct MidiOutput { + data: Array +} + +trait MidiOutputTrait { + fn new() -> MidiOutput; + fn append_byte(ref self: MidiOutput, value: u8); + fn append_bytes(ref self: MidiOutput, values: Array); + fn len(self: @MidiOutput) -> usize; + fn get_data(self: @MidiOutput) -> Array; +} + +impl MidiOutputImpl of MidiOutputTrait { + fn new() -> MidiOutput { + MidiOutput { data: ArrayTrait::new() } + } + + fn append_byte(ref self: MidiOutput, value: u8) { + self.data.append(value); + } + + fn append_bytes(ref self: MidiOutput, mut values: Array) { + let mut i = 0; + loop { + match values.pop_front() { + Option::Some(value) => { + self.data.append(*value); + }, + Option::None => { break; } + }; + } + } + + fn len(self: @MidiOutput) -> usize { + self.data.len() + } + + fn get_data(self: @MidiOutput) -> Array { + let mut result = ArrayTrait::new(); + let mut data = self.data.clone(); + loop { + match data.pop_front() { + Option::Some(value) => { result.append(*value); }, + Option::None => { break; } + }; + } + result + } +} + +fn output_midi_object(midi: @Midi) -> Array { + let mut output = MidiOutputTrait::new(); + + // Add MIDI header chunk + output.append_bytes(array![0x4D, 0x54, 0x68, 0x64]); // MThd + output.append_bytes(array![0x00, 0x00, 0x00, 0x06]); // Length + output.append_bytes(array![0x00, 0x00]); // Format 0 + output.append_bytes(array![0x00, 0x01]); // Number of tracks + output.append_bytes(array![0x01, 0xE0]); // Division + + // Add track chunk header + output.append_bytes(array![0x4D, 0x54, 0x72, 0x6B]); // MTrk + let track_length_pos = output.len(); + output.append_bytes(array![0x00, 0x00, 0x00, 0x00]); // Length placeholder + + let mut prev_time: u32 = 0; + let mut ev = *midi.events; + + loop { + match ev.pop_front() { + Option::Some(event) => { + match event { + Message::NOTE_ON(note) => { + let delta = note.time.mag - prev_time; + prev_time = note.time.mag; + write_variable_length(delta, ref output); + + output.append_byte(0x90 + note.channel.try_into().unwrap()); + output.append_byte(note.note.try_into().unwrap()); + output.append_byte(note.velocity.try_into().unwrap()); + }, + Message::NOTE_OFF(note) => { + let delta = note.time.mag - prev_time; + prev_time = note.time.mag; + write_variable_length(delta, ref output); + + output.append_byte(0x80 + note.channel.try_into().unwrap()); + output.append_byte(note.note.try_into().unwrap()); + output.append_byte(note.velocity.try_into().unwrap()); + }, + Message::SET_TEMPO(tempo) => { + let time = match tempo.time { + Option::Some(t) => t.mag, + Option::None => prev_time + }; + let delta = time - prev_time; + prev_time = time; + + write_variable_length(delta, ref output); + output.append_bytes(array![0xFF, 0x51, 0x03]); + + let tempo_val: u32 = tempo.tempo; + output.append_byte((tempo_val / 65536).try_into().unwrap()); + output.append_byte(((tempo_val / 256) % 256).try_into().unwrap()); + output.append_byte((tempo_val % 256).try_into().unwrap()); + }, + // Handle other message types as needed + _ => {}, + } + }, + Option::None => { break; } + }; + } + + // Write End of Track + output.append_bytes(array![0x00, 0xFF, 0x2F, 0x00]); + + // Update track length + let track_length = output.len() - track_length_pos - 4; + let mut final_output = MidiOutputTrait::new(); + + // Copy header + let mut header_data = ArrayTrait::new(); + let mut i = 0; + loop { + if i >= track_length_pos { + break; + } + match output.data.get(i) { + Option::Some(value) => { final_output.append_byte(*value); }, + Option::None => { break; } + } + i += 1; + }; + + // Write track length + final_output.append_byte((track_length / 16777216).try_into().unwrap()); + final_output.append_byte(((track_length / 65536) % 256).try_into().unwrap()); + final_output.append_byte(((track_length / 256) % 256).try_into().unwrap()); + final_output.append_byte((track_length % 256).try_into().unwrap()); + + // Copy remaining data + let mut i = track_length_pos + 4; + loop { + if i >= output.len() { + break; + } + match output.data.get(i) { + Option::Some(value) => { final_output.append_byte(*value); }, + Option::None => { break; } + } + i += 1; + }; + + final_output.get_data() +} + +fn write_variable_length(mut value: u32, ref output: MidiOutput) { + if value == 0 { + output.append_byte(0); + return; + } + + let mut buffer = ArrayTrait::new(); + + loop { + if value == 0 { + break; + } + buffer.append((value % 128 + 128).try_into().unwrap()); + value = value / 128; + }; + + let mut i = buffer.len(); + if i > 0 { + i -= 1; + match buffer.get(i) { + Option::Some(byte) => { + output.append_byte(byte % 128); + }, + Option::None => {}, + } + } + + loop { + if i == 0 { + break; + } + i -= 1; + match buffer.get(i) { + Option::Some(byte) => { output.append_byte(*byte); }, + Option::None => {}, + } + } +} \ No newline at end of file diff --git a/src/tests/test_output.cairo b/src/tests/test_output.cairo new file mode 100644 index 0000000..57dd020 --- /dev/null +++ b/src/tests/test_output.cairo @@ -0,0 +1,70 @@ +use koji::midi::output::{output_midi_object, write_variable_length}; +use koji::midi::types::{Midi, Message, NoteOn, ProgramChange, MidiTrait}; +use core::byte_array::ByteArray; + +#[cfg(test)] +mod tests { + use super::*; + use core::array::ArrayTrait; + use koji::midi::types::MidiTrait; + + #[test] + fn test_basic_midi_output() { + let midi = MidiTrait::new(); + let output = output_midi_object(@midi); + + // Check MIDI header + assert(output.at(0) == 0x4D, 'Invalid header M'); // 'M' + assert(output.at(1) == 0x54, 'Invalid header T'); // 'T' + assert(output.at(2) == 0x68, 'Invalid header h'); // 'h' + assert(output.at(3) == 0x64, 'Invalid header d'); // 'd' + + // Check header length + assert(output.at(4) == 0x00, 'Invalid header length 1'); + assert(output.at(5) == 0x00, 'Invalid header length 2'); + assert(output.at(6) == 0x00, 'Invalid header length 3'); + assert(output.at(7) == 0x06, 'Invalid header length 4'); + } + + #[test] + fn test_note_events() { + let mut midi = MidiTrait::new(); + + // Add a note on event + let note_on = NoteOn { + channel: 0, + note: 60, // Middle C + velocity: 100, + time: FP32x32 { mag: 0, sign: false } + }; + midi = midi.append_message(Message::NOTE_ON(note_on)); + + let output = output_midi_object(@midi); + + // Find the note event in the track data (after header and track header) + let track_start = 18; // Header (14) + MTrk header (4) + assert(output.at(track_start + 1) == 0x90, 'Invalid Note On status'); + assert(output.at(track_start + 2) == 60, 'Invalid note number'); + assert(output.at(track_start + 3) == 100, 'Invalid velocity'); + } + + #[test] + fn test_program_change() { + let mut midi = MidiTrait::new(); + + // Add a program change event + let prog_change = ProgramChange { + channel: 0, + program: 1, // Acoustic Grand Piano + time: FP32x32 { mag: 0, sign: false } + }; + midi = midi.append_message(Message::PROGRAM_CHANGE(prog_change)); + + let output = output_midi_object(@midi); + + // Find the program change event + let track_start = 18; + assert(output.at(track_start + 1) == 0xC0, 'Invalid Program Change status'); + assert(output.at(track_start + 2) == 1, 'Invalid program number'); + } +} \ No newline at end of file