Skip to main content

datui_lib/
midi.rs

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