1use std::collections::BTreeMap;
2
3mod score;
4mod write;
5
6pub use score::from_midi;
7pub use write::{ExportOptions, to_midi};
8
9use crate::{
10 defaults::{FloatType, IntegerType},
11 duration::Duration,
12 error::{Error, FormatErrorKind, Position, Result},
13 note::Note,
14 pitch::Pitch,
15 stream::{Stream, StreamElement},
16};
17
18pub const DEFAULT_TICKS_PER_QUARTER: u16 = 480;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[must_use]
25pub struct MidiNote {
26 pub pitch: u8,
28 pub velocity: u8,
30 pub channel: u8,
32 pub start: FloatType,
34 pub duration: FloatType,
36}
37
38impl MidiNote {
39 pub fn new(pitch: u8, start: FloatType, duration: FloatType, velocity: u8) -> Result<Self> {
41 Self::with_channel(pitch, start, duration, velocity, 0)
42 }
43
44 pub fn with_channel(
46 pitch: u8,
47 start: FloatType,
48 duration: FloatType,
49 velocity: u8,
50 channel: u8,
51 ) -> Result<Self> {
52 if pitch > 127 {
53 return Err(Error::midi(format!("MIDI pitch out of range: {pitch}")));
54 }
55 if velocity > 127 {
56 return Err(Error::midi(format!(
57 "MIDI velocity out of range: {velocity}"
58 )));
59 }
60 if channel > 15 {
61 return Err(Error::midi(format!("MIDI channel out of range: {channel}")));
62 }
63 if !start.is_finite() || start < 0.0 {
64 return Err(Error::midi(format!("invalid MIDI note start: {start}")));
65 }
66 if !duration.is_finite() || duration < 0.0 {
67 return Err(Error::midi(format!(
68 "invalid MIDI note duration: {duration}"
69 )));
70 }
71
72 Ok(Self {
73 pitch,
74 velocity,
75 channel,
76 start,
77 duration,
78 })
79 }
80}
81
82pub fn midi_notes_from_stream(stream: &Stream) -> Result<Vec<MidiNote>> {
84 let mut notes = Vec::new();
85 for event in stream.flatten().events() {
88 let start = event.offset();
89 let duration = event.element().quarter_length();
90 match event.element() {
91 StreamElement::Note(note) => {
92 notes.push(note_to_midi_note(note, start, duration)?);
93 }
94 StreamElement::Chord(chord) => {
95 for note in chord.notes() {
96 notes.push(note_to_midi_note(note, start, duration)?);
97 }
98 }
99 StreamElement::Rest(_)
102 | StreamElement::Unpitched(_)
103 | StreamElement::PercussionChord(_)
104 | StreamElement::Stream(_)
105 | StreamElement::Clef(_)
106 | StreamElement::Instrument(_)
107 | StreamElement::KeySignature(_)
108 | StreamElement::Key(_)
109 | StreamElement::TimeSignature(_)
110 | StreamElement::MetronomeMark(_)
111 | StreamElement::TempoText(_)
112 | StreamElement::TextExpression(_)
113 | StreamElement::RepeatExpression(_)
114 | StreamElement::Dynamic(_)
115 | StreamElement::ChordSymbol(_)
116 | StreamElement::Barline(_)
117 | StreamElement::PedalObject(_)
118 | StreamElement::RehearsalMark(_)
119 | StreamElement::MetricModulation(_)
120 | StreamElement::Break(_)
121 | StreamElement::Layout(_)
122 | StreamElement::TextBox(_)
123 | StreamElement::Variant(_)
124 | StreamElement::GlobalComment(_)
125 | StreamElement::MiscTandem(_)
126 | StreamElement::SpineComment(_)
127 | StreamElement::BrailleSegmentDivision(_) => {}
128 }
129 }
130 Ok(notes)
131}
132
133pub fn stream_from_midi_notes(notes: &[MidiNote]) -> Result<Stream> {
135 let mut stream = Stream::new();
136 for midi_note in notes {
137 let note = Note::from_pitch(Pitch::from_midi(midi_note.pitch as IntegerType)?)
138 .with_duration(Duration::new(midi_note.duration)?);
139 stream.insert(midi_note.start, note);
140 }
141 Ok(stream)
142}
143
144pub fn write_midi_bytes(notes: &[MidiNote], tempo_bpm: FloatType) -> Result<Vec<u8>> {
146 if !tempo_bpm.is_finite() || tempo_bpm <= 0.0 {
147 return Err(Error::midi(format!("invalid tempo: {tempo_bpm}")));
148 }
149
150 let mut events = Vec::new();
151 for note in notes {
152 validate_note(*note)?;
153 let start_tick = quarter_to_tick(note.start)?;
154 let end_tick = quarter_to_tick(note.start + note.duration)?;
155 events.push((
156 start_tick,
157 1_u8,
158 [0x90 | note.channel, note.pitch, note.velocity],
159 ));
160 events.push((end_tick, 0_u8, [0x80 | note.channel, note.pitch, 0]));
161 }
162 events.sort_by_key(|event| (event.0, event.1));
163
164 let mut track = Vec::new();
165 write_vlq(0, &mut track);
166 track.extend([0xFF, 0x51, 0x03]);
167 let micros_per_quarter = (60_000_000.0 / tempo_bpm).round() as u32;
168 track.extend([
169 ((micros_per_quarter >> 16) & 0xFF) as u8,
170 ((micros_per_quarter >> 8) & 0xFF) as u8,
171 (micros_per_quarter & 0xFF) as u8,
172 ]);
173
174 let mut last_tick = 0_u32;
175 for (tick, _, bytes) in events {
176 write_vlq(tick.saturating_sub(last_tick), &mut track);
177 track.extend(bytes);
178 last_tick = tick;
179 }
180 write_vlq(0, &mut track);
181 track.extend([0xFF, 0x2F, 0x00]);
182
183 let mut out = Vec::new();
184 out.extend(b"MThd");
185 out.extend(6_u32.to_be_bytes());
186 out.extend(0_u16.to_be_bytes());
187 out.extend(1_u16.to_be_bytes());
188 out.extend(DEFAULT_TICKS_PER_QUARTER.to_be_bytes());
189 out.extend(b"MTrk");
190 out.extend((track.len() as u32).to_be_bytes());
191 out.extend(track);
192 Ok(out)
193}
194
195pub fn read_midi_bytes(bytes: &[u8]) -> Result<Vec<MidiNote>> {
197 read_midi_bytes_with_tempo(bytes).map(|(notes, _tempo)| notes)
198}
199
200pub fn read_midi_bytes_with_tempo(bytes: &[u8]) -> Result<(Vec<MidiNote>, Option<FloatType>)> {
202 let mut pos = 0;
203 expect(bytes, &mut pos, b"MThd")?;
204 let header_len = read_u32(bytes, &mut pos)?;
205 if header_len < 6 {
206 return Err(Error::midi("MIDI header is too short".to_string()));
207 }
208 let _format = read_u16(bytes, &mut pos)?;
209 let tracks = read_u16(bytes, &mut pos)?;
210 let division = read_u16(bytes, &mut pos)?;
211 pos += (header_len - 6) as usize;
212
213 if division & 0x8000 != 0 {
214 return Err(Error::midi("SMPTE MIDI time division is not supported")
215 .of_kind(FormatErrorKind::Unsupported));
216 }
217
218 let mut all_notes = Vec::new();
219 let mut first_tempo = None;
220 for index in 0..usize::from(tracks) {
221 let here = |error: Error| error.placed(|| Position::track(index));
222 expect(bytes, &mut pos, b"MTrk").map_err(here)?;
223 let len = read_u32(bytes, &mut pos).map_err(here)? as usize;
224 let end = pos
225 .checked_add(len)
226 .ok_or_else(|| here(Error::midi("MIDI track length overflow")))?;
227 if end > bytes.len() {
228 return Err(here(
229 Error::midi("MIDI track exceeds file length").of_kind(FormatErrorKind::Malformed),
230 ));
231 }
232 let (mut notes, tempo) = read_track(&bytes[pos..end], division).map_err(here)?;
233 if first_tempo.is_none() {
234 first_tempo = tempo;
235 }
236 all_notes.append(&mut notes);
237 pos = end;
238 }
239 all_notes.sort_by(|left, right| {
240 left.start
241 .partial_cmp(&right.start)
242 .unwrap_or(std::cmp::Ordering::Equal)
243 });
244 Ok((all_notes, first_tempo))
245}
246
247fn note_to_midi_note(note: &Note, start: FloatType, duration: FloatType) -> Result<MidiNote> {
248 let pitch = note.pitch().ps().round() as IntegerType;
249 if !(0..=127).contains(&pitch) {
250 return Err(Error::midi(format!("pitch {pitch} is outside MIDI range")));
251 }
252 MidiNote::new(pitch as u8, start, duration, 64)
253}
254
255fn validate_note(note: MidiNote) -> Result<()> {
256 MidiNote::with_channel(
257 note.pitch,
258 note.start,
259 note.duration,
260 note.velocity,
261 note.channel,
262 )
263 .map(|_| ())
264}
265
266fn quarter_to_tick(value: FloatType) -> Result<u32> {
267 if !value.is_finite() || value < 0.0 {
268 return Err(Error::midi(format!("invalid quarter offset: {value}")));
269 }
270 Ok((value * DEFAULT_TICKS_PER_QUARTER as FloatType).round() as u32)
271}
272
273fn tick_to_quarter(value: u32, division: u16) -> FloatType {
274 value as FloatType / division as FloatType
275}
276
277fn write_vlq(mut value: u32, out: &mut Vec<u8>) {
278 let mut buffer = [0_u8; 5];
279 let mut idx = buffer.len() - 1;
280 buffer[idx] = (value & 0x7F) as u8;
281 value >>= 7;
282 while value > 0 {
283 idx -= 1;
284 buffer[idx] = ((value & 0x7F) as u8) | 0x80;
285 value >>= 7;
286 }
287 out.extend(&buffer[idx..]);
288}
289
290fn read_vlq(bytes: &[u8], pos: &mut usize) -> Result<u32> {
291 let mut value = 0_u32;
292 for _ in 0..4 {
293 let byte = *bytes
294 .get(*pos)
295 .ok_or_else(|| Error::midi("unexpected end of VLQ".to_string()))?;
296 *pos += 1;
297 value = (value << 7) | (byte & 0x7F) as u32;
298 if byte & 0x80 == 0 {
299 return Ok(value);
300 }
301 }
302 Err(Error::midi("VLQ is too long".to_string()))
303}
304
305fn read_track(track: &[u8], division: u16) -> Result<(Vec<MidiNote>, Option<FloatType>)> {
306 let mut pos = 0;
307 let mut tick = 0_u32;
308 let mut running_status = None;
309 let mut active: BTreeMap<(u8, u8), Vec<(u32, u8)>> = BTreeMap::new();
310 let mut notes = Vec::new();
311 let mut tempo = None;
312
313 while pos < track.len() {
314 tick = tick.saturating_add(read_vlq(track, &mut pos)?);
315 let byte = *track
316 .get(pos)
317 .ok_or_else(|| Error::midi("unexpected end of MIDI event".to_string()))?;
318 let status = if byte & 0x80 != 0 {
319 pos += 1;
320 running_status = Some(byte);
321 byte
322 } else {
323 running_status
324 .ok_or_else(|| Error::midi("running status without status byte".to_string()))?
325 };
326
327 match status {
328 0xFF => {
329 let meta_type = read_byte(track, &mut pos)?;
330 let len = read_vlq(track, &mut pos)? as usize;
331 if pos + len > track.len() {
332 return Err(Error::midi("meta event exceeds track length".to_string()));
333 }
334 if meta_type == 0x51 && len == 3 {
335 let micros = ((track[pos] as u32) << 16)
336 | ((track[pos + 1] as u32) << 8)
337 | track[pos + 2] as u32;
338 tempo = Some(60_000_000.0 / micros as FloatType);
339 } else if meta_type == 0x2F {
340 break;
341 }
342 pos += len;
343 }
344 0xF0 | 0xF7 => {
345 let len = read_vlq(track, &mut pos)? as usize;
346 pos = pos
347 .checked_add(len)
348 .ok_or_else(|| Error::midi("sysex length overflow".to_string()))?;
349 if pos > track.len() {
350 return Err(Error::midi("sysex event exceeds track length".to_string()));
351 }
352 }
353 _ => {
354 let event_type = status & 0xF0;
355 let channel = status & 0x0F;
356 let data_len = match event_type {
357 0xC0 | 0xD0 => 1,
358 0x80 | 0x90 | 0xA0 | 0xB0 | 0xE0 => 2,
359 _ => {
360 return Err(Error::midi(format!("unsupported MIDI status {status:#X}"))
361 .of_kind(FormatErrorKind::Unsupported));
362 }
363 };
364 let data1 = read_byte(track, &mut pos)?;
365 let data2 = if data_len == 2 {
366 read_byte(track, &mut pos)?
367 } else {
368 0
369 };
370
371 if event_type == 0x90 && data2 > 0 {
372 active
373 .entry((channel, data1))
374 .or_default()
375 .push((tick, data2));
376 } else if (event_type == 0x80 || event_type == 0x90)
377 && let Some(stack) = active.get_mut(&(channel, data1))
378 && let Some((start_tick, velocity)) = stack.pop()
379 {
380 notes.push(MidiNote::with_channel(
381 data1,
382 tick_to_quarter(start_tick, division),
383 tick_to_quarter(tick.saturating_sub(start_tick), division),
384 velocity,
385 channel,
386 )?);
387 }
388 }
389 }
390 }
391
392 Ok((notes, tempo))
393}
394
395fn expect(bytes: &[u8], pos: &mut usize, expected: &[u8]) -> Result<()> {
396 if bytes.get(*pos..(*pos).saturating_add(expected.len())) == Some(expected) {
397 *pos += expected.len();
398 Ok(())
399 } else {
400 Err(Error::midi(format!(
401 "expected MIDI chunk {:?}",
402 String::from_utf8_lossy(expected)
403 )))
404 }
405}
406
407fn read_byte(bytes: &[u8], pos: &mut usize) -> Result<u8> {
408 let byte = *bytes
409 .get(*pos)
410 .ok_or_else(|| Error::midi("unexpected end of MIDI data".to_string()))?;
411 *pos += 1;
412 Ok(byte)
413}
414
415fn read_u16(bytes: &[u8], pos: &mut usize) -> Result<u16> {
416 let start = *pos;
417 *pos += 2;
418 let data = bytes
419 .get(start..*pos)
420 .ok_or_else(|| Error::midi("unexpected end of MIDI u16".to_string()))?;
421 Ok(u16::from_be_bytes([data[0], data[1]]))
422}
423
424fn read_u32(bytes: &[u8], pos: &mut usize) -> Result<u32> {
425 let start = *pos;
426 *pos += 4;
427 let data = bytes
428 .get(start..*pos)
429 .ok_or_else(|| Error::midi("unexpected end of MIDI u32".to_string()))?;
430 Ok(u32::from_be_bytes([data[0], data[1], data[2], data[3]]))
431}
432
433#[cfg(test)]
434mod tests {
435 #[test]
436 fn a_stream_round_trips_through_midi_bytes() {
437 use super::{
438 MidiNote, midi_notes_from_stream, read_midi_bytes, read_midi_bytes_with_tempo,
439 stream_from_midi_notes, write_midi_bytes,
440 };
441
442 let notes = vec![
443 MidiNote::new(60, 0.0, 1.0, 90).unwrap(),
444 MidiNote::new(64, 1.0, 0.5, 100).unwrap(),
445 ];
446 let bytes = write_midi_bytes(¬es, 120.0).unwrap();
447 let (read, tempo) = read_midi_bytes_with_tempo(&bytes).unwrap();
448 assert_eq!(tempo.map(|bpm| bpm.round()), Some(120.0));
449 assert_eq!(read.len(), 2);
450 assert_eq!(read[0].pitch, 60);
451 assert_eq!(read[1].start, 1.0);
452 assert_eq!(read_midi_bytes(&bytes).unwrap().len(), 2);
453
454 let stream = stream_from_midi_notes(¬es).unwrap();
455 assert_eq!(midi_notes_from_stream(&stream).unwrap().len(), 2);
456 assert!(read_midi_bytes(b"not a midi file").is_err());
457 assert!(read_midi_bytes(&bytes[..10]).is_err());
458 }
459
460 use super::*;
461
462 #[test]
463 fn midi_roundtrip_bytes() {
464 let notes = vec![MidiNote::new(60, 0.0, 1.0, 90).unwrap()];
465 let bytes = write_midi_bytes(¬es, 120.0).unwrap();
466 assert!(bytes.starts_with(b"MThd"));
467 let (roundtrip, tempo) = read_midi_bytes_with_tempo(&bytes).unwrap();
468 assert_eq!(tempo, Some(120.0));
469 assert_eq!(roundtrip, notes);
470 }
471
472 #[test]
473 fn stream_converts_to_midi_notes() {
474 let mut stream = Stream::new();
475 stream.push(
476 Note::from_name("C4")
477 .unwrap()
478 .with_duration(Duration::half()),
479 );
480 let notes = midi_notes_from_stream(&stream).unwrap();
481 assert_eq!(notes[0].pitch, 60);
482 assert_eq!(notes[0].duration, 2.0);
483 }
484
485 #[test]
486 fn midi_note_validation_rejects_invalid_values() {
487 assert!(MidiNote::with_channel(128, 0.0, 1.0, 64, 0).is_err());
488 assert!(MidiNote::with_channel(60, 0.0, 1.0, 128, 0).is_err());
489 assert!(MidiNote::with_channel(60, 0.0, 1.0, 64, 16).is_err());
490 assert!(MidiNote::with_channel(60, -0.25, 1.0, 64, 0).is_err());
491 assert!(MidiNote::with_channel(60, 0.0, FloatType::INFINITY, 64, 0).is_err());
492 assert!(write_midi_bytes(&[], 0.0).is_err());
493 }
494
495 #[test]
496 fn midi_reader_rejects_invalid_files() {
497 assert!(read_midi_bytes(b"not midi").is_err());
498
499 let mut short_header = Vec::new();
500 short_header.extend(b"MThd");
501 short_header.extend(4_u32.to_be_bytes());
502 short_header.extend([0, 0, 0, 1]);
503 assert!(read_midi_bytes(&short_header).is_err());
504
505 let mut smpte = Vec::new();
506 smpte.extend(b"MThd");
507 smpte.extend(6_u32.to_be_bytes());
508 smpte.extend(0_u16.to_be_bytes());
509 smpte.extend(0_u16.to_be_bytes());
510 smpte.extend(0x8000_u16.to_be_bytes());
511 let refused = read_midi_bytes(&smpte).unwrap_err();
512 assert_eq!(
513 refused.format_error().unwrap().kind,
514 FormatErrorKind::Unsupported
515 );
516
517 let mut cut = Vec::new();
519 cut.extend(b"MThd");
520 cut.extend(6_u32.to_be_bytes());
521 cut.extend([0, 1, 0, 2, 0, 96]);
522 cut.extend(b"MTrk");
523 cut.extend(4_u32.to_be_bytes());
524 cut.extend([0, 0xFF, 0x2F, 0]);
525 cut.extend(b"MTrk");
526 cut.extend(9_u32.to_be_bytes());
527 let refused = read_midi_bytes(&cut).unwrap_err();
528 assert_eq!(
529 refused.to_string(),
530 "Midi error: MIDI track exceeds file length"
531 );
532 assert_eq!(refused.position(), Some(&Position::track(1)));
533 }
534}