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
12pub const DEFAULT_TICKS_PER_QUARTER: u16 = 480;
14
15#[derive(Clone, Copy, Debug, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[must_use]
19pub struct MidiNote {
20 pub pitch: u8,
22 pub velocity: u8,
24 pub channel: u8,
26 pub start: FloatType,
28 pub duration: FloatType,
30}
31
32impl MidiNote {
33 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 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
76pub fn midi_notes_from_stream(stream: &Stream) -> Result<Vec<MidiNote>> {
78 let mut notes = Vec::new();
79 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 StreamElement::Rest(_)
96 | StreamElement::Stream(_)
97 | StreamElement::KeySignature(_)
98 | StreamElement::TimeSignature(_)
99 | StreamElement::MetronomeMark(_) => {}
100 }
101 }
102 Ok(notes)
103}
104
105pub 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
116pub 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
167pub fn read_midi_bytes(bytes: &[u8]) -> Result<Vec<MidiNote>> {
169 read_midi_bytes_with_tempo(bytes).map(|(notes, _tempo)| notes)
170}
171
172pub 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(¬es, 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(¬es).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(¬es, 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}