Skip to main content

music21_rs/
midi.rs

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