#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RtEvent {
ClockTick {
subdivision: u8,
beat: u64,
tempo_bpm: f64,
timestamp_ns: u64,
},
Transport(TransportEvent),
MidiInput {
input_port_index: u8,
timestamp_ns: u64,
message: MidiMessage,
},
SongPosition {
position: u16,
},
NonFatalError(RtErrorCode),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportEvent {
Start,
Stop,
Continue,
}
pub type MidiMessage = oxurack_midi::MidiWire;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EcsCommand {
SendMidi {
output_port_index: u8,
message: MidiMessage,
},
SetTempo {
bpm: f64,
},
SendTransport(TransportEvent),
SendSongPosition {
position: u16,
},
Shutdown,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum MidiClassification {
Clock,
Start,
Stop,
Continue,
SongPosition {
position: u16,
},
ActiveSensing,
SystemReset,
Channel(MidiMessage),
}
pub(crate) fn classify_midi(bytes: &[u8]) -> Option<MidiClassification> {
let &status = bytes.first()?;
if status < 0x80 {
return None; }
match status {
0xF8 => Some(MidiClassification::Clock),
0xFA => Some(MidiClassification::Start),
0xFB => Some(MidiClassification::Continue),
0xFC => Some(MidiClassification::Stop),
0xFE => Some(MidiClassification::ActiveSensing),
0xFF => Some(MidiClassification::SystemReset),
0xF2 => {
let lsb = *bytes.get(1).unwrap_or(&0);
let msb = *bytes.get(2).unwrap_or(&0);
let position = (lsb as u16) | ((msb as u16) << 7);
Some(MidiClassification::SongPosition { position })
}
0x80..=0xEF => {
let msg = MidiMessage::from_bytes(bytes)?;
Some(MidiClassification::Channel(msg))
}
_ => None, }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RtErrorCode {
OutputPortLost,
InputPortLost,
QueueOverflow,
ClockNotLocked,
ClockDropout,
PriorityElevationFailed,
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_midi_message_size() {
assert_eq!(std::mem::size_of::<MidiMessage>(), 4);
}
#[test]
fn test_rt_event_fits_cache_line() {
assert!(std::mem::size_of::<RtEvent>() <= 64);
}
#[test]
fn test_ecs_command_fits_cache_line() {
assert!(std::mem::size_of::<EcsCommand>() <= 64);
}
fn _assert_rt_event_is_copy_send()
where
RtEvent: Copy + Send + 'static,
{
}
fn _assert_ecs_command_is_copy_send()
where
EcsCommand: Copy + Send + 'static,
{
}
#[test]
fn test_note_on() {
let msg = MidiMessage::note_on(0, 60, 100);
assert_eq!(msg.status, 0x90);
assert_eq!(msg.data1, 60);
assert_eq!(msg.data2, 100);
assert_eq!(msg.length, 3);
}
#[test]
fn test_note_off() {
let msg = MidiMessage::note_off(1, 64, 0);
assert_eq!(msg.status, 0x81);
assert_eq!(msg.data1, 64);
assert_eq!(msg.data2, 0);
assert_eq!(msg.length, 3);
}
#[test]
fn test_cc() {
let msg = MidiMessage::cc(2, 74, 127);
assert_eq!(msg.status, 0xB2);
assert_eq!(msg.data1, 74);
assert_eq!(msg.data2, 127);
assert_eq!(msg.length, 3);
}
#[test]
fn test_program_change() {
let msg = MidiMessage::program_change(5, 42);
assert_eq!(msg.status, 0xC5);
assert_eq!(msg.data1, 42);
assert_eq!(msg.data2, 0);
assert_eq!(msg.length, 2);
}
#[test]
fn test_pitch_bend() {
let msg = MidiMessage::pitch_bend(0, 0, 64);
assert_eq!(msg.status, 0xE0);
assert_eq!(msg.data1, 0);
assert_eq!(msg.data2, 64);
assert_eq!(msg.length, 3);
}
#[test]
fn test_note_on_roundtrip() {
let original = MidiMessage::note_on(3, 72, 110);
let bytes = original.to_bytes();
let reconstructed = MidiMessage::from_bytes(&bytes);
assert_eq!(Some(original), reconstructed);
}
#[test]
fn test_program_change_roundtrip() {
let original = MidiMessage::program_change(7, 99);
let bytes = original.to_bytes();
let reconstructed = MidiMessage::from_bytes(&bytes);
assert_eq!(Some(original), reconstructed);
}
#[test]
fn test_from_bytes_empty_returns_none() {
assert_eq!(MidiMessage::from_bytes(&[]), None);
}
#[test]
fn test_from_bytes_data_byte_returns_none() {
assert_eq!(MidiMessage::from_bytes(&[0x7F, 0x60, 0x40]), None);
}
#[test]
fn test_from_bytes_system_returns_none() {
assert_eq!(MidiMessage::from_bytes(&[0xF0, 0x7E, 0x7F]), None);
}
#[test]
fn test_to_bytes_pads_short_messages() {
let msg = MidiMessage::program_change(0, 5);
let bytes = msg.to_bytes();
assert_eq!(bytes, [0xC0, 5, 0]);
}
#[test]
fn test_classify_clock() {
assert_eq!(classify_midi(&[0xF8]), Some(MidiClassification::Clock));
}
#[test]
fn test_classify_start() {
assert_eq!(classify_midi(&[0xFA]), Some(MidiClassification::Start));
}
#[test]
fn test_classify_stop() {
assert_eq!(classify_midi(&[0xFC]), Some(MidiClassification::Stop));
}
#[test]
fn test_classify_continue() {
assert_eq!(classify_midi(&[0xFB]), Some(MidiClassification::Continue));
}
#[test]
fn test_classify_active_sensing() {
assert_eq!(
classify_midi(&[0xFE]),
Some(MidiClassification::ActiveSensing)
);
}
#[test]
fn test_classify_system_reset() {
assert_eq!(
classify_midi(&[0xFF]),
Some(MidiClassification::SystemReset)
);
}
#[test]
fn test_classify_note_on() {
assert_eq!(
classify_midi(&[0x90, 60, 100]),
Some(MidiClassification::Channel(MidiMessage {
status: 0x90,
data1: 60,
data2: 100,
length: 3,
}))
);
}
#[test]
fn test_classify_program_change() {
assert_eq!(
classify_midi(&[0xC0, 42]),
Some(MidiClassification::Channel(MidiMessage {
status: 0xC0,
data1: 42,
data2: 0,
length: 2,
}))
);
}
#[test]
fn test_classify_song_position() {
assert_eq!(
classify_midi(&[0xF2, 0x10, 0x02]),
Some(MidiClassification::SongPosition { position: 272 })
);
}
#[test]
fn test_classify_empty_returns_none() {
assert_eq!(classify_midi(&[]), None);
}
#[test]
fn test_classify_data_byte_returns_none() {
assert_eq!(classify_midi(&[0x60]), None);
}
#[test]
fn test_classify_sysex_returns_none() {
assert_eq!(classify_midi(&[0xF0, 0x7E, 0xF7]), None);
}
#[test]
fn test_classify_mtc_quarter_frame() {
assert_eq!(classify_midi(&[0xF1, 0x00]), None);
}
#[test]
fn test_classify_song_select() {
assert_eq!(classify_midi(&[0xF3, 0x00]), None);
}
#[test]
fn test_classify_tune_request() {
assert_eq!(classify_midi(&[0xF6]), None);
}
#[test]
fn test_from_bytes_note_on_missing_data2() {
let msg = MidiMessage::from_bytes(&[0x90, 60]);
assert!(msg.is_some(), "should parse partial Note On");
let msg = msg.unwrap();
assert_eq!(msg.status, 0x90);
assert_eq!(msg.data1, 60);
assert_eq!(msg.data2, 0);
assert_eq!(msg.length, 3);
}
#[test]
fn test_from_bytes_single_status_byte() {
let msg = MidiMessage::from_bytes(&[0x90]);
assert!(msg.is_some(), "should parse status-only Note On");
let msg = msg.unwrap();
assert_eq!(msg.status, 0x90);
assert_eq!(msg.data1, 0);
assert_eq!(msg.data2, 0);
assert_eq!(msg.length, 3);
}
#[test]
fn test_from_bytes_program_change_single_byte() {
let msg = MidiMessage::from_bytes(&[0xC0]);
assert!(msg.is_some(), "should parse status-only Program Change");
let msg = msg.unwrap();
assert_eq!(msg.status, 0xC0);
assert_eq!(msg.data1, 0);
assert_eq!(msg.data2, 0);
assert_eq!(msg.length, 2);
}
#[test]
fn test_to_bytes_note_on() {
let msg = MidiMessage::note_on(0, 60, 100);
assert_eq!(msg.to_bytes(), [0x90, 60, 100]);
}
#[test]
fn test_to_bytes_note_off() {
let msg = MidiMessage::note_off(1, 64, 0);
assert_eq!(msg.to_bytes(), [0x81, 64, 0]);
}
#[test]
fn test_to_bytes_cc() {
let msg = MidiMessage::cc(2, 74, 127);
assert_eq!(msg.to_bytes(), [0xB2, 74, 127]);
}
#[test]
fn test_to_bytes_program_change() {
let msg = MidiMessage::program_change(5, 42);
assert_eq!(msg.to_bytes(), [0xC5, 42, 0]);
}
#[test]
fn test_to_bytes_pitch_bend() {
let msg = MidiMessage::pitch_bend(0, 0, 64);
assert_eq!(msg.to_bytes(), [0xE0, 0, 64]);
}
#[test]
fn test_classify_song_position_zero() {
assert_eq!(
classify_midi(&[0xF2, 0x00, 0x00]),
Some(MidiClassification::SongPosition { position: 0 })
);
}
#[test]
fn test_classify_song_position_max() {
assert_eq!(
classify_midi(&[0xF2, 0x7F, 0x7F]),
Some(MidiClassification::SongPosition { position: 16383 })
);
}
#[test]
fn test_classify_song_position_missing_bytes() {
assert_eq!(
classify_midi(&[0xF2]),
Some(MidiClassification::SongPosition { position: 0 })
);
}
#[test]
fn test_classify_channel_pressure() {
let result = classify_midi(&[0xD0, 100]);
assert_eq!(
result,
Some(MidiClassification::Channel(MidiMessage {
status: 0xD0,
data1: 100,
data2: 0,
length: 2,
}))
);
}
}