Skip to main content

music21/midi/
mod.rs

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
18/// Default MIDI pulses per quarter note used by the byte import/export helpers.
19pub const DEFAULT_TICKS_PER_QUARTER: u16 = 480;
20
21/// A note event in quarter-length time.
22#[derive(Clone, Copy, Debug, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[must_use]
25pub struct MidiNote {
26    /// MIDI key number, from 0 to 127.
27    pub pitch: u8,
28    /// MIDI note-on velocity, from 0 to 127.
29    pub velocity: u8,
30    /// MIDI channel, from 0 to 15.
31    pub channel: u8,
32    /// Start offset measured in quarter lengths.
33    pub start: FloatType,
34    /// Duration measured in quarter lengths.
35    pub duration: FloatType,
36}
37
38impl MidiNote {
39    /// Creates a MIDI note event.
40    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    /// Creates a MIDI note event with an explicit channel.
45    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
82/// Extracts MIDI note events from a stream.
83pub fn midi_notes_from_stream(stream: &Stream) -> Result<Vec<MidiNote>> {
84    let mut notes = Vec::new();
85    // Flattened, so that a score's parts and measures sound at the offsets
86    // their nesting puts them at rather than not at all.
87    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            // A rest is silence, and the marks that say what is in force
100            // carry no pitch; a nested stream is gone by now.
101            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
133/// Builds a stream from MIDI note events.
134pub 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
144/// Writes a minimal format-0 Standard MIDI File.
145pub 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
195/// Reads note events from a Standard MIDI File.
196pub fn read_midi_bytes(bytes: &[u8]) -> Result<Vec<MidiNote>> {
197    read_midi_bytes_with_tempo(bytes).map(|(notes, _tempo)| notes)
198}
199
200/// Reads note events and the first tempo marking from a Standard MIDI File.
201pub 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(&notes, 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(&notes).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(&notes, 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        // A second track promised and cut short.
518        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}