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