use std::io::Cursor;
use sim_lib_midi_core::{
Channel, ChannelMessage, MetaBucket, MetaEvent, MidiEvent, MidiPayload, SysExEvent, TickTime,
U7, U14, synthetic_origin,
};
use crate::{
SmfError, SmfFile, SmfFormat, SmfTrack, SmfWriteOptions, decode_vlq, encode_vlq, read_smf,
write_smf_with_options,
};
#[test]
fn vlq_known_vectors_decode_and_encode_exactly() {
assert_eq!(encode_vlq(0), vec![0x00]);
assert_eq!(encode_vlq(0x7f), vec![0x7f]);
assert_eq!(encode_vlq(0x80), vec![0x81, 0x00]);
assert_eq!(encode_vlq(0x3fff), vec![0xff, 0x7f]);
assert_eq!(
decode_vlq(&mut Cursor::new(vec![0x81, 0x00])).unwrap(),
0x80
);
assert_eq!(
decode_vlq(&mut Cursor::new(vec![0xff, 0x7f])).unwrap(),
0x3fff
);
}
#[test]
fn malformed_running_status_returns_structured_error() {
let bytes = [
b'M', b'T', b'h', b'd', 0, 0, 0, 6, 0, 0, 0, 1, 1, 0xE0, b'M', b'T', b'r', b'k', 0, 0, 0,
4, 0x00, 0x3c, 0x40, 0xff, 0x2f, 0x00,
];
let error = read_smf(&bytes).unwrap_err();
assert_eq!(error, SmfError::MalformedRunningStatus { offset: 23 });
}
#[test]
fn smf_headers_round_trip_for_formats_0_1_and_2() {
for (format, tracks) in [
(SmfFormat::SingleTrack, vec![minimal_track(480)]),
(
SmfFormat::Simultaneous,
vec![minimal_track(480), minimal_track(480)],
),
(
SmfFormat::Independent,
vec![minimal_track(480), minimal_track(480)],
),
] {
let file = SmfFile {
format,
tpq: 480,
tracks,
};
let bytes = write_smf_with_options(&file, SmfWriteOptions::default()).unwrap();
let decoded = read_smf(&bytes).unwrap();
assert_eq!(decoded.format, format);
assert_eq!(decoded.tpq, 480);
assert_eq!(decoded.tracks.len(), file.tracks.len());
}
}
#[test]
fn canonical_fixtures_round_trip_byte_identically_without_running_status() {
let fixture = canonical_format_zero_fixture(false);
let decoded = read_smf(&fixture).unwrap();
let encoded = write_smf_with_options(&decoded, SmfWriteOptions::default()).unwrap();
assert_eq!(encoded, fixture);
}
#[test]
fn canonical_fixtures_round_trip_byte_identically_with_running_status() {
let fixture = canonical_format_zero_fixture(true);
let decoded = read_smf(&fixture).unwrap();
let encoded = write_smf_with_options(
&decoded,
SmfWriteOptions {
running_status: true,
},
)
.unwrap();
assert_eq!(encoded, fixture);
}
#[test]
fn running_status_files_read_correctly() {
let fixture = canonical_format_zero_fixture(true);
let decoded = read_smf(&fixture).unwrap();
let events = &decoded.tracks[0].events;
assert!(matches!(
events[1].payload,
MidiPayload::Channel(ChannelMessage::NoteOn { .. })
));
assert!(matches!(
events[2].payload,
MidiPayload::Channel(ChannelMessage::NoteOn { .. })
));
}
#[test]
fn multi_track_reader_emits_time_sorted_events_and_preserves_last_track() {
let file = read_smf(&format_one_merge_fixture()).unwrap();
let merged = file.merged_events();
let pairs = merged
.iter()
.map(|tracked| {
(
tracked.last_track,
tracked.event.time.ticks,
tracked.event.payload.clone(),
)
})
.collect::<Vec<_>>();
assert_eq!(pairs[0].0, 0);
assert_eq!(pairs[1].0, 1);
assert_eq!(pairs[2].0, 0);
assert_eq!(pairs[3].0, 1);
assert!(pairs.windows(2).all(|window| window[0].1 <= window[1].1));
}
#[test]
fn unknown_meta_and_sysex_round_trip() {
let file = SmfFile {
format: SmfFormat::SingleTrack,
tpq: 480,
tracks: vec![SmfTrack {
events: vec![
MidiEvent {
time: TickTime::new(0, 480).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::Meta(MetaEvent::Other(MetaBucket {
type_byte: 0x7f,
data: vec![1, 2, 3],
})),
},
MidiEvent {
time: TickTime::new(120, 480).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::SysEx(SysExEvent::F0 {
data: vec![0x7d, 0x10, 0x11],
}),
},
],
}],
};
let bytes = write_smf_with_options(&file, SmfWriteOptions::default()).unwrap();
let decoded = read_smf(&bytes).unwrap();
assert_eq!(
decoded.tracks[0].events[0].payload,
file.tracks[0].events[0].payload
);
assert_eq!(
decoded.tracks[0].events[1].payload,
file.tracks[0].events[1].payload
);
}
#[test]
fn channel_control_pitch_and_pressure_messages_round_trip() {
let channel = Channel::new(0).unwrap();
let file = SmfFile {
format: SmfFormat::SingleTrack,
tpq: 480,
tracks: vec![SmfTrack {
events: vec![
MidiEvent {
time: TickTime::new(0, 480).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::Channel(ChannelMessage::ControlChange {
ch: channel,
cc: U7(74),
value: U7(64),
}),
},
MidiEvent {
time: TickTime::new(120, 480).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::Channel(ChannelMessage::PitchBend {
ch: channel,
value: U14(8192),
}),
},
MidiEvent {
time: TickTime::new(240, 480).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::Channel(ChannelMessage::PolyAftertouch {
ch: channel,
key: U7(60),
pressure: U7(70),
}),
},
],
}],
};
let bytes = write_smf_with_options(&file, SmfWriteOptions::default()).unwrap();
let decoded = read_smf(&bytes).unwrap();
assert!(decoded.tracks[0].events.iter().any(|event| matches!(
event.payload,
MidiPayload::Channel(ChannelMessage::ControlChange {
cc: U7(74),
value: U7(64),
..
})
)));
assert!(decoded.tracks[0].events.iter().any(|event| matches!(
event.payload,
MidiPayload::Channel(ChannelMessage::PitchBend {
value: U14(8192),
..
})
)));
assert!(decoded.tracks[0].events.iter().any(|event| matches!(
event.payload,
MidiPayload::Channel(ChannelMessage::PolyAftertouch {
pressure: U7(70),
..
})
)));
}
fn minimal_track(tpq: u32) -> SmfTrack {
SmfTrack {
events: vec![MidiEvent {
time: TickTime::new(0, tpq).unwrap(),
origin: synthetic_origin(),
payload: MidiPayload::Meta(MetaEvent::EndOfTrack),
}],
}
}
fn canonical_format_zero_fixture(running_status: bool) -> Vec<u8> {
let track = if running_status {
vec![
0x00, 0xff, 0x51, 0x03, 0x07, 0xa1, 0x20, 0x00, 0x90, 60, 100, 0x78, 62, 96, 0x78,
0x80, 60, 0, 0x00, 62, 0, 0x00, 0xff, 0x2f, 0x00,
]
} else {
vec![
0x00, 0xff, 0x51, 0x03, 0x07, 0xa1, 0x20, 0x00, 0x90, 60, 100, 0x78, 0x90, 62, 96,
0x78, 0x80, 60, 0, 0x00, 0x80, 62, 0, 0x00, 0xff, 0x2f, 0x00,
]
};
wrap_track(SmfFormat::SingleTrack, 480, vec![track])
}
fn format_one_merge_fixture() -> Vec<u8> {
let track0 = vec![
0x00, 0xff, 0x51, 0x03, 0x07, 0xa1, 0x20, 0x78, 0x90, 60, 100, 0x78, 0x80, 60, 0, 0x00,
0xff, 0x2f, 0x00,
];
let track1 = vec![
0x3c, 0x91, 67, 110, 0x78, 0x81, 67, 0, 0x00, 0xff, 0x2f, 0x00,
];
wrap_track(SmfFormat::Simultaneous, 480, vec![track0, track1])
}
fn wrap_track(format: SmfFormat, tpq: u16, tracks: Vec<Vec<u8>>) -> Vec<u8> {
let format_u16 = match format {
SmfFormat::SingleTrack => 0u16,
SmfFormat::Simultaneous => 1u16,
SmfFormat::Independent => 2u16,
};
let mut bytes = Vec::new();
bytes.extend_from_slice(b"MThd");
bytes.extend_from_slice(&6u32.to_be_bytes());
bytes.extend_from_slice(&format_u16.to_be_bytes());
bytes.extend_from_slice(&(tracks.len() as u16).to_be_bytes());
bytes.extend_from_slice(&tpq.to_be_bytes());
for track in tracks {
bytes.extend_from_slice(b"MTrk");
bytes.extend_from_slice(&(track.len() as u32).to_be_bytes());
bytes.extend_from_slice(&track);
}
bytes
}