use midly::{
Format, Header, MetaMessage, MidiMessage, Smf, Timing, TrackEvent, TrackEventKind,
num::{u4, u7, u15, u24, u28},
};
use acorde_core::{Score, TupletInfo};
use crate::Error;
const PPQ: u32 = 480;
pub fn serialize_midi(score: &Score) -> Result<Vec<u8>, Error> {
let seq = acorde_core::measure_sequence(score);
serialize_midi_impl(score, &seq)
}
pub fn serialize_midi_region(score: &Score, region: (usize, usize)) -> Result<Vec<u8>, Error> {
let (start, end) = region;
if start > end {
return Err(Error::Midi(format!("invalid region: start={start} > end={end}")));
}
let full_seq = acorde_core::measure_sequence(score);
let seq: Vec<usize> = full_seq.into_iter().filter(|&m| m >= start && m <= end).collect();
if seq.is_empty() {
return Err(Error::Midi(format!("region [{start}, {end}] contains no measures")));
}
serialize_midi_impl(score, &seq)
}
fn serialize_midi_impl(score: &Score, seq: &[usize]) -> Result<Vec<u8>, Error> {
let header = Header::new(
Format::Parallel,
Timing::Metrical(u15::from(PPQ as u16)),
);
let mut smf = Smf::new(header);
smf.tracks.push(build_meta_track(score, seq));
for part in &score.parts {
smf.tracks.push(build_part_track(part, seq));
}
let mut bytes = Vec::new();
smf.write_std(&mut bytes).map_err(|e| Error::Midi(e.to_string()))?;
Ok(bytes)
}
#[inline]
fn clamp_delta(delta: u64) -> u28 {
u28::from(delta.min(0x0FFF_FFFF) as u32)
}
fn build_meta_track<'a>(score: &Score, seq: &[usize]) -> Vec<TrackEvent<'a>> {
let ts = &score.settings.time_signature;
let den_log2 = (ts.denominator as u32).trailing_zeros() as u8;
let beats_per_measure = ts.numerator as f64 * 4.0 / ts.denominator as f64;
let ticks_per_measure = (beats_per_measure * PPQ as f64) as u64;
let first_staff_opt = score.parts.first().and_then(|p| p.staves.first());
let initial_bpm = first_staff_opt
.and_then(|s| seq.first().and_then(|&i| s.measures.get(i)).and_then(|m| m.tempo))
.map(|b| b.max(1) as u32)
.unwrap_or_else(|| score.settings.tempo_bpm.max(1) as u32);
let us_per_beat = 60_000_000u32 / initial_bpm;
let mut events: Vec<TrackEvent<'static>> = vec![
TrackEvent {
delta: u28::from(0u32),
kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::from(us_per_beat))),
},
TrackEvent {
delta: u28::from(0u32),
kind: TrackEventKind::Meta(MetaMessage::TimeSignature(
ts.numerator,
den_log2,
24,
8,
)),
},
];
if let Some(first_staff) = first_staff_opt {
let mut cursor_tick: u64 = 0;
let mut prev_event_tick: u64 = 0;
for &idx in seq {
if cursor_tick > 0
&& let Some(bpm) = first_staff.measures.get(idx).and_then(|m| m.tempo) {
let us = 60_000_000u32 / bpm.max(1) as u32;
events.push(TrackEvent {
delta: clamp_delta(cursor_tick - prev_event_tick),
kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::from(us))),
});
prev_event_tick = cursor_tick;
}
cursor_tick += ticks_per_measure;
}
}
events.push(TrackEvent {
delta: u28::from(0u32),
kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
});
events
}
struct TimedEvent {
abs_tick: u64,
sort_key: u8,
channel: u4,
message: MidiMessage,
}
fn note_ticks(duration: &acorde_core::Duration, dot_count: u8, tuplet: Option<&TupletInfo>) -> u64 {
let base = duration.to_ticks(dot_count) as u64;
match tuplet {
Some(t) if t.actual_notes > 0 => {
(base * t.normal_notes as u64 / t.actual_notes as u64).max(1)
}
_ => base,
}
}
fn build_part_track(part: &acorde_core::Part, seq: &[usize]) -> Vec<TrackEvent<'static>> {
let channel = u4::from(part.midi_channel.min(15));
let mut events: Vec<TimedEvent> = Vec::new();
events.push(TimedEvent {
abs_tick: 0,
sort_key: 0,
channel,
message: MidiMessage::ProgramChange {
program: u7::from(part.midi_program),
},
});
for staff in &part.staves {
for voice_idx in 0..4usize {
let mut cursor: u64 = 0;
for &idx in seq {
let measure = match staff.measures.get(idx) {
Some(m) => m,
None => continue,
};
let transpose = if part.midi_channel == 9 { 0i8 } else { staff.transpose_semitones };
for note in &measure.voices[voice_idx] {
let ticks = note_ticks(¬e.duration, note.dot_count, note.tuplet.as_ref());
if !note.is_rest && !note.is_grace {
let vel = note.dynamic
.as_ref()
.map(|d| d.to_velocity())
.unwrap_or(64u8);
for pitch in ¬e.pitches {
let midi = (pitch.to_midi() + transpose as i16)
.clamp(0, 127) as u8;
events.push(TimedEvent {
abs_tick: cursor,
sort_key: 1,
channel,
message: MidiMessage::NoteOn {
key: u7::from(midi),
vel: u7::from(vel),
},
});
events.push(TimedEvent {
abs_tick: cursor + ticks,
sort_key: 0,
channel,
message: MidiMessage::NoteOff {
key: u7::from(midi),
vel: u7::from(0u8),
},
});
}
}
if !note.is_grace {
cursor += ticks;
}
}
}
}
}
events.sort_by_key(|e| (e.abs_tick, e.sort_key));
let mut track: Vec<TrackEvent<'static>> = Vec::with_capacity(events.len() + 1);
let mut prev: u64 = 0;
for ev in events {
track.push(TrackEvent {
delta: clamp_delta(ev.abs_tick - prev),
kind: TrackEventKind::Midi { channel: ev.channel, message: ev.message },
});
prev = ev.abs_tick;
}
track.push(TrackEvent {
delta: u28::from(0u32),
kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
});
track
}
#[cfg(test)]
mod tests {
use super::*;
use acorde_core::{Duration, Note, Pitch, Score, Step};
#[test]
fn header_magic_bytes() {
let score = Score::default();
let bytes = serialize_midi(&score).unwrap();
assert_eq!(&bytes[0..4], b"MThd");
}
#[test]
fn single_note_roundtrip() {
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let bytes = serialize_midi(&score).unwrap();
let score2 = acorde_io_parse_midi(&bytes);
let notes: Vec<_> = score2.parts[0].staves[0].measures[0].voices[0]
.iter().filter(|n| !n.is_rest).collect();
assert!(!notes.is_empty());
assert_eq!(notes[0].pitches[0].step, Step::C);
}
fn acorde_io_parse_midi(data: &[u8]) -> Score {
super::super::parse_midi(data).expect("parse_midi failed")
}
#[test]
fn chord_produces_bytes() {
let mut score = Score::new("T", 120, 4, 4, 0, 1);
let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
note.pitches.push(Pitch::new(Step::E, 4));
score.parts[0].staves[0].measures[0].voices[0] = vec![note];
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.len() > 20);
}
#[test]
fn tempo_120_bpm_encoded() {
let score = Score::new("T", 120, 4, 4, 0, 1);
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(3).any(|w| w == [0x07, 0xA1, 0x20]));
}
#[test]
fn empty_parts_produce_valid_midi() {
let score = Score::new("T", 120, 4, 4, 0, 0);
let bytes = serialize_midi(&score).unwrap();
assert_eq!(&bytes[0..4], b"MThd");
}
#[test]
fn program_change_present_in_track() {
let score = Score::default();
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(2).any(|w| w[0] == 0xC0 && w[1] == 0));
}
#[test]
fn non_zero_program_encoded() {
let mut score = Score::default();
score.parts[0].midi_program = 40;
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(2).any(|w| w[0] == 0xC0 && w[1] == 40));
}
#[test]
fn transpose_semitones_shifts_midi_note() {
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].staves[0].transpose_semitones = -2;
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(2).any(|w| w[0] == 0x90 && w[1] == 58));
}
#[test]
fn percussion_channel_9_ignores_transpose_semitones() {
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].midi_channel = 9;
score.parts[0].staves[0].transpose_semitones = -2;
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(2).any(|w| w[0] == 0x99 && w[1] == 60));
assert!(!bytes.windows(2).any(|w| w[0] == 0x99 && w[1] == 58));
}
#[test]
fn midi_serialize_includes_tempo_change() {
let mut score = Score::new("T", 120, 4, 4, 0, 2);
score.parts[0].staves[0].measures[1].tempo = Some(60);
let bytes = serialize_midi(&score).unwrap();
assert!(bytes.windows(3).any(|w| w == [0x0F, 0x42, 0x40]));
}
#[test]
fn midi_import_preserves_program_change() {
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].midi_program = 40;
score.parts[0].midi_channel = 2;
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let bytes = serialize_midi(&score).unwrap();
let score2 = acorde_io_parse_midi(&bytes);
assert_eq!(score2.parts[0].midi_program, 40);
assert_eq!(score2.parts[0].midi_channel, 2);
}
#[test]
fn serialize_midi_region_all_measures_matches_full() {
let mut score = Score::new("T", 120, 4, 4, 0, 3);
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let full = serialize_midi(&score).unwrap();
let region = serialize_midi_region(&score, (0, 2)).unwrap();
assert_eq!(full, region);
}
#[test]
fn serialize_midi_region_single_measure_excludes_other() {
let mut score = Score::new("T", 120, 4, 4, 0, 2);
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
score.parts[0].staves[0].measures[1].voices[0] =
vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
let bytes = serialize_midi_region(&score, (0, 0)).unwrap();
assert!(bytes.windows(2).any(|w| w[0] == 0x90 && w[1] == 60), "C4 should be present");
assert!(!bytes.windows(2).any(|w| w[0] == 0x90 && w[1] == 64), "E4 should be absent");
}
#[test]
fn serialize_midi_region_invalid_range_returns_err() {
let score = Score::new("T", 120, 4, 4, 0, 2);
let result = serialize_midi_region(&score, (2, 0));
assert!(result.is_err(), "start > end should return Err");
}
#[test]
fn serialize_midi_region_out_of_bounds_returns_err() {
let score = Score::default();
let result = serialize_midi_region(&score, (99, 99));
assert!(result.is_err(), "non-existent region should return Err");
}
}