Skip to main content

datui_lib/formats/
midi.rs

1//! Standard MIDI Files as a table of events. A hand-written parser (`MThd`/`MTrk`
2//! chunks, variable-length deltas, running status, sysex, meta events) checks every
3//! stated length against the remaining bytes, so a hostile file errors rather than
4//! panics or over-allocates. Rows are a small eager frame made lazy; the header, track
5//! names and tempo range go to a [`MidiSummary`] for Info's MIDI tab.
6
7use std::collections::{HashMap, VecDeque};
8use std::path::{Path, PathBuf};
9
10use color_eyre::Result;
11use color_eyre::eyre::eyre;
12use polars::prelude::*;
13
14use crate::widgets::info::{clock, count_of, group_u64};
15
16/// What datui does with a MIDI file: see [`crate::formats::readers`].
17pub(crate) const READER: crate::formats::readers::Reader = crate::formats::readers::Reader {
18    scan,
19    signatures: &[crate::formats::readers::Signature {
20        says: |head, _| looks_like_midi(head),
21        kind: crate::formats::readers::Kind::Magic,
22        trusted: crate::formats::readers::EVERYWHERE,
23    }],
24    ..crate::formats::readers::BASE
25};
26
27/// The most bytes of a sysex or unknown meta event written out as hex.
28const HEX_SHOWN: usize = 256;
29/// The tempo until a file sets one: 120 beats per minute.
30const DEFAULT_TEMPO: u32 = 500_000;
31
32/// How a file counts time: ticks per quarter note, or SMPTE frames.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Division {
35    /// Ticks per quarter note; the tempo map turns ticks into seconds.
36    Ppq(u16),
37    /// Frames per second (24, 25, 29 for 29.97 drop-frame, or 30) and ticks per
38    /// frame. Time is absolute; tempo events do not change it.
39    Smpte { fps: u8, ticks_per_frame: u8 },
40}
41
42impl Division {
43    /// `480 ticks per quarter`, `25 fps, 40 ticks per frame`.
44    pub fn label(self) -> String {
45        match self {
46            Division::Ppq(n) => format!("{n} ticks per quarter"),
47            Division::Smpte {
48                fps,
49                ticks_per_frame,
50            } => {
51                let fps = if fps == 29 {
52                    "29.97".to_string()
53                } else {
54                    fps.to_string()
55                };
56                format!("{fps} fps, {ticks_per_frame} ticks per frame")
57            }
58        }
59    }
60}
61
62/// One event as the file has it, its bytes borrowed from the file.
63#[derive(Debug, Clone, Copy)]
64pub struct Event<'a> {
65    /// Ticks from the start of its track.
66    pub tick: u64,
67    pub body: Body<'a>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum Body<'a> {
72    /// A channel message: the status byte (kind in the high nibble, channel 0-15 in
73    /// the low) and its one or two data bytes. `b` is 0 for a one-byte message.
74    Channel { status: u8, a: u8, b: u8 },
75    /// `F0` (a whole or first sysex packet) or `F7` (a continuation or escape), with
76    /// the bytes after its length.
77    Sysex { escape: bool, data: &'a [u8] },
78    /// `FF`: the meta type and its data.
79    Meta { kind: u8, data: &'a [u8] },
80    /// A system common (`F1`-`F6`) or real-time (`F8`-`FE`) message, which a file
81    /// should not hold but some do, and its data bytes: 0 where it has none.
82    System { status: u8, a: u8, b: u8 },
83}
84
85/// A Standard MIDI File, parsed.
86#[derive(Debug, Clone)]
87pub struct Smf<'a> {
88    /// 0 (one track), 1 (tracks played together) or 2 (independent sequences).
89    pub format: u16,
90    pub division: Division,
91    /// Each `MTrk` chunk's events, in file order.
92    pub tracks: Vec<Vec<Event<'a>>>,
93}
94
95/// Whether `head` starts like a MIDI file: `MThd` with its length of 6, or a RIFF
96/// `RMID` wrapper.
97pub fn looks_like_midi(head: &[u8]) -> bool {
98    head.starts_with(b"MThd\0\0\0\x06")
99        || (head.len() >= 12 && head.starts_with(b"RIFF") && &head[8..12] == b"RMID")
100}
101
102/// A cursor over the bytes of one chunk that never reads past its end.
103struct Bytes<'a> {
104    data: &'a [u8],
105    at: usize,
106}
107
108impl<'a> Bytes<'a> {
109    fn new(data: &'a [u8]) -> Self {
110        Self { data, at: 0 }
111    }
112
113    fn left(&self) -> usize {
114        self.data.len() - self.at
115    }
116
117    fn u8(&mut self) -> Option<u8> {
118        let b = *self.data.get(self.at)?;
119        self.at += 1;
120        Some(b)
121    }
122
123    fn take(&mut self, n: usize) -> Option<&'a [u8]> {
124        if n > self.left() {
125            return None;
126        }
127        let out = &self.data[self.at..self.at + n];
128        self.at += n;
129        Some(out)
130    }
131
132    fn u32(&mut self) -> Option<u32> {
133        self.take(4)
134            .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
135    }
136
137    /// A variable-length quantity: seven bits a byte, high bit set on all but the
138    /// last, at most four bytes (28 bits), as the specification bounds it.
139    fn vlq(&mut self) -> Result<u32, VlqError> {
140        let mut value = 0u32;
141        for _ in 0..4 {
142            let b = self.u8().ok_or(VlqError::CutShort)?;
143            value = (value << 7) | u32::from(b & 0x7f);
144            if b & 0x80 == 0 {
145                return Ok(value);
146            }
147        }
148        Err(VlqError::TooLong)
149    }
150}
151
152enum VlqError {
153    CutShort,
154    TooLong,
155}
156
157/// Where in a track an error was found, for its message.
158fn at(track: usize, offset: usize) -> String {
159    format!("track {}, byte {offset}", track + 1)
160}
161
162/// The SMF inside a RIFF `RMID` wrapper: the contents of its `data` chunk.
163fn unwrap_rmid(bytes: &[u8]) -> Result<&[u8]> {
164    if bytes.get(8..12) != Some(b"RMID".as_slice()) {
165        return Err(eyre!("Not a MIDI file: a RIFF file that is not RIFF MIDI"));
166    }
167    let mut r = Bytes::new(&bytes[12..]);
168    while r.left() >= 8 {
169        let id = r.take(4).unwrap_or_default();
170        let len = r
171            .take(4)
172            .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
173        let len = len.unwrap_or(0) as usize;
174        let Some(body) = r.take(len) else {
175            return Err(eyre!("Not a MIDI file: a RIFF chunk runs past the end"));
176        };
177        if id == b"data" {
178            return Ok(body);
179        }
180        // Chunks are padded to an even length.
181        if len % 2 == 1 {
182            r.take(1);
183        }
184    }
185    Err(eyre!(
186        "Not a MIDI file: the RIFF MIDI wrapper has no data chunk"
187    ))
188}
189
190/// Parse a Standard MIDI File (or a RIFF `RMID` wrapper). Strict where leniency would
191/// fake a whole table: an overrunning track, a cut event, a data byte without status,
192/// or fewer tracks than the header says are errors. Non-`MTrk` chunks and anything
193/// after the promised tracks are skipped; a missing End of Track is fine.
194pub fn parse(bytes: &[u8]) -> Result<Smf<'_>> {
195    let bytes = if bytes.starts_with(b"RIFF") {
196        unwrap_rmid(bytes)?
197    } else {
198        bytes
199    };
200    let mut r = Bytes::new(bytes);
201    if r.take(4) != Some(b"MThd".as_slice()) {
202        return Err(eyre!("Not a MIDI file: it does not start with MThd"));
203    }
204    let header_len = r.u32().ok_or_else(|| eyre!("MIDI header is cut short"))? as usize;
205    if header_len < 6 {
206        return Err(eyre!("MIDI header is {header_len} bytes; it needs 6"));
207    }
208    let header = r
209        .take(header_len)
210        .ok_or_else(|| eyre!("MIDI header is cut short"))?;
211    let format = u16::from_be_bytes([header[0], header[1]]);
212    let declared = u16::from_be_bytes([header[2], header[3]]) as usize;
213    let raw_division = u16::from_be_bytes([header[4], header[5]]);
214    if format > 2 {
215        return Err(eyre!("MIDI format {format} is not one of 0, 1 or 2"));
216    }
217    if declared == 0 {
218        return Err(eyre!("MIDI header says the file has no tracks"));
219    }
220    let division = if raw_division & 0x8000 != 0 {
221        // The high byte is the frame rate, negated as a signed byte.
222        let fps = (-i16::from((raw_division >> 8) as u8 as i8)) as u8;
223        let ticks_per_frame = (raw_division & 0xff) as u8;
224        if !matches!(fps, 24 | 25 | 29 | 30) || ticks_per_frame == 0 {
225            return Err(eyre!(
226                "MIDI header's SMPTE timing ({fps} fps, {ticks_per_frame} ticks per frame) is not usable"
227            ));
228        }
229        Division::Smpte {
230            fps,
231            ticks_per_frame,
232        }
233    } else if raw_division == 0 {
234        return Err(eyre!("MIDI header says 0 ticks per quarter note"));
235    } else {
236        Division::Ppq(raw_division)
237    };
238
239    // Not sized by `declared`: a header may claim 65,535 tracks in a file of 20 bytes.
240    let mut tracks = Vec::new();
241    let mut events = 0usize;
242    while tracks.len() < declared {
243        if r.left() < 8 {
244            return Err(eyre!(
245                "MIDI header says {declared} {}; the file holds {}",
246                if declared == 1 { "track" } else { "tracks" },
247                tracks.len()
248            ));
249        }
250        let id = r.take(4).unwrap_or_default();
251        let len = r.u32().unwrap_or(0) as usize;
252        let left = r.left();
253        let Some(body) = r.take(len) else {
254            if id == b"MTrk" {
255                return Err(eyre!(
256                    "MIDI track {} is cut short: it says {len} bytes and {left} remain",
257                    tracks.len() + 1
258                ));
259            }
260            return Err(eyre!(
261                "MIDI file is cut short: a chunk says {len} bytes and {left} remain"
262            ));
263        };
264        if id != b"MTrk" {
265            continue;
266        }
267        let track = parse_track(body, tracks.len(), &mut events)?;
268        tracks.push(track);
269    }
270    Ok(Smf {
271        format,
272        division,
273        tracks,
274    })
275}
276
277/// One `MTrk` chunk's events. `events` counts across tracks, for `limits.midi_events`.
278fn parse_track<'a>(body: &'a [u8], track: usize, events: &mut usize) -> Result<Vec<Event<'a>>> {
279    let mut r = Bytes::new(body);
280    let mut out = Vec::new();
281    let mut tick = 0u64;
282    // The last channel status, which a data byte in its place repeats. Sysex and meta
283    // events cancel it.
284    let mut running: Option<u8> = None;
285    while r.left() > 0 {
286        let start = r.at;
287        let delta = r.vlq().map_err(|e| match e {
288            VlqError::CutShort => eyre!("MIDI {}: a delta time is cut short", at(track, start)),
289            VlqError::TooLong => eyre!(
290                "MIDI {}: a delta time is longer than four bytes",
291                at(track, start)
292            ),
293        })?;
294        tick += u64::from(delta);
295        let cut = || eyre!("MIDI {}: an event is cut short", at(track, start));
296        let first = r.u8().ok_or_else(cut)?;
297        let body = match first {
298            0xff => {
299                running = None;
300                let kind = r.u8().ok_or_else(cut)?;
301                let len = vlq_len(&mut r, track, start)?;
302                let data = r.take(len).ok_or_else(cut)?;
303                Body::Meta { kind, data }
304            }
305            0xf0 | 0xf7 => {
306                running = None;
307                let len = vlq_len(&mut r, track, start)?;
308                let data = r.take(len).ok_or_else(cut)?;
309                Body::Sysex {
310                    escape: first == 0xf7,
311                    data,
312                }
313            }
314            // System real-time: one byte, and running status survives it, as on the
315            // wire. A file should not hold one, but its length is never in doubt.
316            0xf8..=0xfe => Body::System {
317                status: first,
318                a: 0,
319                b: 0,
320            },
321            // System common: its data bytes as the wire defines them. It cancels
322            // running status. 0xF4 and 0xF5 have no definition, so no length.
323            0xf1 | 0xf2 | 0xf3 | 0xf6 => {
324                running = None;
325                let (a, b) = match first {
326                    0xf1 | 0xf3 => (r.u8().ok_or_else(cut)?, 0),
327                    0xf2 => (r.u8().ok_or_else(cut)?, r.u8().ok_or_else(cut)?),
328                    _ => (0, 0),
329                };
330                if a & 0x80 != 0 || b & 0x80 != 0 {
331                    return Err(eyre!(
332                        "MIDI {}: a data byte has its high bit set",
333                        at(track, start)
334                    ));
335                }
336                Body::System {
337                    status: first,
338                    a,
339                    b,
340                }
341            }
342            0xf4 | 0xf5 => {
343                return Err(eyre!(
344                    "MIDI {}: status {first:#04X} is undefined",
345                    at(track, start)
346                ));
347            }
348            _ => {
349                let (status, a) = if first & 0x80 != 0 {
350                    running = Some(first);
351                    (first, r.u8().ok_or_else(cut)?)
352                } else {
353                    let status = running.ok_or_else(|| {
354                        eyre!(
355                            "MIDI {}: a data byte with no status before it",
356                            at(track, start)
357                        )
358                    })?;
359                    (status, first)
360                };
361                let b = if matches!(status & 0xf0, 0xc0 | 0xd0) {
362                    0
363                } else {
364                    r.u8().ok_or_else(cut)?
365                };
366                if a & 0x80 != 0 || b & 0x80 != 0 {
367                    return Err(eyre!(
368                        "MIDI {}: a data byte has its high bit set",
369                        at(track, start)
370                    ));
371                }
372                Body::Channel { status, a, b }
373            }
374        };
375        *events += 1;
376        let most = crate::limits::get().midi_events;
377        if *events > most {
378            return Err(eyre!(
379                "MIDI has more than {most} events, the most datui reads; limits.midi_events raises it"
380            ));
381        }
382        out.push(Event { tick, body });
383        if matches!(body, Body::Meta { kind: 0x2f, .. }) {
384            // End of Track: whatever follows in the chunk is not events.
385            break;
386        }
387    }
388    Ok(out)
389}
390
391/// A length for a sysex or meta event.
392fn vlq_len(r: &mut Bytes<'_>, track: usize, start: usize) -> Result<usize> {
393    r.vlq().map(|n| n as usize).map_err(|e| match e {
394        VlqError::CutShort => eyre!("MIDI {}: an event is cut short", at(track, start)),
395        VlqError::TooLong => eyre!(
396            "MIDI {}: a length is longer than four bytes",
397            at(track, start)
398        ),
399    })
400}
401
402/// Every note's name, made once: a song has thousands of notes and a name each.
403static NOTE_NAMES: std::sync::LazyLock<Vec<String>> =
404    std::sync::LazyLock::new(|| (0..=127).map(note_name).collect());
405
406/// [`note_name`], borrowed from [`NOTE_NAMES`].
407fn note_name_of(note: u8) -> &'static str {
408    NOTE_NAMES[usize::from(note & 0x7f)].as_str()
409}
410
411/// A note number as a name, middle C (60) as `C4`.
412pub fn note_name(note: u8) -> String {
413    const NAMES: [&str; 12] = [
414        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
415    ];
416    format!(
417        "{}{}",
418        NAMES[(note % 12) as usize],
419        i32::from(note / 12) - 1
420    )
421}
422
423/// A key signature as written: sharps (positive) or flats (negative), and minor.
424fn key_name(sf: i8, minor: bool) -> Option<String> {
425    const MAJOR: [&str; 15] = [
426        "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#",
427    ];
428    const MINOR: [&str; 15] = [
429        "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "D#", "A#",
430    ];
431    let i = usize::try_from(i16::from(sf) + 7)
432        .ok()
433        .filter(|i| *i < 15)?;
434    Some(if minor {
435        format!("{} minor", MINOR[i])
436    } else {
437        format!("{} major", MAJOR[i])
438    })
439}
440
441/// Beats per minute for a tempo in microseconds per quarter note: `120`, `92.31`.
442pub(crate) fn bpm(tempo: u32) -> String {
443    if tempo == 0 {
444        return "-".to_string();
445    }
446    let s = format!("{:.2}", 60_000_000.0 / f64::from(tempo));
447    s.trim_end_matches('0').trim_end_matches('.').to_string()
448}
449
450/// Text from a meta event: UTF-8 when it is, otherwise Latin-1, which older files
451/// mostly are.
452fn meta_text(data: &[u8]) -> String {
453    match std::str::from_utf8(data) {
454        Ok(s) => s.to_string(),
455        Err(_) => data.iter().map(|&b| char::from(b)).collect(),
456    }
457}
458
459/// Bytes as spaced hex, cut at [`HEX_SHOWN`] with how many more there are.
460fn hex(prefix: Option<u8>, data: &[u8]) -> String {
461    use std::fmt::Write;
462    let mut out = String::new();
463    if let Some(p) = prefix {
464        let _ = write!(out, "{p:02X}");
465    }
466    for b in data.iter().take(HEX_SHOWN) {
467        if !out.is_empty() {
468            out.push(' ');
469        }
470        let _ = write!(out, "{b:02X}");
471    }
472    if data.len() > HEX_SHOWN {
473        let _ = write!(out, " ... ({} more bytes)", data.len() - HEX_SHOWN);
474    }
475    out
476}
477
478/// The tempo changes that apply to a track, as ticks to microseconds.
479struct TempoMap {
480    /// Tick, microseconds × ticks-per-quarter up to it, tempo from it. Sorted by tick,
481    /// starting at 0.
482    segments: Vec<(u64, u128, u32)>,
483    ppq: u128,
484}
485
486impl TempoMap {
487    /// From `(tick, tempo)` changes in file order; the last change at a tick wins.
488    fn new(mut changes: Vec<(u64, u32)>, ppq: u16) -> Self {
489        changes.sort_by_key(|(tick, _)| *tick);
490        let mut segments: Vec<(u64, u128, u32)> = vec![(0, 0, DEFAULT_TEMPO)];
491        for (tick, tempo) in changes {
492            let &(last_tick, last_base, last_tempo) = segments.last().expect("starts with one");
493            if tick == last_tick {
494                segments.last_mut().expect("starts with one").2 = tempo;
495            } else {
496                let base = last_base + u128::from(tick - last_tick) * u128::from(last_tempo);
497                segments.push((tick, base, tempo));
498            }
499        }
500        Self {
501            segments,
502            ppq: u128::from(ppq),
503        }
504    }
505
506    fn seconds(&self, tick: u64) -> f64 {
507        let i = self.segments.partition_point(|(t, _, _)| *t <= tick) - 1;
508        let (t, base, tempo) = self.segments[i];
509        // Exact in integers to here; one division at the end, so long songs do not
510        // gather rounding error from each tempo change.
511        let num = base + u128::from(tick - t) * u128::from(tempo);
512        num as f64 / (self.ppq as f64 * 1e6)
513    }
514}
515
516/// Ticks to seconds for one track.
517enum Clock<'m> {
518    Tempo(&'m TempoMap),
519    /// Microseconds per tick as a fraction: SMPTE time ignores tempo.
520    Smpte {
521        num: u128,
522        den: u128,
523    },
524}
525
526impl Clock<'_> {
527    fn seconds(&self, tick: u64) -> f64 {
528        match self {
529            Clock::Tempo(map) => map.seconds(tick),
530            Clock::Smpte { num, den } => (u128::from(tick) * num) as f64 / (*den as f64 * 1e6),
531        }
532    }
533}
534
535/// The tempo changes in some tracks: a tempo meta event with its three bytes.
536fn tempo_events<'a>(tracks: impl IntoIterator<Item = &'a Vec<Event<'a>>>) -> Vec<(u64, u32)> {
537    tracks
538        .into_iter()
539        .flatten()
540        .filter_map(|e| match e.body {
541            Body::Meta {
542                kind: 0x51,
543                data: [a, b, c],
544            } => Some((e.tick, u32::from_be_bytes([0, *a, *b, *c]))),
545            _ => None,
546        })
547        .collect()
548}
549
550/// One track, as the Info panel lists it.
551#[derive(Debug, Clone, Default, PartialEq, Eq)]
552pub struct TrackSummary {
553    /// Its first track name event.
554    pub name: Option<String>,
555    /// Its first instrument name event.
556    pub instrument: Option<String>,
557    pub events: usize,
558    /// Note-on events with a velocity above zero.
559    pub notes: usize,
560    /// The channels its channel messages use, 1-16, ascending.
561    pub channels: Vec<u8>,
562}
563
564/// What a MIDI file says besides its events, for the Info panel's MIDI tab.
565#[derive(Debug, Clone, Default)]
566pub struct MidiSummary {
567    /// How many files the table holds.
568    pub files: usize,
569    /// The SMF format, when every file has the same one.
570    pub format: Option<u16>,
571    /// The timing, when every file has the same one.
572    pub division: Option<Division>,
573    /// Each track of a single file; empty for many files.
574    pub tracks: Vec<TrackSummary>,
575    /// Tracks across every file.
576    pub track_count: usize,
577    pub events: usize,
578    /// Note-on events with a velocity above zero.
579    pub notes: usize,
580    /// Notes that start and never end: no note-off for them in their track.
581    pub unended: usize,
582    /// The time of the last event, the longest file's.
583    pub length_seconds: f64,
584    /// The first tempo, then the fewest and most microseconds per quarter: the fastest
585    /// and the slowest.
586    pub tempo: Option<(u32, u32, u32)>,
587    /// Tempo events after the first of each file.
588    pub tempo_changes: usize,
589    /// The first time signature, as `6/8`.
590    pub time_signature: Option<String>,
591    /// The first key signature, as `D major`.
592    pub key: Option<String>,
593    /// The first copyright notice.
594    pub copyright: Option<String>,
595    /// Files of a directory that could not be read, with why. They are not in the
596    /// table.
597    pub unreadable: Vec<(String, String)>,
598}
599
600/// The columns a MIDI file's events fill, one value a row.
601#[derive(Default)]
602struct Columns<'a> {
603    file: Vec<&'a str>,
604    track: Vec<u16>,
605    tick: Vec<u64>,
606    seconds: Vec<f64>,
607    kind: Vec<&'static str>,
608    channel: Vec<Option<u8>>,
609    note: Vec<Option<u8>>,
610    note_name: Vec<Option<&'static str>>,
611    velocity: Vec<Option<u8>>,
612    controller: Vec<Option<u8>>,
613    value: Vec<Option<i32>>,
614    length: Vec<Option<f64>>,
615    text: Vec<Option<String>>,
616}
617
618impl Columns<'_> {
619    fn push(&mut self, track: u16, tick: u64, seconds: f64, kind: &'static str) {
620        self.track.push(track);
621        self.tick.push(tick);
622        self.seconds.push(seconds);
623        self.kind.push(kind);
624        self.channel.push(None);
625        self.note.push(None);
626        self.note_name.push(None);
627        self.velocity.push(None);
628        self.controller.push(None);
629        self.value.push(None);
630        self.length.push(None);
631        self.text.push(None);
632    }
633
634    fn rows(&self) -> usize {
635        self.track.len()
636    }
637}
638
639/// The summary of one file's events, added into `summary`; its rows into `cols`.
640fn add_file<'a>(
641    smf: &Smf<'_>,
642    file: Option<&'a str>,
643    cols: &mut Columns<'a>,
644    summary: &mut MidiSummary,
645) {
646    let tempo_map = match smf.division {
647        Division::Ppq(ppq) if smf.format != 2 => {
648            Some(TempoMap::new(tempo_events(&smf.tracks), ppq))
649        }
650        _ => None,
651    };
652    let mut tracks = Vec::with_capacity(smf.tracks.len());
653    let mut tempos: Vec<u32> = Vec::new();
654    for (index, events) in smf.tracks.iter().enumerate() {
655        // Format 2's tracks are independent sequences, each with its own tempo.
656        let own_map;
657        let clock = match (smf.division, &tempo_map) {
658            (
659                Division::Smpte {
660                    fps,
661                    ticks_per_frame,
662                },
663                _,
664            ) => {
665                let (fps_num, fps_den) = if fps == 29 {
666                    (30_000u128, 1001u128)
667                } else {
668                    (u128::from(fps), 1)
669                };
670                Clock::Smpte {
671                    num: 1_000_000 * fps_den,
672                    den: fps_num * u128::from(ticks_per_frame),
673                }
674            }
675            (_, Some(map)) => Clock::Tempo(map),
676            (Division::Ppq(ppq), None) => {
677                own_map = TempoMap::new(tempo_events([events]), ppq);
678                Clock::Tempo(&own_map)
679            }
680        };
681        let track_no = u16::try_from(index + 1).unwrap_or(u16::MAX);
682        let mut track = TrackSummary {
683            events: events.len(),
684            ..Default::default()
685        };
686        let mut channels = [false; 16];
687        // Notes sounding, by channel and note, oldest first: a note-off ends the
688        // earliest of its note's starts.
689        let mut sounding: HashMap<(u8, u8), VecDeque<usize>> = HashMap::new();
690        for event in events {
691            let time = clock.seconds(event.tick);
692            summary.length_seconds = summary.length_seconds.max(time);
693            let row = cols.rows();
694            match event.body {
695                Body::Channel { status, a, b } => {
696                    let ch = status & 0x0f;
697                    channels[ch as usize] = true;
698                    let kind = match status & 0xf0 {
699                        0x90 if b > 0 => "note_on",
700                        0x80 | 0x90 => "note_off",
701                        0xa0 => "poly_aftertouch",
702                        0xb0 => "cc",
703                        0xc0 => "program",
704                        0xd0 => "channel_aftertouch",
705                        _ => "pitch_bend",
706                    };
707                    cols.push(track_no, event.tick, time, kind);
708                    cols.channel[row] = Some(ch + 1);
709                    match kind {
710                        "note_on" | "note_off" | "poly_aftertouch" => {
711                            cols.note[row] = Some(a);
712                            cols.note_name[row] = Some(note_name_of(a));
713                            if kind == "poly_aftertouch" {
714                                cols.value[row] = Some(i32::from(b));
715                            } else {
716                                cols.velocity[row] = Some(b);
717                            }
718                        }
719                        "cc" => {
720                            cols.controller[row] = Some(a);
721                            cols.value[row] = Some(i32::from(b));
722                        }
723                        "program" | "channel_aftertouch" => cols.value[row] = Some(i32::from(a)),
724                        _ => cols.value[row] = Some(((i32::from(b) << 7) | i32::from(a)) - 8192),
725                    }
726                    if kind == "note_on" {
727                        track.notes += 1;
728                        sounding.entry((ch, a)).or_default().push_back(row);
729                    } else if kind == "note_off"
730                        && let Some(start) = sounding.get_mut(&(ch, a)).and_then(|q| q.pop_front())
731                    {
732                        cols.length[start] = Some(time - cols.seconds[start]);
733                    }
734                }
735                Body::Sysex { escape, data } => {
736                    cols.push(
737                        track_no,
738                        event.tick,
739                        time,
740                        if escape { "sysex_escape" } else { "sysex" },
741                    );
742                    cols.value[row] = Some(i32::try_from(data.len()).unwrap_or(i32::MAX));
743                    cols.text[row] = Some(hex((!escape).then_some(0xf0), data));
744                }
745                Body::System { status, a, b } => {
746                    let kind = match status {
747                        0xf1 => "mtc_quarter_frame",
748                        0xf2 => "song_position",
749                        0xf3 => "song_select",
750                        0xf6 => "tune_request",
751                        0xf8 => "clock",
752                        0xfa => "start",
753                        0xfb => "continue",
754                        0xfc => "stop",
755                        0xfe => "active_sensing",
756                        _ => "realtime",
757                    };
758                    cols.push(track_no, event.tick, time, kind);
759                    cols.value[row] = match status {
760                        0xf1 | 0xf3 => Some(i32::from(a)),
761                        0xf2 => Some((i32::from(b) << 7) | i32::from(a)),
762                        _ => None,
763                    };
764                }
765                Body::Meta { kind, data } => {
766                    let name = meta_kind(kind);
767                    cols.push(track_no, event.tick, time, name);
768                    let (value, text) = meta_value(kind, data);
769                    cols.value[row] = value;
770                    cols.text[row] = text;
771                    if kind == 0x20
772                        && let [ch] = data
773                    {
774                        cols.channel[row] = Some((ch & 0x0f) + 1);
775                    }
776                    match (kind, data) {
777                        (0x51, [a, b, c]) => tempos.push(u32::from_be_bytes([0, *a, *b, *c])),
778                        (0x02, _) if summary.copyright.is_none() => {
779                            summary.copyright = cols.text[row].clone();
780                        }
781                        (0x03, _) if track.name.is_none() => track.name = cols.text[row].clone(),
782                        (0x04, _) if track.instrument.is_none() => {
783                            track.instrument = cols.text[row].clone();
784                        }
785                        (0x58, _) if summary.time_signature.is_none() => {
786                            summary.time_signature = cols.text[row].clone();
787                        }
788                        (0x59, _) if summary.key.is_none() => summary.key = cols.text[row].clone(),
789                        _ => {}
790                    }
791                }
792            }
793            if let Some(file) = file {
794                cols.file.push(file);
795            }
796        }
797        track.channels = (1..=16u8).filter(|c| channels[(c - 1) as usize]).collect();
798        summary.unended += sounding.values().map(VecDeque::len).sum::<usize>();
799        summary.notes += track.notes;
800        tracks.push(track);
801    }
802    summary.events += smf.tracks.iter().map(Vec::len).sum::<usize>();
803    summary.track_count += smf.tracks.len();
804    // The first tempo sets it; each after that changes it.
805    summary.tempo_changes += tempos.len().saturating_sub(1);
806    if let Some(&first) = tempos.first() {
807        let (lo, hi) = tempos
808            .iter()
809            .fold((u32::MAX, 0), |(lo, hi), &t| (lo.min(t), hi.max(t)));
810        summary.tempo = Some(match summary.tempo {
811            Some((f, l, h)) => (f, l.min(lo), h.max(hi)),
812            None => (first, lo, hi),
813        });
814    }
815    summary.format = match (summary.files, summary.format) {
816        (0, _) => Some(smf.format),
817        (_, Some(f)) if f == smf.format => Some(f),
818        _ => None,
819    };
820    summary.division = match (summary.files, summary.division) {
821        (0, _) => Some(smf.division),
822        (_, Some(d)) if d == smf.division => Some(d),
823        _ => None,
824    };
825    summary.tracks = if summary.files == 0 {
826        tracks
827    } else {
828        Vec::new()
829    };
830    summary.files += 1;
831}
832
833/// A meta event's column name.
834fn meta_kind(kind: u8) -> &'static str {
835    match kind {
836        0x00 => "sequence_number",
837        0x01 | 0x0a..=0x0f => "text",
838        0x02 => "copyright",
839        0x03 => "track_name",
840        0x04 => "instrument",
841        0x05 => "lyric",
842        0x06 => "marker",
843        0x07 => "cue",
844        0x08 => "program_name",
845        0x09 => "device_name",
846        0x20 => "channel_prefix",
847        0x21 => "port",
848        0x2f => "end_of_track",
849        0x51 => "tempo",
850        0x54 => "smpte_offset",
851        0x58 => "time_signature",
852        0x59 => "key_signature",
853        0x7f => "sequencer_specific",
854        _ => "meta",
855    }
856}
857
858/// A meta event's `value` and `text`. Data of the wrong length for its type is shown
859/// as hex rather than read.
860fn meta_value(kind: u8, data: &[u8]) -> (Option<i32>, Option<String>) {
861    match (kind, data) {
862        (0x01..=0x0f, _) => (None, Some(meta_text(data))),
863        (0x00, [a, b]) => (Some(i32::from(u16::from_be_bytes([*a, *b]))), None),
864        (0x20 | 0x21, [a]) => (Some(i32::from(*a)), None),
865        (0x2f, []) => (None, None),
866        (0x51, [a, b, c]) => {
867            let tempo = u32::from_be_bytes([0, *a, *b, *c]);
868            (
869                Some(i32::try_from(tempo).unwrap_or(i32::MAX)),
870                Some(format!("{} bpm", bpm(tempo))),
871            )
872        }
873        (0x54, [hr, mn, se, fr, ff]) => (
874            None,
875            Some(format!(
876                "{:02}:{:02}:{:02}:{:02}.{:02}",
877                hr & 0x1f,
878                mn,
879                se,
880                fr,
881                ff
882            )),
883        ),
884        (0x58, [nn, dd, _, _]) => {
885            let denominator = 1u64.checked_shl(u32::from(*dd)).unwrap_or(0);
886            (None, Some(format!("{nn}/{denominator}")))
887        }
888        (0x59, [sf, mi]) => {
889            let sf = *sf as i8;
890            match key_name(sf, *mi == 1) {
891                Some(key) => (Some(i32::from(sf)), Some(key)),
892                None => (Some(i32::from(sf)), Some(hex(None, data))),
893            }
894        }
895        (0x7f, _) => (None, Some(hex(None, data))),
896        _ => (None, Some(format!("type {kind:02X}: {}", hex(None, data)))),
897    }
898}
899
900/// The table and the summary for parsed files; `names` are their files, written to a
901/// `file` column when there is more than one.
902pub fn build(files: &[(String, Smf<'_>)]) -> Result<(LazyFrame, MidiSummary)> {
903    let many = files.len() > 1;
904    let mut cols = Columns::default();
905    let mut summary = MidiSummary::default();
906    for (name, smf) in files {
907        add_file(smf, many.then_some(name.as_str()), &mut cols, &mut summary);
908    }
909    Ok((frame(cols, many)?, summary))
910}
911
912/// The columns as a table, with the `file` column first when there is one.
913fn frame(cols: Columns<'_>, many: bool) -> Result<LazyFrame> {
914    let rows = cols.rows();
915    let mut columns: Vec<Column> = Vec::new();
916    if many {
917        columns.push(Series::new("file".into(), cols.file).into());
918    }
919    columns.push(Series::new("track".into(), cols.track).into());
920    columns.push(Series::new("tick".into(), cols.tick).into());
921    columns.push(Series::new("seconds".into(), cols.seconds).into());
922    columns.push(Series::new("kind".into(), cols.kind).into());
923    columns.push(Series::new("channel".into(), cols.channel).into());
924    columns.push(Series::new("note".into(), cols.note).into());
925    columns.push(Series::new("note_name".into(), cols.note_name).into());
926    columns.push(Series::new("velocity".into(), cols.velocity).into());
927    columns.push(Series::new("controller".into(), cols.controller).into());
928    columns.push(Series::new("value".into(), cols.value).into());
929    columns.push(Series::new("length".into(), cols.length).into());
930    columns.push(Series::new("text".into(), cols.text).into());
931    Ok(DataFrame::new(rows, columns)?.lazy())
932}
933
934/// Read one file's bytes, refusing one past `limits.midi_bytes`.
935fn read_bytes(path: &Path) -> Result<Vec<u8>> {
936    use std::io::Read;
937    let file = std::fs::File::open(path)?;
938    let len = file.metadata()?.len();
939    let most = crate::limits::get().midi_bytes.bytes();
940    if len > most {
941        let size = crate::numfmt::bytes;
942        return Err(eyre!(
943            "MIDI file is {}; datui reads MIDI files up to {}, and limits.midi_bytes raises it",
944            size(len),
945            size(most)
946        ));
947    }
948    let mut bytes = Vec::with_capacity(len as usize);
949    file.take(most).read_to_end(&mut bytes)?;
950    Ok(bytes)
951}
952
953/// Read `paths` as one events table (with a `file` column for several). One unreadable
954/// file is an error; among several, unreadable ones are skipped and named in the summary
955/// unless all fail. One file's bytes are held at a time.
956pub fn read_midi(paths: &[PathBuf]) -> Result<(LazyFrame, MidiSummary)> {
957    if paths.is_empty() {
958        return Err(eyre!("No MIDI files to read"));
959    }
960    let names: Vec<String> = paths
961        .iter()
962        .map(|p| {
963            p.file_name()
964                .map(|n| n.to_string_lossy().into_owned())
965                .unwrap_or_else(|| p.display().to_string())
966        })
967        .collect();
968    let many = paths.len() > 1;
969    let mut cols = Columns::default();
970    let mut summary = MidiSummary::default();
971    let mut unreadable = Vec::new();
972    for (path, name) in paths.iter().zip(&names) {
973        let read = read_bytes(path);
974        let parsed = read
975            .as_deref()
976            .map_err(|e| eyre!(crate::error_display::user_message_from_report(e, None)))
977            .and_then(parse);
978        let smf = match parsed {
979            Ok(smf) => smf,
980            Err(e) if !many => return Err(e),
981            Err(e) => {
982                unreadable.push((name.clone(), e.to_string()));
983                continue;
984            }
985        };
986        let events = smf.tracks.iter().map(Vec::len).sum::<usize>();
987        let most = crate::limits::get().midi_events;
988        if summary.events + events > most {
989            return Err(eyre!(
990                "These MIDI files have more than {most} events, the most datui reads; limits.midi_events raises it"
991            ));
992        }
993        add_file(&smf, many.then_some(name.as_str()), &mut cols, &mut summary);
994    }
995    if summary.files == 0 {
996        let (name, why) = unreadable
997            .first()
998            .cloned()
999            .unwrap_or_else(|| (String::new(), "no files".to_string()));
1000        return Err(eyre!("No MIDI file could be read; \"{name}\": {why}"));
1001    }
1002    summary.unreadable = unreadable;
1003    Ok((frame(cols, many)?, summary))
1004}
1005
1006/// What the open has to say about the events: notes that never end, and files that
1007/// could not be read.
1008pub fn notes(summary: &MidiSummary) -> Vec<crate::notes::Note> {
1009    let mut out = Vec::new();
1010    if summary.unended > 0 {
1011        let n = summary.unended;
1012        out.push(crate::notes::Note {
1013            summary: format!(
1014                "{} {} {} length null",
1015                crate::widgets::info::group_u64(n as u64),
1016                if n == 1 {
1017                    "note never ends"
1018                } else {
1019                    "notes never end"
1020                },
1021                crate::glyphs::get().middot
1022            ),
1023            scope: "from every event".to_string(),
1024            read_as_text: None,
1025            passed_over: None,
1026        });
1027    }
1028    if !summary.unreadable.is_empty() {
1029        let n = summary.unreadable.len();
1030        let (name, why) = &summary.unreadable[0];
1031        out.push(crate::notes::Note {
1032            summary: format!(
1033                "{} unreadable, left out: {name}: {why}",
1034                if n == 1 {
1035                    "1 file".to_string()
1036                } else {
1037                    format!("{} files", crate::widgets::info::group_u64(n as u64))
1038                },
1039            ),
1040            scope: "from every file".to_string(),
1041            read_as_text: None,
1042            passed_over: None,
1043        });
1044    }
1045    out
1046}
1047
1048/// The MIDI tab's lines above its list: format and timing, length and counts, tempo,
1049/// meter and key, copyright.
1050fn facts(midi: &MidiSummary, sep: &str) -> Vec<String> {
1051    let mut head = match midi.format {
1052        Some(format) => format!("MIDI format {format}"),
1053        None => "MIDI".to_string(),
1054    };
1055    if midi.files > 1 {
1056        head.push_str(sep);
1057        head.push_str(&count_of(midi.files as u64, "file", "files"));
1058    }
1059    if let Some(division) = midi.division {
1060        head.push_str(sep);
1061        head.push_str(&division.label());
1062    }
1063    head.push_str(sep);
1064    head.push_str(&count_of(midi.track_count as u64, "track", "tracks"));
1065    let mut counts = format!(
1066        "Length: {}{sep}{}{sep}{}",
1067        clock(midi.length_seconds),
1068        count_of(midi.events as u64, "event", "events"),
1069        count_of(midi.notes as u64, "note", "notes"),
1070    );
1071    if midi.unended > 0 {
1072        let n = midi.unended;
1073        let verb = if n == 1 { "ends" } else { "end" };
1074        counts.push_str(&format!(" ({} never {verb})", group_u64(n as u64)));
1075    }
1076    let mut lines = vec![head, counts];
1077    let mut music = Vec::new();
1078    // In microseconds a quarter, so the fewest is the fastest.
1079    if let Some((first, fastest, slowest)) = midi.tempo {
1080        let bpm = bpm;
1081        let n = midi.tempo_changes;
1082        let changes = format!(
1083            "{} {}",
1084            group_u64(n as u64),
1085            if n == 1 { "change" } else { "changes" }
1086        );
1087        music.push(match (midi.files > 1, slowest == fastest) {
1088            (_, true) => format!("Tempo: {} bpm", bpm(first)),
1089            // Of many songs, the first one's tempo says nothing of the rest.
1090            (true, false) => format!("Tempo: {}-{} bpm", bpm(slowest), bpm(fastest)),
1091            (false, false) => format!(
1092                "Tempo: {} bpm ({}-{}, {changes})",
1093                bpm(first),
1094                bpm(slowest),
1095                bpm(fastest)
1096            ),
1097        });
1098    }
1099    // The first of each, which for a directory of songs would be one song's.
1100    if midi.files == 1 {
1101        if let Some(time) = &midi.time_signature {
1102            music.push(format!("Time: {time}"));
1103        }
1104        if let Some(key) = &midi.key {
1105            music.push(format!("Key: {key}"));
1106        }
1107    }
1108    if !music.is_empty() {
1109        lines.push(music.join(sep));
1110    }
1111    if let Some(copyright) = midi.copyright.as_ref().filter(|_| midi.files == 1) {
1112        lines.push(format!("Copyright: {copyright}"));
1113    }
1114    lines
1115}
1116
1117/// Each track as a key and a value: `2 Piano` and `2,000 events · 600 notes · channel 1`.
1118fn track_rows(
1119    midi: &MidiSummary,
1120    sep: &str,
1121) -> Vec<(String, crate::formats::model_files::MetaValue)> {
1122    midi.tracks
1123        .iter()
1124        .enumerate()
1125        .map(|(i, t)| {
1126            let key = match &t.name {
1127                Some(name) if !name.trim().is_empty() => format!("{} {}", i + 1, name.trim()),
1128                _ => (i + 1).to_string(),
1129            };
1130            let mut parts = vec![count_of(t.events as u64, "event", "events")];
1131            if t.notes > 0 {
1132                parts.push(count_of(t.notes as u64, "note", "notes"));
1133            }
1134            if !t.channels.is_empty() {
1135                let channels: Vec<String> = t.channels.iter().map(u8::to_string).collect();
1136                let label = if channels.len() == 1 {
1137                    "channel"
1138                } else {
1139                    "channels"
1140                };
1141                parts.push(format!("{label} {}", channels.join(", ")));
1142            }
1143            if let Some(instrument) = t.instrument.as_deref().filter(|i| !i.trim().is_empty()) {
1144                parts.push(instrument.trim().to_string());
1145            }
1146            (
1147                key,
1148                crate::formats::model_files::MetaValue::Text(parts.join(sep)),
1149            )
1150        })
1151        .collect()
1152}
1153
1154/// The MIDI tab: the header, timing and tempo, then the tracks; for a directory of
1155/// songs, the totals, then the files that could not be read.
1156pub fn detail(midi: &MidiSummary) -> crate::formats::text_formats::Detail {
1157    let sep = format!(" {} ", crate::glyphs::get().middot);
1158    let (list_title, list) = if midi.files > 1 {
1159        let rows = midi
1160            .unreadable
1161            .iter()
1162            .map(|(file, why)| {
1163                (
1164                    file.clone(),
1165                    crate::formats::model_files::MetaValue::Text(why.clone()),
1166                )
1167            })
1168            .collect();
1169        ("Unreadable", rows)
1170    } else {
1171        ("Tracks", track_rows(midi, &sep))
1172    };
1173    crate::formats::text_formats::Detail {
1174        tab: crate::formats::text_formats::tab(crate::FileFormat::Midi),
1175        lines: facts(midi, &sep),
1176        list_title,
1177        list,
1178        // The columns are the same for every file; what is particular to it is here.
1179        first: true,
1180        own_columns: true,
1181        ..Default::default()
1182    }
1183}
1184
1185/// The scan of MIDI files: their events, with the header, tracks and tempo.
1186fn scan(input: crate::formats::readers::ScanIn<'_>) -> Result<crate::loading::scan::Scan> {
1187    let (lf, summary) = read_midi(input.paths)?;
1188    input.report.opened = Some(Arc::new(crate::formats::members::Opened {
1189        detail: Some(Arc::new(detail(&summary))),
1190        notes: notes(&summary),
1191        ..Default::default()
1192    }));
1193    Ok(lf.into())
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199    use crate::tests::fixtures::{smf, vlq};
1200
1201    /// Every way a MIDI file is refused names the file, in the one shape; of several
1202    /// that none can be read, the first is named.
1203    #[test]
1204    fn errors_name_the_file() {
1205        let mut short_track = smf(0, 96, &[&[0x00, 0xff, 0x2f, 0x00]]);
1206        short_track.truncate(short_track.len() - 2);
1207        crate::formats::readers::bad_input::each_names_its_file(
1208            crate::FileFormat::Midi,
1209            &[
1210                ("text.mid", b"hello there", "MThd"),
1211                ("cut.mid", b"MThd\0\0", "cut short"),
1212                (
1213                    "fmt3.mid",
1214                    &smf(3, 96, &[&[0x00, 0xff, 0x2f, 0x00]]),
1215                    "format 3",
1216                ),
1217                ("none.mid", &smf(0, 96, &[]), "no tracks"),
1218                ("track.mid", &short_track, "cut short"),
1219                ("status.mid", &smf(0, 96, &[&[0x00, 0xf4]]), "undefined"),
1220                ("running.mid", &smf(0, 96, &[&[0x00, 60, 100]]), "no status"),
1221            ],
1222        );
1223        let dir = tempfile::tempdir().unwrap();
1224        let paths: Vec<PathBuf> = ["a.mid", "b.mid"]
1225            .iter()
1226            .map(|name| {
1227                let path = dir.path().join(name);
1228                std::fs::write(&path, b"not midi").unwrap();
1229                path
1230            })
1231            .collect();
1232        let err = read_midi(&paths).err().unwrap().to_string();
1233        assert!(err.contains("; \"a.mid\": Not a MIDI file"), "{err}");
1234    }
1235
1236    fn table(bytes: &[u8]) -> (DataFrame, MidiSummary) {
1237        let smf = parse(bytes).unwrap();
1238        let (lf, summary) = build(&[("a.mid".to_string(), smf)]).unwrap();
1239        (lf.collect().unwrap(), summary)
1240    }
1241
1242    fn col<'a>(df: &'a DataFrame, name: &str) -> &'a Series {
1243        df.column(name).unwrap().as_materialized_series()
1244    }
1245
1246    #[test]
1247    fn vlq_reads_the_specification_examples() {
1248        for (bytes, value) in [
1249            (&[0x00][..], 0u32),
1250            (&[0x40], 0x40),
1251            (&[0x7f], 0x7f),
1252            (&[0x81, 0x00], 0x80),
1253            (&[0xc0, 0x00], 0x2000),
1254            (&[0xff, 0x7f], 0x3fff),
1255            (&[0x81, 0x80, 0x00], 0x4000),
1256            (&[0xff, 0xff, 0x7f], 0x1f_ffff),
1257            (&[0x81, 0x80, 0x80, 0x00], 0x20_0000),
1258            (&[0xff, 0xff, 0xff, 0x7f], 0x0fff_ffff),
1259        ] {
1260            assert_eq!(Bytes::new(bytes).vlq().ok(), Some(value), "{bytes:02x?}");
1261            assert_eq!(vlq(value), bytes);
1262        }
1263        assert!(matches!(
1264            Bytes::new(&[0x80, 0x80, 0x80, 0x80, 0x00]).vlq(),
1265            Err(VlqError::TooLong)
1266        ));
1267        assert!(matches!(Bytes::new(&[0x81]).vlq(), Err(VlqError::CutShort)));
1268    }
1269
1270    #[test]
1271    fn running_status_repeats_the_last_channel_status() {
1272        // Note on C4, then E4 and G4 by running status, then a note on at velocity 0,
1273        // which is a note off.
1274        let track = [
1275            0x00, 0x90, 60, 100, 0x00, 64, 90, 0x00, 67, 80, 0x60, 60, 0, 0x00, 0xff, 0x2f, 0x00,
1276        ];
1277        let (df, summary) = table(&smf(0, 96, &[&track]));
1278        let kinds: Vec<_> = col(&df, "kind").str().unwrap().iter().flatten().collect();
1279        assert_eq!(
1280            kinds,
1281            ["note_on", "note_on", "note_on", "note_off", "end_of_track"]
1282        );
1283        let names: Vec<_> = col(&df, "note_name").str().unwrap().iter().collect();
1284        assert_eq!(names[..4], [Some("C4"), Some("E4"), Some("G4"), Some("C4")]);
1285        assert_eq!(col(&df, "channel").u8().unwrap().get(0), Some(1));
1286        // The narrowest type each holds: a track number past 255 is rare but legal.
1287        for (name, dtype) in [
1288            ("track", DataType::UInt16),
1289            ("channel", DataType::UInt8),
1290            ("note", DataType::UInt8),
1291            ("velocity", DataType::UInt8),
1292            ("controller", DataType::UInt8),
1293            ("value", DataType::Int32),
1294        ] {
1295            assert_eq!(df.column(name).unwrap().dtype(), &dtype, "{name}");
1296        }
1297        assert_eq!(summary.notes, 3);
1298        assert_eq!(summary.unended, 2, "E4 and G4 never end");
1299        // 96 ticks at 120 bpm and 96 per quarter is half a second.
1300        assert_eq!(col(&df, "length").f64().unwrap().get(0), Some(0.5));
1301        assert_eq!(col(&df, "length").null_count(), 4);
1302    }
1303
1304    #[test]
1305    fn a_data_byte_with_no_status_is_an_error() {
1306        let err = parse(&smf(0, 96, &[&[0x00, 60, 100]])).unwrap_err();
1307        assert!(err.to_string().contains("no status"), "{err}");
1308        // Meta events cancel running status.
1309        let track = [0x00, 0x90, 60, 100, 0x00, 0xff, 0x01, 0x00, 0x00, 60, 0];
1310        assert!(parse(&smf(0, 96, &[&track])).is_err());
1311        // So do sysex and escape packets.
1312        for sysex in [0xf0, 0xf7] {
1313            let track = [0x00, 0x90, 60, 100, 0x00, sysex, 0x01, 0xf7, 0x00, 60, 0];
1314            assert!(parse(&smf(0, 96, &[&track])).is_err(), "{sysex:02X}");
1315        }
1316        // And system common messages; 0xF4 has no definition, so no length to skip.
1317        let track = [0x00, 0x90, 60, 100, 0x00, 0xf3, 0x02, 0x00, 60, 0];
1318        assert!(parse(&smf(0, 96, &[&track])).is_err());
1319        let err = parse(&smf(0, 96, &[&[0x00, 0xf4]])).unwrap_err();
1320        assert!(err.to_string().contains("undefined"), "{err}");
1321    }
1322
1323    /// A real-time byte a file should not hold is read as itself, and the note after
1324    /// it still has the status before it, as on the wire.
1325    #[test]
1326    fn real_time_bytes_keep_running_status() {
1327        let track = [
1328            0x00, 0x90, 60, 100, 0x00, 0xf8, 0x00, 60, 0, 0x00, 0xf2, 0x10, 0x01, 0x00, 0xff, 0x2f,
1329            0x00,
1330        ];
1331        let (df, _) = table(&smf(0, 96, &[&track]));
1332        let kinds: Vec<_> = col(&df, "kind").str().unwrap().iter().flatten().collect();
1333        assert_eq!(
1334            kinds,
1335            [
1336                "note_on",
1337                "clock",
1338                "note_off",
1339                "song_position",
1340                "end_of_track"
1341            ]
1342        );
1343        assert_eq!(col(&df, "value").i32().unwrap().get(3), Some(0x90));
1344    }
1345
1346    /// Hostile lengths: past the end, overlong quantities, more tracks than are there.
1347    #[test]
1348    fn lengths_are_checked_before_use() {
1349        // A meta event claiming 2^28 - 1 bytes.
1350        let track = [0x00, 0xff, 0x01, 0xff, 0xff, 0xff, 0x7f, b'a'];
1351        assert!(parse(&smf(0, 96, &[&track])).is_err());
1352        // A track chunk longer than the file.
1353        let mut bytes = smf(0, 96, &[&[0x00, 0xff, 0x2f, 0x00]]);
1354        bytes[18..22].copy_from_slice(&u32::MAX.to_be_bytes());
1355        let err = parse(&bytes).unwrap_err().to_string();
1356        assert!(err.contains("cut short"), "{err}");
1357        // 65,535 tracks declared, one present.
1358        let mut bytes = smf(1, 96, &[&[0x00, 0xff, 0x2f, 0x00]]);
1359        bytes[10..12].copy_from_slice(&u16::MAX.to_be_bytes());
1360        let err = parse(&bytes).unwrap_err().to_string();
1361        assert!(err.contains("65535 tracks"), "{err}");
1362        // A delta of five bytes.
1363        let track = [0x80, 0x80, 0x80, 0x80, 0x00, 0xff, 0x2f, 0x00];
1364        assert!(parse(&smf(0, 96, &[&track])).is_err());
1365        // An event cut off by the end of its chunk.
1366        assert!(parse(&smf(0, 96, &[&[0x00, 0x90, 60]])).is_err());
1367        // Not MIDI, or a header that says nothing usable.
1368        assert!(parse(b"MThd").is_err());
1369        assert!(parse(&smf(3, 96, &[&[]])).is_err());
1370        assert!(parse(&smf(0, 0, &[&[]])).is_err());
1371        assert!(parse(&smf(0, 96, &[])).is_err());
1372    }
1373
1374    #[test]
1375    fn the_tempo_map_turns_ticks_into_time() {
1376        // Format 1: the tempo track sets 120 bpm, then 60 bpm at tick 480. The other
1377        // track has notes at 0, 480 and 960 ticks of 480 per quarter.
1378        let mut tempo = vec![0x00, 0xff, 0x51, 0x03, 0x07, 0xa1, 0x20];
1379        tempo.extend(vlq(480));
1380        tempo.extend([0xff, 0x51, 0x03, 0x0f, 0x42, 0x40, 0x00, 0xff, 0x2f, 0x00]);
1381        let mut notes = vec![0x00, 0x90, 60, 100];
1382        notes.extend(vlq(480));
1383        notes.extend([62, 100]);
1384        notes.extend(vlq(480));
1385        notes.extend([64, 100]);
1386        let (df, summary) = table(&smf(1, 480, &[&tempo, &notes]));
1387        let times: Vec<_> = col(&df, "seconds")
1388            .f64()
1389            .unwrap()
1390            .into_no_null_iter()
1391            .collect();
1392        // Tempo track: 0, 0.5 s, 0.5 s (end of track); notes: 0, 0.5 s, 1.5 s.
1393        assert_eq!(times, [0.0, 0.5, 0.5, 0.0, 0.5, 1.5]);
1394        assert_eq!(summary.tempo, Some((500_000, 500_000, 1_000_000)));
1395        assert_eq!(summary.length_seconds, 1.5);
1396        let text = col(&df, "text").str().unwrap();
1397        assert_eq!(text.get(0), Some("120 bpm"));
1398        assert_eq!(text.get(1), Some("60 bpm"));
1399        assert_eq!(summary.tracks.len(), 2);
1400        assert_eq!(summary.tracks[1].channels, [1]);
1401    }
1402
1403    #[test]
1404    fn smpte_time_ignores_tempo() {
1405        // 25 fps, 40 ticks per frame: 1000 ticks a second.
1406        let division = (((-25i8) as u8 as u16) << 8) | 40;
1407        let mut track = vec![0x00, 0xff, 0x51, 0x03, 0x0f, 0x42, 0x40];
1408        track.extend(vlq(1500));
1409        track.extend([0x90, 60, 1]);
1410        let (df, summary) = table(&smf(0, division, &[&track]));
1411        assert_eq!(col(&df, "seconds").f64().unwrap().get(1), Some(1.5));
1412        assert_eq!(
1413            summary.division.map(Division::label).as_deref(),
1414            Some("25 fps, 40 ticks per frame")
1415        );
1416    }
1417
1418    #[test]
1419    fn format_2_tracks_keep_their_own_tempo() {
1420        let mut slow = vec![0x00, 0xff, 0x51, 0x03, 0x0f, 0x42, 0x40];
1421        slow.extend(vlq(96));
1422        slow.extend([0x90, 60, 1]);
1423        let mut plain = vlq(96);
1424        plain.extend([0x90, 60, 1]);
1425        let (df, _) = table(&smf(2, 96, &[&slow, &plain]));
1426        let times: Vec<_> = col(&df, "seconds")
1427            .f64()
1428            .unwrap()
1429            .into_no_null_iter()
1430            .collect();
1431        assert_eq!(times, [0.0, 1.0, 0.5]);
1432    }
1433
1434    #[test]
1435    fn meta_and_channel_events_fill_their_columns() {
1436        let mut t = vec![];
1437        t.extend([0x00, 0xff, 0x03, 0x05]);
1438        t.extend(b"Piano");
1439        t.extend([0x00, 0xff, 0x58, 0x04, 6, 3, 24, 8]);
1440        t.extend([0x00, 0xff, 0x59, 0x02, 0xfd, 0x01]); // 3 flats, minor
1441        t.extend([0x00, 0xff, 0x05, 0x02, 0xe9, b'a']); // Latin-1 lyric
1442        t.extend([0x00, 0xb3, 64, 127]); // sustain on, channel 4
1443        t.extend([0x00, 0xc3, 5]);
1444        t.extend([0x00, 0xe3, 0x00, 0x00]); // pitch bend all the way down
1445        t.extend([0x00, 0xf0, 0x03, 0x7e, 0x7f, 0xf7]);
1446        let (df, summary) = table(&smf(0, 96, &[&t]));
1447        let kind = col(&df, "kind").str().unwrap();
1448        let text = col(&df, "text").str().unwrap();
1449        let value = col(&df, "value").i32().unwrap();
1450        assert_eq!(kind.get(1), Some("time_signature"));
1451        assert_eq!(text.get(1), Some("6/8"));
1452        assert_eq!(text.get(2), Some("C minor"));
1453        assert_eq!(value.get(2), Some(-3));
1454        assert_eq!(text.get(3), Some("éa"));
1455        assert_eq!(kind.get(4), Some("cc"));
1456        assert_eq!(col(&df, "controller").u8().unwrap().get(4), Some(64));
1457        assert_eq!(value.get(4), Some(127));
1458        assert_eq!(col(&df, "channel").u8().unwrap().get(4), Some(4));
1459        assert_eq!(value.get(5), Some(5));
1460        assert_eq!(value.get(6), Some(-8192));
1461        assert_eq!(kind.get(7), Some("sysex"));
1462        assert_eq!(text.get(7), Some("F0 7E 7F F7"));
1463        assert_eq!(summary.tracks[0].name.as_deref(), Some("Piano"));
1464        assert_eq!(summary.time_signature.as_deref(), Some("6/8"));
1465        assert_eq!(summary.key.as_deref(), Some("C minor"));
1466    }
1467
1468    #[test]
1469    fn note_names_put_middle_c_in_octave_4() {
1470        assert_eq!(note_name(60), "C4");
1471        assert_eq!(note_name(0), "C-1");
1472        assert_eq!(note_name(69), "A4");
1473        assert_eq!(note_name(127), "G9");
1474        assert_eq!(key_name(0, false).as_deref(), Some("C major"));
1475        assert_eq!(key_name(7, false).as_deref(), Some("C# major"));
1476        assert_eq!(key_name(-7, true).as_deref(), Some("Ab minor"));
1477        assert_eq!(key_name(8, false), None);
1478        assert_eq!(bpm(500_000), "120");
1479        assert_eq!(bpm(650_000), "92.31");
1480    }
1481
1482    #[test]
1483    fn midi_is_known_by_its_first_bytes_and_unwrapped_from_riff() {
1484        let inner = smf(0, 96, &[&[0x00, 0x90, 60, 100]]);
1485        assert!(looks_like_midi(&inner));
1486        let mut riff = b"RIFF".to_vec();
1487        riff.extend(((inner.len() + 12) as u32).to_le_bytes());
1488        riff.extend(b"RMIDdata");
1489        riff.extend((inner.len() as u32).to_le_bytes());
1490        riff.extend(&inner);
1491        assert!(looks_like_midi(&riff));
1492        assert_eq!(parse(&riff).unwrap().tracks[0].len(), 1);
1493        assert!(!looks_like_midi(b"RIFF\0\0\0\0WAVEfmt "));
1494        assert!(!looks_like_midi(b"MThd\0\0\0\x07"));
1495    }
1496
1497    /// Of a directory, a file too large or broken is left out and named; alone, it is
1498    /// the error. The `file` column stays when only one of the files is read.
1499    #[test]
1500    fn a_directory_leaves_out_what_it_cannot_read() {
1501        let dir = tempfile::tempdir().unwrap();
1502        let good = dir.path().join("good.mid");
1503        std::fs::write(&good, smf(0, 96, &[&[0x00, 0x90, 60, 100]])).unwrap();
1504        let huge = dir.path().join("huge.mid");
1505        // Sparse: the size is all that is read before the refusal.
1506        std::fs::File::create(&huge)
1507            .unwrap()
1508            .set_len(crate::limits::get().midi_bytes.bytes() + 1)
1509            .unwrap();
1510        let err = read_midi(std::slice::from_ref(&huge))
1511            .err()
1512            .expect("a file too large is refused")
1513            .to_string();
1514        assert!(
1515            err.contains("up to 64.0 MiB, and limits.midi_bytes"),
1516            "{err}"
1517        );
1518        let (lf, summary) = read_midi(&[huge, good]).unwrap();
1519        let df = lf.collect().unwrap();
1520        assert_eq!(df.height(), 1);
1521        assert_eq!(col(&df, "file").str().unwrap().get(0), Some("good.mid"));
1522        assert_eq!(summary.unreadable.len(), 1);
1523        assert_eq!(summary.unreadable[0].0, "huge.mid");
1524    }
1525
1526    #[test]
1527    fn unknown_chunks_are_skipped_and_trailing_bytes_ignored() {
1528        let mut bytes = smf(0, 96, &[]);
1529        bytes[11] = 1;
1530        bytes.extend(b"XFIH\0\0\0\x02ab");
1531        bytes.extend(b"MTrk\0\0\0\x04\x00\xff\x2f\x00");
1532        bytes.extend(b"junk");
1533        assert_eq!(parse(&bytes).unwrap().tracks.len(), 1);
1534    }
1535}