1use 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
23pub(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
34pub const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
37pub const MAX_EVENTS: usize = 10_000_000;
40const HEX_SHOWN: usize = 256;
42const DEFAULT_TEMPO: u32 = 500_000;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Division {
48 Ppq(u16),
50 Smpte { fps: u8, ticks_per_frame: u8 },
53}
54
55impl Division {
56 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#[derive(Debug, Clone, Copy)]
77pub struct Event<'a> {
78 pub tick: u64,
80 pub body: Body<'a>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum Body<'a> {
85 Channel { status: u8, a: u8, b: u8 },
88 Sysex { escape: bool, data: &'a [u8] },
91 Meta { kind: u8, data: &'a [u8] },
93 System { status: u8, a: u8, b: u8 },
96}
97
98#[derive(Debug, Clone)]
100pub struct Smf<'a> {
101 pub format: u16,
103 pub division: Division,
104 pub tracks: Vec<Vec<Event<'a>>>,
106}
107
108pub 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
115struct 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 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
170fn at(track: usize, offset: usize) -> String {
172 format!("track {}, byte {offset}", track + 1)
173}
174
175fn 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 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
203pub 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 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 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
293fn 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 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 0xf8..=0xfe => Body::System {
333 status: first,
334 a: 0,
335 b: 0,
336 },
337 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 break;
401 }
402 }
403 Ok(out)
404}
405
406fn 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
417static NOTE_NAMES: std::sync::LazyLock<Vec<String>> =
419 std::sync::LazyLock::new(|| (0..=127).map(note_name).collect());
420
421fn note_name_of(note: u8) -> &'static str {
423 NOTE_NAMES[usize::from(note & 0x7f)].as_str()
424}
425
426pub 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
438fn 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
456pub(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
465fn 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
474fn 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
493struct TempoMap {
495 segments: Vec<(u64, u128, u32)>,
498 ppq: u128,
499}
500
501impl TempoMap {
502 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 let num = base + u128::from(tick - t) * u128::from(tempo);
527 num as f64 / (self.ppq as f64 * 1e6)
528 }
529}
530
531enum Clock<'m> {
533 Tempo(&'m TempoMap),
534 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
550fn 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
567pub struct TrackSummary {
568 pub name: Option<String>,
570 pub instrument: Option<String>,
572 pub events: usize,
573 pub notes: usize,
575 pub channels: Vec<u8>,
577}
578
579#[derive(Debug, Clone, Default)]
581pub struct MidiSummary {
582 pub files: usize,
584 pub format: Option<u16>,
586 pub division: Option<Division>,
588 pub tracks: Vec<TrackSummary>,
590 pub track_count: usize,
592 pub events: usize,
593 pub notes: usize,
595 pub unended: usize,
597 pub length_seconds: f64,
599 pub tempo: Option<(u32, u32, u32)>,
602 pub tempo_changes: usize,
604 pub time_signature: Option<String>,
606 pub key: Option<String>,
608 pub copyright: Option<String>,
610 pub unreadable: Vec<(String, String)>,
613}
614
615#[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
654fn 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 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 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 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
848fn 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
873fn 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
915pub 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
927fn 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
949fn 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
967pub 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
1023pub 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
1065fn 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 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 (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 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
1134fn 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
1165pub 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 first: true,
1191 own_columns: true,
1192 ..Default::default()
1193 }
1194}
1195
1196fn 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 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 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 #[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 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 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 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 let track = [0x00, 0x90, 60, 100, 0x00, 0xff, 0x01, 0x00, 0x00, 60, 0];
1345 assert!(parse(&smf(0, 96, &[&track])).is_err());
1346 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 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 #[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 #[test]
1383 fn lengths_are_checked_before_use() {
1384 let track = [0x00, 0xff, 0x01, 0xff, 0xff, 0xff, 0x7f, b'a'];
1386 assert!(parse(&smf(0, 96, &[&track])).is_err());
1387 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 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 let track = [0x80, 0x80, 0x80, 0x80, 0x00, 0xff, 0x2f, 0x00];
1399 assert!(parse(&smf(0, 96, &[&track])).is_err());
1400 assert!(parse(&smf(0, 96, &[&[0x00, 0x90, 60]])).is_err());
1402 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 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, ¬es]));
1422 let times: Vec<_> = col(&df, "seconds")
1423 .f64()
1424 .unwrap()
1425 .into_no_null_iter()
1426 .collect();
1427 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 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]); t.extend([0x00, 0xff, 0x05, 0x02, 0xe9, b'a']); t.extend([0x00, 0xb3, 64, 127]); t.extend([0x00, 0xc3, 5]);
1479 t.extend([0x00, 0xe3, 0x00, 0x00]); 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 #[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 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}