1use serde::Serialize;
14use std::path::Path;
15
16#[derive(Debug, Clone, Serialize, Default)]
17pub struct MidiInfo {
18 pub format: u16,
19 #[serde(rename = "trackCount")]
20 pub track_count: u16,
21 pub ppqn: u16,
22 pub tempo: f64,
23 #[serde(rename = "timeSignature")]
24 pub time_signature: String,
25 #[serde(rename = "keySignature")]
26 pub key_signature: String,
27 #[serde(rename = "noteCount")]
28 pub note_count: u32,
29 pub duration: f64,
30 #[serde(rename = "trackNames")]
31 pub track_names: Vec<String>,
32 #[serde(rename = "channelsUsed")]
33 pub channels_used: u16,
34}
35
36pub fn parse_midi(path: &Path) -> Option<MidiInfo> {
38 let data = std::fs::read(path).ok()?;
39 if data.len() < 14 {
40 return None;
41 }
42
43 if &data[0..4] != b"MThd" {
45 return None;
46 }
47 let header_len = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
48 if data.len() < 8 + header_len {
49 return None;
50 }
51
52 let format = u16::from_be_bytes([data[8], data[9]]);
53 let track_count = u16::from_be_bytes([data[10], data[11]]);
54 let division = u16::from_be_bytes([data[12], data[13]]);
55
56 let ppqn = if division & 0x8000 == 0 {
58 division
59 } else {
60 480
61 }; let mut info = MidiInfo {
64 format,
65 track_count,
66 ppqn,
67 tempo: 120.0, time_signature: "4/4".into(),
69 ..Default::default()
70 };
71
72 let mut channel_mask = 0u16;
73 let mut total_ticks = 0u32;
74 let mut tempo_us = 500_000u32; let mut pos = 8 + header_len;
78 for _ in 0..track_count {
79 if pos + 8 > data.len() {
80 break;
81 }
82 if &data[pos..pos + 4] != b"MTrk" {
83 break;
84 }
85 let track_len =
86 u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
87 as usize;
88 let track_end = (pos + 8 + track_len).min(data.len());
89 let mut tp = pos + 8;
90 let mut track_ticks = 0u32;
91 let mut running_status = 0u8;
92
93 while tp < track_end {
94 let (delta, bytes_read) = read_var_len(&data, tp);
96 tp += bytes_read;
97 track_ticks += delta;
98
99 if tp >= track_end {
100 break;
101 }
102
103 let status = data[tp];
104
105 if status == 0xFF {
106 tp += 1;
108 if tp >= track_end {
109 break;
110 }
111 let meta_type = data[tp];
112 tp += 1;
113 let (meta_len, vl_bytes) = read_var_len(&data, tp);
114 tp += vl_bytes;
115 let meta_end = (tp + meta_len as usize).min(track_end);
116
117 match meta_type {
118 0x03 => {
119 if let Ok(name) = std::str::from_utf8(&data[tp..meta_end]) {
121 let name = name.trim();
122 if !name.is_empty() {
123 info.track_names.push(name.to_string());
124 }
125 }
126 }
127 0x51 => {
128 if meta_end - tp >= 3 {
130 tempo_us = ((data[tp] as u32) << 16)
131 | ((data[tp + 1] as u32) << 8)
132 | (data[tp + 2] as u32);
133 if tempo_us > 0 {
134 info.tempo = 60_000_000.0 / tempo_us as f64;
135 }
136 }
137 }
138 0x58 => {
139 if meta_end - tp >= 2 {
141 let nn = data[tp];
142 let dd = data[tp + 1];
143 let denom = 1u32 << dd;
144 info.time_signature = format!("{nn}/{denom}");
145 }
146 }
147 0x59 => {
148 if meta_end - tp >= 2 {
150 let sf = data[tp] as i8;
151 let mi = data[tp + 1];
152 let key_names_major = [
153 "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E",
154 "B", "F#", "C#",
155 ];
156 let key_names_minor = [
157 "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#",
158 "G#", "D#", "A#",
159 ];
160 let idx = (sf + 7) as usize;
161 if idx < 15 {
162 let name = if mi == 0 {
163 key_names_major[idx]
164 } else {
165 key_names_minor[idx]
166 };
167 let mode = if mi == 0 { "major" } else { "minor" };
168 info.key_signature = format!("{name} {mode}");
169 }
170 }
171 }
172 _ => {}
173 }
174 tp = meta_end;
175 } else if status == 0xF0 || status == 0xF7 {
176 tp += 1;
178 let (sysex_len, vl_bytes) = read_var_len(&data, tp);
179 tp += vl_bytes + sysex_len as usize;
180 } else if status & 0x80 != 0 {
181 running_status = status;
183 tp += 1;
184 let msg = status & 0xF0;
185 let channel = status & 0x0F;
186 channel_mask |= 1 << channel;
187 match msg {
188 0x80 | 0xA0 | 0xB0 | 0xE0 => {
189 tp += 2;
190 } 0x90 => {
192 if tp + 1 < track_end && data[tp + 1] > 0 {
194 info.note_count += 1;
195 }
196 tp += 2;
197 }
198 0xC0 | 0xD0 => {
199 tp += 1;
200 } _ => {
202 tp += 2;
203 }
204 }
205 } else {
206 let msg = running_status & 0xF0;
208 let channel = running_status & 0x0F;
209 channel_mask |= 1 << channel;
210 match msg {
211 0x80 | 0xA0 | 0xB0 | 0xE0 => {
212 tp += 2;
213 }
214 0x90 => {
215 if tp + 1 < track_end && data[tp + 1] > 0 {
216 info.note_count += 1;
217 }
218 tp += 2;
219 }
220 0xC0 | 0xD0 => {
221 tp += 1;
222 }
223 _ => {
224 tp += 1;
225 }
226 }
227 }
228 }
229
230 if track_ticks > total_ticks {
231 total_ticks = track_ticks;
232 }
233 pos = track_end;
234 }
235
236 info.channels_used = channel_mask.count_ones() as u16;
237
238 if ppqn > 0 && tempo_us > 0 {
240 let beats = total_ticks as f64 / ppqn as f64;
241 info.duration = beats * (tempo_us as f64 / 1_000_000.0);
242 }
243
244 info.tempo = (info.tempo * 10.0).round() / 10.0;
246 info.duration = (info.duration * 100.0).round() / 100.0;
247
248 Some(info)
249}
250
251fn read_var_len(data: &[u8], pos: usize) -> (u32, usize) {
253 let mut val = 0u32;
254 let mut i = pos;
255 loop {
256 if i >= data.len() {
257 return (val, i - pos);
258 }
259 let b = data[i];
260 val = (val << 7) | (b & 0x7F) as u32;
261 i += 1;
262 if b & 0x80 == 0 {
263 break;
264 }
265 if i - pos > 4 {
266 break;
267 } }
269 (val, i - pos)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 fn make_midi(format: u16, tracks: u16, ppqn: u16, track_data: &[u8]) -> Vec<u8> {
277 let mut data = Vec::new();
278 data.extend_from_slice(b"MThd");
280 data.extend_from_slice(&6u32.to_be_bytes()); data.extend_from_slice(&format.to_be_bytes());
282 data.extend_from_slice(&tracks.to_be_bytes());
283 data.extend_from_slice(&ppqn.to_be_bytes());
284 data.extend_from_slice(b"MTrk");
286 data.extend_from_slice(&(track_data.len() as u32).to_be_bytes());
287 data.extend_from_slice(track_data);
288 data
289 }
290
291 fn build_midi_with_track_bodies(format: u16, ppqn: u16, track_bodies: &[Vec<u8>]) -> Vec<u8> {
292 let mut data = Vec::new();
293 data.extend_from_slice(b"MThd");
294 data.extend_from_slice(&6u32.to_be_bytes());
295 data.extend_from_slice(&format.to_be_bytes());
296 data.extend_from_slice(&(track_bodies.len() as u16).to_be_bytes());
297 data.extend_from_slice(&ppqn.to_be_bytes());
298 for body in track_bodies {
299 data.extend_from_slice(b"MTrk");
300 data.extend_from_slice(&(body.len() as u32).to_be_bytes());
301 data.extend_from_slice(body);
302 }
303 data
304 }
305
306 #[test]
307 fn test_read_var_len_single_byte() {
308 let data = [0x40u8];
309 assert_eq!(read_var_len(&data, 0), (0x40, 1));
310 }
311
312 #[test]
313 fn test_read_var_len_empty_slice() {
314 let data: [u8; 0] = [];
315 assert_eq!(read_var_len(&data, 0), (0, 0));
316 }
317
318 #[test]
319 fn test_read_var_len_pos_past_end_returns_zero_bytes_consumed() {
320 let data = [0x40u8];
321 assert_eq!(read_var_len(&data, 1), (0, 0));
322 }
323
324 #[test]
325 fn test_read_var_len_128_two_bytes() {
326 let data = [0x81u8, 0x00];
327 assert_eq!(read_var_len(&data, 0), (128, 2));
328 }
329
330 #[test]
331 fn test_read_var_len_offset() {
332 let data = [0x00u8, 0x81, 0x00];
333 assert_eq!(read_var_len(&data, 1), (128, 2));
334 }
335
336 #[test]
337 fn test_read_var_len_incomplete_uses_partial() {
338 let data = [0x81u8]; let (v, n) = read_var_len(&data, 0);
340 assert_eq!(n, 1);
341 assert_eq!(v, 1); }
343
344 #[test]
345 fn test_read_var_len_large_value() {
346 let data = [0xC0u8, 0x00];
348 assert_eq!(read_var_len(&data, 0), (0x2000, 2));
349 }
350
351 #[test]
353 fn test_read_var_len_max_28bit_value() {
354 let data = [0xFFu8, 0xFF, 0xFF, 0x7F];
355 assert_eq!(read_var_len(&data, 0), (268_435_455, 4));
356 }
357
358 #[test]
359 fn test_read_var_len_fifth_byte_triggers_safety_break() {
360 let data = [0xFFu8, 0xFF, 0xFF, 0xFF, 0x7F];
362 let (v, n) = read_var_len(&data, 0);
363 assert_eq!(n, 5);
364 assert!(v > 0);
365 }
366
367 #[test]
368 fn test_parse_midi_wrong_magic_returns_none() {
369 let tmp = std::env::temp_dir().join("test_midi_wrong_magic.mid");
370 std::fs::write(&tmp, b"RIFFxxxxNOTMThd").unwrap();
371 assert!(parse_midi(&tmp).is_none());
372 let _ = std::fs::remove_file(&tmp);
373 }
374
375 #[test]
376 fn test_parse_midi_truncated_header_returns_none() {
377 let tmp = std::env::temp_dir().join("test_midi_trunc.mid");
378 std::fs::write(&tmp, b"MThd").unwrap();
379 assert!(parse_midi(&tmp).is_none());
380 let _ = std::fs::remove_file(&tmp);
381 }
382
383 #[test]
384 fn test_parse_empty_midi() {
385 let track = vec![0x00, 0xFF, 0x2F, 0x00]; let data = make_midi(0, 1, 480, &track);
388 let tmp = std::env::temp_dir().join("test_empty.mid");
389 std::fs::write(&tmp, &data).unwrap();
390 let info = parse_midi(&tmp).unwrap();
391 assert_eq!(info.format, 0);
392 assert_eq!(info.track_count, 1);
393 assert_eq!(info.ppqn, 480);
394 assert_eq!(info.note_count, 0);
395 assert_eq!(info.tempo, 120.0); let _ = std::fs::remove_file(&tmp);
397 }
398
399 #[test]
400 fn test_parse_tempo() {
401 let track = vec![
403 0x00, 0xFF, 0x51, 0x03, 0x06, 0x8A, 0x7B, 0x00, 0xFF, 0x2F, 0x00, ];
406 let data = make_midi(0, 1, 480, &track);
407 let tmp = std::env::temp_dir().join("test_tempo.mid");
408 std::fs::write(&tmp, &data).unwrap();
409 let info = parse_midi(&tmp).unwrap();
410 assert!(
411 (info.tempo - 140.0).abs() < 0.5,
412 "tempo should be ~140, got {}",
413 info.tempo
414 );
415 let _ = std::fs::remove_file(&tmp);
416 }
417
418 #[test]
419 fn test_parse_time_signature() {
420 let track = vec![
422 0x00, 0xFF, 0x58, 0x04, 0x03, 0x02, 0x18, 0x08, 0x00, 0xFF, 0x2F, 0x00,
424 ];
425 let data = make_midi(0, 1, 480, &track);
426 let tmp = std::env::temp_dir().join("test_timesig.mid");
427 std::fs::write(&tmp, &data).unwrap();
428 let info = parse_midi(&tmp).unwrap();
429 assert_eq!(info.time_signature, "3/4");
430 let _ = std::fs::remove_file(&tmp);
431 }
432
433 #[test]
434 fn test_parse_note_on_velocity_zero_not_counted() {
435 let track = vec![
437 0x00, 0x90, 60, 0, 0x00, 0xFF, 0x2F, 0x00,
439 ];
440 let data = make_midi(0, 1, 480, &track);
441 let tmp = std::env::temp_dir().join("test_vel0_note_on.mid");
442 std::fs::write(&tmp, &data).unwrap();
443 let info = parse_midi(&tmp).unwrap();
444 assert_eq!(info.note_count, 0);
445 let _ = std::fs::remove_file(&tmp);
446 }
447
448 #[test]
449 fn test_parse_notes() {
450 let track = vec![
451 0x00, 0x90, 60, 100, 0x60, 0x80, 60, 0, 0x00, 0x90, 64, 80, 0x60, 0x80, 64, 0, 0x00, 0x90, 67, 90, 0x60, 0x80, 67, 0, 0x00, 0xFF, 0x2F, 0x00,
458 ];
459 let data = make_midi(0, 1, 480, &track);
460 let tmp = std::env::temp_dir().join("test_notes.mid");
461 std::fs::write(&tmp, &data).unwrap();
462 let info = parse_midi(&tmp).unwrap();
463 assert_eq!(info.note_count, 3);
464 assert!(info.channels_used >= 1);
465 let _ = std::fs::remove_file(&tmp);
466 }
467
468 #[test]
469 fn test_parse_track_name() {
470 let name = b"Piano";
471 let mut track = vec![0x00, 0xFF, 0x03, name.len() as u8];
472 track.extend_from_slice(name);
473 track.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
474 let data = make_midi(0, 1, 480, &track);
475 let tmp = std::env::temp_dir().join("test_trackname.mid");
476 std::fs::write(&tmp, &data).unwrap();
477 let info = parse_midi(&tmp).unwrap();
478 assert_eq!(info.track_names, vec!["Piano"]);
479 let _ = std::fs::remove_file(&tmp);
480 }
481
482 #[test]
483 fn test_parse_key_signature() {
484 let track = vec![
486 0x00, 0xFF, 0x59, 0x02, 0x00, 0x00, 0x00, 0xFF, 0x2F, 0x00,
488 ];
489 let data = make_midi(0, 1, 480, &track);
490 let tmp = std::env::temp_dir().join("test_keysig.mid");
491 std::fs::write(&tmp, &data).unwrap();
492 let info = parse_midi(&tmp).unwrap();
493 assert_eq!(info.key_signature, "C major");
494 let _ = std::fs::remove_file(&tmp);
495 }
496
497 #[test]
498 fn test_parse_key_signature_minor() {
499 let track = vec![
501 0x00, 0xFF, 0x59, 0x02, 0x00, 0x01, 0x00, 0xFF, 0x2F, 0x00,
503 ];
504 let data = make_midi(0, 1, 480, &track);
505 let tmp = std::env::temp_dir().join("test_keysig_minor.mid");
506 std::fs::write(&tmp, &data).unwrap();
507 let info = parse_midi(&tmp).unwrap();
508 assert_eq!(info.key_signature, "A minor");
509 let _ = std::fs::remove_file(&tmp);
510 }
511
512 #[test]
513 fn test_parse_duration() {
514 let track = vec![
516 0x00, 0x90, 60, 100, 0x83, 0x60, 0x80, 60, 0, 0x00, 0xFF, 0x2F, 0x00,
519 ];
520 let data = make_midi(0, 1, 480, &track);
521 let tmp = std::env::temp_dir().join("test_duration.mid");
522 std::fs::write(&tmp, &data).unwrap();
523 let info = parse_midi(&tmp).unwrap();
524 assert!(
525 (info.duration - 0.5).abs() < 0.1,
526 "duration should be ~0.5s, got {}",
527 info.duration
528 );
529 let _ = std::fs::remove_file(&tmp);
530 }
531
532 #[test]
533 fn test_not_midi() {
534 let tmp = std::env::temp_dir().join("test_not_midi.mid");
535 std::fs::write(&tmp, b"not a midi file").unwrap();
536 assert!(parse_midi(&tmp).is_none());
537 let _ = std::fs::remove_file(&tmp);
538 }
539
540 #[test]
541 fn test_parse_midi_wrong_magic_not_mthd() {
542 let tmp = std::env::temp_dir().join("test_wrong_magic.mid");
543 std::fs::write(&tmp, b"RIFF....WAVEfmt ").unwrap();
544 assert!(parse_midi(&tmp).is_none());
545 let _ = std::fs::remove_file(&tmp);
546 }
547
548 #[test]
549 fn test_parse_midi_file_too_short_for_minimum_header() {
550 let tmp = std::env::temp_dir().join("test_midi_too_short.mid");
551 std::fs::write(&tmp, b"MThd\x00\x00\x00\x06").unwrap();
552 assert!(parse_midi(&tmp).is_none());
553 let _ = std::fs::remove_file(&tmp);
554 }
555
556 #[test]
557 fn test_parse_midi_smpte_division_defaults_ppqn() {
558 let tmp = std::env::temp_dir().join("test_midi_smpte_ppqn.mid");
559 let mut data = Vec::new();
560 data.extend_from_slice(b"MThd");
561 data.extend_from_slice(&6u32.to_be_bytes());
562 data.extend_from_slice(&0u16.to_be_bytes());
563 data.extend_from_slice(&0u16.to_be_bytes());
564 data.extend_from_slice(&0x8001u16.to_be_bytes());
565 std::fs::write(&tmp, &data).unwrap();
566 let info = parse_midi(&tmp).unwrap();
567 assert_eq!(
568 info.ppqn, 480,
569 "SMPTE timecode division → internal PPQN default"
570 );
571 let _ = std::fs::remove_file(&tmp);
572 }
573
574 #[test]
575 fn test_parse_midi_ppqn_960_standard_division() {
576 let track = vec![0x00, 0xFF, 0x2F, 0x00];
577 let data = make_midi(0, 1, 960, &track);
578 let tmp = std::env::temp_dir().join("test_midi_ppqn_960.mid");
579 std::fs::write(&tmp, &data).unwrap();
580 let info = parse_midi(&tmp).unwrap();
581 assert_eq!(
582 info.ppqn, 960,
583 "PPQN ticks/quarter when division bit 15 is clear"
584 );
585 let _ = std::fs::remove_file(&tmp);
586 }
587
588 #[test]
589 fn test_parse_midi_running_status_counts_second_note_on() {
590 let track = vec![
592 0x00, 0x90, 60, 100, 0x00, 64, 90, 0x00, 0xFF, 0x2F, 0x00,
595 ];
596 let data = make_midi(0, 1, 480, &track);
597 let tmp = std::env::temp_dir().join("test_midi_running_status.mid");
598 std::fs::write(&tmp, &data).unwrap();
599 let info = parse_midi(&tmp).unwrap();
600 assert_eq!(info.note_count, 2);
601 let _ = std::fs::remove_file(&tmp);
602 }
603
604 #[test]
605 fn test_parse_midi_program_change_advances_without_note_count() {
606 let track = vec![
607 0x00, 0xC0, 7, 0x00, 0xFF, 0x2F, 0x00,
609 ];
610 let data = make_midi(0, 1, 480, &track);
611 let tmp = std::env::temp_dir().join("test_midi_program_change.mid");
612 std::fs::write(&tmp, &data).unwrap();
613 let info = parse_midi(&tmp).unwrap();
614 assert_eq!(info.note_count, 0);
615 let _ = std::fs::remove_file(&tmp);
616 }
617
618 #[test]
619 fn test_parse_midi_pitch_bend_two_data_bytes_no_note_count() {
620 let track = vec![
621 0x00, 0xE0, 0x00, 0x40, 0x00, 0xFF, 0x2F, 0x00,
623 ];
624 let data = make_midi(0, 1, 480, &track);
625 let tmp = std::env::temp_dir().join("test_midi_pitch_bend.mid");
626 std::fs::write(&tmp, &data).unwrap();
627 let info = parse_midi(&tmp).unwrap();
628 assert_eq!(info.note_count, 0);
629 assert!(info.channels_used >= 1);
630 let _ = std::fs::remove_file(&tmp);
631 }
632
633 #[test]
634 fn test_parse_midi_format_1_two_tracks_merges_track_names() {
635 let tr1 = vec![
636 0x00, 0xFF, 0x03, 0x05, b'A', b'l', b'p', b'h', b'a', 0x00, 0xFF, 0x2F, 0x00,
637 ];
638 let tr2 = vec![
639 0x00, 0xFF, 0x03, 0x04, b'B', b'e', b't', b'a', 0x00, 0xFF, 0x2F, 0x00,
640 ];
641 let data = build_midi_with_track_bodies(1, 480, &[tr1, tr2]);
642 let tmp = std::env::temp_dir().join("test_midi_two_tracks.mid");
643 std::fs::write(&tmp, &data).unwrap();
644 let info = parse_midi(&tmp).unwrap();
645 assert_eq!(info.format, 1);
646 assert_eq!(info.track_count, 2);
647 assert!(
648 info.track_names.contains(&"Alpha".into()) && info.track_names.contains(&"Beta".into()),
649 "expected Alpha and Beta in {:?}",
650 info.track_names
651 );
652 let _ = std::fs::remove_file(&tmp);
653 }
654
655 #[test]
656 fn test_var_len() {
657 assert_eq!(read_var_len(&[0x00], 0), (0, 1));
658 assert_eq!(read_var_len(&[0x7F], 0), (127, 1));
659 assert_eq!(read_var_len(&[0x81, 0x00], 0), (128, 2));
660 assert_eq!(read_var_len(&[0x83, 0x60], 0), (480, 2));
661 }
662
663 #[test]
664 fn test_parse_midi_sysex_advances_parser_without_note_count() {
665 let track = vec![
667 0x00, 0xF0, 0x03, 0x01, 0x02, 0x03, 0x00, 0xFF, 0x2F, 0x00,
669 ];
670 let data = make_midi(0, 1, 480, &track);
671 let tmp = std::env::temp_dir().join("test_midi_sysex.mid");
672 std::fs::write(&tmp, &data).unwrap();
673 let info = parse_midi(&tmp).unwrap();
674 assert_eq!(info.note_count, 0);
675 let _ = std::fs::remove_file(&tmp);
676 }
677
678 #[test]
679 fn test_parse_midi_track_name_zero_length_not_listed() {
680 let track = vec![
681 0x00, 0xFF, 0x03, 0x00, 0x00, 0xFF, 0x2F, 0x00,
683 ];
684 let data = make_midi(0, 1, 480, &track);
685 let tmp = std::env::temp_dir().join("test_midi_empty_name.mid");
686 std::fs::write(&tmp, &data).unwrap();
687 let info = parse_midi(&tmp).unwrap();
688 assert!(
689 info.track_names.is_empty(),
690 "empty meta 0x03 must not add a track name"
691 );
692 let _ = std::fs::remove_file(&tmp);
693 }
694
695 #[test]
697 fn test_parse_midi_flow_track_name_tempo_notes_and_counts() {
698 let track = vec![
699 0x00, 0xFF, 0x03, 0x07, b'M', b'y', b'T', b'r', b'a', b'c', b'k', 0x00, 0xFF, 0x51,
700 0x03, 0x07, 0xA1, 0x20, 0x00, 0x90, 60, 100, 0x00, 0x90, 64, 100, 0x00, 0xFF, 0x2F, 0x00,
704 ];
705 let data = make_midi(0, 1, 480, &track);
706 let tmp = std::env::temp_dir().join("test_midi_flow.mid");
707 std::fs::write(&tmp, &data).unwrap();
708 let info = parse_midi(&tmp).unwrap();
709 assert_eq!(info.track_names, vec!["MyTrack"]);
710 assert!(
711 (info.tempo - 120.0).abs() < 0.5,
712 "tempo meta should yield ~120 BPM, got {}",
713 info.tempo
714 );
715 assert_eq!(info.note_count, 2);
716 let _ = std::fs::remove_file(&tmp);
717 }
718
719 #[test]
721 fn test_parse_midi_flow_track_name_tempo_time_sig_key_sig_one_note() {
722 let track = vec![
723 0x00, 0xFF, 0x03, 0x04, b'T', b'e', b's', b't', 0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1,
724 0x20, 0x00, 0xFF, 0x58, 0x04, 0x06, 0x03, 0x18, 0x08, 0x00, 0xFF, 0x59, 0x02, 0xFF, 0x01, 0x00, 0x90, 60, 100, 0x00, 0xFF, 0x2F, 0x00,
727 ];
728 let data = make_midi(0, 1, 480, &track);
729 let tmp = std::env::temp_dir().join("test_midi_flow_meta.mid");
730 std::fs::write(&tmp, &data).unwrap();
731 let info = parse_midi(&tmp).unwrap();
732 assert_eq!(info.track_names, vec!["Test"]);
733 assert!((info.tempo - 120.0).abs() < 0.5);
734 assert_eq!(info.time_signature, "6/8");
735 assert_eq!(info.key_signature, "D minor");
736 assert_eq!(info.note_count, 1);
737 let _ = std::fs::remove_file(&tmp);
738 }
739
740 #[test]
741 fn test_parse_midi_note_ons_on_two_channels_sets_channels_used() {
742 let track = vec![
743 0x00, 0x90, 60, 100, 0x00, 0x91, 64, 100, 0x00, 0xFF, 0x2F, 0x00,
746 ];
747 let data = make_midi(0, 1, 480, &track);
748 let tmp = std::env::temp_dir().join("test_midi_chans.mid");
749 std::fs::write(&tmp, &data).unwrap();
750 let info = parse_midi(&tmp).unwrap();
751 assert_eq!(info.note_count, 2);
752 assert_eq!(info.channels_used, 2);
753 let _ = std::fs::remove_file(&tmp);
754 }
755}