-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from raizo07/midi-object
feat: midi fun contract and test
- Loading branch information
Showing
3 changed files
with
278 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ mod modes; | |
mod pitch; | ||
mod velocitycurve; | ||
mod euclidean; | ||
mod output; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 => {}, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |