Skip to main content

rustysynth_ext/
midifile.rs

1use std::cmp::Ordering;
2use std::io::Read;
3
4use crate::MidiFileError;
5use crate::MidiFileLoopType;
6use crate::binary_reader::BinaryReader;
7use crate::four_cc::FourCC;
8use crate::read_counter::ReadCounter;
9
10/// A single event of a merged MIDI file.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum MidiMessage {
14    /// A channel message; `status` includes the channel in its low nibble.
15    Normal { status: u8, data1: u8, data2: u8 },
16    /// A tempo change in microseconds per quarter note (FF 51 03).
17    ///
18    /// Only seen while parsing; tempo changes are folded into the event times
19    /// and never appear in the merged list.
20    TempoChange { bytes: [u8; 3] },
21    /// Loop start marker (loop extensions only).
22    LoopStart,
23    /// Loop end marker (loop extensions only).
24    LoopEnd,
25    /// End of track.
26    EndOfTrack,
27}
28
29impl MidiMessage {
30    pub(crate) fn common1(status: u8, data1: u8) -> Self {
31        Self::Normal {
32            status,
33            data1,
34            data2: 0,
35        }
36    }
37
38    pub(crate) fn common2(status: u8, data1: u8, data2: u8, loop_type: MidiFileLoopType) -> Self {
39        let command = status & 0xF0;
40
41        if command == 0xB0 {
42            match loop_type {
43                MidiFileLoopType::RpgMaker if data1 == 111 => {
44                    return MidiMessage::LoopStart;
45                }
46
47                MidiFileLoopType::IncredibleMachine => {
48                    if data1 == 110 {
49                        return MidiMessage::LoopStart;
50                    }
51                    if data1 == 111 {
52                        return MidiMessage::LoopEnd;
53                    }
54                }
55
56                MidiFileLoopType::FinalFantasy => {
57                    if data1 == 116 {
58                        return MidiMessage::LoopStart;
59                    }
60                    if data1 == 117 {
61                        return MidiMessage::LoopEnd;
62                    }
63                }
64
65                _ => (),
66            }
67        }
68
69        Self::Normal {
70            status,
71            data1,
72            data2,
73        }
74    }
75
76    pub(crate) fn tempo_change(tempo: i32) -> Self {
77        // Truncate to u24
78        let bytes = tempo.to_be_bytes()[1..].try_into().unwrap();
79        Self::TempoChange { bytes }
80    }
81}
82
83/// Represents a standard MIDI file.
84#[derive(Debug)]
85#[non_exhaustive]
86pub struct MidiFile {
87    pub(crate) messages: Vec<MidiMessage>,
88    pub(crate) times: Vec<f64>,
89}
90
91impl MidiFile {
92    /// Loads a MIDI file from the stream.
93    ///
94    /// # Arguments
95    ///
96    /// * `reader` - The data stream used to load the MIDI file.
97    pub fn new<R: Read>(reader: &mut R) -> Result<Self, MidiFileError> {
98        MidiFile::new_with_loop_type(reader, MidiFileLoopType::LoopPoint(0))
99    }
100
101    /// Loads a MIDI file from the stream with a specified loop type.
102    ///
103    /// # Arguments
104    ///
105    /// * `reader` - The data stream used to load the MIDI file.
106    /// * `loop_type` - The type of the loop extension to be used.
107    ///
108    /// # Remarks
109    ///
110    /// `MidiFileLoopType` has the following variants:
111    /// * `LoopPoint(usize)` - Specifies the loop start point by a tick value.
112    /// * `RpgMaker` - The RPG Maker style loop.
113    ///   CC #111 will be the loop start point.
114    /// * `IncredibleMachine` - The Incredible Machine style loop.
115    ///   CC #110 and #111 will be the start and end points of the loop.
116    /// * `FinalFantasy` - The Final Fantasy style loop.
117    ///   CC #116 and #117 will be the start and end points of the loop.
118    pub fn new_with_loop_type<R: Read>(
119        reader: &mut R,
120        loop_type: MidiFileLoopType,
121    ) -> Result<Self, MidiFileError> {
122        let chunk_type = BinaryReader::read_four_cc(reader)?;
123        if chunk_type != b"MThd" {
124            return Err(MidiFileError::InvalidChunkType {
125                expected: FourCC::from_bytes(*b"MThd"),
126                actual: chunk_type,
127            });
128        }
129
130        let size = BinaryReader::read_i32_big_endian(reader)?;
131        if size != 6 {
132            return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
133                *b"MThd",
134            )));
135        }
136
137        let format = BinaryReader::read_i16_big_endian(reader)?;
138        if !(format == 0 || format == 1) {
139            return Err(MidiFileError::UnsupportedFormat(format));
140        }
141
142        // The SMF specification requires at least one track chunk.
143        let track_count = BinaryReader::read_i16_big_endian(reader)?;
144        if track_count <= 0 {
145            return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
146                *b"MThd",
147            )));
148        }
149
150        // The time division must be a positive number of ticks per quarter
151        // note. SMPTE timecode divisions are encoded with bit 15 set (i.e. a
152        // negative value here) and are not supported.
153        let resolution = BinaryReader::read_i16_big_endian(reader)?;
154        if resolution <= 0 {
155            return Err(MidiFileError::InvalidTimeDivision(resolution));
156        }
157        let track_count = track_count as i32;
158        let resolution = resolution as i32;
159
160        let mut message_lists: Vec<Vec<MidiMessage>> = Vec::with_capacity(track_count as usize);
161        let mut tick_lists: Vec<Vec<i32>> = Vec::with_capacity(track_count as usize);
162
163        for _i in 0..track_count {
164            let (message_list, tick_list) = MidiFile::read_track(reader, loop_type)?;
165            message_lists.push(message_list);
166            tick_lists.push(tick_list);
167        }
168
169        match loop_type {
170            MidiFileLoopType::LoopPoint(loop_point) if loop_point != 0 => {
171                let loop_point = loop_point as i32;
172                let tick_list = &mut tick_lists[0];
173                let message_list = &mut message_lists[0];
174
175                if loop_point <= *tick_list.last().unwrap() {
176                    for i in 0..tick_list.len() {
177                        if tick_list[i] >= loop_point {
178                            tick_list.insert(i, loop_point);
179                            message_list.insert(i, MidiMessage::LoopStart);
180                            break;
181                        }
182                    }
183                } else {
184                    tick_list.push(loop_point);
185                    message_list.push(MidiMessage::LoopStart);
186                }
187            }
188            _ => (),
189        }
190
191        let (messages, times) = MidiFile::merge_tracks(&message_lists, &tick_lists, resolution);
192
193        Ok(Self { messages, times })
194    }
195
196    fn discard_data<R: Read>(reader: &mut R) -> Result<(), MidiFileError> {
197        let size = BinaryReader::read_i32_variable_length(reader)? as usize;
198        BinaryReader::discard_data(reader, size)?;
199        Ok(())
200    }
201
202    fn read_tempo<R: Read>(reader: &mut R) -> Result<i32, MidiFileError> {
203        let size = BinaryReader::read_i32_variable_length(reader)?;
204        if size != 3 {
205            return Err(MidiFileError::InvalidTempoValue);
206        }
207
208        let b1 = BinaryReader::read_u8(reader)? as i32;
209        let b2 = BinaryReader::read_u8(reader)? as i32;
210        let b3 = BinaryReader::read_u8(reader)? as i32;
211
212        Ok((b1 << 16) | (b2 << 8) | b3)
213    }
214
215    fn read_track<R: Read>(
216        reader: &mut R,
217        loop_type: MidiFileLoopType,
218    ) -> Result<(Vec<MidiMessage>, Vec<i32>), MidiFileError> {
219        let chunk_type = BinaryReader::read_four_cc(reader)?;
220        if chunk_type != b"MTrk" {
221            return Err(MidiFileError::InvalidChunkType {
222                expected: FourCC::from_bytes(*b"MTrk"),
223                actual: chunk_type,
224            });
225        }
226
227        let size_i32 = BinaryReader::read_i32_big_endian(reader)?;
228        // A negative chunk size is malformed data; casting it straight to
229        // `usize` would wrap into a huge allocation bound / skip bound.
230        if size_i32 < 0 {
231            return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
232                *b"MTrk",
233            )));
234        }
235        let size = size_i32 as usize;
236        let reader = &mut ReadCounter::new(reader);
237
238        let mut messages: Vec<MidiMessage> = Vec::new();
239        let mut ticks: Vec<i32> = Vec::new();
240
241        let mut tick: i32 = 0;
242        let mut last_status: u8 = 0;
243
244        loop {
245            // Stay inside this track chunk: a track without an EndOfTrack, or
246            // a chunk size smaller than the event data, must not bleed into
247            // the bytes of the following chunk.
248            if reader.bytes_read() >= size {
249                return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
250                    *b"MTrk",
251                )));
252            }
253
254            let delta = BinaryReader::read_i32_variable_length(reader)?;
255            let first = BinaryReader::read_u8(reader)?;
256
257            // A delta time that pushes the tick past the range of the type would
258            // make the timing jump backwards instead of failing the load.
259            // https://github.com/sinshu/rustysynth/issues/57
260            let Some(next) = tick.checked_add(delta) else {
261                return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
262                    *b"MTrk",
263                )));
264            };
265            tick = next;
266
267            if (first & 128) == 0 {
268                let command = last_status & 0xF0;
269                if command == 0xC0 || command == 0xD0 {
270                    messages.push(MidiMessage::common1(last_status, first));
271                    ticks.push(tick);
272                } else {
273                    let data2 = BinaryReader::read_u8(reader)?;
274                    messages.push(MidiMessage::common2(last_status, first, data2, loop_type));
275                    ticks.push(tick);
276                }
277
278                continue;
279            }
280
281            match first {
282                0xF0 => MidiFile::discard_data(reader)?,
283                0xF7 => MidiFile::discard_data(reader)?,
284                0xFF => match BinaryReader::read_u8(reader)? {
285                    0x2F => {
286                        BinaryReader::read_u8(reader)?;
287                        messages.push(MidiMessage::EndOfTrack);
288                        ticks.push(tick);
289
290                        // Some MIDI files may have events inserted after the EOT.
291                        // Such events should be ignored. An EOT past the
292                        // declared chunk end means the size itself is corrupt:
293                        // error out instead of underflowing the subtraction.
294                        match reader.bytes_read().cmp(&size) {
295                            Ordering::Less => {
296                                BinaryReader::discard_data(reader, size - reader.bytes_read())?;
297                            }
298                            Ordering::Equal => (),
299                            Ordering::Greater => {
300                                return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
301                                    *b"MTrk",
302                                )));
303                            }
304                        }
305
306                        return Ok((messages, ticks));
307                    }
308                    0x51 => {
309                        messages.push(MidiMessage::tempo_change(MidiFile::read_tempo(reader)?));
310                        ticks.push(tick);
311                    }
312                    _ => MidiFile::discard_data(reader)?,
313                },
314                _ => {
315                    let command = first & 0xF0;
316                    if command == 0xC0 || command == 0xD0 {
317                        let data1 = BinaryReader::read_u8(reader)?;
318                        messages.push(MidiMessage::common1(first, data1));
319                        ticks.push(tick);
320                    } else {
321                        let data1 = BinaryReader::read_u8(reader)?;
322                        let data2 = BinaryReader::read_u8(reader)?;
323                        messages.push(MidiMessage::common2(first, data1, data2, loop_type));
324                        ticks.push(tick);
325                    }
326                }
327            }
328
329            // Per the SMF specification, only channel messages update the
330            // running status byte; SysEx and Meta events interrupt (clear) it.
331            last_status = if first >= 0xF0 { 0 } else { first };
332        }
333    }
334
335    fn merge_tracks(
336        message_lists: &[Vec<MidiMessage>],
337        tick_lists: &[Vec<i32>],
338        resolution: i32,
339    ) -> (Vec<MidiMessage>, Vec<f64>) {
340        // Every merged message corresponds to one entry in one of the tick lists.
341        let total_messages: usize = tick_lists.iter().map(Vec::len).sum();
342        let mut merged_messages: Vec<MidiMessage> = Vec::with_capacity(total_messages);
343        let mut merged_times: Vec<f64> = Vec::with_capacity(total_messages);
344
345        let mut indices: Vec<usize> = vec![0; message_lists.len()];
346
347        let mut current_tick: i32 = 0;
348        let mut current_time: f64 = 0.0;
349
350        let mut tempo: f64 = 120.0;
351
352        loop {
353            // A tick can be as large as i32::MAX, so the sentinel that marks
354            // "no track left" has to stay outside the range of the values it is
355            // compared against.
356            let mut min_tick = i64::MAX;
357            let mut min_index: i32 = -1;
358
359            for ch in 0..tick_lists.len() {
360                if indices[ch] < tick_lists[ch].len() {
361                    let tick = tick_lists[ch][indices[ch]] as i64;
362                    if tick < min_tick {
363                        min_tick = tick;
364                        min_index = ch as i32;
365                    }
366                }
367            }
368
369            if min_index == -1 {
370                break;
371            }
372
373            let next_tick = tick_lists[min_index as usize][indices[min_index as usize]];
374            let delta_tick = next_tick as i64 - current_tick as i64;
375            let delta_time = 60.0 / (resolution as f64 * tempo) * delta_tick as f64;
376
377            current_tick = next_tick;
378            current_time += delta_time;
379
380            let message = message_lists[min_index as usize][indices[min_index as usize]];
381            if let MidiMessage::TempoChange { bytes } = message {
382                let tempo_i32 = i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]);
383                // A tempo of 0 microseconds per quarter note is invalid data;
384                // keep the previous tempo instead of dividing by zero.
385                if tempo_i32 > 0 {
386                    tempo = 60000000.0 / tempo_i32 as f64;
387                }
388            } else {
389                merged_messages.push(message);
390                merged_times.push(current_time);
391            }
392
393            indices[min_index as usize] += 1;
394        }
395
396        (merged_messages, merged_times)
397    }
398
399    /// Get the length of the MIDI file in seconds.
400    pub fn get_length(&self) -> f64 {
401        self.times.last().copied().unwrap_or(0.0)
402    }
403
404    /// Get the merged message list of the MIDI file.
405    ///
406    /// Parallel to [`get_times`](Self::get_times): message *i* occurs at
407    /// `get_times()[i]`. Tempo changes are already folded into the times.
408    pub fn get_messages(&self) -> &[MidiMessage] {
409        &self.messages
410    }
411
412    /// Get the time of every message in absolute seconds.
413    ///
414    /// Parallel to [`get_messages`](Self::get_messages).
415    pub fn get_times(&self) -> &[f64] {
416        &self.times
417    }
418
419    /// Creates a MIDI file from an event list without parsing.
420    ///
421    /// Times must be in non-decreasing order (same-time events allowed);
422    /// returns [`MidiFileError::InvalidEventList`] otherwise. Accepts any
423    /// iterator of `(time, message)` pairs.
424    pub fn new_with_events<I>(events: I) -> Result<Self, MidiFileError>
425    where
426        I: IntoIterator<Item = (f64, MidiMessage)>,
427    {
428        let mut file = Self::default();
429        file.extend_events(events)?;
430        Ok(file)
431    }
432
433    /// Appends events to the end of the MIDI file.
434    ///
435    /// Appended times must be non-decreasing and not earlier than the last
436    /// existing time; otherwise returns [`MidiFileError::InvalidEventList`]
437    /// and leaves the file unchanged. To replace all events, call
438    /// [`clear`](Self::clear) first.
439    pub fn extend_events<I>(&mut self, events: I) -> Result<(), MidiFileError>
440    where
441        I: IntoIterator<Item = (f64, MidiMessage)>,
442    {
443        let base = self.messages.len();
444        let mut last_time = self.times.last().copied().unwrap_or(f64::NEG_INFINITY);
445
446        let iter = events.into_iter();
447        let (min, max) = iter.size_hint();
448        // `Vec::reserve` is a no-op when the current capacity already
449        // suffices, so appending a similar amount of events reuses the
450        // existing allocation.
451        let capacity = max.unwrap_or(min);
452        self.messages.reserve(capacity);
453        self.times.reserve(capacity);
454
455        for (time, message) in iter {
456            // `partial_cmp` yields `None` for NaN, so NaN times are also
457            // rejected here.
458            if !matches!(
459                time.partial_cmp(&last_time),
460                Some(Ordering::Greater | Ordering::Equal)
461            ) {
462                self.messages.truncate(base);
463                self.times.truncate(base);
464                return Err(MidiFileError::InvalidEventList);
465            }
466            last_time = time;
467            self.messages.push(message);
468            self.times.push(time);
469        }
470        Ok(())
471    }
472
473    /// Removes all events from the MIDI file.
474    pub fn clear(&mut self) {
475        self.messages.clear();
476        self.times.clear();
477    }
478}
479
480impl Default for MidiFile {
481    /// Creates an empty MIDI file.
482    fn default() -> Self {
483        Self {
484            messages: Vec::new(),
485            times: Vec::new(),
486        }
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_message_size() {
496        // Avoid increasing the size of the MidiMessage type
497        assert_eq!(size_of::<MidiMessage>(), 4);
498    }
499
500    /// Encodes a value as a MIDI variable-length quantity.
501    fn vlq(mut value: u32) -> Vec<u8> {
502        let mut groups = Vec::new();
503        loop {
504            groups.push((value & 0x7F) as u8);
505            value >>= 7;
506            if value == 0 {
507                break;
508            }
509        }
510        groups.reverse();
511        let last = groups.len() - 1;
512        for (i, byte) in groups.iter_mut().enumerate() {
513            if i != last {
514                *byte |= 0x80;
515            }
516        }
517        groups
518    }
519
520    /// Builds an `MThd` header chunk.
521    fn mthd(format: u16, track_count: u16, resolution: u16) -> Vec<u8> {
522        let mut bytes = Vec::new();
523        bytes.extend_from_slice(b"MThd");
524        bytes.extend_from_slice(&6u32.to_be_bytes());
525        bytes.extend_from_slice(&format.to_be_bytes());
526        bytes.extend_from_slice(&track_count.to_be_bytes());
527        bytes.extend_from_slice(&resolution.to_be_bytes());
528        bytes
529    }
530
531    /// Appends an `MTrk` chunk with the given event data.
532    fn mtrk(bytes: &mut Vec<u8>, data: &[u8]) {
533        bytes.extend_from_slice(b"MTrk");
534        bytes.extend_from_slice(&(data.len() as u32).to_be_bytes());
535        bytes.extend_from_slice(data);
536    }
537
538    // Events: delta 480: note on (60, 100); delta 480: end of track.
539    // PPQ 480 at the default 120 BPM: 480 ticks = 0.5 s.
540    fn note_at_480() -> Vec<u8> {
541        let mut data = mthd(0, 1, 480);
542        let mut track = vlq(480);
543        track.extend_from_slice(&[0x90, 0x3C, 0x64]);
544        track.extend_from_slice(&vlq(480));
545        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
546        mtrk(&mut data, &track);
547        data
548    }
549
550    fn assert_close(actual: f64, expected: f64) {
551        assert!(
552            (actual - expected).abs() < 1e-6,
553            "expected {expected}, but was {actual}"
554        );
555    }
556
557    #[test]
558    fn test_default_tempo_timing() {
559        let data = note_at_480();
560        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
561
562        assert_eq!(midi.messages.len(), 2);
563        assert_eq!(
564            midi.messages[0],
565            MidiMessage::Normal {
566                status: 0x90,
567                data1: 0x3C,
568                data2: 0x64,
569            }
570        );
571        assert_eq!(midi.messages[1], MidiMessage::EndOfTrack);
572        assert_eq!(midi.times.len(), 2);
573        assert_close(midi.times[0], 0.5);
574        assert_close(midi.times[1], 1.0);
575        assert_close(midi.get_length(), 1.0);
576    }
577
578    #[test]
579    fn test_tempo_change_timing() {
580        // Tempo change to 250000 us/quarter = 240 BPM at tick 0, then a note
581        // at tick 480 (0.25 s at 240 BPM) and EOT at tick 960 (0.5 s).
582        let mut data = mthd(0, 1, 480);
583        let mut track = vlq(0);
584        track.extend_from_slice(&[0xFF, 0x51, 0x03, 0x03, 0xD0, 0x90]);
585        track.extend_from_slice(&vlq(480));
586        track.extend_from_slice(&[0x90, 0x3C, 0x64]);
587        track.extend_from_slice(&vlq(480));
588        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
589        mtrk(&mut data, &track);
590
591        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
592
593        // The tempo change is consumed during the merge and not emitted.
594        assert_eq!(midi.messages.len(), 2);
595        assert_close(midi.times[0], 0.25);
596        assert_close(midi.times[1], 0.5);
597        assert_close(midi.get_length(), 0.5);
598    }
599
600    #[test]
601    fn test_invalid_tempo_value_is_ignored() {
602        // A tempo of 0 us/quarter is invalid; the previous tempo (the 120 BPM
603        // default) must be kept, so timing stays intact instead of collapsing.
604        let mut data = mthd(0, 1, 480);
605        let mut track = vlq(0);
606        track.extend_from_slice(&[0xFF, 0x51, 0x03, 0x00, 0x00, 0x00]);
607        track.extend_from_slice(&vlq(480));
608        track.extend_from_slice(&[0x90, 0x3C, 0x64]);
609        track.extend_from_slice(&vlq(480));
610        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
611        mtrk(&mut data, &track);
612
613        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
614        assert_eq!(midi.messages.len(), 2);
615        assert_close(midi.times[0], 0.5);
616        assert_close(midi.times[1], 1.0);
617    }
618
619    #[test]
620    fn test_running_status_within_channel_messages() {
621        // Three note-on events in a row using running status.
622        let mut data = mthd(0, 1, 480);
623        let mut track = vlq(0);
624        track.extend_from_slice(&[0x90, 0x3C, 0x64]); // full status
625        track.extend_from_slice(&vlq(0));
626        track.extend_from_slice(&[0x3D, 0x60]); // running status
627        track.extend_from_slice(&vlq(0));
628        track.extend_from_slice(&[0x3E, 0x50]); // running status
629        track.extend_from_slice(&vlq(0));
630        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
631        mtrk(&mut data, &track);
632
633        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
634        assert_eq!(
635            midi.messages,
636            vec![
637                MidiMessage::Normal {
638                    status: 0x90,
639                    data1: 0x3C,
640                    data2: 0x64,
641                },
642                MidiMessage::Normal {
643                    status: 0x90,
644                    data1: 0x3D,
645                    data2: 0x60,
646                },
647                MidiMessage::Normal {
648                    status: 0x90,
649                    data1: 0x3E,
650                    data2: 0x50,
651                },
652                MidiMessage::EndOfTrack,
653            ]
654        );
655    }
656
657    #[test]
658    fn test_running_status_cleared_by_meta_event() {
659        // Per the SMF specification, Meta events interrupt (clear) running
660        // status, so the data byte after the tempo event must not reuse the
661        // previous channel status. It is parsed with status 0 instead.
662        let mut data = mthd(0, 1, 480);
663        let mut track = vlq(0);
664        track.extend_from_slice(&[0x90, 0x3C, 0x64]);
665        track.extend_from_slice(&vlq(0));
666        track.extend_from_slice(&[0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]); // 500000 us
667        track.extend_from_slice(&vlq(0));
668        track.extend_from_slice(&[0x3D, 0x64]); // data byte after a Meta event
669        track.extend_from_slice(&vlq(0));
670        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
671        mtrk(&mut data, &track);
672
673        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
674        assert_eq!(
675            midi.messages,
676            vec![
677                MidiMessage::Normal {
678                    status: 0x90,
679                    data1: 0x3C,
680                    data2: 0x64,
681                },
682                MidiMessage::Normal {
683                    status: 0x00,
684                    data1: 0x3D,
685                    data2: 0x64,
686                },
687                MidiMessage::EndOfTrack,
688            ]
689        );
690    }
691
692    #[test]
693    fn test_smpte_time_division_is_rejected() {
694        // Bit 15 set => SMPTE timecode division (0xE7 0x28 = 25 fps x 40).
695        // Negative resolutions cannot produce meaningful tick-to-second
696        // timing, so the file is rejected with a clear error.
697        let data = mthd(0, 1, 0xE728);
698        assert!(matches!(
699            MidiFile::new(&mut data.as_slice()),
700            Err(MidiFileError::InvalidTimeDivision(-6360))
701        ));
702    }
703
704    #[test]
705    fn test_zero_time_division_is_rejected() {
706        let data = mthd(0, 1, 0x0000);
707        assert!(matches!(
708            MidiFile::new(&mut data.as_slice()),
709            Err(MidiFileError::InvalidTimeDivision(0))
710        ));
711    }
712
713    #[test]
714    fn test_empty_track_list_is_rejected() {
715        // The SMF specification requires at least one track chunk.
716        let data = mthd(0, 0, 480);
717        assert!(matches!(
718            MidiFile::new(&mut data.as_slice()),
719            Err(MidiFileError::InvalidChunkData(_))
720        ));
721    }
722
723    #[test]
724    fn test_message_accessors() {
725        let data = note_at_480();
726        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
727
728        assert_eq!(midi.get_messages().len(), 2);
729        assert_eq!(midi.get_times().len(), 2);
730        assert_close(midi.get_times()[0], 0.5);
731        assert_close(midi.get_times()[1], 1.0);
732
733        let events: Vec<(f64, MidiMessage)> = midi
734            .get_times()
735            .iter()
736            .copied()
737            .zip(midi.get_messages().iter().copied())
738            .collect();
739        assert_eq!(events.len(), 2);
740        assert_close(events[0].0, 0.5);
741        assert_eq!(
742            events[0].1,
743            MidiMessage::Normal {
744                status: 0x90,
745                data1: 0x3C,
746                data2: 0x64,
747            }
748        );
749        assert_close(events[1].0, 1.0);
750        assert_eq!(events[1].1, MidiMessage::EndOfTrack);
751    }
752
753    #[test]
754    fn test_construct_with_events() {
755        let midi = MidiFile::new_with_events(vec![
756            (
757                0.25,
758                MidiMessage::Normal {
759                    status: 0x90,
760                    data1: 60,
761                    data2: 100,
762                },
763            ),
764            (0.5, MidiMessage::EndOfTrack),
765        ])
766        .unwrap();
767
768        assert_eq!(midi.get_messages().len(), 2);
769        assert_eq!(midi.get_times(), &[0.25, 0.5]);
770        assert_close(midi.get_length(), 0.5);
771        assert_close(midi.get_times()[0], 0.25);
772        assert_eq!(
773            midi.get_messages()[0],
774            MidiMessage::Normal {
775                status: 0x90,
776                data1: 60,
777                data2: 100,
778            }
779        );
780    }
781
782    #[test]
783    fn test_construct_allows_same_time_events() {
784        let midi = MidiFile::new_with_events(vec![
785            (
786                0.0,
787                MidiMessage::Normal {
788                    status: 0x90,
789                    data1: 60,
790                    data2: 100,
791                },
792            ),
793            (
794                0.0,
795                MidiMessage::Normal {
796                    status: 0x90,
797                    data1: 61,
798                    data2: 100,
799                },
800            ),
801        ])
802        .unwrap();
803        assert_eq!(midi.get_messages().len(), 2);
804    }
805
806    #[test]
807    fn test_construct_from_iterator_without_vec() {
808        // Any iterator of (time, message) pairs is accepted, so the caller
809        // never has to build an intermediate Vec.
810        let midi = MidiFile::new_with_events((0..3).map(|i| {
811            (
812                i as f64 * 0.5,
813                MidiMessage::Normal {
814                    status: 0x90,
815                    data1: i as u8,
816                    data2: 100,
817                },
818            )
819        }))
820        .unwrap();
821        assert_eq!(midi.get_messages().len(), 3);
822        assert_eq!(midi.get_times(), &[0.0, 0.5, 1.0]);
823    }
824
825    #[test]
826    fn test_construct_rejects_unsorted_times() {
827        let result = MidiFile::new_with_events(vec![
828            (0.5, MidiMessage::EndOfTrack),
829            (
830                0.25,
831                MidiMessage::Normal {
832                    status: 0x90,
833                    data1: 60,
834                    data2: 100,
835                },
836            ),
837        ]);
838        assert!(matches!(result, Err(MidiFileError::InvalidEventList)));
839    }
840
841    #[test]
842    fn test_construct_rejects_nan_time() {
843        let result = MidiFile::new_with_events(vec![(f64::NAN, MidiMessage::EndOfTrack)]);
844        assert!(matches!(result, Err(MidiFileError::InvalidEventList)));
845    }
846
847    #[test]
848    fn test_extend_events_and_clear() {
849        let mut midi = MidiFile::default();
850        assert!(midi.get_messages().is_empty());
851        assert_close(midi.get_length(), 0.0);
852
853        midi.extend_events(vec![(0.5, MidiMessage::EndOfTrack)])
854            .unwrap();
855        assert_eq!(midi.get_times(), &[0.5]);
856        assert_close(midi.get_length(), 0.5);
857
858        // Appending keeps the existing events; same-time events are allowed.
859        midi.extend_events(vec![(0.5, MidiMessage::EndOfTrack)])
860            .unwrap();
861        assert_eq!(midi.get_messages().len(), 2);
862        assert_eq!(midi.get_times(), &[0.5, 0.5]);
863
864        midi.clear();
865        assert!(midi.get_messages().is_empty());
866        assert_close(midi.get_length(), 0.0);
867    }
868
869    #[test]
870    fn test_extend_events_reuses_allocation() {
871        let mut midi = MidiFile::default();
872
873        let events: Vec<(f64, MidiMessage)> = (0..100)
874            .map(|i| {
875                (
876                    i as f64 * 0.01,
877                    MidiMessage::Normal {
878                        status: 0x90,
879                        data1: i as u8,
880                        data2: 0,
881                    },
882                )
883            })
884            .collect();
885        midi.extend_events(events.iter().copied()).unwrap();
886        let messages_capacity = midi.messages.capacity();
887        let times_capacity = midi.times.capacity();
888        assert!(messages_capacity >= 100);
889        assert!(times_capacity >= 100);
890
891        // clear() + extend() is the way to replace all events; it must reuse
892        // the existing buffers instead of allocating new ones.
893        midi.clear();
894        midi.extend_events(vec![(0.0, MidiMessage::EndOfTrack)])
895            .unwrap();
896        assert_eq!(midi.messages.capacity(), messages_capacity);
897        assert_eq!(midi.times.capacity(), times_capacity);
898        assert_eq!(midi.get_messages().len(), 1);
899        assert_eq!(midi.get_times(), &[0.0]);
900    }
901
902    #[test]
903    fn test_extend_events_invalid_input_is_transactional() {
904        let mut midi = MidiFile::new_with_events(vec![(0.0, MidiMessage::EndOfTrack)]).unwrap();
905
906        // Out-of-order input (earlier than an existing event, or descending
907        // within the batch) must leave the file unchanged.
908        let result = midi.extend_events(vec![
909            (0.5, MidiMessage::EndOfTrack),
910            (0.25, MidiMessage::EndOfTrack),
911        ]);
912        assert!(matches!(result, Err(MidiFileError::InvalidEventList)));
913        assert_eq!(midi.get_messages().len(), 1);
914        assert_eq!(midi.get_times(), &[0.0]);
915
916        // An event earlier than the last existing one (0.0) must also be
917        // rejected, leaving the file unchanged.
918        let result = midi.extend_events(vec![(-0.5, MidiMessage::EndOfTrack)]);
919        assert!(matches!(result, Err(MidiFileError::InvalidEventList)));
920        assert_eq!(midi.get_times(), &[0.0]);
921    }
922
923    #[test]
924    fn test_vlq_encoding() {
925        assert_eq!(vlq(0), vec![0x00]);
926        assert_eq!(vlq(0x40), vec![0x40]);
927        assert_eq!(vlq(0x7F), vec![0x7F]);
928        assert_eq!(vlq(0x80), vec![0x81, 0x00]);
929        assert_eq!(vlq(480), vec![0x83, 0x60]);
930        assert_eq!(vlq(0x1FFFFF), vec![0xFF, 0xFF, 0x7F]);
931    }
932
933    #[test]
934    fn test_negative_mtrk_size_is_rejected() {
935        // A negative MTrk chunk size is malformed; it must not wrap into a
936        // huge `usize` bound when cast.
937        let mut data = mthd(0, 1, 480);
938        data.extend_from_slice(b"MTrk");
939        data.extend_from_slice(&(-1i32).to_be_bytes());
940        data.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
941        assert!(matches!(
942            MidiFile::new(&mut data.as_slice()),
943            Err(MidiFileError::InvalidChunkData(_))
944        ));
945    }
946
947    #[test]
948    fn test_empty_mtrk_is_rejected() {
949        // Zero-length track data holds no EndOfTrack, so the track is
950        // truncated rather than empty-but-valid.
951        let mut data = mthd(0, 1, 480);
952        mtrk(&mut data, &[]);
953        assert!(matches!(
954            MidiFile::new(&mut data.as_slice()),
955            Err(MidiFileError::InvalidChunkData(_))
956        ));
957    }
958
959    #[test]
960    fn test_track_without_eot_does_not_bleed_into_next_chunk() {
961        // Track 1 has a note but no EndOfTrack and its declared size covers
962        // exactly that note. The parser must stop at the chunk end instead
963        // of consuming track 2's header bytes as events.
964        let mut data = mthd(0, 2, 480);
965        let mut track1 = vlq(0);
966        track1.extend_from_slice(&[0x90, 0x3C, 0x64]);
967        mtrk(&mut data, &track1);
968        let mut track2 = vlq(480);
969        track2.extend_from_slice(&[0x90, 0x3C, 0x64]);
970        track2.extend_from_slice(&vlq(480));
971        track2.extend_from_slice(&[0xFF, 0x2F, 0x00]);
972        mtrk(&mut data, &track2);
973        assert!(matches!(
974            MidiFile::new(&mut data.as_slice()),
975            Err(MidiFileError::InvalidChunkData(_))
976        ));
977    }
978
979    #[test]
980    fn test_eot_past_chunk_end_is_rejected() {
981        // The EndOfTrack event ends beyond the declared chunk size: the size
982        // itself is corrupt. This must error instead of underflowing the
983        // trailing-bytes subtraction.
984        let mut data = mthd(0, 1, 480);
985        data.extend_from_slice(b"MTrk");
986        data.extend_from_slice(&2u32.to_be_bytes());
987        data.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
988        assert!(matches!(
989            MidiFile::new(&mut data.as_slice()),
990            Err(MidiFileError::InvalidChunkData(_))
991        ));
992    }
993
994    // Delta times accumulate into an i32 tick. Summing past the range of the
995    // type used to wrap around, which made the timing jump backwards (#57).
996    #[test]
997    fn test_a_tick_overflow_is_rejected() {
998        let mut data = mthd(0, 1, 480);
999        let mut track = Vec::new();
1000
1001        // 2^28 - 1 is the largest delta a four byte variable length quantity
1002        // can encode, so eight of them approach the i32 limit and the ninth
1003        // passes it.
1004        for _ in 0..10 {
1005            track.extend_from_slice(&vlq(0x0FFF_FFFF));
1006            track.extend_from_slice(&[0x90, 0x3C, 0x64]);
1007        }
1008        mtrk(&mut data, &track);
1009
1010        assert!(matches!(
1011            MidiFile::new(&mut data.as_slice()),
1012            Err(MidiFileError::InvalidChunkData(_))
1013        ));
1014    }
1015
1016    // A tick is allowed to reach exactly i32::MAX. When the sentinel used to
1017    // find the next event was also i32::MAX, an event on that tick was never
1018    // selected and the whole remaining sequence was dropped without an error.
1019    #[test]
1020    fn test_a_tick_at_the_type_limit_is_kept() {
1021        let mut data = mthd(0, 1, 480);
1022        let mut track = Vec::new();
1023
1024        // Eight of the largest deltas reach 2^31 - 8, and a delta of 7 puts the
1025        // last event exactly on i32::MAX.
1026        for _ in 0..8 {
1027            track.extend_from_slice(&vlq(0x0FFF_FFFF));
1028            track.extend_from_slice(&[0x90, 0x3C, 0x64]);
1029        }
1030        track.extend_from_slice(&vlq(7));
1031        track.extend_from_slice(&[0xFF, 0x2F, 0x00]);
1032        mtrk(&mut data, &track);
1033
1034        let midi = MidiFile::new(&mut data.as_slice()).unwrap();
1035        assert_eq!(midi.messages.len(), 9);
1036        assert_eq!(*midi.messages.last().unwrap(), MidiMessage::EndOfTrack);
1037    }
1038}