Skip to content

Commit

Permalink
Merge pull request #21 from raizo07/midi-object
Browse files Browse the repository at this point in the history
feat: midi fun contract and test
  • Loading branch information
caseywescott authored Dec 3, 2024
2 parents 9d59c3e + 474bbd8 commit 0669a0e
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/midi.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ mod modes;
mod pitch;
mod velocitycurve;
mod euclidean;
mod output;
207 changes: 207 additions & 0 deletions src/midi/output.cairo
Original file line number Diff line number Diff line change
@@ -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<u8>
}

trait MidiOutputTrait {
fn new() -> MidiOutput;
fn append_byte(ref self: MidiOutput, value: u8);
fn append_bytes(ref self: MidiOutput, values: Array<u8>);
fn len(self: @MidiOutput) -> usize;
fn get_data(self: @MidiOutput) -> Array<u8>;
}

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<u8>) {
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<u8> {
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<u8> {
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 => {},
}
}
}
70 changes: 70 additions & 0 deletions src/tests/test_output.cairo
Original file line number Diff line number Diff line change
@@ -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');
}
}

0 comments on commit 0669a0e

Please sign in to comment.