use oxideav_core::{Error, Result};
pub const MAX_VLQ_BYTES: usize = 4;
pub const MAX_EVENTS_PER_FILE: usize = 1_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SmfFormat {
SingleTrack,
MultiTrackSimultaneous,
MultiTrackIndependent,
}
impl SmfFormat {
fn from_u16(v: u16) -> Result<Self> {
match v {
0 => Ok(Self::SingleTrack),
1 => Ok(Self::MultiTrackSimultaneous),
2 => Ok(Self::MultiTrackIndependent),
other => Err(Error::invalid(format!(
"SMF: unknown format value {other} (expected 0, 1, or 2)",
))),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Division {
TicksPerQuarter(u16),
Smpte {
frames_per_second: u8,
ticks_per_frame: u8,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SmfHeader {
pub format: SmfFormat,
pub ntrks: u16,
pub division: Division,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrackEvent {
pub delta: u32,
pub kind: Event,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
Channel(ChannelMessage),
Sysex {
escape: bool,
data: Vec<u8>,
},
Meta(MetaEvent),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChannelMessage {
pub channel: u8,
pub body: ChannelBody,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChannelBody {
NoteOff { key: u8, velocity: u8 },
NoteOn { key: u8, velocity: u8 },
PolyAftertouch { key: u8, pressure: u8 },
ControlChange { controller: u8, value: u8 },
ProgramChange { program: u8 },
ChannelAftertouch { pressure: u8 },
PitchBend { value: u16 },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MetaEvent {
SequenceNumber(u16),
Text { kind: u8, text: Vec<u8> },
ChannelPrefix(u8),
Port(u8),
EndOfTrack,
Tempo(u32),
SmpteOffset {
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
subframes: u8,
},
TimeSignature {
numerator: u8,
denominator_pow2: u8,
clocks_per_click: u8,
notated_32nd_per_quarter: u8,
},
KeySignature { sharps_flats: i8, mode: u8 },
SequencerSpecific(Vec<u8>),
Unknown { type_byte: u8, data: Vec<u8> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Track {
pub events: Vec<TrackEvent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SmfFile {
pub header: SmfHeader,
pub tracks: Vec<Track>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimeSignatureChange {
pub tick: u64,
pub track: usize,
pub numerator: u8,
pub denominator_pow2: u8,
pub clocks_per_click: u8,
pub notated_32nd_per_quarter: u8,
}
impl TimeSignatureChange {
pub fn denominator(&self) -> u32 {
if self.denominator_pow2 >= 32 {
u32::MAX
} else {
1u32 << self.denominator_pow2
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TempoChange {
pub tick: u64,
pub track: usize,
pub microseconds_per_quarter_note: u32,
pub bpm: f64,
}
impl TempoChange {
pub fn new(tick: u64, track: usize, microseconds_per_quarter_note: u32) -> Self {
let bpm = if microseconds_per_quarter_note == 0 {
f64::INFINITY
} else {
60_000_000.0 / microseconds_per_quarter_note as f64
};
Self {
tick,
track,
microseconds_per_quarter_note,
bpm,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeySignatureChange {
pub tick: u64,
pub track: usize,
pub sharps_flats: i8,
pub mode: u8,
}
impl KeySignatureChange {
pub fn is_minor(&self) -> bool {
self.mode == 1
}
pub fn is_major(&self) -> bool {
self.mode == 0
}
pub fn tonic_name(&self) -> Option<&'static str> {
if !(-7..=7).contains(&self.sharps_flats) {
return None;
}
let idx = (self.sharps_flats + 7) as usize;
let names = match self.mode {
0 => MAJOR_TONICS,
1 => MINOR_TONICS,
_ => return None,
};
Some(names[idx])
}
pub fn name(&self) -> Option<&'static str> {
if !(-7..=7).contains(&self.sharps_flats) {
return None;
}
let idx = (self.sharps_flats + 7) as usize;
let table = match self.mode {
0 => MAJOR_KEY_NAMES,
1 => MINOR_KEY_NAMES,
_ => return None,
};
Some(table[idx])
}
}
const MAJOR_TONICS: [&str; 15] = [
"Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#", ];
const MINOR_TONICS: [&str; 15] = [
"Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "D#", "A#", ];
const MAJOR_KEY_NAMES: [&str; 15] = [
"Cb major", "Gb major", "Db major", "Ab major", "Eb major", "Bb major", "F major", "C major",
"G major", "D major", "A major", "E major", "B major", "F# major", "C# major",
];
const MINOR_KEY_NAMES: [&str; 15] = [
"Ab minor", "Eb minor", "Bb minor", "F minor", "C minor", "G minor", "D minor", "A minor",
"E minor", "B minor", "F# minor", "C# minor", "G# minor", "D# minor", "A# minor",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MarkerEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl MarkerEvent {
pub fn text_bytes(&self) -> &[u8] {
&self.text
}
pub fn text_lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.text)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LyricEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl LyricEvent {
pub fn text_bytes(&self) -> &[u8] {
&self.text
}
pub fn text_lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.text)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CueEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl CueEvent {
pub fn text_bytes(&self) -> &[u8] {
&self.text
}
pub fn text_lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.text)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrackNameEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl TrackNameEvent {
pub fn text_bytes(&self) -> &[u8] {
&self.text
}
pub fn text_lossy(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.text)
}
}
impl SmfFile {
pub fn tempo_map(&self) -> Vec<TempoChange> {
let mut out: Vec<TempoChange> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::Tempo(us_per_qn)) = &ev.kind {
out.push(TempoChange::new(abs, track_idx, *us_per_qn));
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn time_signatures(&self) -> Vec<TimeSignatureChange> {
let mut out: Vec<TimeSignatureChange> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::TimeSignature {
numerator,
denominator_pow2,
clocks_per_click,
notated_32nd_per_quarter,
}) = &ev.kind
{
out.push(TimeSignatureChange {
tick: abs,
track: track_idx,
numerator: *numerator,
denominator_pow2: *denominator_pow2,
clocks_per_click: *clocks_per_click,
notated_32nd_per_quarter: *notated_32nd_per_quarter,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn key_signatures(&self) -> Vec<KeySignatureChange> {
let mut out: Vec<KeySignatureChange> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::KeySignature { sharps_flats, mode }) = &ev.kind {
out.push(KeySignatureChange {
tick: abs,
track: track_idx,
sharps_flats: *sharps_flats,
mode: *mode,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn markers(&self) -> Vec<MarkerEvent> {
let mut out: Vec<MarkerEvent> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::Text { kind: 0x06, text }) = &ev.kind {
out.push(MarkerEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn lyrics(&self) -> Vec<LyricEvent> {
let mut out: Vec<LyricEvent> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::Text { kind: 0x05, text }) = &ev.kind {
out.push(LyricEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn cue_points(&self) -> Vec<CueEvent> {
let mut out: Vec<CueEvent> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::Text { kind: 0x07, text }) = &ev.kind {
out.push(CueEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn track_names(&self) -> Vec<TrackNameEvent> {
let mut out: Vec<TrackNameEvent> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for ev in &track.events {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Meta(MetaEvent::Text { kind: 0x03, text }) = &ev.kind {
out.push(TrackNameEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
}
pub fn parse(bytes: &[u8]) -> Result<SmfFile> {
let mut cursor = Cursor::new(bytes);
let header = parse_header(&mut cursor)?;
let mut tracks: Vec<Track> = Vec::new();
let mut total_events: usize = 0;
while !cursor.is_empty() {
let tag = cursor.take(4)?;
let chunk_len = read_u32_be(&mut cursor)? as usize;
if chunk_len > cursor.remaining() {
return Err(Error::invalid(format!(
"SMF: chunk '{}' declares {chunk_len} bytes but only {} remain",
fmt_tag(tag),
cursor.remaining(),
)));
}
let payload = cursor.take(chunk_len)?;
if tag == b"MTrk" {
let track = parse_track(payload, total_events)?;
total_events += track.events.len();
if total_events > MAX_EVENTS_PER_FILE {
return Err(Error::invalid(format!(
"SMF: cumulative event count {total_events} exceeds cap of \
{MAX_EVENTS_PER_FILE}",
)));
}
tracks.push(track);
}
}
if (tracks.len() as u16) != header.ntrks {
}
Ok(SmfFile { header, tracks })
}
fn parse_header(cursor: &mut Cursor<'_>) -> Result<SmfHeader> {
let tag = cursor.take(4)?;
if tag != b"MThd" {
return Err(Error::invalid(format!(
"SMF: expected 'MThd' header chunk, got '{}'",
fmt_tag(tag),
)));
}
let chunk_len = read_u32_be(cursor)? as usize;
if chunk_len < 6 {
return Err(Error::invalid(format!(
"SMF: MThd chunk length is {chunk_len}, expected at least 6",
)));
}
if chunk_len > cursor.remaining() {
return Err(Error::invalid(format!(
"SMF: MThd declares {chunk_len} bytes but only {} remain",
cursor.remaining(),
)));
}
let body = cursor.take(chunk_len)?;
let format = SmfFormat::from_u16(u16::from_be_bytes([body[0], body[1]]))?;
let ntrks = u16::from_be_bytes([body[2], body[3]]);
let div_raw = u16::from_be_bytes([body[4], body[5]]);
let division = if div_raw & 0x8000 == 0 {
if div_raw == 0 {
return Err(Error::invalid(
"SMF: division of 0 ticks-per-quarter is not legal",
));
}
Division::TicksPerQuarter(div_raw)
} else {
let upper = (div_raw >> 8) as i8;
let frames_per_second = (-(upper as i16)) as u8;
let ticks_per_frame = (div_raw & 0xFF) as u8;
if !matches!(frames_per_second, 24 | 25 | 29 | 30) {
return Err(Error::invalid(format!(
"SMF: SMPTE frame rate {frames_per_second} not in {{24, 25, 29, 30}}",
)));
}
Division::Smpte {
frames_per_second,
ticks_per_frame,
}
};
Ok(SmfHeader {
format,
ntrks,
division,
})
}
fn parse_track(payload: &[u8], events_so_far: usize) -> Result<Track> {
let mut cursor = Cursor::new(payload);
let mut events: Vec<TrackEvent> = Vec::new();
let mut running_status: Option<u8> = None;
let mut local_total = events_so_far;
while !cursor.is_empty() {
let delta = read_vlq(&mut cursor)?;
let evt = read_event(&mut cursor, &mut running_status)?;
let is_eot = matches!(&evt, Event::Meta(MetaEvent::EndOfTrack));
events.push(TrackEvent { delta, kind: evt });
local_total = local_total.saturating_add(1);
if local_total > MAX_EVENTS_PER_FILE {
return Err(Error::invalid(format!(
"SMF: cumulative event count {local_total} exceeds cap of \
{MAX_EVENTS_PER_FILE}",
)));
}
if is_eot {
break;
}
}
Ok(Track { events })
}
fn read_event(cursor: &mut Cursor<'_>, running: &mut Option<u8>) -> Result<Event> {
let first = cursor.peek_u8()?;
if first == 0xFF {
cursor.advance(1)?;
let type_byte = cursor.read_u8()?;
let len = read_vlq(cursor)? as usize;
if len > cursor.remaining() {
return Err(Error::invalid(format!(
"SMF: meta event 0x{type_byte:02X} declares {len} bytes but only {} remain",
cursor.remaining(),
)));
}
let data = cursor.take(len)?;
*running = None;
Ok(Event::Meta(parse_meta(type_byte, data)?))
} else if first == 0xF0 || first == 0xF7 {
cursor.advance(1)?;
let len = read_vlq(cursor)? as usize;
if len > cursor.remaining() {
return Err(Error::invalid(format!(
"SMF: sysex 0x{first:02X} declares {len} bytes but only {} remain",
cursor.remaining(),
)));
}
let data = cursor.take(len)?.to_vec();
*running = None;
Ok(Event::Sysex {
escape: first == 0xF7,
data,
})
} else if first & 0x80 != 0 {
cursor.advance(1)?;
if first >= 0xF1 {
return Err(Error::invalid(format!(
"SMF: status byte 0x{first:02X} is System Common/Real-Time, \
not legal inside an MTrk chunk",
)));
}
*running = Some(first);
read_channel_message(cursor, first)
} else {
let status = running.ok_or_else(|| {
Error::invalid(format!(
"SMF: data byte 0x{first:02X} appeared without a prior status byte \
(no running status to inherit)",
))
})?;
read_channel_message(cursor, status)
}
}
fn read_channel_message(cursor: &mut Cursor<'_>, status: u8) -> Result<Event> {
let channel = status & 0x0F;
let kind = status & 0xF0;
let body = match kind {
0x80 => {
let key = cursor.read_data_byte()?;
let velocity = cursor.read_data_byte()?;
ChannelBody::NoteOff { key, velocity }
}
0x90 => {
let key = cursor.read_data_byte()?;
let velocity = cursor.read_data_byte()?;
ChannelBody::NoteOn { key, velocity }
}
0xA0 => {
let key = cursor.read_data_byte()?;
let pressure = cursor.read_data_byte()?;
ChannelBody::PolyAftertouch { key, pressure }
}
0xB0 => {
let controller = cursor.read_data_byte()?;
let value = cursor.read_data_byte()?;
ChannelBody::ControlChange { controller, value }
}
0xC0 => {
let program = cursor.read_data_byte()?;
ChannelBody::ProgramChange { program }
}
0xD0 => {
let pressure = cursor.read_data_byte()?;
ChannelBody::ChannelAftertouch { pressure }
}
0xE0 => {
let lsb = cursor.read_data_byte()? as u16;
let msb = cursor.read_data_byte()? as u16;
ChannelBody::PitchBend {
value: (msb << 7) | lsb,
}
}
_ => unreachable!("status nibble {kind:02X} is not a channel-voice message"),
};
Ok(Event::Channel(ChannelMessage { channel, body }))
}
fn parse_meta(type_byte: u8, data: &[u8]) -> Result<MetaEvent> {
Ok(match type_byte {
0x00 if data.len() == 2 => {
MetaEvent::SequenceNumber(u16::from_be_bytes([data[0], data[1]]))
}
0x01..=0x0F => MetaEvent::Text {
kind: type_byte,
text: data.to_vec(),
},
0x20 if data.len() == 1 => MetaEvent::ChannelPrefix(data[0]),
0x21 if data.len() == 1 => MetaEvent::Port(data[0]),
0x2F if data.is_empty() => MetaEvent::EndOfTrack,
0x51 if data.len() == 3 => {
MetaEvent::Tempo(((data[0] as u32) << 16) | ((data[1] as u32) << 8) | (data[2] as u32))
}
0x54 if data.len() == 5 => MetaEvent::SmpteOffset {
hours: data[0],
minutes: data[1],
seconds: data[2],
frames: data[3],
subframes: data[4],
},
0x58 if data.len() == 4 => MetaEvent::TimeSignature {
numerator: data[0],
denominator_pow2: data[1],
clocks_per_click: data[2],
notated_32nd_per_quarter: data[3],
},
0x59 if data.len() == 2 => MetaEvent::KeySignature {
sharps_flats: data[0] as i8,
mode: data[1],
},
0x7F => MetaEvent::SequencerSpecific(data.to_vec()),
_ => MetaEvent::Unknown {
type_byte,
data: data.to_vec(),
},
})
}
struct Cursor<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len() - self.pos
}
fn is_empty(&self) -> bool {
self.pos >= self.bytes.len()
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
if self.remaining() < n {
return Err(Error::invalid(format!(
"SMF: short read — wanted {n} bytes, {} remain",
self.remaining()
)));
}
let s = &self.bytes[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn read_u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
fn read_data_byte(&mut self) -> Result<u8> {
let b = self.read_u8()?;
if b & 0x80 != 0 {
return Err(Error::invalid(format!(
"SMF: expected data byte (high bit clear), got 0x{b:02X}",
)));
}
Ok(b)
}
fn peek_u8(&self) -> Result<u8> {
if self.is_empty() {
return Err(Error::invalid("SMF: short read — wanted 1 byte, 0 remain"));
}
Ok(self.bytes[self.pos])
}
fn advance(&mut self, n: usize) -> Result<()> {
if self.remaining() < n {
return Err(Error::invalid(format!(
"SMF: short advance — wanted {n} bytes, {} remain",
self.remaining()
)));
}
self.pos += n;
Ok(())
}
}
fn read_u32_be(cursor: &mut Cursor<'_>) -> Result<u32> {
let s = cursor.take(4)?;
Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
}
fn read_vlq(cursor: &mut Cursor<'_>) -> Result<u32> {
let mut value: u32 = 0;
for i in 0..MAX_VLQ_BYTES {
let b = cursor.read_u8()?;
value = (value << 7) | ((b & 0x7F) as u32);
if b & 0x80 == 0 {
return Ok(value);
}
if i == MAX_VLQ_BYTES - 1 {
return Err(Error::invalid(format!(
"SMF: VLQ exceeded {MAX_VLQ_BYTES}-byte cap (continuation bit set on final byte)",
)));
}
}
unreachable!("loop returns or errors before this point");
}
fn fmt_tag(tag: &[u8]) -> String {
String::from_utf8_lossy(tag).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn encode_vlq(mut v: u32) -> Vec<u8> {
let mut buf = vec![v & 0x7F];
v >>= 7;
while v != 0 {
buf.push((v & 0x7F) | 0x80);
v >>= 7;
}
buf.into_iter().rev().map(|b| b as u8).collect()
}
fn header_chunk(format: u16, ntrks: u16, division: u16) -> Vec<u8> {
let mut b = vec![];
b.extend_from_slice(b"MThd");
b.extend_from_slice(&6u32.to_be_bytes());
b.extend_from_slice(&format.to_be_bytes());
b.extend_from_slice(&ntrks.to_be_bytes());
b.extend_from_slice(&division.to_be_bytes());
b
}
fn track_chunk(events: &[u8]) -> Vec<u8> {
let mut b = vec![];
b.extend_from_slice(b"MTrk");
b.extend_from_slice(&(events.len() as u32).to_be_bytes());
b.extend_from_slice(events);
b
}
#[test]
fn vlq_one_byte() {
let mut c = Cursor::new(&[0x00]);
assert_eq!(read_vlq(&mut c).unwrap(), 0);
let mut c = Cursor::new(&[0x40]);
assert_eq!(read_vlq(&mut c).unwrap(), 0x40);
let mut c = Cursor::new(&[0x7F]);
assert_eq!(read_vlq(&mut c).unwrap(), 0x7F);
}
#[test]
fn vlq_multi_byte() {
let cases: &[(u32, &[u8])] = &[
(0x80, &[0x81, 0x00]),
(0x2000, &[0xC0, 0x00]),
(0x3FFF, &[0xFF, 0x7F]),
(0x10_0000, &[0xC0, 0x80, 0x00]),
(0x1F_FFFF, &[0xFF, 0xFF, 0x7F]),
(0x20_0000, &[0x81, 0x80, 0x80, 0x00]),
(0x0FFF_FFFF, &[0xFF, 0xFF, 0xFF, 0x7F]),
];
for (v, bytes) in cases {
let mut c = Cursor::new(bytes);
assert_eq!(
read_vlq(&mut c).unwrap(),
*v,
"decode VLQ {v:#x} from {bytes:?}",
);
assert_eq!(encode_vlq(*v), bytes.to_vec(), "round-trip VLQ {v:#x}");
}
}
#[test]
fn vlq_rejects_5_byte() {
let mut c = Cursor::new(&[0xFF, 0xFF, 0xFF, 0xFF, 0x7F]);
let err = read_vlq(&mut c).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn header_format_0_ticks_per_quarter() {
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&[0x00, 0xFF, 0x2F, 0x00]));
let smf = parse(&blob).unwrap();
assert_eq!(smf.header.format, SmfFormat::SingleTrack);
assert_eq!(smf.header.ntrks, 1);
assert_eq!(smf.header.division, Division::TicksPerQuarter(480));
assert_eq!(smf.tracks.len(), 1);
assert_eq!(smf.tracks[0].events.len(), 1);
assert!(matches!(
smf.tracks[0].events[0].kind,
Event::Meta(MetaEvent::EndOfTrack)
));
}
#[test]
fn header_smpte_division() {
let div = u16::from_be_bytes([0xE7, 0x28]);
let mut blob = header_chunk(0, 1, div);
blob.extend(track_chunk(&[0x00, 0xFF, 0x2F, 0x00]));
let smf = parse(&blob).unwrap();
assert_eq!(
smf.header.division,
Division::Smpte {
frames_per_second: 25,
ticks_per_frame: 40,
},
);
}
#[test]
fn type_0_single_track_with_note_pair_and_tempo() {
let mut events = vec![];
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0x80, 0x3C, 0x40]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert_eq!(smf.header.format, SmfFormat::SingleTrack);
let evs = &smf.tracks[0].events;
assert_eq!(evs.len(), 5);
assert!(matches!(
evs[0].kind,
Event::Meta(MetaEvent::Tempo(500_000))
));
assert!(matches!(
evs[1].kind,
Event::Meta(MetaEvent::TimeSignature {
numerator: 4,
denominator_pow2: 2,
clocks_per_click: 24,
notated_32nd_per_quarter: 8,
})
));
match &evs[2].kind {
Event::Channel(ChannelMessage {
channel: 0,
body:
ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
}) => {}
other => panic!("unexpected event #2: {other:?}"),
}
assert_eq!(evs[3].delta, 480);
match &evs[3].kind {
Event::Channel(ChannelMessage {
channel: 0,
body:
ChannelBody::NoteOff {
key: 60,
velocity: 0x40,
},
}) => {}
other => panic!("unexpected event #3: {other:?}"),
}
assert!(matches!(evs[4].kind, Event::Meta(MetaEvent::EndOfTrack)));
}
#[test]
fn running_status_is_honoured() {
let events: &[u8] = &[
0x00, 0x90, 0x3C, 0x64, 0x0A, 0x3D, 0x64, 0x0A, 0x3E, 0x64, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
let evs = &smf.tracks[0].events;
assert_eq!(evs.len(), 4);
for (i, &expected_key) in [60u8, 61, 62].iter().enumerate() {
match &evs[i].kind {
Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn { key, velocity: 100 },
}) if *key == expected_key => {}
other => panic!("event #{i}: expected NoteOn key={expected_key}, got {other:?}"),
}
}
}
#[test]
fn type_1_multi_track() {
let track1: &[u8] = &[
0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20, 0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18,
0x08, 0x00, 0xFF, 0x2F, 0x00,
];
let mut track2 = vec![0x00, 0x91, 0x40, 0x5A];
track2.extend_from_slice(&encode_vlq(0x2000));
track2.extend_from_slice(&[0x81, 0x40, 0x40, 0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(track1));
blob.extend(track_chunk(&track2));
let smf = parse(&blob).unwrap();
assert_eq!(smf.header.format, SmfFormat::MultiTrackSimultaneous);
assert_eq!(smf.tracks.len(), 2);
assert_eq!(smf.tracks[0].events.len(), 3);
assert_eq!(smf.tracks[1].events.len(), 3);
match &smf.tracks[1].events[0].kind {
Event::Channel(ChannelMessage {
channel: 1,
body:
ChannelBody::NoteOn {
key: 64,
velocity: 90,
},
}) => {}
other => panic!("track 2 event 0 unexpected: {other:?}"),
}
assert_eq!(smf.tracks[1].events[1].delta, 0x2000);
}
#[test]
fn unknown_chunk_is_skipped() {
let mut blob = header_chunk(0, 1, 96);
blob.extend_from_slice(b"XYZW");
blob.extend_from_slice(&3u32.to_be_bytes());
blob.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
blob.extend(track_chunk(&[0x00, 0xFF, 0x2F, 0x00]));
let smf = parse(&blob).unwrap();
assert_eq!(smf.tracks.len(), 1);
}
#[test]
fn meta_text_events() {
let mut events = vec![0x00, 0xFF, 0x03, 0x06];
events.extend_from_slice(b"Track1");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
match &smf.tracks[0].events[0].kind {
Event::Meta(MetaEvent::Text { kind: 0x03, text }) => {
assert_eq!(text, b"Track1");
}
other => panic!("expected text event, got {other:?}"),
}
}
#[test]
fn pitch_bend_combines_lsb_msb() {
let events = [0x00, 0xE0, 0x00, 0x40, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert_eq!(smf.tracks[0].events[0].delta, 0);
match &smf.tracks[0].events[0].kind {
Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::PitchBend { value: 0x2000 },
}) => {}
other => panic!("expected pitch bend 0x2000, got {other:?}"),
}
}
#[test]
fn sysex_event() {
let events = [
0x00, 0xF0, 0x04, 0x7E, 0x7F, 0x09, 0x01, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
match &smf.tracks[0].events[0].kind {
Event::Sysex {
escape: false,
data,
} => assert_eq!(data, &[0x7E, 0x7F, 0x09, 0x01]),
other => panic!("expected sysex, got {other:?}"),
}
}
#[test]
fn rejects_chunk_length_overrun() {
let mut blob = vec![];
blob.extend_from_slice(b"MThd");
blob.extend_from_slice(&60u32.to_be_bytes());
blob.extend_from_slice(&[0; 6]);
let err = parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_meta_length_overrun() {
let events: &[u8] = &[0x00, 0xFF, 0x03, 0xFF, 0x7F];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let err = parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_data_byte_without_status() {
let events: &[u8] = &[0x00, 0x40, 0x40];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let err = parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn rejects_system_common_in_track() {
let events: &[u8] = &[0x00, 0xF1, 0x40];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let err = parse(&blob).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn time_signatures_empty_when_no_meta_event_present() {
let events: &[u8] = &[
0x00, 0x90, 0x3C, 0x64, 0x40, 0x80, 0x3C, 0x40, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
assert!(smf.time_signatures().is_empty());
}
#[test]
fn time_signatures_single_change_at_tick_zero() {
let events: &[u8] = &[
0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
let sigs = smf.time_signatures();
assert_eq!(sigs.len(), 1);
let ts = sigs[0];
assert_eq!(ts.tick, 0);
assert_eq!(ts.track, 0);
assert_eq!(ts.numerator, 4);
assert_eq!(ts.denominator_pow2, 2);
assert_eq!(ts.denominator(), 4);
assert_eq!(ts.clocks_per_click, 24);
assert_eq!(ts.notated_32nd_per_quarter, 8);
}
#[test]
fn time_signatures_multiple_changes_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x58, 0x04, 0x03, 0x02, 0x18, 0x08]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x58, 0x04, 0x06, 0x03, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sigs = smf.time_signatures();
assert_eq!(sigs.len(), 3);
assert_eq!(sigs[0].tick, 0);
assert_eq!((sigs[0].numerator, sigs[0].denominator()), (4, 4));
assert_eq!(sigs[1].tick, 480);
assert_eq!((sigs[1].numerator, sigs[1].denominator()), (3, 4));
assert_eq!(sigs[2].tick, 960);
assert_eq!((sigs[2].numerator, sigs[2].denominator()), (6, 8));
}
#[test]
fn time_signatures_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
t0.extend_from_slice(&encode_vlq(1920));
t0.extend_from_slice(&[0xFF, 0x58, 0x04, 0x07, 0x03, 0x18, 0x08]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x58, 0x04, 0x03, 0x02, 0x18, 0x08]);
t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sigs = smf.time_signatures();
assert_eq!(sigs.len(), 3);
assert_eq!((sigs[0].tick, sigs[0].track, sigs[0].numerator), (0, 0, 4));
assert_eq!(
(sigs[1].tick, sigs[1].track, sigs[1].numerator),
(960, 1, 3),
);
assert_eq!(
(sigs[2].tick, sigs[2].track, sigs[2].numerator),
(1920, 0, 7),
);
}
#[test]
fn time_signatures_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x58, 0x04, 0x02, 0x02, 0x18, 0x08]); t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x58, 0x04, 0x05, 0x02, 0x18, 0x08]); t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sigs = smf.time_signatures();
assert_eq!(sigs.len(), 2);
assert_eq!(sigs[0].tick, 240);
assert_eq!(sigs[0].track, 0);
assert_eq!(sigs[0].numerator, 2);
assert_eq!(sigs[1].tick, 240);
assert_eq!(sigs[1].track, 1);
assert_eq!(sigs[1].numerator, 5);
}
#[test]
fn time_signature_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x80, 0x3C, 0x40]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x58, 0x04, 0x0C, 0x03, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sigs = smf.time_signatures();
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].tick, 480);
assert_eq!((sigs[0].numerator, sigs[0].denominator()), (12, 8));
}
#[test]
fn time_signature_denominator_saturates_on_huge_pow2() {
let ts = TimeSignatureChange {
tick: 0,
track: 0,
numerator: 4,
denominator_pow2: 250,
clocks_per_click: 24,
notated_32nd_per_quarter: 8,
};
assert_eq!(ts.denominator(), u32::MAX);
}
#[test]
fn tempo_map_empty_when_no_meta_event_present() {
let events: &[u8] = &[
0x00, 0x90, 0x3C, 0x64, 0x40, 0x80, 0x3C, 0x40, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
assert!(smf.tempo_map().is_empty());
}
#[test]
fn tempo_map_single_change_at_tick_zero() {
let events: &[u8] = &[
0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
let map = smf.tempo_map();
assert_eq!(map.len(), 1);
let tc = map[0];
assert_eq!(tc.tick, 0);
assert_eq!(tc.track, 0);
assert_eq!(tc.microseconds_per_quarter_note, 500_000);
assert!((tc.bpm - 120.0).abs() < 1e-9);
}
#[test]
fn tempo_map_multiple_changes_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x51, 0x03, 0x03, 0xD0, 0x90]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x51, 0x03, 0x0F, 0x42, 0x40]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let map = smf.tempo_map();
assert_eq!(map.len(), 3);
assert_eq!(map[0].tick, 0);
assert_eq!(map[0].microseconds_per_quarter_note, 500_000);
assert!((map[0].bpm - 120.0).abs() < 1e-9);
assert_eq!(map[1].tick, 480);
assert_eq!(map[1].microseconds_per_quarter_note, 250_000);
assert!((map[1].bpm - 240.0).abs() < 1e-9);
assert_eq!(map[2].tick, 960);
assert_eq!(map[2].microseconds_per_quarter_note, 1_000_000);
assert!((map[2].bpm - 60.0).abs() < 1e-9);
}
#[test]
fn tempo_map_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]); t0.extend_from_slice(&encode_vlq(1920));
t0.extend_from_slice(&[0xFF, 0x51, 0x03, 0x0A, 0x2C, 0x2B]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x51, 0x03, 0x03, 0xD0, 0x90]); t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let map = smf.tempo_map();
assert_eq!(map.len(), 3);
assert_eq!(
(
map[0].tick,
map[0].track,
map[0].microseconds_per_quarter_note
),
(0, 0, 500_000)
);
assert_eq!(
(
map[1].tick,
map[1].track,
map[1].microseconds_per_quarter_note
),
(960, 1, 250_000)
);
assert_eq!(
(
map[2].tick,
map[2].track,
map[2].microseconds_per_quarter_note
),
(1920, 0, 666_667)
);
}
#[test]
fn tempo_map_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]); t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x51, 0x03, 0x03, 0xD0, 0x90]); t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let map = smf.tempo_map();
assert_eq!(map.len(), 2);
assert_eq!(map[0].tick, 240);
assert_eq!(map[0].track, 0);
assert_eq!(map[0].microseconds_per_quarter_note, 500_000);
assert_eq!(map[1].tick, 240);
assert_eq!(map[1].track, 1);
assert_eq!(map[1].microseconds_per_quarter_note, 250_000);
}
#[test]
fn tempo_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x80, 0x3C, 0x40]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x51, 0x03, 0x06, 0x1A, 0x80]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let map = smf.tempo_map();
assert_eq!(map.len(), 1);
assert_eq!(map[0].tick, 480);
assert_eq!(map[0].microseconds_per_quarter_note, 400_000);
assert!((map[0].bpm - 150.0).abs() < 1e-9);
}
#[test]
fn tempo_change_zero_us_maps_to_infinite_bpm_without_panic() {
let tc = TempoChange::new(0, 0, 0);
assert_eq!(tc.microseconds_per_quarter_note, 0);
assert!(tc.bpm.is_infinite());
assert!(tc.bpm.is_sign_positive());
}
#[test]
fn key_signatures_empty_when_no_meta_event_present() {
let events: &[u8] = &[
0x00, 0x90, 0x3C, 0x64, 0x40, 0x80, 0x3C, 0x40, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
assert!(smf.key_signatures().is_empty());
}
#[test]
fn key_signatures_single_change_at_tick_zero_c_major() {
let events: &[u8] = &[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(events));
let smf = parse(&blob).unwrap();
let keys = smf.key_signatures();
assert_eq!(keys.len(), 1);
let ks = keys[0];
assert_eq!(ks.tick, 0);
assert_eq!(ks.track, 0);
assert_eq!(ks.sharps_flats, 0);
assert_eq!(ks.mode, 0);
assert!(ks.is_major());
assert!(!ks.is_minor());
assert_eq!(ks.tonic_name(), Some("C"));
assert_eq!(ks.name(), Some("C major"));
}
#[test]
fn key_signatures_multiple_changes_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x59, 0x02, 0x03, 0x00]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x59, 0x02, 0xFD, 0x01]); events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let keys = smf.key_signatures();
assert_eq!(keys.len(), 3);
assert_eq!(keys[0].tick, 0);
assert_eq!((keys[0].sharps_flats, keys[0].mode), (0, 0));
assert_eq!(keys[0].name(), Some("C major"));
assert_eq!(keys[1].tick, 480);
assert_eq!((keys[1].sharps_flats, keys[1].mode), (3, 0));
assert_eq!(keys[1].name(), Some("A major"));
assert_eq!(keys[2].tick, 960);
assert_eq!((keys[2].sharps_flats, keys[2].mode), (-3, 1));
assert_eq!(keys[2].name(), Some("C minor"));
assert!(keys[2].is_minor());
}
#[test]
fn key_signatures_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
t0.extend_from_slice(&encode_vlq(1920));
t0.extend_from_slice(&[0xFF, 0x59, 0x02, 0x04, 0x00]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x59, 0x02, 0xFF, 0x01]); t1.extend_from_slice(&encode_vlq(960));
t1.extend_from_slice(&[0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let keys = smf.key_signatures();
assert_eq!(keys.len(), 3);
assert_eq!(
(
keys[0].tick,
keys[0].track,
keys[0].sharps_flats,
keys[0].mode
),
(0, 0, 0, 0)
);
assert_eq!(
(
keys[1].tick,
keys[1].track,
keys[1].sharps_flats,
keys[1].mode
),
(960, 1, -1, 1)
);
assert_eq!(keys[1].name(), Some("D minor"));
assert_eq!(
(
keys[2].tick,
keys[2].track,
keys[2].sharps_flats,
keys[2].mode
),
(1920, 0, 4, 0)
);
assert_eq!(keys[2].name(), Some("E major"));
}
#[test]
fn key_signatures_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x59, 0x02, 0x02, 0x00]); t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x59, 0x02, 0xFE, 0x01]); t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let keys = smf.key_signatures();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].tick, 240);
assert_eq!(keys[0].track, 0);
assert_eq!(keys[0].name(), Some("D major"));
assert_eq!(keys[1].tick, 240);
assert_eq!(keys[1].track, 1);
assert_eq!(keys[1].name(), Some("G minor"));
}
#[test]
fn key_signature_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x80, 0x3C, 0x40]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x59, 0x02, 0x06, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let keys = smf.key_signatures();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].tick, 480);
assert_eq!(keys[0].name(), Some("F# major"));
}
#[test]
fn key_signature_tonic_table_covers_full_circle_of_fifths() {
let expected_major = [
(-7i8, "Cb major"),
(-6, "Gb major"),
(-5, "Db major"),
(-4, "Ab major"),
(-3, "Eb major"),
(-2, "Bb major"),
(-1, "F major"),
(0, "C major"),
(1, "G major"),
(2, "D major"),
(3, "A major"),
(4, "E major"),
(5, "B major"),
(6, "F# major"),
(7, "C# major"),
];
for (sf, name) in expected_major {
let ks = KeySignatureChange {
tick: 0,
track: 0,
sharps_flats: sf,
mode: 0,
};
assert_eq!(ks.name(), Some(name), "major sf={sf}");
}
let expected_minor = [
(-7i8, "Ab minor"),
(-6, "Eb minor"),
(-5, "Bb minor"),
(-4, "F minor"),
(-3, "C minor"),
(-2, "G minor"),
(-1, "D minor"),
(0, "A minor"),
(1, "E minor"),
(2, "B minor"),
(3, "F# minor"),
(4, "C# minor"),
(5, "G# minor"),
(6, "D# minor"),
(7, "A# minor"),
];
for (sf, name) in expected_minor {
let ks = KeySignatureChange {
tick: 0,
track: 0,
sharps_flats: sf,
mode: 1,
};
assert_eq!(ks.name(), Some(name), "minor sf={sf}");
}
}
#[test]
fn key_signature_out_of_range_or_unknown_mode_yields_none() {
let ks = KeySignatureChange {
tick: 0,
track: 0,
sharps_flats: 8,
mode: 0,
};
assert_eq!(ks.tonic_name(), None);
assert_eq!(ks.name(), None);
let ks = KeySignatureChange {
tick: 0,
track: 0,
sharps_flats: -8,
mode: 1,
};
assert_eq!(ks.tonic_name(), None);
assert_eq!(ks.name(), None);
let ks = KeySignatureChange {
tick: 0,
track: 0,
sharps_flats: 0,
mode: 2,
};
assert_eq!(ks.tonic_name(), None);
assert_eq!(ks.name(), None);
assert!(!ks.is_major());
assert!(!ks.is_minor());
}
#[test]
fn markers_empty_when_no_meta_event_present() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert!(smf.markers().is_empty());
}
#[test]
fn markers_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x06, 0x05];
events.extend_from_slice(b"Verse");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 1);
assert_eq!(mk[0].tick, 0);
assert_eq!(mk[0].track, 0);
assert_eq!(mk[0].text_bytes(), b"Verse");
assert_eq!(mk[0].text_lossy(), "Verse");
}
#[test]
fn markers_multiple_events_within_one_track_are_in_order() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x06, 0x05];
events.extend_from_slice(b"Intro");
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x06, 0x05]);
events.extend_from_slice(b"Verse");
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x06, 0x06]);
events.extend_from_slice(b"Chorus");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 3);
assert_eq!(mk[0].tick, 0);
assert_eq!(mk[0].text_bytes(), b"Intro");
assert_eq!(mk[1].tick, 240);
assert_eq!(mk[1].text_bytes(), b"Verse");
assert_eq!(mk[2].tick, 480);
assert_eq!(mk[2].text_bytes(), b"Chorus");
}
#[test]
fn markers_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x06, 0x01]);
t0.extend_from_slice(b"A");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(120));
t1.extend_from_slice(&[0xFF, 0x06, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x06, 0x01]);
t1.extend_from_slice(b"C");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 3);
assert_eq!(mk[0].tick, 120);
assert_eq!(mk[0].track, 1);
assert_eq!(mk[0].text_bytes(), b"B");
assert_eq!(mk[1].tick, 240);
assert_eq!(mk[1].track, 0);
assert_eq!(mk[1].text_bytes(), b"A");
assert_eq!(mk[2].tick, 360);
assert_eq!(mk[2].track, 1);
assert_eq!(mk[2].text_bytes(), b"C");
}
#[test]
fn markers_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x06, 0x04]);
t0.extend_from_slice(b"trk0");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x06, 0x04]);
t1.extend_from_slice(b"trk1");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 2);
assert_eq!(mk[0].tick, 240);
assert_eq!(mk[0].track, 0);
assert_eq!(mk[0].text_bytes(), b"trk0");
assert_eq!(mk[1].tick, 240);
assert_eq!(mk[1].track, 1);
assert_eq!(mk[1].text_bytes(), b"trk1");
}
#[test]
fn markers_filter_excludes_other_text_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x06];
events.extend_from_slice(b"Track1");
events.extend_from_slice(&[0x00, 0xFF, 0x06, 0x04]);
events.extend_from_slice(b"Mark");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x05]);
events.extend_from_slice(b"lyric");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 1);
assert_eq!(mk[0].text_bytes(), b"Mark");
}
#[test]
fn marker_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x06, 0x01]);
events.extend_from_slice(b"X");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let mk = smf.markers();
assert_eq!(mk.len(), 1);
assert_eq!(mk[0].tick, 240);
assert_eq!(mk[0].text_bytes(), b"X");
}
#[test]
fn marker_text_lossy_replaces_invalid_utf8() {
let mk = MarkerEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = mk.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(mk.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn lyrics_empty_when_no_meta_event_present() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert!(smf.lyrics().is_empty());
}
#[test]
fn lyrics_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x05, 0x04];
events.extend_from_slice(b"love");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 1);
assert_eq!(ly[0].tick, 0);
assert_eq!(ly[0].track, 0);
assert_eq!(ly[0].text_bytes(), b"love");
assert_eq!(ly[0].text_lossy(), "love");
}
#[test]
fn lyrics_multiple_syllables_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x04]);
events.extend_from_slice(b"Twin");
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x05, 0x04]);
events.extend_from_slice(b"kle ");
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x05, 0x04]);
events.extend_from_slice(b"twin");
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x05, 0x04]);
events.extend_from_slice(b"kle ");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 4);
assert_eq!(ly[0].tick, 0);
assert_eq!(ly[0].text_bytes(), b"Twin");
assert_eq!(ly[1].tick, 120);
assert_eq!(ly[1].text_bytes(), b"kle ");
assert_eq!(ly[2].tick, 240);
assert_eq!(ly[2].text_bytes(), b"twin");
assert_eq!(ly[3].tick, 360);
assert_eq!(ly[3].text_bytes(), b"kle ");
}
#[test]
fn lyrics_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x05, 0x01]);
t0.extend_from_slice(b"A");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(120));
t1.extend_from_slice(&[0xFF, 0x05, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x05, 0x01]);
t1.extend_from_slice(b"C");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 3);
assert_eq!(ly[0].tick, 120);
assert_eq!(ly[0].track, 1);
assert_eq!(ly[0].text_bytes(), b"B");
assert_eq!(ly[1].tick, 240);
assert_eq!(ly[1].track, 0);
assert_eq!(ly[1].text_bytes(), b"A");
assert_eq!(ly[2].tick, 360);
assert_eq!(ly[2].track, 1);
assert_eq!(ly[2].text_bytes(), b"C");
}
#[test]
fn lyrics_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x05, 0x04]);
t0.extend_from_slice(b"trk0");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x05, 0x04]);
t1.extend_from_slice(b"trk1");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 2);
assert_eq!(ly[0].tick, 240);
assert_eq!(ly[0].track, 0);
assert_eq!(ly[0].text_bytes(), b"trk0");
assert_eq!(ly[1].tick, 240);
assert_eq!(ly[1].track, 1);
assert_eq!(ly[1].text_bytes(), b"trk1");
}
#[test]
fn lyrics_filter_excludes_other_text_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x06];
events.extend_from_slice(b"Track1");
events.extend_from_slice(&[0x00, 0xFF, 0x06, 0x04]);
events.extend_from_slice(b"Mark");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x04]);
events.extend_from_slice(b"syll");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 1);
assert_eq!(ly[0].text_bytes(), b"syll");
}
#[test]
fn lyric_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ly = smf.lyrics();
assert_eq!(ly.len(), 1);
assert_eq!(ly[0].tick, 240);
assert_eq!(ly[0].text_bytes(), b"la");
}
#[test]
fn lyric_text_lossy_replaces_invalid_utf8() {
let ly = LyricEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = ly.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(ly.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn cue_points_empty_when_no_meta_event_present() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert!(smf.cue_points().is_empty());
}
#[test]
fn cue_points_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x07, 0x05];
events.extend_from_slice(b"Scene");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].tick, 0);
assert_eq!(cp[0].track, 0);
assert_eq!(cp[0].text_bytes(), b"Scene");
assert_eq!(cp[0].text_lossy(), "Scene");
}
#[test]
fn cue_points_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x07, 0x05]);
events.extend_from_slice(b"Intro");
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x07, 0x06]);
events.extend_from_slice(b"SceneA");
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x07, 0x06]);
events.extend_from_slice(b"SceneB");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 3);
assert_eq!(cp[0].tick, 0);
assert_eq!(cp[0].text_bytes(), b"Intro");
assert_eq!(cp[1].tick, 240);
assert_eq!(cp[1].text_bytes(), b"SceneA");
assert_eq!(cp[2].tick, 480);
assert_eq!(cp[2].text_bytes(), b"SceneB");
}
#[test]
fn cue_points_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x07, 0x01]);
t0.extend_from_slice(b"A");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(120));
t1.extend_from_slice(&[0xFF, 0x07, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x07, 0x01]);
t1.extend_from_slice(b"C");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 3);
assert_eq!(cp[0].tick, 120);
assert_eq!(cp[0].track, 1);
assert_eq!(cp[0].text_bytes(), b"B");
assert_eq!(cp[1].tick, 240);
assert_eq!(cp[1].track, 0);
assert_eq!(cp[1].text_bytes(), b"A");
assert_eq!(cp[2].tick, 360);
assert_eq!(cp[2].track, 1);
assert_eq!(cp[2].text_bytes(), b"C");
}
#[test]
fn cue_points_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x07, 0x04]);
t0.extend_from_slice(b"trk0");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x07, 0x04]);
t1.extend_from_slice(b"trk1");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 2);
assert_eq!(cp[0].tick, 240);
assert_eq!(cp[0].track, 0);
assert_eq!(cp[0].text_bytes(), b"trk0");
assert_eq!(cp[1].tick, 240);
assert_eq!(cp[1].track, 1);
assert_eq!(cp[1].text_bytes(), b"trk1");
}
#[test]
fn cue_points_filter_excludes_other_text_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x06];
events.extend_from_slice(b"Track1");
events.extend_from_slice(&[0x00, 0xFF, 0x06, 0x04]);
events.extend_from_slice(b"Mark");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x04]);
events.extend_from_slice(b"syll");
events.extend_from_slice(&[0x00, 0xFF, 0x07, 0x04]);
events.extend_from_slice(b"Cue!");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].text_bytes(), b"Cue!");
assert_eq!(smf.markers().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
}
#[test]
fn cue_point_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x07, 0x02]);
events.extend_from_slice(b"go");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cp = smf.cue_points();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].tick, 240);
assert_eq!(cp[0].text_bytes(), b"go");
}
#[test]
fn cue_point_text_lossy_replaces_invalid_utf8() {
let cp = CueEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = cp.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(cp.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn track_names_empty_when_no_meta_event_present() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
assert!(smf.track_names().is_empty());
}
#[test]
fn track_names_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x06];
events.extend_from_slice(b"Melody");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 1);
assert_eq!(tn[0].tick, 0);
assert_eq!(tn[0].track, 0);
assert_eq!(tn[0].text_bytes(), b"Melody");
assert_eq!(tn[0].text_lossy(), "Melody");
}
#[test]
fn track_names_per_track_in_format_1() {
let mut t0: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x05];
t0.extend_from_slice(b"Drums");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = vec![0x00, 0xFF, 0x03, 0x04];
t1.extend_from_slice(b"Bass");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 2);
assert_eq!(tn[0].tick, 0);
assert_eq!(tn[0].track, 0);
assert_eq!(tn[0].text_bytes(), b"Drums");
assert_eq!(tn[1].tick, 0);
assert_eq!(tn[1].track, 1);
assert_eq!(tn[1].text_bytes(), b"Bass");
}
#[test]
fn track_names_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x05]);
events.extend_from_slice(b"Intro");
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Main");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 2);
assert_eq!(tn[0].tick, 0);
assert_eq!(tn[0].text_bytes(), b"Intro");
assert_eq!(tn[1].tick, 480);
assert_eq!(tn[1].text_bytes(), b"Main");
}
#[test]
fn track_names_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(240));
t0.extend_from_slice(&[0xFF, 0x03, 0x04]);
t0.extend_from_slice(b"trk0");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x03, 0x04]);
t1.extend_from_slice(b"trk1");
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 480);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 2);
assert_eq!(tn[0].tick, 240);
assert_eq!(tn[0].track, 0);
assert_eq!(tn[0].text_bytes(), b"trk0");
assert_eq!(tn[1].tick, 240);
assert_eq!(tn[1].track, 1);
assert_eq!(tn[1].text_bytes(), b"trk1");
}
#[test]
fn track_names_filter_excludes_other_text_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x01, 0x04];
events.extend_from_slice(b"Note");
events.extend_from_slice(&[0x00, 0xFF, 0x02, 0x05]);
events.extend_from_slice(b"(c)26");
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Lead");
events.extend_from_slice(&[0x00, 0xFF, 0x04, 0x05]);
events.extend_from_slice(b"Piano");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x06, 0x05]);
events.extend_from_slice(b"Verse");
events.extend_from_slice(&[0x00, 0xFF, 0x07, 0x04]);
events.extend_from_slice(b"Sync");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 1);
assert_eq!(tn[0].text_bytes(), b"Lead");
assert_eq!(smf.markers().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.cue_points().len(), 1);
}
#[test]
fn track_name_after_channel_events_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0xFF, 0x03, 0x04]);
events.extend_from_slice(b"name");
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 480);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let tn = smf.track_names();
assert_eq!(tn.len(), 1);
assert_eq!(tn[0].tick, 240);
assert_eq!(tn[0].text_bytes(), b"name");
}
#[test]
fn track_name_text_lossy_replaces_invalid_utf8() {
let tn = TrackNameEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = tn.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(tn.text_bytes(), &[0xFF, 0xFE]);
}
}