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)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InstrumentNameEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl InstrumentNameEvent {
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 TextEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl TextEvent {
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 CopyrightEvent {
pub tick: u64,
pub track: usize,
pub text: Vec<u8>,
}
impl CopyrightEvent {
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, Copy, Debug, PartialEq, Eq)]
pub enum FrameRate {
Fps24,
Fps25,
Fps30DropFrame,
Fps30NonDrop,
}
impl FrameRate {
pub fn from_hours_byte(hr: u8) -> Self {
match (hr >> 5) & 0b11 {
0 => FrameRate::Fps24,
1 => FrameRate::Fps25,
2 => FrameRate::Fps30DropFrame,
_ => FrameRate::Fps30NonDrop,
}
}
pub fn frames_per_second(&self) -> u32 {
match self {
FrameRate::Fps24 => 24,
FrameRate::Fps25 => 25,
FrameRate::Fps30DropFrame | FrameRate::Fps30NonDrop => 30,
}
}
pub fn is_drop_frame(&self) -> bool {
matches!(self, FrameRate::Fps30DropFrame)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SmpteOffsetEvent {
pub tick: u64,
pub track: usize,
pub hours_raw: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
pub subframes: u8,
}
impl SmpteOffsetEvent {
pub fn frame_rate(&self) -> FrameRate {
FrameRate::from_hours_byte(self.hours_raw)
}
pub fn hours_count(&self) -> u8 {
self.hours_raw & 0b0001_1111
}
pub fn seconds_total(&self) -> f64 {
let rate = self.frame_rate().frames_per_second() as f64;
let frame_frac = self.frames as f64 + (self.subframes as f64) / 100.0;
(self.hours_count() as f64) * 3600.0
+ (self.minutes as f64) * 60.0
+ (self.seconds as f64)
+ frame_frac / rate
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SequencerSpecificEvent {
pub tick: u64,
pub track: usize,
pub data: Vec<u8>,
}
impl SequencerSpecificEvent {
pub fn data_bytes(&self) -> &[u8] {
&self.data
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SequenceNumberEvent {
pub tick: u64,
pub track: usize,
pub number: u16,
}
impl SequenceNumberEvent {
pub fn number(&self) -> u16 {
self.number
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MidiPortEvent {
pub tick: u64,
pub track: usize,
pub port: u8,
}
impl MidiPortEvent {
pub fn port(&self) -> u8 {
self.port
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChannelPrefixEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
}
impl ChannelPrefixEvent {
pub fn channel(&self) -> Option<u8> {
if self.channel < 16 {
Some(self.channel)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProgramChangeEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
pub program: u8,
}
impl ProgramChangeEvent {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn program(&self) -> u8 {
self.program
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ControlChangeEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
pub controller: u8,
pub value: u8,
}
impl ControlChangeEvent {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn controller(&self) -> u8 {
self.controller
}
pub fn value(&self) -> u8 {
self.value
}
pub fn is_channel_mode(&self) -> bool {
matches!(self.controller, 120..=127)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PitchBendEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
pub value: u16,
}
impl PitchBendEvent {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn value(&self) -> u16 {
self.value
}
pub fn signed_value(&self) -> i16 {
(self.value as i32 - 0x2000) as i16
}
pub fn is_centre(&self) -> bool {
self.value == 0x2000
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PolyAftertouchEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
pub key: u8,
pub pressure: u8,
}
impl PolyAftertouchEvent {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn key(&self) -> u8 {
self.key
}
pub fn pressure(&self) -> u8 {
self.pressure
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChannelPressureEvent {
pub tick: u64,
pub track: usize,
pub channel: u8,
pub pressure: u8,
}
impl ChannelPressureEvent {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn pressure(&self) -> u8 {
self.pressure
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Note {
pub start_tick: u64,
pub end_tick: u64,
pub track: usize,
pub channel: u8,
pub key: u8,
pub velocity: u8,
pub off_velocity: u8,
}
impl Note {
pub fn channel(&self) -> u8 {
self.channel
}
pub fn key(&self) -> u8 {
self.key
}
pub fn velocity(&self) -> u8 {
self.velocity
}
pub fn off_velocity(&self) -> u8 {
self.off_velocity
}
pub fn duration_ticks(&self) -> u64 {
self.end_tick - self.start_tick
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SysExEvent {
pub tick: u64,
pub track: usize,
pub is_escape: bool,
pub data: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UniversalRealm {
NonRealTime,
RealTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UniversalSysEx {
pub realm: UniversalRealm,
pub device_id: u8,
pub sub_id1: UniversalSubId1,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UniversalSysExEvent {
pub tick: u64,
pub track: usize,
pub classification: UniversalSysEx,
pub data: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UniversalSubId1 {
SampleDumpHeader,
SampleDataPacket,
SampleDumpRequest,
MidiTimeCode(UniversalSubId2),
SampleDumpExtensionsOrMtcCueing(UniversalSubId2),
GeneralInformationOrMmcCommands(UniversalSubId2),
FileDumpOrMmcResponses(UniversalSubId2),
MidiTuningStandard(UniversalSubId2),
GeneralMidiOrControllerDestination(UniversalSubId2),
DownloadableSoundsOrKeyBasedInstrumentControl(UniversalSubId2),
FileReferenceOrScalablePolyphonyMip(UniversalSubId2),
MidiVisualControlOrMobilePhoneControl(UniversalSubId2),
MidiCapabilityInquiry(UniversalSubId2),
MidiShowControl(UniversalSubId2),
NotationInformation(UniversalSubId2),
DeviceControl(UniversalSubId2),
EndOfFile,
Wait,
Cancel,
Nak,
Ack,
Other(u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UniversalSubId2 {
NonRtMtcSpecial,
NonRtMtcPunchInPoints,
NonRtMtcPunchOutPoints,
NonRtMtcDeletePunchInPoint,
NonRtMtcDeletePunchOutPoint,
NonRtMtcEventStartPoint,
NonRtMtcEventStopPoint,
NonRtMtcEventStartPointsWithInfo,
NonRtMtcEventStopPointsWithInfo,
NonRtMtcDeleteEventStartPoint,
NonRtMtcDeleteEventStopPoint,
NonRtMtcCuePoints,
NonRtMtcCuePointsWithInfo,
NonRtMtcDeleteCuePoint,
NonRtMtcEventNameWithInfo,
SampleDumpLoopPointsTransmission,
SampleDumpLoopPointsRequest,
SampleDumpSampleNameTransmission,
SampleDumpSampleNameRequest,
SampleDumpExtendedDumpHeader,
SampleDumpExtendedLoopPointsTransmission,
SampleDumpExtendedLoopPointsRequest,
GeneralInformationIdentityRequest,
GeneralInformationIdentityReply,
FileDumpHeader,
FileDumpDataPacket,
FileDumpRequest,
MtsBulkDumpRequest,
MtsBulkDumpReply,
MtsTuningDumpRequest,
MtsKeyBasedTuningDump,
MtsScaleOctaveTuningDump1Byte,
MtsScaleOctaveTuningDump2Byte,
MtsSingleNoteTuningChangeWithBankSelect,
MtsScaleOctaveTuning1Byte,
MtsScaleOctaveTuning2Byte,
MtsRtSingleNoteTuningChange,
GeneralMidi1SystemOn,
GeneralMidiSystemOff,
GeneralMidi2SystemOn,
DlsTurnOn,
DlsTurnOff,
DlsTurnVoiceAllocationOff,
DlsTurnVoiceAllocationOn,
FileReferenceOpenFile,
FileReferenceSelectOrReselectContents,
FileReferenceOpenFileAndSelectContents,
FileReferenceCloseFile,
RtMtcFullMessage,
RtMtcUserBits,
RtMscExtensions,
RtNotationBarNumber,
RtNotationTimeSignatureImmediate,
RtNotationTimeSignatureDelayed,
DeviceControlMasterVolume,
DeviceControlMasterBalance,
DeviceControlMasterFineTuning,
DeviceControlMasterCoarseTuning,
DeviceControlGlobalParameterControl,
RtMtcCueingSpecial,
RtMtcCueingPunchInPoints,
RtMtcCueingPunchOutPoints,
RtMtcCueingEventStartPoints,
RtMtcCueingEventStopPoints,
RtMtcCueingEventStartPointsWithInfo,
RtMtcCueingEventStopPointsWithInfo,
RtMtcCueingCuePoints,
RtMtcCueingCuePointsWithInfo,
RtMtcCueingEventNameWithInfo,
ControllerDestinationChannelPressure,
ControllerDestinationPolyphonicKeyPressure,
ControllerDestinationControlChange,
RtKeyBasedInstrumentControl,
RtScalablePolyphonyMipMessage,
RtMobilePhoneControlMessage,
Other(u8),
}
impl SysExEvent {
pub fn ends_with_eox(&self) -> bool {
matches!(self.data.last(), Some(&0xF7))
}
pub fn is_complete_message(&self) -> bool {
!self.is_escape && self.ends_with_eox()
}
pub fn manufacturer_id(&self) -> Option<u8> {
if self.is_escape {
return None;
}
self.data.first().copied()
}
pub fn universal_classification(&self) -> Option<UniversalSysEx> {
if self.is_escape {
return None;
}
if self.data.len() < 3 {
return None;
}
let realm = match self.data[0] {
0x7E => UniversalRealm::NonRealTime,
0x7F => UniversalRealm::RealTime,
_ => return None,
};
let device_id = self.data[1];
let sub1_byte = self.data[2];
let sub2_byte = self.data.get(3).copied();
let sub_id1 = classify_universal_sub_id1(realm, sub1_byte, sub2_byte);
Some(UniversalSysEx {
realm,
device_id,
sub_id1,
})
}
}
fn classify_universal_sub_id1(
realm: UniversalRealm,
sub1: u8,
sub2: Option<u8>,
) -> UniversalSubId1 {
match sub1 {
0x7B => return UniversalSubId1::EndOfFile,
0x7C => return UniversalSubId1::Wait,
0x7D => return UniversalSubId1::Cancel,
0x7E => return UniversalSubId1::Nak,
0x7F => return UniversalSubId1::Ack,
_ => {}
}
if matches!(realm, UniversalRealm::NonRealTime) {
match sub1 {
0x01 => return UniversalSubId1::SampleDumpHeader,
0x02 => return UniversalSubId1::SampleDataPacket,
0x03 => return UniversalSubId1::SampleDumpRequest,
_ => {}
}
}
match (realm, sub1) {
(_, 0x04) => match realm {
UniversalRealm::NonRealTime => {
UniversalSubId1::MidiTimeCode(classify_nonrt_mtc_setup_sub2(sub2))
}
UniversalRealm::RealTime => {
UniversalSubId1::DeviceControl(classify_rt_device_control_sub2(sub2))
}
},
(_, 0x05) => match realm {
UniversalRealm::NonRealTime => UniversalSubId1::SampleDumpExtensionsOrMtcCueing(
classify_nonrt_sample_dump_extensions_sub2(sub2),
),
UniversalRealm::RealTime => {
UniversalSubId1::SampleDumpExtensionsOrMtcCueing(classify_rt_mtc_cueing_sub2(sub2))
}
},
(_, 0x06) => match realm {
UniversalRealm::NonRealTime => UniversalSubId1::GeneralInformationOrMmcCommands(
classify_nonrt_general_information_sub2(sub2),
),
UniversalRealm::RealTime => {
UniversalSubId1::GeneralInformationOrMmcCommands(passthrough_sub2(sub2))
}
},
(_, 0x07) => match realm {
UniversalRealm::NonRealTime => {
UniversalSubId1::FileDumpOrMmcResponses(classify_nonrt_file_dump_sub2(sub2))
}
UniversalRealm::RealTime => {
UniversalSubId1::FileDumpOrMmcResponses(passthrough_sub2(sub2))
}
},
(_, 0x08) => UniversalSubId1::MidiTuningStandard(classify_mts_sub2(realm, sub2)),
(_, 0x09) => UniversalSubId1::GeneralMidiOrControllerDestination(
classify_realm_sub2_for_0x09(realm, sub2),
),
(_, 0x0A) => UniversalSubId1::DownloadableSoundsOrKeyBasedInstrumentControl(
classify_realm_sub2_for_0x0a(realm, sub2),
),
(_, 0x0B) => UniversalSubId1::FileReferenceOrScalablePolyphonyMip(
classify_realm_sub2_for_0x0b(realm, sub2),
),
(_, 0x0C) => UniversalSubId1::MidiVisualControlOrMobilePhoneControl(
classify_realm_sub2_for_0x0c(realm, sub2),
),
(UniversalRealm::NonRealTime, 0x0D) => {
UniversalSubId1::MidiCapabilityInquiry(passthrough_sub2(sub2))
}
(UniversalRealm::RealTime, 0x01) => {
UniversalSubId1::MidiTimeCode(classify_rt_mtc_sub2(sub2))
}
(UniversalRealm::RealTime, 0x02) => {
UniversalSubId1::MidiShowControl(classify_rt_msc_sub2(sub2))
}
(UniversalRealm::RealTime, 0x03) => {
UniversalSubId1::NotationInformation(classify_rt_notation_sub2(sub2))
}
_ => UniversalSubId1::Other(sub1),
}
}
fn passthrough_sub2(sub2: Option<u8>) -> UniversalSubId2 {
UniversalSubId2::Other(sub2.unwrap_or(0))
}
fn classify_nonrt_mtc_setup_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x00) => UniversalSubId2::NonRtMtcSpecial,
Some(0x01) => UniversalSubId2::NonRtMtcPunchInPoints,
Some(0x02) => UniversalSubId2::NonRtMtcPunchOutPoints,
Some(0x03) => UniversalSubId2::NonRtMtcDeletePunchInPoint,
Some(0x04) => UniversalSubId2::NonRtMtcDeletePunchOutPoint,
Some(0x05) => UniversalSubId2::NonRtMtcEventStartPoint,
Some(0x06) => UniversalSubId2::NonRtMtcEventStopPoint,
Some(0x07) => UniversalSubId2::NonRtMtcEventStartPointsWithInfo,
Some(0x08) => UniversalSubId2::NonRtMtcEventStopPointsWithInfo,
Some(0x09) => UniversalSubId2::NonRtMtcDeleteEventStartPoint,
Some(0x0A) => UniversalSubId2::NonRtMtcDeleteEventStopPoint,
Some(0x0B) => UniversalSubId2::NonRtMtcCuePoints,
Some(0x0C) => UniversalSubId2::NonRtMtcCuePointsWithInfo,
Some(0x0D) => UniversalSubId2::NonRtMtcDeleteCuePoint,
Some(0x0E) => UniversalSubId2::NonRtMtcEventNameWithInfo,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_nonrt_sample_dump_extensions_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::SampleDumpLoopPointsTransmission,
Some(0x02) => UniversalSubId2::SampleDumpLoopPointsRequest,
Some(0x03) => UniversalSubId2::SampleDumpSampleNameTransmission,
Some(0x04) => UniversalSubId2::SampleDumpSampleNameRequest,
Some(0x05) => UniversalSubId2::SampleDumpExtendedDumpHeader,
Some(0x06) => UniversalSubId2::SampleDumpExtendedLoopPointsTransmission,
Some(0x07) => UniversalSubId2::SampleDumpExtendedLoopPointsRequest,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_nonrt_general_information_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::GeneralInformationIdentityRequest,
Some(0x02) => UniversalSubId2::GeneralInformationIdentityReply,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_nonrt_file_dump_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::FileDumpHeader,
Some(0x02) => UniversalSubId2::FileDumpDataPacket,
Some(0x03) => UniversalSubId2::FileDumpRequest,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_mts_sub2(realm: UniversalRealm, sub2: Option<u8>) -> UniversalSubId2 {
match (realm, sub2) {
(_, Some(0x07)) => UniversalSubId2::MtsSingleNoteTuningChangeWithBankSelect,
(_, Some(0x08)) => UniversalSubId2::MtsScaleOctaveTuning1Byte,
(_, Some(0x09)) => UniversalSubId2::MtsScaleOctaveTuning2Byte,
(UniversalRealm::NonRealTime, Some(0x00)) => UniversalSubId2::MtsBulkDumpRequest,
(UniversalRealm::NonRealTime, Some(0x01)) => UniversalSubId2::MtsBulkDumpReply,
(UniversalRealm::NonRealTime, Some(0x03)) => UniversalSubId2::MtsTuningDumpRequest,
(UniversalRealm::NonRealTime, Some(0x04)) => UniversalSubId2::MtsKeyBasedTuningDump,
(UniversalRealm::NonRealTime, Some(0x05)) => UniversalSubId2::MtsScaleOctaveTuningDump1Byte,
(UniversalRealm::NonRealTime, Some(0x06)) => UniversalSubId2::MtsScaleOctaveTuningDump2Byte,
(UniversalRealm::RealTime, Some(0x02)) => UniversalSubId2::MtsRtSingleNoteTuningChange,
(_, other) => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_realm_sub2_for_0x09(realm: UniversalRealm, sub2: Option<u8>) -> UniversalSubId2 {
match (realm, sub2) {
(UniversalRealm::NonRealTime, Some(0x01)) => UniversalSubId2::GeneralMidi1SystemOn,
(UniversalRealm::NonRealTime, Some(0x02)) => UniversalSubId2::GeneralMidiSystemOff,
(UniversalRealm::NonRealTime, Some(0x03)) => UniversalSubId2::GeneralMidi2SystemOn,
(UniversalRealm::RealTime, Some(0x01)) => {
UniversalSubId2::ControllerDestinationChannelPressure
}
(UniversalRealm::RealTime, Some(0x02)) => {
UniversalSubId2::ControllerDestinationPolyphonicKeyPressure
}
(UniversalRealm::RealTime, Some(0x03)) => {
UniversalSubId2::ControllerDestinationControlChange
}
(_, other) => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_realm_sub2_for_0x0a(realm: UniversalRealm, sub2: Option<u8>) -> UniversalSubId2 {
match (realm, sub2) {
(UniversalRealm::NonRealTime, Some(0x01)) => UniversalSubId2::DlsTurnOn,
(UniversalRealm::NonRealTime, Some(0x02)) => UniversalSubId2::DlsTurnOff,
(UniversalRealm::NonRealTime, Some(0x03)) => UniversalSubId2::DlsTurnVoiceAllocationOff,
(UniversalRealm::NonRealTime, Some(0x04)) => UniversalSubId2::DlsTurnVoiceAllocationOn,
(UniversalRealm::RealTime, Some(0x01)) => UniversalSubId2::RtKeyBasedInstrumentControl,
(_, other) => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_realm_sub2_for_0x0b(realm: UniversalRealm, sub2: Option<u8>) -> UniversalSubId2 {
match (realm, sub2) {
(UniversalRealm::NonRealTime, Some(0x01)) => UniversalSubId2::FileReferenceOpenFile,
(UniversalRealm::NonRealTime, Some(0x02)) => {
UniversalSubId2::FileReferenceSelectOrReselectContents
}
(UniversalRealm::NonRealTime, Some(0x03)) => {
UniversalSubId2::FileReferenceOpenFileAndSelectContents
}
(UniversalRealm::NonRealTime, Some(0x04)) => UniversalSubId2::FileReferenceCloseFile,
(UniversalRealm::RealTime, Some(0x01)) => UniversalSubId2::RtScalablePolyphonyMipMessage,
(_, other) => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_realm_sub2_for_0x0c(realm: UniversalRealm, sub2: Option<u8>) -> UniversalSubId2 {
match (realm, sub2) {
(UniversalRealm::RealTime, Some(0x00)) => UniversalSubId2::RtMobilePhoneControlMessage,
(_, other) => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_rt_mtc_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::RtMtcFullMessage,
Some(0x02) => UniversalSubId2::RtMtcUserBits,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_rt_msc_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x00) => UniversalSubId2::RtMscExtensions,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_rt_notation_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::RtNotationBarNumber,
Some(0x02) => UniversalSubId2::RtNotationTimeSignatureImmediate,
Some(0x42) => UniversalSubId2::RtNotationTimeSignatureDelayed,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_rt_device_control_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x01) => UniversalSubId2::DeviceControlMasterVolume,
Some(0x02) => UniversalSubId2::DeviceControlMasterBalance,
Some(0x03) => UniversalSubId2::DeviceControlMasterFineTuning,
Some(0x04) => UniversalSubId2::DeviceControlMasterCoarseTuning,
Some(0x05) => UniversalSubId2::DeviceControlGlobalParameterControl,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
fn classify_rt_mtc_cueing_sub2(sub2: Option<u8>) -> UniversalSubId2 {
match sub2 {
Some(0x00) => UniversalSubId2::RtMtcCueingSpecial,
Some(0x01) => UniversalSubId2::RtMtcCueingPunchInPoints,
Some(0x02) => UniversalSubId2::RtMtcCueingPunchOutPoints,
Some(0x05) => UniversalSubId2::RtMtcCueingEventStartPoints,
Some(0x06) => UniversalSubId2::RtMtcCueingEventStopPoints,
Some(0x07) => UniversalSubId2::RtMtcCueingEventStartPointsWithInfo,
Some(0x08) => UniversalSubId2::RtMtcCueingEventStopPointsWithInfo,
Some(0x0B) => UniversalSubId2::RtMtcCueingCuePoints,
Some(0x0C) => UniversalSubId2::RtMtcCueingCuePointsWithInfo,
Some(0x0E) => UniversalSubId2::RtMtcCueingEventNameWithInfo,
other => UniversalSubId2::Other(other.unwrap_or(0)),
}
}
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 instrument_names(&self) -> Vec<InstrumentNameEvent> {
let mut out: Vec<InstrumentNameEvent> = 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: 0x04, text }) = &ev.kind {
out.push(InstrumentNameEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn texts(&self) -> Vec<TextEvent> {
let mut out: Vec<TextEvent> = 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: 0x01, text }) = &ev.kind {
out.push(TextEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn copyrights(&self) -> Vec<CopyrightEvent> {
let mut out: Vec<CopyrightEvent> = 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: 0x02, text }) = &ev.kind {
out.push(CopyrightEvent {
tick: abs,
track: track_idx,
text: text.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn smpte_offsets(&self) -> Vec<SmpteOffsetEvent> {
let mut out: Vec<SmpteOffsetEvent> = 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::SmpteOffset {
hours,
minutes,
seconds,
frames,
subframes,
}) = &ev.kind
{
out.push(SmpteOffsetEvent {
tick: abs,
track: track_idx,
hours_raw: *hours,
minutes: *minutes,
seconds: *seconds,
frames: *frames,
subframes: *subframes,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn sequencer_specifics(&self) -> Vec<SequencerSpecificEvent> {
let mut out: Vec<SequencerSpecificEvent> = 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::SequencerSpecific(data)) = &ev.kind {
out.push(SequencerSpecificEvent {
tick: abs,
track: track_idx,
data: data.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn sequence_numbers(&self) -> Vec<SequenceNumberEvent> {
let mut out: Vec<SequenceNumberEvent> = 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::SequenceNumber(number)) = &ev.kind {
out.push(SequenceNumberEvent {
tick: abs,
track: track_idx,
number: *number,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn midi_ports(&self) -> Vec<MidiPortEvent> {
let mut out: Vec<MidiPortEvent> = 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::Port(port)) = &ev.kind {
out.push(MidiPortEvent {
tick: abs,
track: track_idx,
port: *port,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn channel_prefixes(&self) -> Vec<ChannelPrefixEvent> {
let mut out: Vec<ChannelPrefixEvent> = 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::ChannelPrefix(channel)) = &ev.kind {
out.push(ChannelPrefixEvent {
tick: abs,
track: track_idx,
channel: *channel,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn program_changes(&self) -> Vec<ProgramChangeEvent> {
let mut out: Vec<ProgramChangeEvent> = 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::Channel(ChannelMessage {
channel,
body: ChannelBody::ProgramChange { program },
}) = &ev.kind
{
out.push(ProgramChangeEvent {
tick: abs,
track: track_idx,
channel: *channel,
program: *program,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn control_changes(&self) -> Vec<ControlChangeEvent> {
let mut out: Vec<ControlChangeEvent> = 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::Channel(ChannelMessage {
channel,
body: ChannelBody::ControlChange { controller, value },
}) = &ev.kind
{
out.push(ControlChangeEvent {
tick: abs,
track: track_idx,
channel: *channel,
controller: *controller,
value: *value,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn pitch_bends(&self) -> Vec<PitchBendEvent> {
let mut out: Vec<PitchBendEvent> = 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::Channel(ChannelMessage {
channel,
body: ChannelBody::PitchBend { value },
}) = &ev.kind
{
out.push(PitchBendEvent {
tick: abs,
track: track_idx,
channel: *channel,
value: *value,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn poly_aftertouches(&self) -> Vec<PolyAftertouchEvent> {
let mut out: Vec<PolyAftertouchEvent> = 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::Channel(ChannelMessage {
channel,
body: ChannelBody::PolyAftertouch { key, pressure },
}) = &ev.kind
{
out.push(PolyAftertouchEvent {
tick: abs,
track: track_idx,
channel: *channel,
key: *key,
pressure: *pressure,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn channel_pressures(&self) -> Vec<ChannelPressureEvent> {
let mut out: Vec<ChannelPressureEvent> = 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::Channel(ChannelMessage {
channel,
body: ChannelBody::ChannelAftertouch { pressure },
}) = &ev.kind
{
out.push(ChannelPressureEvent {
tick: abs,
track: track_idx,
channel: *channel,
pressure: *pressure,
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn notes(&self) -> Vec<Note> {
struct AbsNote<'a> {
tick: u64,
track: usize,
order: usize,
body: &'a ChannelBody,
channel: u8,
}
let mut merged: Vec<AbsNote> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for (order, ev) in track.events.iter().enumerate() {
abs = abs.saturating_add(ev.delta as u64);
if let Event::Channel(ChannelMessage { channel, body }) = &ev.kind {
if matches!(
body,
ChannelBody::NoteOn { .. } | ChannelBody::NoteOff { .. }
) {
merged.push(AbsNote {
tick: abs,
track: track_idx,
order,
body,
channel: *channel,
});
}
}
}
}
merged.sort_by(|a, b| {
a.tick
.cmp(&b.tick)
.then_with(|| a.track.cmp(&b.track))
.then_with(|| a.order.cmp(&b.order))
});
struct Pending {
start_tick: u64,
track: usize,
velocity: u8,
}
let mut open: Vec<Vec<Pending>> = (0..(16 * 128)).map(|_| Vec::new()).collect();
let slot = |channel: u8, key: u8| -> usize { (channel as usize) * 128 + key as usize };
let mut out: Vec<Note> = Vec::new();
for ev in &merged {
match ev.body {
ChannelBody::NoteOn { key, velocity } if *velocity > 0 => {
open[slot(ev.channel, *key)].push(Pending {
start_tick: ev.tick,
track: ev.track,
velocity: *velocity,
});
}
ChannelBody::NoteOn { key, velocity: _ } => {
let fifo = &mut open[slot(ev.channel, *key)];
if !fifo.is_empty() {
let p = fifo.remove(0);
out.push(Note {
start_tick: p.start_tick,
end_tick: ev.tick,
track: p.track,
channel: ev.channel,
key: *key,
velocity: p.velocity,
off_velocity: 0,
});
}
}
ChannelBody::NoteOff { key, velocity } => {
let fifo = &mut open[slot(ev.channel, *key)];
if !fifo.is_empty() {
let p = fifo.remove(0);
out.push(Note {
start_tick: p.start_tick,
end_tick: ev.tick,
track: p.track,
channel: ev.channel,
key: *key,
velocity: p.velocity,
off_velocity: *velocity,
});
}
}
_ => {}
}
}
out.sort_by(|a, b| {
a.start_tick
.cmp(&b.start_tick)
.then_with(|| a.track.cmp(&b.track))
});
out
}
pub fn active_notes_at(&self, tick: u64) -> Vec<Note> {
self.notes()
.into_iter()
.filter(|n| n.start_tick <= tick && n.end_tick > tick)
.collect()
}
pub fn sysex_events(&self) -> Vec<SysExEvent> {
let mut out: Vec<SysExEvent> = 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::Sysex { escape, data } = &ev.kind {
out.push(SysExEvent {
tick: abs,
track: track_idx,
is_escape: *escape,
data: data.clone(),
});
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn universal_sysex_events(&self) -> Vec<UniversalSysExEvent> {
let mut out: Vec<UniversalSysExEvent> = 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::Sysex { escape, data } = &ev.kind {
let scratch = SysExEvent {
tick: abs,
track: track_idx,
is_escape: *escape,
data: data.clone(),
};
if let Some(classification) = scratch.universal_classification() {
out.push(UniversalSysExEvent {
tick: abs,
track: track_idx,
classification,
data: scratch.data,
});
}
}
}
}
out.sort_by_key(|c| c.tick);
out
}
pub fn channel_snapshot_at(&self, channel: u8, tick: u64) -> SmfChannelSnapshot {
let mut snap = SmfChannelSnapshot::default();
if channel >= 16 {
return snap;
}
let mut events: Vec<(u64, usize, usize, ChannelBody)> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for (in_track_idx, ev) in track.events.iter().enumerate() {
abs = abs.saturating_add(ev.delta as u64);
if abs > tick {
break;
}
if let Event::Channel(ChannelMessage { channel: ch, body }) = &ev.kind {
if *ch == channel {
events.push((abs, track_idx, in_track_idx, *body));
}
}
}
}
events.sort_by_key(|(t, tr, ix, _)| (*t, *tr, *ix));
for (_, _, _, body) in events {
snap.apply(&body);
}
snap
}
pub fn channel_snapshots_at(&self, tick: u64) -> [SmfChannelSnapshot; 16] {
let mut snaps: [SmfChannelSnapshot; 16] =
std::array::from_fn(|_| SmfChannelSnapshot::default());
let mut events: Vec<(u64, usize, usize, u8, ChannelBody)> = Vec::new();
for (track_idx, track) in self.tracks.iter().enumerate() {
let mut abs: u64 = 0;
for (in_track_idx, ev) in track.events.iter().enumerate() {
abs = abs.saturating_add(ev.delta as u64);
if abs > tick {
break;
}
if let Event::Channel(ChannelMessage { channel, body }) = &ev.kind {
events.push((abs, track_idx, in_track_idx, *channel, *body));
}
}
}
events.sort_by_key(|(t, tr, ix, _, _)| (*t, *tr, *ix));
for (_, _, _, channel, body) in events {
if (channel as usize) < snaps.len() {
snaps[channel as usize].apply(&body);
}
}
snaps
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SmfChannelSnapshot {
pub program: Option<u8>,
pub bank_msb: Option<u8>,
pub bank_lsb: Option<u8>,
pub volume: u8,
pub pan: u8,
pub expression: u8,
pub modulation: u8,
pub sustain: bool,
pub pitch_bend: u16,
}
impl Default for SmfChannelSnapshot {
fn default() -> Self {
Self {
program: None,
bank_msb: None,
bank_lsb: None,
volume: 100,
pan: 64,
expression: 127,
modulation: 0,
sustain: false,
pitch_bend: 0x2000,
}
}
}
impl SmfChannelSnapshot {
pub fn apply(&mut self, body: &ChannelBody) {
match *body {
ChannelBody::ControlChange { controller, value } => match controller {
0 => self.bank_msb = Some(value),
1 => self.modulation = value,
7 => self.volume = value,
10 => self.pan = value,
11 => self.expression = value,
32 => self.bank_lsb = Some(value),
64 => self.sustain = value >= 64,
_ => {} },
ChannelBody::ProgramChange { program } => self.program = Some(program),
ChannelBody::PitchBend { value } => self.pitch_bend = value,
ChannelBody::NoteOn { .. }
| ChannelBody::NoteOff { .. }
| ChannelBody::PolyAftertouch { .. }
| ChannelBody::ChannelAftertouch { .. } => {
}
}
}
}
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()
}
pub const MAX_VLQ_VALUE: u32 = 0x0FFF_FFFF;
impl SmfFile {
pub fn to_bytes(&self) -> Result<Vec<u8>> {
if self.header.ntrks as usize != self.tracks.len() {
return Err(Error::invalid(format!(
"SMF: header.ntrks ({}) does not match tracks.len() ({}) — \
fix one side before encode so the wire format stays consistent",
self.header.ntrks,
self.tracks.len(),
)));
}
let mut out: Vec<u8> = Vec::new();
write_header_chunk(&mut out, &self.header)?;
for (i, track) in self.tracks.iter().enumerate() {
write_track_chunk(&mut out, track)
.map_err(|e| Error::invalid(format!("SMF: track {i}: {e}", e = err_msg(&e))))?;
}
Ok(out)
}
}
impl Track {
pub fn to_bytes_chunk(&self) -> Result<Vec<u8>> {
let mut out: Vec<u8> = Vec::new();
write_track_chunk(&mut out, self)?;
Ok(out)
}
}
fn err_msg(e: &Error) -> String {
match e {
Error::InvalidData(s) => s.clone(),
other => format!("{other:?}"),
}
}
fn write_header_chunk(out: &mut Vec<u8>, header: &SmfHeader) -> Result<()> {
out.extend_from_slice(b"MThd");
out.extend_from_slice(&6u32.to_be_bytes());
let format_code: u16 = match header.format {
SmfFormat::SingleTrack => 0,
SmfFormat::MultiTrackSimultaneous => 1,
SmfFormat::MultiTrackIndependent => 2,
};
out.extend_from_slice(&format_code.to_be_bytes());
out.extend_from_slice(&header.ntrks.to_be_bytes());
let division_word: u16 = match header.division {
Division::TicksPerQuarter(t) => {
if t == 0 || t & 0x8000 != 0 {
return Err(Error::invalid(format!(
"SMF: TicksPerQuarter division {t} out of range (1..=0x7FFF)",
)));
}
t
}
Division::Smpte {
frames_per_second,
ticks_per_frame,
} => {
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}}",
)));
}
let upper = (-(frames_per_second as i16)) as i8 as u8;
(u16::from(upper) << 8) | u16::from(ticks_per_frame)
}
};
out.extend_from_slice(&division_word.to_be_bytes());
Ok(())
}
fn write_track_chunk(out: &mut Vec<u8>, track: &Track) -> Result<()> {
let eot_count = track
.events
.iter()
.filter(|ev| matches!(&ev.kind, Event::Meta(MetaEvent::EndOfTrack)))
.count();
match eot_count {
0 => {
return Err(Error::invalid(
"SMF: track has no MetaEvent::EndOfTrack — append one before encode",
));
}
1 => {
if !matches!(
track.events.last().map(|ev| &ev.kind),
Some(Event::Meta(MetaEvent::EndOfTrack)),
) {
return Err(Error::invalid(
"SMF: MetaEvent::EndOfTrack must be the last event in the track",
));
}
}
n => {
return Err(Error::invalid(format!(
"SMF: track has {n} MetaEvent::EndOfTrack entries — exactly 1 (at the tail) is legal",
)));
}
}
out.extend_from_slice(b"MTrk");
let len_pos = out.len();
out.extend_from_slice(&[0u8; 4]); let body_start = out.len();
for (i, ev) in track.events.iter().enumerate() {
write_event(out, ev)
.map_err(|e| Error::invalid(format!("SMF: event {i}: {msg}", msg = err_msg(&e))))?;
}
let body_len = out.len() - body_start;
let len_be = (body_len as u32).to_be_bytes();
out[len_pos..len_pos + 4].copy_from_slice(&len_be);
Ok(())
}
fn write_event(out: &mut Vec<u8>, ev: &TrackEvent) -> Result<()> {
write_vlq(out, ev.delta)?;
match &ev.kind {
Event::Channel(msg) => write_channel(out, msg),
Event::Sysex { escape, data } => {
out.push(if *escape { 0xF7 } else { 0xF0 });
write_vlq(out, u32_len(data.len(), "sysex payload")?)?;
out.extend_from_slice(data);
Ok(())
}
Event::Meta(meta) => write_meta(out, meta),
}
}
fn write_channel(out: &mut Vec<u8>, msg: &ChannelMessage) -> Result<()> {
if msg.channel > 0x0F {
return Err(Error::invalid(format!(
"SMF: channel {} out of range 0..=15",
msg.channel,
)));
}
let chan = msg.channel & 0x0F;
match msg.body {
ChannelBody::NoteOff { key, velocity } => {
check_data_byte(key, "NoteOff.key")?;
check_data_byte(velocity, "NoteOff.velocity")?;
out.extend_from_slice(&[0x80 | chan, key, velocity]);
}
ChannelBody::NoteOn { key, velocity } => {
check_data_byte(key, "NoteOn.key")?;
check_data_byte(velocity, "NoteOn.velocity")?;
out.extend_from_slice(&[0x90 | chan, key, velocity]);
}
ChannelBody::PolyAftertouch { key, pressure } => {
check_data_byte(key, "PolyAftertouch.key")?;
check_data_byte(pressure, "PolyAftertouch.pressure")?;
out.extend_from_slice(&[0xA0 | chan, key, pressure]);
}
ChannelBody::ControlChange { controller, value } => {
check_data_byte(controller, "ControlChange.controller")?;
check_data_byte(value, "ControlChange.value")?;
out.extend_from_slice(&[0xB0 | chan, controller, value]);
}
ChannelBody::ProgramChange { program } => {
check_data_byte(program, "ProgramChange.program")?;
out.extend_from_slice(&[0xC0 | chan, program]);
}
ChannelBody::ChannelAftertouch { pressure } => {
check_data_byte(pressure, "ChannelAftertouch.pressure")?;
out.extend_from_slice(&[0xD0 | chan, pressure]);
}
ChannelBody::PitchBend { value } => {
if value > 0x3FFF {
return Err(Error::invalid(format!(
"SMF: PitchBend value {value:#06X} exceeds 14-bit range 0..=0x3FFF",
)));
}
let lsb = (value & 0x7F) as u8;
let msb = ((value >> 7) & 0x7F) as u8;
out.extend_from_slice(&[0xE0 | chan, lsb, msb]);
}
}
Ok(())
}
fn write_meta(out: &mut Vec<u8>, meta: &MetaEvent) -> Result<()> {
out.push(0xFF);
match meta {
MetaEvent::SequenceNumber(n) => {
out.push(0x00);
out.push(0x02);
out.extend_from_slice(&n.to_be_bytes());
}
MetaEvent::Text { kind, text } => {
if !(0x01..=0x0F).contains(kind) {
return Err(Error::invalid(format!(
"SMF: Text.kind 0x{kind:02X} out of range 0x01..=0x0F",
)));
}
out.push(*kind);
write_vlq(out, u32_len(text.len(), "Text payload")?)?;
out.extend_from_slice(text);
}
MetaEvent::ChannelPrefix(c) => {
check_data_byte(*c, "ChannelPrefix")?;
out.extend_from_slice(&[0x20, 0x01, *c]);
}
MetaEvent::Port(p) => {
check_data_byte(*p, "Port")?;
out.extend_from_slice(&[0x21, 0x01, *p]);
}
MetaEvent::EndOfTrack => {
out.extend_from_slice(&[0x2F, 0x00]);
}
MetaEvent::Tempo(us_per_qn) => {
if *us_per_qn > 0x00FF_FFFF {
return Err(Error::invalid(format!(
"SMF: Tempo {us_per_qn} exceeds 24-bit range",
)));
}
out.push(0x51);
out.push(0x03);
out.push(((us_per_qn >> 16) & 0xFF) as u8);
out.push(((us_per_qn >> 8) & 0xFF) as u8);
out.push((us_per_qn & 0xFF) as u8);
}
MetaEvent::SmpteOffset {
hours,
minutes,
seconds,
frames,
subframes,
} => {
out.push(0x54);
out.push(0x05);
out.extend_from_slice(&[*hours, *minutes, *seconds, *frames, *subframes]);
}
MetaEvent::TimeSignature {
numerator,
denominator_pow2,
clocks_per_click,
notated_32nd_per_quarter,
} => {
out.push(0x58);
out.push(0x04);
out.extend_from_slice(&[
*numerator,
*denominator_pow2,
*clocks_per_click,
*notated_32nd_per_quarter,
]);
}
MetaEvent::KeySignature { sharps_flats, mode } => {
if !matches!(mode, 0 | 1) {
return Err(Error::invalid(format!(
"SMF: KeySignature.mode {mode} not in {{0, 1}}",
)));
}
out.push(0x59);
out.push(0x02);
out.push(*sharps_flats as u8);
out.push(*mode);
}
MetaEvent::SequencerSpecific(data) => {
out.push(0x7F);
write_vlq(out, u32_len(data.len(), "SequencerSpecific payload")?)?;
out.extend_from_slice(data);
}
MetaEvent::Unknown { type_byte, data } => {
if *type_byte == 0x2F {
return Err(Error::invalid(
"SMF: cannot emit Unknown { type_byte: 0x2F } — use MetaEvent::EndOfTrack",
));
}
out.push(*type_byte);
write_vlq(out, u32_len(data.len(), "Unknown meta payload")?)?;
out.extend_from_slice(data);
}
}
Ok(())
}
fn check_data_byte(b: u8, field: &str) -> Result<()> {
if b & 0x80 != 0 {
return Err(Error::invalid(format!(
"SMF: {field} value 0x{b:02X} has the MIDI status bit set (must be 0..=0x7F)",
)));
}
Ok(())
}
fn u32_len(n: usize, what: &str) -> Result<u32> {
if n > MAX_VLQ_VALUE as usize {
return Err(Error::invalid(format!(
"SMF: {what} length {n} exceeds VLQ cap {MAX_VLQ_VALUE}",
)));
}
Ok(n as u32)
}
fn write_vlq(out: &mut Vec<u8>, value: u32) -> Result<()> {
if value > MAX_VLQ_VALUE {
return Err(Error::invalid(format!(
"SMF: VLQ value {value:#X} exceeds 4-byte cap {MAX_VLQ_VALUE:#X}",
)));
}
let mut groups: [u8; 4] = [0; 4];
let mut n = 0usize;
let mut v = value;
loop {
groups[n] = (v & 0x7F) as u8;
n += 1;
v >>= 7;
if v == 0 {
break;
}
}
for i in (0..n).rev() {
let last = i == 0;
let byte = groups[i] | if last { 0x00 } else { 0x80 };
out.push(byte);
}
Ok(())
}
#[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]);
}
#[test]
fn instrument_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.instrument_names().is_empty());
}
#[test]
fn instrument_names_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x04, 0x0B];
events.extend_from_slice(b"Grand Piano");
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 inst = smf.instrument_names();
assert_eq!(inst.len(), 1);
assert_eq!(inst[0].tick, 0);
assert_eq!(inst[0].track, 0);
assert_eq!(inst[0].text_bytes(), b"Grand Piano");
assert_eq!(inst[0].text_lossy(), "Grand Piano");
}
#[test]
fn instrument_names_per_track_in_format_1() {
let mut t0: Vec<u8> = vec![0x00, 0xFF, 0x04, 0x08];
t0.extend_from_slice(b"Drum Kit");
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = vec![0x00, 0xFF, 0x04, 0x07];
t1.extend_from_slice(b"Trumpet");
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 inst = smf.instrument_names();
assert_eq!(inst.len(), 2);
assert_eq!(inst[0].tick, 0);
assert_eq!(inst[0].track, 0);
assert_eq!(inst[0].text_bytes(), b"Drum Kit");
assert_eq!(inst[1].tick, 0);
assert_eq!(inst[1].track, 1);
assert_eq!(inst[1].text_bytes(), b"Trumpet");
}
#[test]
fn instrument_names_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x04, 0x05]);
events.extend_from_slice(b"Piano");
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x04, 0x05]);
events.extend_from_slice(b"Organ");
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 inst = smf.instrument_names();
assert_eq!(inst.len(), 2);
assert_eq!(inst[0].tick, 0);
assert_eq!(inst[0].text_bytes(), b"Piano");
assert_eq!(inst[1].tick, 480);
assert_eq!(inst[1].text_bytes(), b"Organ");
}
#[test]
fn instrument_names_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, 0x04, 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, 0x04, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x04, 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 inst = smf.instrument_names();
assert_eq!(inst.len(), 3);
assert_eq!(inst[0].tick, 120);
assert_eq!(inst[0].track, 1);
assert_eq!(inst[0].text_bytes(), b"B");
assert_eq!(inst[1].tick, 240);
assert_eq!(inst[1].track, 0);
assert_eq!(inst[1].text_bytes(), b"A");
assert_eq!(inst[2].tick, 360);
assert_eq!(inst[2].track, 1);
assert_eq!(inst[2].text_bytes(), b"C");
}
#[test]
fn instrument_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, 0x04, 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, 0x04, 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 inst = smf.instrument_names();
assert_eq!(inst.len(), 2);
assert_eq!(inst[0].tick, 240);
assert_eq!(inst[0].track, 0);
assert_eq!(inst[0].text_bytes(), b"trk0");
assert_eq!(inst[1].tick, 240);
assert_eq!(inst[1].track, 1);
assert_eq!(inst[1].text_bytes(), b"trk1");
}
#[test]
fn instrument_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 inst = smf.instrument_names();
assert_eq!(inst.len(), 1);
assert_eq!(inst[0].text_bytes(), b"Piano");
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.markers().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.cue_points().len(), 1);
}
#[test]
fn instrument_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, 0x04, 0x04]);
events.extend_from_slice(b"harp");
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 inst = smf.instrument_names();
assert_eq!(inst.len(), 1);
assert_eq!(inst[0].tick, 240);
assert_eq!(inst[0].text_bytes(), b"harp");
}
#[test]
fn instrument_name_coexists_with_track_name_on_same_track() {
let mut events: Vec<u8> = vec![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, 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();
let inst = smf.instrument_names();
assert_eq!(tn.len(), 1);
assert_eq!(tn[0].text_bytes(), b"Lead");
assert_eq!(inst.len(), 1);
assert_eq!(inst[0].text_bytes(), b"Piano");
}
#[test]
fn instrument_name_text_lossy_replaces_invalid_utf8() {
let inst = InstrumentNameEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = inst.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(inst.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn texts_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.texts().is_empty());
}
#[test]
fn texts_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x01, 0x0B];
events.extend_from_slice(b"do not edit");
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 tx = smf.texts();
assert_eq!(tx.len(), 1);
assert_eq!(tx[0].tick, 0);
assert_eq!(tx[0].track, 0);
assert_eq!(tx[0].text_bytes(), b"do not edit");
assert_eq!(tx[0].text_lossy(), "do not edit");
}
#[test]
fn texts_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"head");
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x01, 0x04]);
events.extend_from_slice(b"mid1");
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 tx = smf.texts();
assert_eq!(tx.len(), 2);
assert_eq!(tx[0].tick, 0);
assert_eq!(tx[0].text_bytes(), b"head");
assert_eq!(tx[1].tick, 480);
assert_eq!(tx[1].text_bytes(), b"mid1");
}
#[test]
fn texts_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, 0x01, 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, 0x01, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x01, 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 tx = smf.texts();
assert_eq!(tx.len(), 3);
assert_eq!(tx[0].tick, 120);
assert_eq!(tx[0].track, 1);
assert_eq!(tx[0].text_bytes(), b"B");
assert_eq!(tx[1].tick, 240);
assert_eq!(tx[1].track, 0);
assert_eq!(tx[1].text_bytes(), b"A");
assert_eq!(tx[2].tick, 360);
assert_eq!(tx[2].track, 1);
assert_eq!(tx[2].text_bytes(), b"C");
}
#[test]
fn texts_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, 0x01, 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, 0x01, 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 tx = smf.texts();
assert_eq!(tx.len(), 2);
assert_eq!(tx[0].track, 0);
assert_eq!(tx[0].text_bytes(), b"trk0");
assert_eq!(tx[1].track, 1);
assert_eq!(tx[1].text_bytes(), b"trk1");
}
#[test]
fn texts_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 tx = smf.texts();
assert_eq!(tx.len(), 1);
assert_eq!(tx[0].text_bytes(), b"Note");
assert_eq!(smf.copyrights().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.instrument_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.markers().len(), 1);
assert_eq!(smf.cue_points().len(), 1);
}
#[test]
fn text_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, 0x01, 0x04]);
events.extend_from_slice(b"note");
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 tx = smf.texts();
assert_eq!(tx.len(), 1);
assert_eq!(tx[0].tick, 240);
assert_eq!(tx[0].text_bytes(), b"note");
}
#[test]
fn text_text_lossy_replaces_invalid_utf8() {
let ev = TextEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = ev.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(ev.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn copyrights_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.copyrights().is_empty());
}
#[test]
fn copyrights_single_event_at_tick_zero() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x02, 0x0C];
events.extend_from_slice(b"(c) 2026 KLB");
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.copyrights();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].tick, 0);
assert_eq!(cp[0].track, 0);
assert_eq!(cp[0].text_bytes(), b"(c) 2026 KLB");
assert_eq!(cp[0].text_lossy(), "(c) 2026 KLB");
}
#[test]
fn copyrights_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x02, 0x04]);
events.extend_from_slice(b"(c)A");
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x02, 0x04]);
events.extend_from_slice(b"(c)B");
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.copyrights();
assert_eq!(cp.len(), 2);
assert_eq!(cp[0].tick, 0);
assert_eq!(cp[0].text_bytes(), b"(c)A");
assert_eq!(cp[1].tick, 480);
assert_eq!(cp[1].text_bytes(), b"(c)B");
}
#[test]
fn copyrights_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, 0x02, 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, 0x02, 0x01]);
t1.extend_from_slice(b"B");
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x02, 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.copyrights();
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 copyrights_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, 0x02, 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, 0x02, 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.copyrights();
assert_eq!(cp.len(), 2);
assert_eq!(cp[0].track, 0);
assert_eq!(cp[0].text_bytes(), b"trk0");
assert_eq!(cp[1].track, 1);
assert_eq!(cp[1].text_bytes(), b"trk1");
}
#[test]
fn copyrights_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 cp = smf.copyrights();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].text_bytes(), b"(c)26");
assert_eq!(smf.texts().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.instrument_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.markers().len(), 1);
assert_eq!(smf.cue_points().len(), 1);
}
#[test]
fn copyright_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, 0x02, 0x04]);
events.extend_from_slice(b"(c)2");
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.copyrights();
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].tick, 240);
assert_eq!(cp[0].text_bytes(), b"(c)2");
}
#[test]
fn copyright_text_lossy_replaces_invalid_utf8() {
let ev = CopyrightEvent {
tick: 0,
track: 0,
text: vec![0xFF, 0xFE],
};
let lossy = ev.text_lossy();
assert!(lossy.contains('\u{FFFD}'));
assert_eq!(ev.text_bytes(), &[0xFF, 0xFE]);
}
#[test]
fn texts_and_copyrights_independent_on_same_track() {
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)KL");
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 tx = smf.texts();
let cp = smf.copyrights();
assert_eq!(tx.len(), 1);
assert_eq!(tx[0].text_bytes(), b"note");
assert_eq!(cp.len(), 1);
assert_eq!(cp[0].text_bytes(), b"(c)KL");
}
#[test]
fn smpte_offsets_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.smpte_offsets().is_empty());
}
#[test]
fn smpte_offsets_single_event_at_tick_zero_24fps() {
let events: Vec<u8> = vec![
0x00, 0xFF, 0x54, 0x05, 0x01, 0x20, 0x10, 0x05, 0x32, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let so = smf.smpte_offsets();
assert_eq!(so.len(), 1);
assert_eq!(so[0].tick, 0);
assert_eq!(so[0].track, 0);
assert_eq!(so[0].hours_raw, 0x01);
assert_eq!(so[0].hours_count(), 1);
assert_eq!(so[0].frame_rate(), FrameRate::Fps24);
assert_eq!(so[0].minutes, 0x20);
assert_eq!(so[0].seconds, 0x10);
assert_eq!(so[0].frames, 0x05);
assert_eq!(so[0].subframes, 0x32);
}
#[test]
fn smpte_offsets_decodes_all_four_frame_rates() {
let mk_track = |hr: u8| -> Vec<u8> {
vec![
0x00, 0xFF, 0x54, 0x05, hr, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x2F, 0x00,
]
};
let mut blob = header_chunk(2, 4, 96);
blob.extend(track_chunk(&mk_track(0b0000_0001))); blob.extend(track_chunk(&mk_track(0b0010_0001))); blob.extend(track_chunk(&mk_track(0b0100_0001))); blob.extend(track_chunk(&mk_track(0b0110_0001))); let smf = parse(&blob).unwrap();
let so = smf.smpte_offsets();
assert_eq!(so.len(), 4);
assert_eq!(so[0].track, 0);
assert_eq!(so[0].frame_rate(), FrameRate::Fps24);
assert_eq!(so[0].hours_count(), 1);
assert_eq!(so[1].track, 1);
assert_eq!(so[1].frame_rate(), FrameRate::Fps25);
assert_eq!(so[1].hours_count(), 1);
assert_eq!(so[2].track, 2);
assert_eq!(so[2].frame_rate(), FrameRate::Fps30DropFrame);
assert!(so[2].frame_rate().is_drop_frame());
assert_eq!(so[2].hours_count(), 1);
assert_eq!(so[3].track, 3);
assert_eq!(so[3].frame_rate(), FrameRate::Fps30NonDrop);
assert!(!so[3].frame_rate().is_drop_frame());
assert_eq!(so[3].hours_count(), 1);
assert_eq!(so[0].frame_rate().frames_per_second(), 24);
assert_eq!(so[1].frame_rate().frames_per_second(), 25);
assert_eq!(so[2].frame_rate().frames_per_second(), 30);
assert_eq!(so[3].frame_rate().frames_per_second(), 30);
}
#[test]
fn smpte_offsets_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, 0x54, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00]);
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, 0x54, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00]);
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 so = smf.smpte_offsets();
assert_eq!(so.len(), 2);
assert_eq!(so[0].tick, 120);
assert_eq!(so[0].track, 1);
assert_eq!(so[0].hours_raw, 0x02);
assert_eq!(so[1].tick, 240);
assert_eq!(so[1].track, 0);
assert_eq!(so[1].hours_raw, 0x01);
}
#[test]
fn smpte_offsets_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, 0x54, 0x05, 0x0A, 0x00, 0x00, 0x00, 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, 0x54, 0x05, 0x0B, 0x00, 0x00, 0x00, 0x00]);
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 so = smf.smpte_offsets();
assert_eq!(so.len(), 2);
assert_eq!(so[0].track, 0);
assert_eq!(so[0].hours_raw, 0x0A);
assert_eq!(so[1].track, 1);
assert_eq!(so[1].hours_raw, 0x0B);
}
#[test]
fn smpte_offsets_filter_excludes_other_meta_kinds() {
let mut events: Vec<u8> = Vec::new();
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, 0xFF, 0x59, 0x02, 0x00, 0x00]); events.extend_from_slice(&[0x00, 0xFF, 0x54, 0x05, 0x21, 0x1E, 0x2D, 0x10, 0x32]); events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"note");
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 so = smf.smpte_offsets();
assert_eq!(so.len(), 1);
assert_eq!(so[0].hours_raw, 0x21);
assert_eq!(so[0].hours_count(), 1);
assert_eq!(so[0].frame_rate(), FrameRate::Fps25);
assert_eq!(so[0].minutes, 0x1E);
assert_eq!(so[0].seconds, 0x2D);
assert_eq!(so[0].frames, 0x10);
assert_eq!(so[0].subframes, 0x32);
assert_eq!(smf.tempo_map().len(), 1);
assert_eq!(smf.time_signatures().len(), 1);
assert_eq!(smf.key_signatures().len(), 1);
assert_eq!(smf.texts().len(), 1);
}
#[test]
fn smpte_offsets_seconds_total_24fps() {
let ev = SmpteOffsetEvent {
tick: 0,
track: 0,
hours_raw: 0x01,
minutes: 30,
seconds: 15,
frames: 12,
subframes: 50,
};
assert_eq!(ev.frame_rate(), FrameRate::Fps24);
let expected = 1.0 * 3600.0 + 30.0 * 60.0 + 15.0 + 12.5 / 24.0;
assert!((ev.seconds_total() - expected).abs() < 1e-9);
}
#[test]
fn smpte_offsets_seconds_total_30fps_non_drop_at_origin() {
let ev = SmpteOffsetEvent {
tick: 0,
track: 0,
hours_raw: 0x60,
minutes: 0,
seconds: 0,
frames: 0,
subframes: 0,
};
assert_eq!(ev.frame_rate(), FrameRate::Fps30NonDrop);
assert_eq!(ev.hours_count(), 0);
assert_eq!(ev.seconds_total(), 0.0);
}
#[test]
fn smpte_offsets_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, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 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 so = smf.smpte_offsets();
assert_eq!(so.len(), 1);
assert_eq!(so[0].tick, 240);
assert_eq!(so[0].frame_rate(), FrameRate::Fps24);
assert_eq!(so[0].hours_count(), 0);
}
#[test]
fn frame_rate_from_hours_byte_only_uses_bits_5_and_6() {
assert_eq!(FrameRate::from_hours_byte(0b0000_0000), FrameRate::Fps24);
assert_eq!(FrameRate::from_hours_byte(0b1001_1111), FrameRate::Fps24); assert_eq!(FrameRate::from_hours_byte(0b0010_0000), FrameRate::Fps25);
assert_eq!(
FrameRate::from_hours_byte(0b0100_0000),
FrameRate::Fps30DropFrame
);
assert_eq!(
FrameRate::from_hours_byte(0b0110_0000),
FrameRate::Fps30NonDrop
);
}
#[test]
fn sequencer_specifics_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.sequencer_specifics().is_empty());
}
#[test]
fn sequencer_specifics_single_event_at_tick_zero() {
let events: Vec<u8> = vec![
0x00, 0xFF, 0x7F, 0x04, 0x41, 0x00, 0x01, 0x02, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 1);
assert_eq!(sx[0].tick, 0);
assert_eq!(sx[0].track, 0);
assert_eq!(sx[0].data_bytes(), &[0x41, 0x00, 0x01, 0x02]);
}
#[test]
fn sequencer_specifics_empty_payload_is_surfaced() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 1);
assert!(sx[0].data_bytes().is_empty());
}
#[test]
fn sequencer_specifics_multiple_within_one_track_are_in_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x7F, 0x02, 0x41, 0x10]);
events.extend_from_slice(&encode_vlq(480));
events.extend_from_slice(&[0xFF, 0x7F, 0x02, 0x41, 0x11]);
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 sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 2);
assert_eq!(sx[0].tick, 0);
assert_eq!(sx[0].data_bytes(), &[0x41, 0x10]);
assert_eq!(sx[1].tick, 480);
assert_eq!(sx[1].data_bytes(), &[0x41, 0x11]);
}
#[test]
fn sequencer_specifics_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, 0x7F, 0x01, 0xAA]);
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, 0x7F, 0x01, 0xBB]);
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x7F, 0x01, 0xCC]);
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 sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 3);
assert_eq!(sx[0].tick, 120);
assert_eq!(sx[0].track, 1);
assert_eq!(sx[0].data_bytes(), &[0xBB]);
assert_eq!(sx[1].tick, 240);
assert_eq!(sx[1].track, 0);
assert_eq!(sx[1].data_bytes(), &[0xAA]);
assert_eq!(sx[2].tick, 360);
assert_eq!(sx[2].track, 1);
assert_eq!(sx[2].data_bytes(), &[0xCC]);
}
#[test]
fn sequencer_specifics_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, 0x7F, 0x01, 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, 0x7F, 0x01, 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 sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 2);
assert_eq!(sx[0].track, 0);
assert_eq!(sx[0].data_bytes(), &[0x00]);
assert_eq!(sx[1].track, 1);
assert_eq!(sx[1].data_bytes(), &[0x01]);
}
#[test]
fn sequencer_specifics_filter_excludes_other_meta_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x7F, 0x03, 0x41, 0x10, 0x42];
events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"Note");
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Lead");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&[0x00, 0xFF, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
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 sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 1);
assert_eq!(sx[0].data_bytes(), &[0x41, 0x10, 0x42]);
assert_eq!(smf.texts().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.tempo_map().len(), 1);
assert_eq!(smf.smpte_offsets().len(), 1);
assert_eq!(smf.time_signatures().len(), 1);
assert_eq!(smf.key_signatures().len(), 1);
}
#[test]
fn sequencer_specifics_after_channel_events_track_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, 0x7F, 0x02, 0x7D, 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 sx = smf.sequencer_specifics();
assert_eq!(sx.len(), 1);
assert_eq!(sx[0].tick, 240);
assert_eq!(sx[0].data_bytes(), &[0x7D, 0x01]);
}
#[test]
fn sequence_numbers_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.sequence_numbers().is_empty());
}
#[test]
fn sequence_numbers_single_label_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x00, 0x02, 0x00, 0x2A, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sn = smf.sequence_numbers();
assert_eq!(sn.len(), 1);
assert_eq!(sn[0].tick, 0);
assert_eq!(sn[0].track, 0);
assert_eq!(sn[0].number(), 42);
}
#[test]
fn sequence_numbers_big_endian_decode() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x00, 0x02, 0x12, 0x34, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sn = smf.sequence_numbers();
assert_eq!(sn.len(), 1);
assert_eq!(sn[0].number, 0x1234);
}
#[test]
fn sequence_numbers_full_u16_range_round_trips() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x00, 0x02, 0xFF, 0xFF, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sn = smf.sequence_numbers();
assert_eq!(sn.len(), 1);
assert_eq!(sn[0].number, 0xFFFF);
}
#[test]
fn sequence_numbers_format_2_per_pattern_labels() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x01]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x02]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(2, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sn = smf.sequence_numbers();
assert_eq!(sn.len(), 2);
assert_eq!(sn[0].tick, 0);
assert_eq!(sn[0].track, 0);
assert_eq!(sn[0].number, 1);
assert_eq!(sn[1].tick, 0);
assert_eq!(sn[1].track, 1);
assert_eq!(sn[1].number, 2);
}
#[test]
fn sequence_numbers_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x10]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x11]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sn = smf.sequence_numbers();
assert_eq!(sn.len(), 2);
assert_eq!(sn[0].track, 0);
assert_eq!(sn[0].number, 0x10);
assert_eq!(sn[1].track, 1);
assert_eq!(sn[1].number, 0x11);
}
#[test]
fn sequence_numbers_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x00, 0x02, 0x07, 0xD0]);
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 sn = smf.sequence_numbers();
assert_eq!(sn.len(), 1);
assert_eq!(sn[0].tick, 240);
assert_eq!(sn[0].number, 2000);
}
#[test]
fn sequence_numbers_filter_excludes_other_meta_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x00, 0x02, 0x00, 0x05];
events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"Note");
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Lead");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&[0x00, 0xFF, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x7F, 0x02, 0x41, 0x10]);
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 sn = smf.sequence_numbers();
assert_eq!(sn.len(), 1);
assert_eq!(sn[0].number, 5);
assert_eq!(smf.texts().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.tempo_map().len(), 1);
assert_eq!(smf.smpte_offsets().len(), 1);
assert_eq!(smf.time_signatures().len(), 1);
assert_eq!(smf.key_signatures().len(), 1);
assert_eq!(smf.sequencer_specifics().len(), 1);
}
#[test]
fn midi_ports_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.midi_ports().is_empty());
}
#[test]
fn midi_ports_single_hint_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x21, 0x01, 0x02, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ports = smf.midi_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0].tick, 0);
assert_eq!(ports[0].track, 0);
assert_eq!(ports[0].port(), 2);
}
#[test]
fn midi_ports_full_seven_bit_range_round_trips() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x21, 0x01, 0x7F, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ports = smf.midi_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0].port, 0x7F);
}
#[test]
fn midi_ports_per_track_routing_in_format_1() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xFF, 0x21, 0x01, 0x00]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xFF, 0x21, 0x01, 0x01]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t2: Vec<u8> = Vec::new();
t2.extend_from_slice(&[0x00, 0xFF, 0x21, 0x01, 0x02]);
t2.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 3, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
blob.extend(track_chunk(&t2));
let smf = parse(&blob).unwrap();
let ports = smf.midi_ports();
assert_eq!(ports.len(), 3);
assert_eq!(ports[0].track, 0);
assert_eq!(ports[0].port, 0);
assert_eq!(ports[1].track, 1);
assert_eq!(ports[1].port, 1);
assert_eq!(ports[2].track, 2);
assert_eq!(ports[2].port, 2);
}
#[test]
fn midi_ports_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, 0x21, 0x01, 0x05]);
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, 0x21, 0x01, 0x06]);
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 ports = smf.midi_ports();
assert_eq!(ports.len(), 2);
assert_eq!(ports[0].track, 0);
assert_eq!(ports[0].port, 5);
assert_eq!(ports[1].track, 1);
assert_eq!(ports[1].port, 6);
}
#[test]
fn midi_ports_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, 0x21, 0x01, 0x0A]);
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, 0x21, 0x01, 0x0B]);
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x21, 0x01, 0x0C]);
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 ports = smf.midi_ports();
assert_eq!(ports.len(), 3);
assert_eq!(ports[0].tick, 120);
assert_eq!(ports[0].track, 1);
assert_eq!(ports[0].port, 0x0B);
assert_eq!(ports[1].tick, 240);
assert_eq!(ports[1].track, 0);
assert_eq!(ports[1].port, 0x0A);
assert_eq!(ports[2].tick, 360);
assert_eq!(ports[2].track, 1);
assert_eq!(ports[2].port, 0x0C);
}
#[test]
fn midi_ports_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xFF, 0x21, 0x01, 0x03]);
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 ports = smf.midi_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0].tick, 240);
assert_eq!(ports[0].port, 3);
}
#[test]
fn midi_ports_filter_excludes_channel_prefix_and_other_meta_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x21, 0x01, 0x04];
events.extend_from_slice(&[0x00, 0xFF, 0x20, 0x01, 0x05]);
events.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x42]);
events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"Note");
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Lead");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&[0x00, 0xFF, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x7F, 0x02, 0x41, 0x10]);
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 ports = smf.midi_ports();
assert_eq!(ports.len(), 1);
assert_eq!(ports[0].port, 4);
assert_eq!(smf.texts().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.tempo_map().len(), 1);
assert_eq!(smf.smpte_offsets().len(), 1);
assert_eq!(smf.time_signatures().len(), 1);
assert_eq!(smf.key_signatures().len(), 1);
assert_eq!(smf.sequencer_specifics().len(), 1);
assert_eq!(smf.sequence_numbers().len(), 1);
}
#[test]
fn channel_prefixes_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.channel_prefixes().is_empty());
}
#[test]
fn channel_prefixes_single_binding_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x20, 0x01, 0x03, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_prefixes();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].tick, 0);
assert_eq!(cps[0].track, 0);
assert_eq!(cps[0].channel, 3);
assert_eq!(cps[0].channel(), Some(3));
}
#[test]
fn channel_prefixes_spec_nibble_range_round_trips() {
for cc in 0u8..=15 {
let events: Vec<u8> = vec![0x00, 0xFF, 0x20, 0x01, cc, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_prefixes();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].channel, cc);
assert_eq!(cps[0].channel(), Some(cc));
}
}
#[test]
fn channel_prefixes_out_of_spec_byte_surfaces_raw_and_channel_returns_none() {
let events: Vec<u8> = vec![0x00, 0xFF, 0x20, 0x01, 0x20, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_prefixes();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].channel, 0x20);
assert_eq!(cps[0].channel(), None);
}
#[test]
fn channel_prefixes_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, 0x20, 0x01, 0x02]);
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, 0x20, 0x01, 0x05]);
t1.extend_from_slice(&encode_vlq(240));
t1.extend_from_slice(&[0xFF, 0x20, 0x01, 0x09]);
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 cps = smf.channel_prefixes();
assert_eq!(cps.len(), 3);
assert_eq!(cps[0].tick, 120);
assert_eq!(cps[0].track, 1);
assert_eq!(cps[0].channel, 0x05);
assert_eq!(cps[1].tick, 240);
assert_eq!(cps[1].track, 0);
assert_eq!(cps[1].channel, 0x02);
assert_eq!(cps[2].tick, 360);
assert_eq!(cps[2].track, 1);
assert_eq!(cps[2].channel, 0x09);
}
#[test]
fn channel_prefixes_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, 0x20, 0x01, 0x07]);
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, 0x20, 0x01, 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 cps = smf.channel_prefixes();
assert_eq!(cps.len(), 2);
assert_eq!(cps[0].track, 0);
assert_eq!(cps[0].channel, 0x07);
assert_eq!(cps[1].track, 1);
assert_eq!(cps[1].channel, 0x08);
}
#[test]
fn channel_prefixes_filter_excludes_port_and_other_meta_kinds() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x20, 0x01, 0x06];
events.extend_from_slice(&[0x00, 0xFF, 0x21, 0x01, 0x04]);
events.extend_from_slice(&[0x00, 0xFF, 0x00, 0x02, 0x00, 0x42]);
events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x04]);
events.extend_from_slice(b"Note");
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x04]);
events.extend_from_slice(b"Lead");
events.extend_from_slice(&[0x00, 0xFF, 0x05, 0x02]);
events.extend_from_slice(b"la");
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
events.extend_from_slice(&[0x00, 0xFF, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x58, 0x04, 0x04, 0x02, 0x18, 0x08]);
events.extend_from_slice(&[0x00, 0xFF, 0x59, 0x02, 0x00, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x7F, 0x02, 0x41, 0x10]);
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 cps = smf.channel_prefixes();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].channel, 6);
assert_eq!(smf.midi_ports().len(), 1);
assert_eq!(smf.texts().len(), 1);
assert_eq!(smf.track_names().len(), 1);
assert_eq!(smf.lyrics().len(), 1);
assert_eq!(smf.tempo_map().len(), 1);
assert_eq!(smf.smpte_offsets().len(), 1);
assert_eq!(smf.time_signatures().len(), 1);
assert_eq!(smf.key_signatures().len(), 1);
assert_eq!(smf.sequencer_specifics().len(), 1);
assert_eq!(smf.sequence_numbers().len(), 1);
}
#[test]
fn channel_prefixes_to_bytes_round_trip() {
let mut events: Vec<u8> = vec![0x00, 0xFF, 0x20, 0x01, 0x0B];
events.extend_from_slice(&encode_vlq(96));
events.extend_from_slice(&[0xFF, 0x20, 0x01, 0x0C]);
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 rewritten = smf.to_bytes().unwrap();
let reparsed = parse(&rewritten).unwrap();
let cps = reparsed.channel_prefixes();
assert_eq!(cps.len(), 2);
assert_eq!(cps[0].tick, 0);
assert_eq!(cps[0].channel, 0x0B);
assert_eq!(cps[1].tick, 96);
assert_eq!(cps[1].channel, 0x0C);
}
#[test]
fn sysex_events_empty_when_no_sysex_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.sysex_events().is_empty());
}
#[test]
fn sysex_events_universal_gm_on_at_tick_zero() {
let events: Vec<u8> = vec![
0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sx = smf.sysex_events();
assert_eq!(sx.len(), 1);
assert_eq!(sx[0].tick, 0);
assert_eq!(sx[0].track, 0);
assert!(!sx[0].is_escape);
assert_eq!(sx[0].data, vec![0x7E, 0x7F, 0x09, 0x01, 0xF7]);
assert!(sx[0].ends_with_eox());
assert!(sx[0].is_complete_message());
assert_eq!(sx[0].manufacturer_id(), Some(0x7E));
}
#[test]
fn sysex_events_f0_without_trailing_f7_marks_multipacket_start() {
let events: Vec<u8> = vec![
0x00, 0xF0, 0x03, 0x41, 0x10, 0x42, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sx = smf.sysex_events();
assert_eq!(sx.len(), 1);
assert!(!sx[0].is_escape);
assert_eq!(sx[0].data, vec![0x41, 0x10, 0x42]);
assert!(!sx[0].ends_with_eox());
assert!(!sx[0].is_complete_message());
assert_eq!(sx[0].manufacturer_id(), Some(0x41));
}
#[test]
fn sysex_events_f7_continuation_pairs_after_f0() {
let mut events: Vec<u8> = vec![
0x00, 0xF0, 0x03, 0x41, 0x10, 0x42, 0x08, 0xF7, 0x02, 0x7B, 0xF7, ];
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 sx = smf.sysex_events();
assert_eq!(sx.len(), 2);
assert!(!sx[0].is_escape);
assert_eq!(sx[0].tick, 0);
assert_eq!(sx[0].manufacturer_id(), Some(0x41));
assert!(!sx[0].is_complete_message());
assert!(sx[1].is_escape);
assert_eq!(sx[1].tick, 8);
assert_eq!(sx[1].data, vec![0x7B, 0xF7]);
assert!(sx[1].ends_with_eox());
assert!(!sx[1].is_complete_message()); assert_eq!(sx[1].manufacturer_id(), None); }
#[test]
fn sysex_events_empty_payload_surfaces_verbatim() {
let events: Vec<u8> = vec![
0x00, 0xF0, 0x00, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let sx = smf.sysex_events();
assert_eq!(sx.len(), 1);
assert!(!sx[0].is_escape);
assert!(sx[0].data.is_empty());
assert!(!sx[0].ends_with_eox());
assert!(!sx[0].is_complete_message());
assert_eq!(sx[0].manufacturer_id(), None);
}
#[test]
fn sysex_events_merge_across_tracks_sorted_by_tick() {
let t0: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(100));
v.extend_from_slice(&[0xF0, 0x02, 0x7E, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let t1: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(50));
v.extend_from_slice(&[0xF0, 0x02, 0x41, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sx = smf.sysex_events();
assert_eq!(sx.len(), 2);
assert_eq!(sx[0].tick, 50);
assert_eq!(sx[0].track, 1);
assert_eq!(sx[0].manufacturer_id(), Some(0x41));
assert_eq!(sx[1].tick, 100);
assert_eq!(sx[1].track, 0);
assert_eq!(sx[1].manufacturer_id(), Some(0x7E));
}
#[test]
fn sysex_events_stable_sort_keeps_track0_before_track1_at_same_tick() {
let t0: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(64));
v.extend_from_slice(&[0xF0, 0x02, 0x10, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let t1: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(64));
v.extend_from_slice(&[0xF0, 0x02, 0x20, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let sx = smf.sysex_events();
assert_eq!(sx.len(), 2);
assert_eq!(sx[0].tick, 64);
assert_eq!(sx[0].track, 0);
assert_eq!(sx[0].manufacturer_id(), Some(0x10));
assert_eq!(sx[1].tick, 64);
assert_eq!(sx[1].track, 1);
assert_eq!(sx[1].manufacturer_id(), Some(0x20));
}
#[test]
fn sysex_events_filter_excludes_meta_and_channel_events() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x03, 0x02, b'A', b'B']); events.extend_from_slice(&[0x00, 0xFF, 0x01, 0x01, b'X']); events.extend_from_slice(&[0x00, 0xFF, 0x21, 0x01, 0x02]); events.extend_from_slice(&[0x00, 0xFF, 0x20, 0x01, 0x03]); events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]); events.extend_from_slice(&[0x00, 0xF0, 0x03, 0x7F, 0x10, 0xF7]); events.extend_from_slice(&[0x00, 0xFF, 0x7F, 0x02, 0xAA, 0xBB]); events.extend_from_slice(&[0x00, 0xF7, 0x02, 0xCC, 0xF7]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x64]); events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x40]); 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 sx = smf.sysex_events();
assert_eq!(sx.len(), 2);
assert!(!sx[0].is_escape);
assert_eq!(sx[0].data, vec![0x7F, 0x10, 0xF7]);
assert!(sx[1].is_escape);
assert_eq!(sx[1].data, vec![0xCC, 0xF7]);
let ss = smf.sequencer_specifics();
assert_eq!(ss.len(), 1);
assert_eq!(ss[0].data, vec![0xAA, 0xBB]);
}
#[test]
fn sysex_events_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
events.extend_from_slice(&encode_vlq(48));
events.extend_from_slice(&[0xF0, 0x03, 0x41, 0x10, 0x42]); events.extend_from_slice(&[0x08, 0xF7, 0x02, 0x7B, 0xF7]); 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 rewritten = smf.to_bytes().unwrap();
let reparsed = parse(&rewritten).unwrap();
let sx = reparsed.sysex_events();
assert_eq!(sx.len(), 3);
assert_eq!(sx[0].tick, 0);
assert!(!sx[0].is_escape);
assert_eq!(sx[0].data, vec![0x7E, 0x7F, 0x09, 0x01, 0xF7]);
assert!(sx[0].is_complete_message());
assert_eq!(sx[1].tick, 48);
assert!(!sx[1].is_escape);
assert_eq!(sx[1].data, vec![0x41, 0x10, 0x42]);
assert_eq!(sx[1].manufacturer_id(), Some(0x41));
assert_eq!(sx[2].tick, 56);
assert!(sx[2].is_escape);
assert_eq!(sx[2].data, vec![0x7B, 0xF7]);
assert!(sx[2].ends_with_eox());
}
#[test]
fn universal_sysex_events_empty_when_no_sysex_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.universal_sysex_events().is_empty());
}
#[test]
fn universal_sysex_events_filters_out_manufacturer_prefixed_and_escape_packets() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x06, 0x41, 0x10, 0x42, 0x01, 0x02, 0xF7]);
events.extend_from_slice(&[0x08, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
events.extend_from_slice(&[0x08, 0xF7, 0x02, 0xCC, 0xF7]);
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();
assert_eq!(smf.sysex_events().len(), 3);
let u = smf.universal_sysex_events();
assert_eq!(u.len(), 1);
assert_eq!(u[0].tick, 8);
assert_eq!(u[0].track, 0);
assert_eq!(u[0].classification.realm, UniversalRealm::NonRealTime);
assert_eq!(u[0].classification.device_id, 0x7F);
assert_eq!(
u[0].classification.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
assert_eq!(u[0].data, vec![0x7E, 0x7F, 0x09, 0x01, 0xF7]);
}
#[test]
fn universal_sysex_events_classifies_both_realms() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
events.extend_from_slice(&[0x40, 0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, 0x30, 0x30, 0xF7]);
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 u = smf.universal_sysex_events();
assert_eq!(u.len(), 2);
assert_eq!(u[0].tick, 0);
assert_eq!(u[0].classification.realm, UniversalRealm::NonRealTime);
assert_eq!(
u[0].classification.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
assert_eq!(u[1].tick, 64);
assert_eq!(u[1].classification.realm, UniversalRealm::RealTime);
assert_eq!(u[1].classification.device_id, 0x7F);
assert_eq!(
u[1].classification.sub_id1,
UniversalSubId1::DeviceControl(UniversalSubId2::DeviceControlMasterVolume),
);
assert_eq!(u[1].data, vec![0x7F, 0x7F, 0x04, 0x01, 0x30, 0x30, 0xF7]);
}
#[test]
fn universal_sysex_events_merge_across_tracks_sorted_by_tick() {
let t0: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(100));
v.extend_from_slice(&[0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let t1: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(50));
v.extend_from_slice(&[0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, 0x30, 0x30, 0xF7]);
v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let u = smf.universal_sysex_events();
assert_eq!(u.len(), 2);
assert_eq!(u[0].tick, 50);
assert_eq!(u[0].track, 1);
assert_eq!(u[0].classification.realm, UniversalRealm::RealTime);
assert_eq!(u[1].tick, 100);
assert_eq!(u[1].track, 0);
assert_eq!(u[1].classification.realm, UniversalRealm::NonRealTime);
}
#[test]
fn universal_sysex_events_stable_sort_keeps_track0_before_track1_at_same_tick() {
let t0: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(64));
v.extend_from_slice(&[0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]); v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let t1: Vec<u8> = {
let mut v: Vec<u8> = Vec::new();
v.extend_from_slice(&encode_vlq(64));
v.extend_from_slice(&[0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, 0x40, 0x40, 0xF7]); v.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
v
};
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let u = smf.universal_sysex_events();
assert_eq!(u.len(), 2);
assert_eq!(u[0].tick, 64);
assert_eq!(u[0].track, 0);
assert_eq!(u[0].classification.realm, UniversalRealm::NonRealTime);
assert_eq!(u[1].tick, 64);
assert_eq!(u[1].track, 1);
assert_eq!(u[1].classification.realm, UniversalRealm::RealTime);
}
#[test]
fn universal_sysex_events_drops_truncated_universal_packets() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x02, 0x7E, 0x7F]);
events.extend_from_slice(&[0x08, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
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();
assert_eq!(smf.sysex_events().len(), 2);
let u = smf.universal_sysex_events();
assert_eq!(u.len(), 1);
assert_eq!(u[0].tick, 8);
assert_eq!(
u[0].classification.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
}
#[test]
fn universal_sysex_events_classification_matches_per_event_call() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]); events.extend_from_slice(&[0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x02, 0xF7]); events.extend_from_slice(&[0x00, 0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, 0x30, 0x30, 0xF7]); events.extend_from_slice(&[
0x00, 0xF0, 0x09, 0x7F, 0x00, 0x01, 0x01, 0x10, 0x20, 0x30, 0x40, 0xF7,
]); 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 u = smf.universal_sysex_events();
let parallel: Vec<(u64, usize, UniversalSysEx, Vec<u8>)> = smf
.sysex_events()
.into_iter()
.filter_map(|ev| {
ev.universal_classification()
.map(|c| (ev.tick, ev.track, c, ev.data.clone()))
})
.collect();
assert_eq!(u.len(), parallel.len());
assert_eq!(u.len(), 4);
for (lhs, rhs) in u.iter().zip(parallel.iter()) {
assert_eq!(lhs.tick, rhs.0);
assert_eq!(lhs.track, rhs.1);
assert_eq!(lhs.classification, rhs.2);
assert_eq!(lhs.data, rhs.3);
}
}
fn make_sysex(payload: &[u8]) -> SysExEvent {
SysExEvent {
tick: 0,
track: 0,
is_escape: false,
data: payload.to_vec(),
}
}
fn make_sysex_escape(payload: &[u8]) -> SysExEvent {
SysExEvent {
tick: 0,
track: 0,
is_escape: true,
data: payload.to_vec(),
}
}
#[test]
fn universal_classification_returns_none_for_f7_continuation_packet() {
let ev = make_sysex_escape(&[0x7E, 0x7F, 0x09, 0x01, 0xF7]);
assert!(ev.universal_classification().is_none());
}
#[test]
fn universal_classification_returns_none_for_manufacturer_prefixed_packet() {
let ev = make_sysex(&[0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7]);
assert!(ev.universal_classification().is_none());
}
#[test]
fn universal_classification_returns_none_for_three_byte_expanded_manufacturer_id() {
let ev = make_sysex(&[0x00, 0x20, 0x33, 0x01, 0xF7]);
assert!(ev.universal_classification().is_none());
}
#[test]
fn universal_classification_returns_none_for_truncated_packet_below_three_bytes() {
let ev = make_sysex(&[0x7E, 0x7F]); assert!(ev.universal_classification().is_none());
let ev = make_sysex(&[0x7E]); assert!(ev.universal_classification().is_none());
let ev = make_sysex(&[]); assert!(ev.universal_classification().is_none());
}
#[test]
fn universal_classification_gm1_system_on() {
let ev = make_sysex(&[0x7E, 0x7F, 0x09, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(u.device_id, 0x7F);
assert_eq!(
u.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
}
#[test]
fn universal_classification_gm_system_off() {
let ev = make_sysex(&[0x7E, 0x7F, 0x09, 0x02, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(
u.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidiSystemOff
)
);
}
#[test]
fn universal_classification_gm2_system_on() {
let ev = make_sysex(&[0x7E, 0x7F, 0x09, 0x03, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(
u.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi2SystemOn,
)
);
}
#[test]
fn universal_classification_master_volume_real_time() {
let ev = make_sysex(&[0x7F, 0x7F, 0x04, 0x01, 0x40, 0x60, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::RealTime);
assert_eq!(u.device_id, 0x7F);
assert_eq!(
u.sub_id1,
UniversalSubId1::DeviceControl(UniversalSubId2::DeviceControlMasterVolume)
);
}
#[test]
fn universal_classification_master_balance_fine_coarse_global() {
let cases = [
(0x02u8, UniversalSubId2::DeviceControlMasterBalance),
(0x03, UniversalSubId2::DeviceControlMasterFineTuning),
(0x04, UniversalSubId2::DeviceControlMasterCoarseTuning),
(0x05, UniversalSubId2::DeviceControlGlobalParameterControl),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7F, 0x7F, 0x04, sub2, 0x00, 0x40, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::RealTime);
assert_eq!(u.sub_id1, UniversalSubId1::DeviceControl(expected));
}
}
#[test]
fn universal_classification_realm_disambiguates_shared_byte_pair() {
let nrt = make_sysex(&[0x7E, 0x7F, 0x09, 0x01, 0xF7]);
let rt = make_sysex(&[0x7F, 0x7F, 0x09, 0x01, 0xF7]);
let unrt = nrt.universal_classification().unwrap();
let urt = rt.universal_classification().unwrap();
assert_eq!(
unrt.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
assert_eq!(
urt.sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::ControllerDestinationChannelPressure,
)
);
}
#[test]
fn universal_classification_mtc_full_message_real_time() {
let ev = make_sysex(&[0x7F, 0x00, 0x01, 0x01, 0x21, 0x18, 0x2D, 0x00, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::RealTime);
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiTimeCode(UniversalSubId2::RtMtcFullMessage)
);
}
#[test]
fn universal_classification_mtc_user_bits_real_time() {
let ev = make_sysex(&[0x7F, 0x00, 0x01, 0x02, 0x00, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiTimeCode(UniversalSubId2::RtMtcUserBits)
);
}
#[test]
fn universal_classification_nonrt_mtc_setup_punch_in_points() {
let ev = make_sysex(&[0x7E, 0x00, 0x04, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiTimeCode(UniversalSubId2::NonRtMtcPunchInPoints)
);
}
#[test]
fn universal_classification_nonrt_mtc_full_setup_table() {
let cases = [
(0x00u8, UniversalSubId2::NonRtMtcSpecial),
(0x01, UniversalSubId2::NonRtMtcPunchInPoints),
(0x02, UniversalSubId2::NonRtMtcPunchOutPoints),
(0x03, UniversalSubId2::NonRtMtcDeletePunchInPoint),
(0x04, UniversalSubId2::NonRtMtcDeletePunchOutPoint),
(0x05, UniversalSubId2::NonRtMtcEventStartPoint),
(0x06, UniversalSubId2::NonRtMtcEventStopPoint),
(0x07, UniversalSubId2::NonRtMtcEventStartPointsWithInfo),
(0x08, UniversalSubId2::NonRtMtcEventStopPointsWithInfo),
(0x09, UniversalSubId2::NonRtMtcDeleteEventStartPoint),
(0x0A, UniversalSubId2::NonRtMtcDeleteEventStopPoint),
(0x0B, UniversalSubId2::NonRtMtcCuePoints),
(0x0C, UniversalSubId2::NonRtMtcCuePointsWithInfo),
(0x0D, UniversalSubId2::NonRtMtcDeleteCuePoint),
(0x0E, UniversalSubId2::NonRtMtcEventNameWithInfo),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7E, 0x7F, 0x04, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.sub_id1, UniversalSubId1::MidiTimeCode(expected));
}
}
#[test]
fn universal_classification_sample_dump_singletons_nonrt() {
let cases = [
(0x01u8, UniversalSubId1::SampleDumpHeader),
(0x02, UniversalSubId1::SampleDataPacket),
(0x03, UniversalSubId1::SampleDumpRequest),
];
for (sub1, expected) in cases {
let ev = make_sysex(&[0x7E, 0x00, sub1, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(u.sub_id1, expected);
}
}
#[test]
fn universal_classification_general_information_identity_pair() {
let req = make_sysex(&[0x7E, 0x7F, 0x06, 0x01, 0xF7]);
let rep = make_sysex(&[0x7E, 0x00, 0x06, 0x02, 0xF7]);
let ureq = req.universal_classification().unwrap();
let urep = rep.universal_classification().unwrap();
assert_eq!(
ureq.sub_id1,
UniversalSubId1::GeneralInformationOrMmcCommands(
UniversalSubId2::GeneralInformationIdentityRequest,
)
);
assert_eq!(
urep.sub_id1,
UniversalSubId1::GeneralInformationOrMmcCommands(
UniversalSubId2::GeneralInformationIdentityReply,
)
);
}
#[test]
fn universal_classification_file_dump_header_data_request() {
let cases = [
(0x01u8, UniversalSubId2::FileDumpHeader),
(0x02, UniversalSubId2::FileDumpDataPacket),
(0x03, UniversalSubId2::FileDumpRequest),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7E, 0x00, 0x07, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.sub_id1, UniversalSubId1::FileDumpOrMmcResponses(expected));
}
}
#[test]
fn universal_classification_mts_nonrt_full_table() {
let cases = [
(0x00u8, UniversalSubId2::MtsBulkDumpRequest),
(0x01, UniversalSubId2::MtsBulkDumpReply),
(0x03, UniversalSubId2::MtsTuningDumpRequest),
(0x04, UniversalSubId2::MtsKeyBasedTuningDump),
(0x05, UniversalSubId2::MtsScaleOctaveTuningDump1Byte),
(0x06, UniversalSubId2::MtsScaleOctaveTuningDump2Byte),
(
0x07,
UniversalSubId2::MtsSingleNoteTuningChangeWithBankSelect,
),
(0x08, UniversalSubId2::MtsScaleOctaveTuning1Byte),
(0x09, UniversalSubId2::MtsScaleOctaveTuning2Byte),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7E, 0x7F, 0x08, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(u.sub_id1, UniversalSubId1::MidiTuningStandard(expected));
}
}
#[test]
fn universal_classification_mts_rt_full_table() {
let cases = [
(0x02u8, UniversalSubId2::MtsRtSingleNoteTuningChange),
(
0x07,
UniversalSubId2::MtsSingleNoteTuningChangeWithBankSelect,
),
(0x08, UniversalSubId2::MtsScaleOctaveTuning1Byte),
(0x09, UniversalSubId2::MtsScaleOctaveTuning2Byte),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7F, 0x7F, 0x08, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::RealTime);
assert_eq!(u.sub_id1, UniversalSubId1::MidiTuningStandard(expected));
}
}
#[test]
fn universal_classification_dls_quartet_nonrt() {
let cases = [
(0x01u8, UniversalSubId2::DlsTurnOn),
(0x02, UniversalSubId2::DlsTurnOff),
(0x03, UniversalSubId2::DlsTurnVoiceAllocationOff),
(0x04, UniversalSubId2::DlsTurnVoiceAllocationOn),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7E, 0x7F, 0x0A, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::DownloadableSoundsOrKeyBasedInstrumentControl(expected)
);
}
}
#[test]
fn universal_classification_file_reference_quartet_nonrt() {
let cases = [
(0x01u8, UniversalSubId2::FileReferenceOpenFile),
(0x02, UniversalSubId2::FileReferenceSelectOrReselectContents),
(
0x03,
UniversalSubId2::FileReferenceOpenFileAndSelectContents,
),
(0x04, UniversalSubId2::FileReferenceCloseFile),
];
for (sub2, expected) in cases {
let ev = make_sysex(&[0x7E, 0x7F, 0x0B, sub2, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::FileReferenceOrScalablePolyphonyMip(expected)
);
}
}
#[test]
fn universal_classification_mvc_and_capability_inquiry_route_through() {
let ev = make_sysex(&[0x7E, 0x7F, 0x0C, 0x12, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiVisualControlOrMobilePhoneControl(UniversalSubId2::Other(0x12))
);
let ev = make_sysex(&[0x7E, 0x7F, 0x0D, 0x34, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiCapabilityInquiry(UniversalSubId2::Other(0x34))
);
}
#[test]
fn universal_classification_singletons_eof_wait_cancel_nak_ack() {
let cases = [
(0x7Bu8, UniversalSubId1::EndOfFile),
(0x7C, UniversalSubId1::Wait),
(0x7D, UniversalSubId1::Cancel),
(0x7E, UniversalSubId1::Nak),
(0x7F, UniversalSubId1::Ack),
];
for (sub1, expected) in cases {
let ev = make_sysex(&[0x7E, 0x00, sub1, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.realm, UniversalRealm::NonRealTime);
assert_eq!(u.sub_id1, expected);
}
}
#[test]
fn universal_classification_realm_zero_device_id_preserved() {
let ev = make_sysex(&[0x7E, 0x00, 0x09, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.device_id, 0x00);
let ev = make_sysex(&[0x7E, 0x05, 0x09, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.device_id, 0x05);
let ev = make_sysex(&[0x7E, 0x7F, 0x09, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.device_id, 0x7F);
}
#[test]
fn universal_classification_notation_information_real_time() {
let bar = make_sysex(&[0x7F, 0x7F, 0x03, 0x01, 0x00, 0x00, 0xF7]);
let imm = make_sysex(&[0x7F, 0x7F, 0x03, 0x02, 0x04, 0x02, 0xF7]);
let del = make_sysex(&[0x7F, 0x7F, 0x03, 0x42, 0x04, 0x02, 0xF7]);
assert_eq!(
bar.universal_classification().unwrap().sub_id1,
UniversalSubId1::NotationInformation(UniversalSubId2::RtNotationBarNumber)
);
assert_eq!(
imm.universal_classification().unwrap().sub_id1,
UniversalSubId1::NotationInformation(UniversalSubId2::RtNotationTimeSignatureImmediate)
);
assert_eq!(
del.universal_classification().unwrap().sub_id1,
UniversalSubId1::NotationInformation(UniversalSubId2::RtNotationTimeSignatureDelayed)
);
}
#[test]
fn universal_classification_msc_extensions_distinct_from_msc_commands() {
let ext = make_sysex(&[0x7F, 0x00, 0x02, 0x00, 0xF7]);
assert_eq!(
ext.universal_classification().unwrap().sub_id1,
UniversalSubId1::MidiShowControl(UniversalSubId2::RtMscExtensions)
);
let cmd = make_sysex(&[0x7F, 0x00, 0x02, 0x01, 0xF7]);
assert_eq!(
cmd.universal_classification().unwrap().sub_id1,
UniversalSubId1::MidiShowControl(UniversalSubId2::Other(0x01))
);
}
#[test]
fn universal_classification_realm_distinguishes_nonrt_02_from_rt_02() {
let nrt = make_sysex(&[0x7E, 0x00, 0x02, 0xF7]);
let rt = make_sysex(&[0x7F, 0x00, 0x02, 0x00, 0xF7]);
assert_eq!(
nrt.universal_classification().unwrap().sub_id1,
UniversalSubId1::SampleDataPacket
);
assert_eq!(
rt.universal_classification().unwrap().sub_id1,
UniversalSubId1::MidiShowControl(UniversalSubId2::RtMscExtensions)
);
}
#[test]
fn universal_classification_unknown_sub_id1_surfaces_through_other() {
let ev = make_sysex(&[0x7E, 0x7F, 0x40, 0x01, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(u.sub_id1, UniversalSubId1::Other(0x40));
}
#[test]
fn universal_classification_unknown_sub_id2_surfaces_through_other() {
let ev = make_sysex(&[0x7E, 0x7F, 0x04, 0x77, 0xF7]);
let u = ev.universal_classification().unwrap();
assert_eq!(
u.sub_id1,
UniversalSubId1::MidiTimeCode(UniversalSubId2::Other(0x77))
);
}
#[test]
fn universal_classification_threaded_through_sysex_events_collection() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xF0, 0x05, 0x7E, 0x7F, 0x09, 0x01, 0xF7]);
events.extend_from_slice(&[0x00, 0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, 0x40, 0x60, 0xF7]);
events.extend_from_slice(&[
0x00, 0xF0, 0x0A, 0x7F, 0x7F, 0x04, 0x03, 0x00, 0x40, 0x00, 0x00, 0x00, 0xF7,
]);
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 sx = smf.sysex_events();
assert_eq!(sx.len(), 3);
let classes: Vec<_> = sx
.iter()
.map(|s| s.universal_classification().unwrap())
.collect();
assert_eq!(
classes[0].sub_id1,
UniversalSubId1::GeneralMidiOrControllerDestination(
UniversalSubId2::GeneralMidi1SystemOn,
)
);
assert_eq!(
classes[1].sub_id1,
UniversalSubId1::DeviceControl(UniversalSubId2::DeviceControlMasterVolume)
);
assert_eq!(
classes[2].sub_id1,
UniversalSubId1::DeviceControl(UniversalSubId2::DeviceControlMasterFineTuning)
);
assert_eq!(classes[0].realm, UniversalRealm::NonRealTime);
assert_eq!(classes[1].realm, UniversalRealm::RealTime);
assert_eq!(classes[2].realm, UniversalRealm::RealTime);
}
#[test]
fn channel_snapshot_default_when_no_events() {
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();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap, SmfChannelSnapshot::default());
assert_eq!(snap.program, None);
assert_eq!(snap.bank_msb, None);
assert_eq!(snap.bank_lsb, None);
assert_eq!(snap.volume, 100);
assert_eq!(snap.pan, 64);
assert_eq!(snap.expression, 127);
assert_eq!(snap.modulation, 0);
assert!(!snap.sustain);
assert_eq!(snap.pitch_bend, 0x2000);
}
#[test]
fn channel_snapshot_program_change_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xC0, 0x05, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.program, Some(5));
let other = smf.channel_snapshot_at(1, 0);
assert_eq!(other.program, None);
}
#[test]
fn channel_snapshot_cc_volume_pan_expression_mod() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x07, 0x50, 0x00, 0xB0, 0x0A, 0x00, 0x00, 0xB0, 0x0B, 0x64, 0x00, 0xB0, 0x01, 0x32, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.volume, 80);
assert_eq!(snap.pan, 0);
assert_eq!(snap.expression, 100);
assert_eq!(snap.modulation, 50);
}
#[test]
fn channel_snapshot_sustain_pedal_threshold_64() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x40, 0x3F, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert!(!snap.sustain);
let events: Vec<u8> = vec![
0x00, 0xB0, 0x40, 0x40, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert!(snap.sustain);
}
#[test]
fn channel_snapshot_bank_select_msb_and_lsb_independent() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x00, 0x07, 0x00, 0xB0, 0x20, 0x03, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.bank_msb, Some(7));
assert_eq!(snap.bank_lsb, Some(3));
}
#[test]
fn channel_snapshot_pitch_bend_14bit_decoded() {
let events: Vec<u8> = vec![
0x00, 0xE0, 0x40, 0x20, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.pitch_bend, 0x1040);
let other = smf.channel_snapshot_at(1, 0);
assert_eq!(other.pitch_bend, 0x2000);
}
#[test]
fn channel_snapshot_tick_filter_includes_events_at_tick_exactly() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xB0, 0x07, 0x32]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xB0, 0x07, 0x5A]); 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.channel_snapshot_at(0, 0).volume, 100);
assert_eq!(smf.channel_snapshot_at(0, 99).volume, 100);
assert_eq!(smf.channel_snapshot_at(0, 100).volume, 50);
assert_eq!(smf.channel_snapshot_at(0, 199).volume, 50);
assert_eq!(smf.channel_snapshot_at(0, 200).volume, 90);
assert_eq!(smf.channel_snapshot_at(0, 99_999).volume, 90);
}
#[test]
fn channel_snapshot_running_status_program_changes_replayed() {
let events: Vec<u8> = vec![
0x00, 0xC0, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.program, Some(3));
}
#[test]
fn channel_snapshot_multi_channel_independence() {
let events: Vec<u8> = vec![
0x00, 0xC0, 0x0A, 0x00, 0xC5, 0x32, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap0 = smf.channel_snapshot_at(0, 0);
let snap5 = smf.channel_snapshot_at(5, 0);
assert_eq!(snap0.program, Some(10));
assert_eq!(snap5.program, Some(50));
assert_eq!(smf.channel_snapshot_at(1, 0).program, None);
}
#[test]
fn channel_snapshot_notes_and_aftertouch_do_not_alter_state() {
let events: Vec<u8> = vec![
0x00, 0x90, 0x3C, 0x64, 0x00, 0x80, 0x3C, 0x00, 0x00, 0xA0, 0x3C, 0x40, 0x00, 0xD0, 0x55, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 999);
assert_eq!(snap, SmfChannelSnapshot::default());
}
#[test]
fn channel_snapshot_unknown_ccs_ignored() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x63, 0x10, 0x00, 0xB0, 0x64, 0x20, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let snap = smf.channel_snapshot_at(0, 999);
assert_eq!(snap, SmfChannelSnapshot::default());
}
#[test]
fn channel_snapshot_invalid_channel_returns_default() {
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_eq!(
smf.channel_snapshot_at(16, 0),
SmfChannelSnapshot::default()
);
assert_eq!(
smf.channel_snapshot_at(255, 0),
SmfChannelSnapshot::default()
);
}
#[test]
fn channel_snapshots_at_returns_sixteen_independent_states() {
let mut events: Vec<u8> = Vec::new();
for ch in 0u8..16 {
let status = 0xC0 | ch;
let program = 2 * ch + 1;
events.extend_from_slice(&[0x00, status, program]);
}
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 snaps = smf.channel_snapshots_at(0);
for (ch, snap) in snaps.iter().enumerate() {
assert_eq!(
snap.program,
Some((2 * ch + 1) as u8),
"channel {ch} expected program {}",
2 * ch + 1
);
}
for ch in 0u8..16 {
assert_eq!(snaps[ch as usize], smf.channel_snapshot_at(ch, 0));
}
}
#[test]
fn channel_snapshots_at_merge_across_tracks_track0_wins_at_same_tick() {
let t0: Vec<u8> = vec![0x00, 0xB0, 0x07, 0x1E, 0x00, 0xFF, 0x2F, 0x00];
let t1: Vec<u8> = vec![0x00, 0xB0, 0x07, 0x46, 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 snap = smf.channel_snapshot_at(0, 0);
assert_eq!(snap.volume, 70);
}
#[test]
fn channel_snapshot_apply_is_public_reusable() {
let mut snap = SmfChannelSnapshot::default();
snap.apply(&ChannelBody::ProgramChange { program: 42 });
snap.apply(&ChannelBody::ControlChange {
controller: 7,
value: 10,
});
snap.apply(&ChannelBody::ControlChange {
controller: 64,
value: 80,
});
snap.apply(&ChannelBody::PitchBend { value: 12345 });
assert_eq!(snap.program, Some(42));
assert_eq!(snap.volume, 10);
assert!(snap.sustain);
assert_eq!(snap.pitch_bend, 12345);
let before = snap;
snap.apply(&ChannelBody::NoteOn {
key: 60,
velocity: 100,
});
assert_eq!(snap, before);
}
#[test]
fn write_vlq_matches_test_helper_across_spec_examples() {
let cases: &[(u32, &[u8])] = &[
(0, &[0x00]),
(0x40, &[0x40]),
(0x7F, &[0x7F]),
(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 buf = Vec::new();
write_vlq(&mut buf, *v).unwrap();
assert_eq!(buf, bytes.to_vec(), "encode VLQ {v:#x}");
let mut c = Cursor::new(&buf);
assert_eq!(read_vlq(&mut c).unwrap(), *v);
}
}
#[test]
fn write_vlq_rejects_over_cap() {
let mut buf = Vec::new();
let err = write_vlq(&mut buf, MAX_VLQ_VALUE + 1).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn header_round_trip_format_0_tpqn() {
let header = SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(480),
};
let track = Track {
events: vec![TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
}],
};
let smf = SmfFile {
header,
tracks: vec![track],
};
let bytes = smf.to_bytes().unwrap();
assert_eq!(bytes.len(), 14 + 12);
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn header_round_trip_smpte_minus_25_fps() {
let header = SmfHeader {
format: SmfFormat::MultiTrackSimultaneous,
ntrks: 1,
division: Division::Smpte {
frames_per_second: 25,
ticks_per_frame: 40,
},
};
let track = Track {
events: vec![TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
}],
};
let smf = SmfFile {
header,
tracks: vec![track],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn round_trip_type_0_full_track() {
let track = Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::Tempo(500_000)),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::TimeSignature {
numerator: 4,
denominator_pow2: 2,
clocks_per_click: 24,
notated_32nd_per_quarter: 8,
}),
},
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
}),
},
TrackEvent {
delta: 480,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOff {
key: 60,
velocity: 0x40,
},
}),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
};
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(480),
},
tracks: vec![track],
};
let bytes = smf.to_bytes().unwrap();
assert_eq!(&bytes[..4], b"MThd");
assert_eq!(&bytes[4..8], &6u32.to_be_bytes());
assert_eq!(&bytes[8..10], &0u16.to_be_bytes());
assert_eq!(&bytes[10..12], &1u16.to_be_bytes());
assert_eq!(&bytes[12..14], &480u16.to_be_bytes());
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn round_trip_all_meta_event_kinds() {
let metas = vec![
MetaEvent::SequenceNumber(0xBEEF),
MetaEvent::Text {
kind: 0x01,
text: b"hello".to_vec(),
},
MetaEvent::Text {
kind: 0x05,
text: b"la".to_vec(),
},
MetaEvent::ChannelPrefix(3),
MetaEvent::Port(7),
MetaEvent::Tempo(500_000),
MetaEvent::SmpteOffset {
hours: 0x21,
minutes: 12,
seconds: 34,
frames: 5,
subframes: 50,
},
MetaEvent::TimeSignature {
numerator: 6,
denominator_pow2: 3,
clocks_per_click: 36,
notated_32nd_per_quarter: 8,
},
MetaEvent::KeySignature {
sharps_flats: -3,
mode: 1,
},
MetaEvent::SequencerSpecific(vec![0x41, 0x10, 0x42]),
MetaEvent::Unknown {
type_byte: 0x60,
data: vec![0xDE, 0xAD],
},
];
let mut events: Vec<TrackEvent> = metas
.into_iter()
.map(|m| TrackEvent {
delta: 0,
kind: Event::Meta(m),
})
.collect();
events.push(TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
});
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track { events }],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn round_trip_all_channel_voice_kinds() {
let bodies = vec![
ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
ChannelBody::NoteOff {
key: 60,
velocity: 64,
},
ChannelBody::PolyAftertouch {
key: 60,
pressure: 80,
},
ChannelBody::ControlChange {
controller: 7,
value: 100,
},
ChannelBody::ProgramChange { program: 32 },
ChannelBody::ChannelAftertouch { pressure: 90 },
ChannelBody::PitchBend { value: 0x2000 },
ChannelBody::PitchBend { value: 0x3FFF },
ChannelBody::PitchBend { value: 0 },
];
let mut events: Vec<TrackEvent> = bodies
.into_iter()
.map(|b| TrackEvent {
delta: 10,
kind: Event::Channel(ChannelMessage {
channel: 5,
body: b,
}),
})
.collect();
events.push(TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
});
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(480),
},
tracks: vec![Track { events }],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn round_trip_sysex_f0_and_f7() {
let events = vec![
TrackEvent {
delta: 0,
kind: Event::Sysex {
escape: false,
data: vec![0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7],
},
},
TrackEvent {
delta: 20,
kind: Event::Sysex {
escape: true,
data: vec![0xFE], },
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
];
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track { events }],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn round_trip_format_1_multi_track() {
let t0 = Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::Tempo(400_000)),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
};
let t1 = Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 72,
velocity: 110,
},
}),
},
TrackEvent {
delta: 240,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOff {
key: 72,
velocity: 0,
},
}),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
};
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::MultiTrackSimultaneous,
ntrks: 2,
division: Division::TicksPerQuarter(96),
},
tracks: vec![t0, t1],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn write_rejects_track_missing_end_of_track() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track {
events: vec![TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
}),
}],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn write_rejects_eot_not_last() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
}),
},
],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn write_rejects_ntrks_mismatch() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::MultiTrackSimultaneous,
ntrks: 3,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track {
events: vec![TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
}],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn write_rejects_pitch_bend_out_of_range() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::PitchBend { value: 0x4000 },
}),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn write_rejects_data_byte_with_status_bit() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
tracks: vec![Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 0x80,
velocity: 100,
},
}),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn write_rejects_invalid_smpte_fps() {
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::Smpte {
frames_per_second: 60,
ticks_per_frame: 40,
},
},
tracks: vec![Track {
events: vec![TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
}],
}],
};
let err = smf.to_bytes().unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn round_trip_large_delta_uses_multi_byte_vlq() {
let events = vec![
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOn {
key: 60,
velocity: 100,
},
}),
},
TrackEvent {
delta: 0x20_0000,
kind: Event::Channel(ChannelMessage {
channel: 0,
body: ChannelBody::NoteOff {
key: 60,
velocity: 64,
},
}),
},
TrackEvent {
delta: 0,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
];
let smf = SmfFile {
header: SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(480),
},
tracks: vec![Track { events }],
};
let bytes = smf.to_bytes().unwrap();
let parsed = parse(&bytes).unwrap();
assert_eq!(parsed, smf);
}
#[test]
fn track_to_bytes_chunk_round_trips_inside_outer_file() {
let track = Track {
events: vec![
TrackEvent {
delta: 0,
kind: Event::Channel(ChannelMessage {
channel: 9, body: ChannelBody::NoteOn {
key: 36,
velocity: 110,
},
}),
},
TrackEvent {
delta: 120,
kind: Event::Meta(MetaEvent::EndOfTrack),
},
],
};
let chunk = track.to_bytes_chunk().unwrap();
assert_eq!(&chunk[..4], b"MTrk");
let body_len = u32::from_be_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]) as usize;
assert_eq!(chunk.len(), 8 + body_len);
let mut bytes = Vec::new();
write_header_chunk(
&mut bytes,
&SmfHeader {
format: SmfFormat::SingleTrack,
ntrks: 1,
division: Division::TicksPerQuarter(96),
},
)
.unwrap();
bytes.extend_from_slice(&chunk);
let smf = parse(&bytes).unwrap();
assert_eq!(smf.tracks[0], track);
}
#[test]
fn program_changes_empty_when_no_patch_select_present() {
let events: Vec<u8> = vec![
0x00, 0x90, 0x3C, 0x64, 0x60, 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.program_changes().is_empty());
}
#[test]
fn program_changes_single_patch_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xC0, 0x28, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 1);
assert_eq!(pcs[0].tick, 0);
assert_eq!(pcs[0].track, 0);
assert_eq!(pcs[0].channel(), 0);
assert_eq!(pcs[0].program(), 40);
}
#[test]
fn program_changes_full_seven_bit_program_range_round_trips() {
let events: Vec<u8> = vec![0x00, 0xC5, 0x7F, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 1);
assert_eq!(pcs[0].channel, 5);
assert_eq!(pcs[0].program, 127);
}
#[test]
fn program_changes_low_nibble_decodes_channel_index() {
let events: Vec<u8> = vec![0x00, 0xCF, 0x01, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 1);
assert_eq!(pcs[0].channel(), 15);
assert_eq!(pcs[0].program(), 1);
}
#[test]
fn program_changes_running_status_chain_decodes_each_change() {
let events: Vec<u8> = vec![0x00, 0xC0, 0x28, 0x00, 0x29, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 2);
assert_eq!(pcs[0].channel, 0);
assert_eq!(pcs[0].program, 40);
assert_eq!(pcs[1].channel, 0);
assert_eq!(pcs[1].program, 41);
}
#[test]
fn program_changes_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xC2, 0x49]);
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 pcs = smf.program_changes();
assert_eq!(pcs.len(), 1);
assert_eq!(pcs[0].tick, 240);
assert_eq!(pcs[0].channel, 2);
assert_eq!(pcs[0].program, 73);
}
#[test]
fn program_changes_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xC0, 0x10]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xC1, 0x11]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 2);
assert_eq!(pcs[0].track, 0);
assert_eq!(pcs[0].channel, 0);
assert_eq!(pcs[0].program, 0x10);
assert_eq!(pcs[1].track, 1);
assert_eq!(pcs[1].channel, 1);
assert_eq!(pcs[1].program, 0x11);
}
#[test]
fn program_changes_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(100));
t0.extend_from_slice(&[0xC0, 0x20]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(50));
t1.extend_from_slice(&[0xC1, 0x30]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pcs = smf.program_changes();
assert_eq!(pcs.len(), 2);
assert_eq!(pcs[0].tick, 50);
assert_eq!(pcs[0].track, 1);
assert_eq!(pcs[0].channel, 1);
assert_eq!(pcs[0].program, 0x30);
assert_eq!(pcs[1].tick, 100);
assert_eq!(pcs[1].track, 0);
assert_eq!(pcs[1].channel, 0);
assert_eq!(pcs[1].program, 0x20);
}
#[test]
fn program_changes_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x40]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]); 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 pcs = smf.program_changes();
assert_eq!(pcs.len(), 1);
assert_eq!(pcs[0].channel, 0);
assert_eq!(pcs[0].program, 5);
}
#[test]
fn program_changes_seek_initialisation_matches_channel_snapshot() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xC0, 0x00]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xC0, 0x28]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xC0, 0x49]); 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 pcs = smf.program_changes();
assert_eq!(pcs.len(), 3);
for (i, pc) in pcs.iter().enumerate() {
let snap = smf.channel_snapshot_at(0, pc.tick);
assert_eq!(
snap.program,
Some(pc.program),
"snapshot at change {i} (tick {}) should resolve to program {}",
pc.tick,
pc.program
);
}
let snap_mid = smf.channel_snapshot_at(0, 150);
assert_eq!(snap_mid.program, Some(40));
}
#[test]
fn control_changes_empty_when_no_cc_present() {
let events: Vec<u8> = vec![
0x00, 0xC0, 0x05, 0x00, 0x90, 0x3C, 0x64, 0x60, 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.control_changes().is_empty());
}
#[test]
fn control_changes_single_volume_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xB0, 0x07, 0x64, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].tick, 0);
assert_eq!(ccs[0].track, 0);
assert_eq!(ccs[0].channel(), 0);
assert_eq!(ccs[0].controller(), 7);
assert_eq!(ccs[0].value(), 100);
assert!(!ccs[0].is_channel_mode());
}
#[test]
fn control_changes_full_seven_bit_value_range_round_trips() {
let events: Vec<u8> = vec![0x00, 0xB5, 0x0A, 0x7F, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].channel, 5);
assert_eq!(ccs[0].controller, 10);
assert_eq!(ccs[0].value, 127);
}
#[test]
fn control_changes_low_nibble_decodes_channel_index() {
let events: Vec<u8> = vec![0x00, 0xBF, 0x01, 0x40, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].channel(), 15);
assert_eq!(ccs[0].controller(), 1);
assert_eq!(ccs[0].value(), 0x40);
}
#[test]
fn control_changes_running_status_chain_decodes_each_change() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x01, 0x00, 0x00, 0x01, 0x20, 0x00, 0x07, 0x64, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 3);
assert_eq!(ccs[0].channel, 0);
assert_eq!(ccs[0].controller, 1);
assert_eq!(ccs[0].value, 0);
assert_eq!(ccs[1].channel, 0);
assert_eq!(ccs[1].controller, 1);
assert_eq!(ccs[1].value, 32);
assert_eq!(ccs[2].channel, 0);
assert_eq!(ccs[2].controller, 7);
assert_eq!(ccs[2].value, 100);
}
#[test]
fn control_changes_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xB2, 0x0B, 0x7F]); 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 ccs = smf.control_changes();
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].tick, 240);
assert_eq!(ccs[0].channel, 2);
assert_eq!(ccs[0].controller, 11);
assert_eq!(ccs[0].value, 127);
}
#[test]
fn control_changes_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xB0, 0x07, 0x40]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xB1, 0x07, 0x60]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 2);
assert_eq!(ccs[0].track, 0);
assert_eq!(ccs[0].channel, 0);
assert_eq!(ccs[0].value, 0x40);
assert_eq!(ccs[1].track, 1);
assert_eq!(ccs[1].channel, 1);
assert_eq!(ccs[1].value, 0x60);
}
#[test]
fn control_changes_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(100));
t0.extend_from_slice(&[0xB0, 0x07, 0x20]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(50));
t1.extend_from_slice(&[0xB1, 0x0A, 0x30]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let ccs = smf.control_changes();
assert_eq!(ccs.len(), 2);
assert_eq!(ccs[0].tick, 50);
assert_eq!(ccs[0].track, 1);
assert_eq!(ccs[0].channel, 1);
assert_eq!(ccs[0].controller, 10);
assert_eq!(ccs[0].value, 0x30);
assert_eq!(ccs[1].tick, 100);
assert_eq!(ccs[1].track, 0);
assert_eq!(ccs[1].channel, 0);
assert_eq!(ccs[1].controller, 7);
assert_eq!(ccs[1].value, 0x20);
}
#[test]
fn control_changes_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x40]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]); 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 ccs = smf.control_changes();
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].channel, 0);
assert_eq!(ccs[0].controller, 7);
assert_eq!(ccs[0].value, 0x50);
}
#[test]
fn control_changes_channel_mode_family_surfaces_with_predicate_set() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xB0, 120, 0x00]); events.extend_from_slice(&[0x00, 0xB0, 121, 0x00]); events.extend_from_slice(&[0x00, 0xB0, 123, 0x00]); events.extend_from_slice(&[0x00, 0xB0, 127, 0x00]); events.extend_from_slice(&[0x00, 0xB0, 7, 100]); 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 ccs = smf.control_changes();
assert_eq!(ccs.len(), 5);
assert!(ccs[0].is_channel_mode());
assert!(ccs[1].is_channel_mode());
assert!(ccs[2].is_channel_mode());
assert!(ccs[3].is_channel_mode());
assert!(!ccs[4].is_channel_mode()); }
#[test]
fn control_changes_seek_initialisation_matches_channel_snapshot() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x10]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xB0, 0x07, 0x40]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xB0, 0x07, 0x7F]); 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 ccs = smf.control_changes();
let vol: Vec<_> = ccs.iter().filter(|c| c.controller == 7).collect();
assert_eq!(vol.len(), 3);
for (i, cc) in vol.iter().enumerate() {
let snap = smf.channel_snapshot_at(0, cc.tick);
assert_eq!(
snap.volume, cc.value,
"snapshot at change {i} (tick {}) should resolve to volume {}",
cc.tick, cc.value
);
}
let snap_mid = smf.channel_snapshot_at(0, 150);
assert_eq!(snap_mid.volume, 0x40);
}
#[test]
fn control_changes_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xB0, 0x01, 0x10]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x55]); events.extend_from_slice(&[0x00, 0xB0, 0x0A, 0x40]); events.extend_from_slice(&[0x00, 0xB0, 0x40, 0x7F]); 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 original = smf.control_changes();
assert_eq!(original.len(), 4);
let muxed = smf.to_bytes().unwrap();
let reparsed = parse(&muxed).unwrap();
let after = reparsed.control_changes();
assert_eq!(original, after);
}
#[test]
fn pitch_bends_empty_when_no_bend_present() {
let events: Vec<u8> = vec![
0x00, 0xB0, 0x07, 0x64, 0x00, 0x90, 0x3C, 0x64, 0x60, 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.pitch_bends().is_empty());
}
#[test]
fn pitch_bends_centre_at_tick_zero() {
let events: Vec<u8> = vec![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();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 1);
assert_eq!(pbs[0].tick, 0);
assert_eq!(pbs[0].track, 0);
assert_eq!(pbs[0].channel(), 0);
assert_eq!(pbs[0].value(), 0x2000);
assert_eq!(pbs[0].signed_value(), 0);
assert!(pbs[0].is_centre());
}
#[test]
fn pitch_bends_lsb_msb_combine_to_14bit_value() {
let events: Vec<u8> = vec![
0x00, 0xE0, 0x7F, 0x7F, 0x00, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 2);
assert_eq!(pbs[0].value(), 0x3FFF);
assert_eq!(pbs[0].signed_value(), 8191);
assert!(!pbs[0].is_centre());
assert_eq!(pbs[1].value(), 0x0000);
assert_eq!(pbs[1].signed_value(), -8192);
assert!(!pbs[1].is_centre());
}
#[test]
fn pitch_bends_low_nibble_decodes_channel_index() {
let events: Vec<u8> = vec![0x00, 0xEF, 0x40, 0x60, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 1);
assert_eq!(pbs[0].channel(), 15);
assert_eq!(pbs[0].value(), 0x3040);
assert_eq!(pbs[0].signed_value(), 0x3040 - 0x2000);
}
#[test]
fn pitch_bends_running_status_chain_decodes_each_bend() {
let events: Vec<u8> = vec![
0x00, 0xE0, 0x00, 0x40, 0x00, 0x00, 0x20, 0x00, 0x7F, 0x5F, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 3);
assert_eq!(pbs[0].value, 0x2000);
assert_eq!(pbs[1].value, 0x1000);
assert_eq!(pbs[2].value, (0x5F << 7) | 0x7F);
for pb in &pbs {
assert_eq!(pb.channel, 0);
}
}
#[test]
fn pitch_bends_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xE2, 0x00, 0x30]); 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 pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 1);
assert_eq!(pbs[0].tick, 240);
assert_eq!(pbs[0].channel, 2);
assert_eq!(pbs[0].value, 0x1800);
}
#[test]
fn pitch_bends_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xE0, 0x00, 0x10]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xE1, 0x00, 0x70]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 2);
assert_eq!(pbs[0].track, 0);
assert_eq!(pbs[0].channel, 0);
assert_eq!(pbs[0].value, 0x0800);
assert_eq!(pbs[1].track, 1);
assert_eq!(pbs[1].channel, 1);
assert_eq!(pbs[1].value, 0x3800);
}
#[test]
fn pitch_bends_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(100));
t0.extend_from_slice(&[0xE0, 0x00, 0x10]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(50));
t1.extend_from_slice(&[0xE1, 0x00, 0x70]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 2);
assert_eq!(pbs[0].tick, 50);
assert_eq!(pbs[0].track, 1);
assert_eq!(pbs[0].channel, 1);
assert_eq!(pbs[0].value, 0x3800);
assert_eq!(pbs[1].tick, 100);
assert_eq!(pbs[1].track, 0);
assert_eq!(pbs[1].channel, 0);
assert_eq!(pbs[1].value, 0x0800);
}
#[test]
fn pitch_bends_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x50]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]); 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 pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 1);
assert_eq!(pbs[0].channel, 0);
assert_eq!(pbs[0].value, 0x2800);
}
#[test]
fn pitch_bends_seek_initialisation_matches_channel_snapshot() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x10]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xE0, 0x00, 0x40]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0xE0, 0x7F, 0x7F]); 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 pbs = smf.pitch_bends();
assert_eq!(pbs.len(), 3);
for pb in &pbs {
let snap = smf.channel_snapshot_at(0, pb.tick);
assert_eq!(
snap.pitch_bend, pb.value,
"snapshot at tick {} should resolve to pitch_bend {:#06X}",
pb.tick, pb.value
);
}
let snap_mid = smf.channel_snapshot_at(0, 150);
assert_eq!(snap_mid.pitch_bend, 0x2000);
}
#[test]
fn pitch_bends_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x00]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x40]); events.extend_from_slice(&[0x00, 0xE0, 0x7F, 0x7F]); 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 original = smf.pitch_bends();
assert_eq!(original.len(), 3);
let muxed = smf.to_bytes().unwrap();
let reparsed = parse(&muxed).unwrap();
let after = reparsed.pitch_bends();
assert_eq!(original, after);
}
#[test]
fn channel_pressures_single_byte_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xD0, 0x40, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_pressures();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].tick, 0);
assert_eq!(cps[0].track, 0);
assert_eq!(cps[0].channel(), 0);
assert_eq!(cps[0].pressure(), 0x40);
}
#[test]
fn channel_pressures_low_nibble_decodes_channel_index() {
let events: Vec<u8> = vec![0x00, 0xDF, 0x7F, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_pressures();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].channel(), 15);
assert_eq!(cps[0].pressure(), 0x7F);
}
#[test]
fn channel_pressures_running_status_chain_decodes_each_value() {
let events: Vec<u8> = vec![
0x00, 0xD0, 0x10, 0x00, 0x40, 0x00, 0x7F, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let cps = smf.channel_pressures();
assert_eq!(cps.len(), 3);
assert_eq!(cps[0].pressure, 0x10);
assert_eq!(cps[1].pressure, 0x40);
assert_eq!(cps[2].pressure, 0x7F);
for cp in &cps {
assert_eq!(cp.channel, 0);
}
}
#[test]
fn channel_pressures_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xD2, 0x55]); 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 cps = smf.channel_pressures();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].tick, 240);
assert_eq!(cps[0].channel, 2);
assert_eq!(cps[0].pressure, 0x55);
}
#[test]
fn channel_pressures_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xD0, 0x11]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xD1, 0x77]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let cps = smf.channel_pressures();
assert_eq!(cps.len(), 2);
assert_eq!(cps[0].track, 0);
assert_eq!(cps[0].channel, 0);
assert_eq!(cps[0].pressure, 0x11);
assert_eq!(cps[1].track, 1);
assert_eq!(cps[1].channel, 1);
assert_eq!(cps[1].pressure, 0x77);
}
#[test]
fn channel_pressures_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(100));
t0.extend_from_slice(&[0xD0, 0x11]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(50));
t1.extend_from_slice(&[0xD1, 0x77]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let cps = smf.channel_pressures();
assert_eq!(cps.len(), 2);
assert_eq!(cps[0].tick, 50);
assert_eq!(cps[0].track, 1);
assert_eq!(cps[0].channel, 1);
assert_eq!(cps[0].pressure, 0x77);
assert_eq!(cps[1].tick, 100);
assert_eq!(cps[1].track, 0);
assert_eq!(cps[1].channel, 0);
assert_eq!(cps[1].pressure, 0x11);
}
#[test]
fn channel_pressures_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x50]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]); 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 cps = smf.channel_pressures();
assert_eq!(cps.len(), 1);
assert_eq!(cps[0].channel, 0);
assert_eq!(cps[0].pressure, 0x40);
assert_eq!(smf.pitch_bends().len(), 1);
assert_eq!(smf.program_changes().len(), 1);
assert_eq!(smf.control_changes().len(), 1);
}
#[test]
fn channel_pressures_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xD0, 0x00]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0xD0, 0x7F]); 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 original = smf.channel_pressures();
assert_eq!(original.len(), 3);
let muxed = smf.to_bytes().unwrap();
let reparsed = parse(&muxed).unwrap();
let after = reparsed.channel_pressures();
assert_eq!(original, after);
}
#[test]
fn poly_aftertouches_single_event_at_tick_zero() {
let events: Vec<u8> = vec![0x00, 0xA0, 0x3C, 0x40, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 1);
assert_eq!(pat[0].tick, 0);
assert_eq!(pat[0].track, 0);
assert_eq!(pat[0].channel(), 0);
assert_eq!(pat[0].key(), 0x3C);
assert_eq!(pat[0].pressure(), 0x40);
}
#[test]
fn poly_aftertouches_low_nibble_decodes_channel_index() {
let events: Vec<u8> = vec![0x00, 0xAF, 0x7F, 0x7F, 0x00, 0xFF, 0x2F, 0x00];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 1);
assert_eq!(pat[0].channel(), 15);
assert_eq!(pat[0].key(), 0x7F);
assert_eq!(pat[0].pressure(), 0x7F);
}
#[test]
fn poly_aftertouches_running_status_chain_decodes_each_pair() {
let events: Vec<u8> = vec![
0x00, 0xA0, 0x3C, 0x10, 0x00, 0x3E, 0x40, 0x00, 0x40, 0x7F, 0x00, 0xFF, 0x2F, 0x00,
];
let mut blob = header_chunk(0, 1, 96);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 3);
assert_eq!((pat[0].key, pat[0].pressure), (0x3C, 0x10));
assert_eq!((pat[1].key, pat[1].pressure), (0x3E, 0x40));
assert_eq!((pat[2].key, pat[2].pressure), (0x40, 0x7F));
for p in &pat {
assert_eq!(p.channel, 0);
}
}
#[test]
fn poly_aftertouches_late_position_tracks_absolute_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0xA2, 0x3C, 0x55]); 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 pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 1);
assert_eq!(pat[0].tick, 240);
assert_eq!(pat[0].channel, 2);
assert_eq!(pat[0].key, 0x3C);
assert_eq!(pat[0].pressure, 0x55);
}
#[test]
fn poly_aftertouches_stable_sort_keeps_track0_before_track1_at_same_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x11]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xA1, 0x40, 0x77]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 2);
assert_eq!(
(pat[0].track, pat[0].channel, pat[0].key, pat[0].pressure),
(0, 0, 0x3C, 0x11)
);
assert_eq!(
(pat[1].track, pat[1].channel, pat[1].key, pat[1].pressure),
(1, 1, 0x40, 0x77)
);
}
#[test]
fn poly_aftertouches_merge_across_tracks_sorted_by_tick() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&encode_vlq(100));
t0.extend_from_slice(&[0xA0, 0x3C, 0x11]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&encode_vlq(50));
t1.extend_from_slice(&[0xA1, 0x40, 0x77]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 2);
assert_eq!(
(pat[0].tick, pat[0].track, pat[0].key, pat[0].pressure),
(50, 1, 0x40, 0x77)
);
assert_eq!(
(pat[1].tick, pat[1].track, pat[1].key, pat[1].pressure),
(100, 0, 0x3C, 0x11)
);
}
#[test]
fn poly_aftertouches_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x50]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]); 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 pat = smf.poly_aftertouches();
assert_eq!(pat.len(), 1);
assert_eq!(pat[0].channel, 0);
assert_eq!(pat[0].key, 0x3C);
assert_eq!(pat[0].pressure, 0x20);
assert_eq!(smf.channel_pressures().len(), 1);
assert_eq!(smf.pitch_bends().len(), 1);
assert_eq!(smf.program_changes().len(), 1);
assert_eq!(smf.control_changes().len(), 1);
}
#[test]
fn poly_aftertouches_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x00]); events.extend_from_slice(&[0x00, 0xA0, 0x40, 0x40]); events.extend_from_slice(&[0x00, 0xA0, 0x43, 0x7F]); 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 original = smf.poly_aftertouches();
assert_eq!(original.len(), 3);
let muxed = smf.to_bytes().unwrap();
let reparsed = parse(&muxed).unwrap();
let after = reparsed.poly_aftertouches();
assert_eq!(original, after);
}
#[test]
fn poly_aftertouches_empty_when_none_present() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]);
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();
assert!(smf.poly_aftertouches().is_empty());
}
#[test]
fn notes_empty_when_no_note_activity() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]); 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();
assert!(smf.notes().is_empty());
}
#[test]
fn notes_single_note_pairs_on_and_off() {
let mut events: Vec<u8> = Vec::new();
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();
let notes = smf.notes();
assert_eq!(notes.len(), 1);
let n = notes[0];
assert_eq!(n.start_tick, 0);
assert_eq!(n.end_tick, 480);
assert_eq!(n.duration_ticks(), 480);
assert_eq!(n.track, 0);
assert_eq!(n.channel(), 0);
assert_eq!(n.key(), 0x3C);
assert_eq!(n.velocity(), 100);
assert_eq!(n.off_velocity(), 0x40);
}
#[test]
fn notes_velocity_zero_note_on_closes_open_note() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&encode_vlq(240));
events.extend_from_slice(&[0x90, 0x3C, 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 notes = smf.notes();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].start_tick, 0);
assert_eq!(notes[0].end_tick, 240);
assert_eq!(notes[0].velocity(), 100);
assert_eq!(notes[0].off_velocity(), 0);
}
#[test]
fn notes_low_nibble_decodes_channel_index() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x9F, 0x40, 0x7F]);
events.extend_from_slice(&encode_vlq(96));
events.extend_from_slice(&[0x8F, 0x40, 0x00]);
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 notes = smf.notes();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].channel(), 15);
assert_eq!(notes[0].key(), 0x40);
assert_eq!(notes[0].velocity(), 0x7F);
}
#[test]
fn notes_overlapping_same_pitch_pair_fifo() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x50]); events.extend_from_slice(&encode_vlq(10));
events.extend_from_slice(&[0x90, 0x3C, 0x60]); events.extend_from_slice(&encode_vlq(10));
events.extend_from_slice(&[0x80, 0x3C, 0x20]); events.extend_from_slice(&encode_vlq(10));
events.extend_from_slice(&[0x80, 0x3C, 0x30]); 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 notes = smf.notes();
assert_eq!(notes.len(), 2);
assert_eq!(notes[0].start_tick, 0);
assert_eq!(notes[0].end_tick, 20);
assert_eq!(notes[0].velocity(), 0x50);
assert_eq!(notes[1].start_tick, 10);
assert_eq!(notes[1].end_tick, 30);
assert_eq!(notes[1].velocity(), 0x60);
}
#[test]
fn notes_chord_keeps_distinct_pitches() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&[0x00, 0x90, 0x40, 0x64]);
events.extend_from_slice(&[0x00, 0x90, 0x43, 0x64]);
events.extend_from_slice(&encode_vlq(192));
events.extend_from_slice(&[0x80, 0x3C, 0x00]);
events.extend_from_slice(&[0x00, 0x80, 0x40, 0x00]);
events.extend_from_slice(&[0x00, 0x80, 0x43, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 192);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let notes = smf.notes();
assert_eq!(notes.len(), 3);
assert_eq!(notes[0].key(), 0x3C);
assert_eq!(notes[1].key(), 0x40);
assert_eq!(notes[2].key(), 0x43);
for n in ¬es {
assert_eq!(n.start_tick, 0);
assert_eq!(n.end_tick, 192);
}
}
#[test]
fn notes_off_on_different_track_still_pairs() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); 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(&[0x80, 0x3C, 0x10]); t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let notes = smf.notes();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].start_tick, 0);
assert_eq!(notes[0].end_tick, 120);
assert_eq!(notes[0].track, 0);
assert_eq!(notes[0].off_velocity(), 0x10);
}
#[test]
fn notes_chord_across_tracks_orders_track0_first() {
let mut t0: Vec<u8> = Vec::new();
t0.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
t0.extend_from_slice(&encode_vlq(96));
t0.extend_from_slice(&[0x80, 0x3C, 0x00]);
t0.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0x91, 0x40, 0x64]);
t1.extend_from_slice(&encode_vlq(96));
t1.extend_from_slice(&[0x81, 0x40, 0x00]);
t1.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(1, 2, 96);
blob.extend(track_chunk(&t0));
blob.extend(track_chunk(&t1));
let smf = parse(&blob).unwrap();
let notes = smf.notes();
assert_eq!(notes.len(), 2);
assert_eq!(notes[0].track, 0);
assert_eq!(notes[0].channel(), 0);
assert_eq!(notes[1].track, 1);
assert_eq!(notes[1].channel(), 1);
}
#[test]
fn notes_hanging_on_without_off_is_dropped() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
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();
assert!(smf.notes().is_empty());
}
#[test]
fn notes_unmatched_off_is_dropped() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x40]);
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();
assert!(smf.notes().is_empty());
}
#[test]
fn notes_zero_duration_when_off_shares_onset_tick() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x00]);
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 notes = smf.notes();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].start_tick, 0);
assert_eq!(notes[0].end_tick, 0);
assert_eq!(notes[0].duration_ticks(), 0);
}
#[test]
fn notes_filter_excludes_other_channel_voice_kinds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xB0, 0x07, 0x50]); events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0xC0, 0x05]); events.extend_from_slice(&[0x00, 0xE0, 0x00, 0x50]); events.extend_from_slice(&[0x00, 0xD0, 0x40]); events.extend_from_slice(&[0x00, 0xA0, 0x3C, 0x20]); events.extend_from_slice(&encode_vlq(48));
events.extend_from_slice(&[0x80, 0x3C, 0x40]); 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 notes = smf.notes();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].start_tick, 0);
assert_eq!(notes[0].end_tick, 48);
assert_eq!(smf.control_changes().len(), 1);
assert_eq!(smf.program_changes().len(), 1);
assert_eq!(smf.pitch_bends().len(), 1);
assert_eq!(smf.channel_pressures().len(), 1);
}
#[test]
fn notes_survive_to_bytes_round_trip() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&[0x00, 0x90, 0x40, 0x50]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x80, 0x3C, 0x10]);
events.extend_from_slice(&encode_vlq(120));
events.extend_from_slice(&[0x80, 0x40, 0x20]);
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 original = smf.notes();
assert_eq!(original.len(), 2);
let muxed = smf.to_bytes().unwrap();
let reparsed = parse(&muxed).unwrap();
assert_eq!(original, reparsed.notes());
}
#[test]
fn active_notes_at_empty_when_no_note_activity() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
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!(smf.active_notes_at(0).is_empty());
assert!(smf.active_notes_at(1000).is_empty());
}
#[test]
fn active_notes_at_half_open_interval_boundaries() {
let mut events: Vec<u8> = Vec::new();
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.active_notes_at(0).len(), 1);
assert_eq!(smf.active_notes_at(479).len(), 1);
assert!(smf.active_notes_at(480).is_empty());
assert!(smf.active_notes_at(481).is_empty());
}
#[test]
fn active_notes_at_before_onset_is_silent() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0x90, 0x3C, 0x64]); events.extend_from_slice(&encode_vlq(100));
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!(smf.active_notes_at(99).is_empty());
assert_eq!(smf.active_notes_at(100).len(), 1);
}
#[test]
fn active_notes_at_zero_duration_note_never_sounds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&[0x00, 0x80, 0x3C, 0x00]); 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();
assert_eq!(smf.notes().len(), 1);
assert_eq!(smf.notes()[0].duration_ticks(), 0);
assert!(smf.active_notes_at(0).is_empty());
}
#[test]
fn active_notes_at_chord_returns_all_held_keys_in_onset_order() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
events.extend_from_slice(&[0x00, 0x90, 0x40, 0x64]);
events.extend_from_slice(&[0x00, 0x90, 0x43, 0x64]);
events.extend_from_slice(&encode_vlq(192));
events.extend_from_slice(&[0x80, 0x3C, 0x00]);
events.extend_from_slice(&[0x00, 0x80, 0x40, 0x00]);
events.extend_from_slice(&[0x00, 0x80, 0x43, 0x00]);
events.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
let mut blob = header_chunk(0, 1, 192);
blob.extend(track_chunk(&events));
let smf = parse(&blob).unwrap();
let held = smf.active_notes_at(96);
assert_eq!(held.len(), 3);
assert_eq!(held[0].key(), 0x3C);
assert_eq!(held[1].key(), 0x40);
assert_eq!(held[2].key(), 0x43);
}
#[test]
fn active_notes_at_staggered_notes_overlap_window() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0x90, 0x40, 0x64]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0x80, 0x3C, 0x00]); events.extend_from_slice(&encode_vlq(100));
events.extend_from_slice(&[0x80, 0x40, 0x00]); 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 at50 = smf.active_notes_at(50);
assert_eq!(at50.len(), 1);
assert_eq!(at50[0].key(), 0x3C);
assert_eq!(smf.active_notes_at(150).len(), 2);
let at250 = smf.active_notes_at(250);
assert_eq!(at250.len(), 1);
assert_eq!(at250[0].key(), 0x40);
}
#[test]
fn active_notes_at_hanging_note_never_sounds() {
let mut events: Vec<u8> = Vec::new();
events.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]); 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();
assert!(smf.notes().is_empty());
assert!(smf.active_notes_at(0).is_empty());
assert!(smf.active_notes_at(50).is_empty());
}
}