use anyhow::Result;
use midly::{Format, Header, MetaMessage, MidiMessage, Smf, Timing, TrackEvent, TrackEventKind};
#[derive(Debug, Clone)]
pub struct MidiNote {
pub key: u8,
pub velocity: u8,
pub start: u32,
pub duration: u32,
pub channel: u8,
}
pub fn render_midi(notes: &[MidiNote], tempo_bpm: u16) -> Result<Vec<u8>> {
let ticks_per_quarter = 480u16;
let mut events: Vec<(u32, TrackEventKind<'static>)> = Vec::new();
let tempo = 60_000_000u32 / tempo_bpm.max(1) as u32;
events.push((0, TrackEventKind::Meta(MetaMessage::Tempo(tempo.into()))));
for note in notes {
let ch = note.channel.min(15).into();
events.push((
note.start,
TrackEventKind::Midi {
channel: ch,
message: MidiMessage::NoteOn {
key: note.key.into(),
vel: note.velocity.into(),
},
},
));
events.push((
note.start + note.duration,
TrackEventKind::Midi {
channel: ch,
message: MidiMessage::NoteOff {
key: note.key.into(),
vel: 0.into(),
},
},
));
}
events.sort_by(|a, b| {
a.0.cmp(&b.0).then_with(|| {
let rank = |k: &TrackEventKind| -> (u8, u8, u8) {
match k {
TrackEventKind::Meta(_) => (0, 0, 0),
TrackEventKind::Midi {
channel,
message: MidiMessage::NoteOff { key, .. },
} => (1, (*channel).into(), (*key).into()),
TrackEventKind::Midi {
channel,
message: MidiMessage::NoteOn { key, .. },
} => (2, (*channel).into(), (*key).into()),
_ => (3, 0, 0),
}
};
rank(&a.1).cmp(&rank(&b.1))
})
});
let mut track: Vec<TrackEvent> = Vec::new();
let mut last_time = 0u32;
for (time, kind) in events {
let delta = time.saturating_sub(last_time);
last_time = time;
track.push(TrackEvent {
delta: delta.into(),
kind,
});
}
track.push(TrackEvent {
delta: 0.into(),
kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
});
let smf = Smf {
header: Header::new(
Format::SingleTrack,
Timing::Metrical(ticks_per_quarter.into()),
),
tracks: vec![track],
};
let mut out = Vec::new();
smf.write_std(&mut out)?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_midi_deterministic_overlap() {
let notes1 = vec![
MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 0,
},
MidiNote {
key: 64,
velocity: 100,
start: 0,
duration: 480,
channel: 1,
},
];
let notes2 = vec![
MidiNote {
key: 64,
velocity: 100,
start: 0,
duration: 480,
channel: 1,
},
MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 0,
},
];
let result1 = render_midi(¬es1, 120).unwrap();
let result2 = render_midi(¬es2, 120).unwrap();
assert_eq!(
result1, result2,
"Output must be deterministic regardless of input note order"
);
}
#[test]
fn render_midi_empty_notes() {
let result = render_midi(&[], 120).unwrap();
assert!(!result.is_empty());
assert_eq!(&result[..4], b"MThd");
}
#[test]
fn render_midi_single_note() {
let notes = vec![MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 0,
}];
let result = render_midi(¬es, 120).unwrap();
assert_eq!(&result[..4], b"MThd");
assert!(result.len() > 14);
}
#[test]
fn render_midi_multiple_notes() {
let notes = vec![
MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 0,
},
MidiNote {
key: 64,
velocity: 80,
start: 480,
duration: 480,
channel: 0,
},
MidiNote {
key: 67,
velocity: 60,
start: 960,
duration: 480,
channel: 1,
},
];
let result = render_midi(¬es, 120).unwrap();
assert_eq!(&result[..4], b"MThd");
}
#[test]
fn render_midi_channel_clamped_to_15() {
let notes = vec![MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 255,
}];
let result = render_midi(¬es, 120).unwrap();
assert!(!result.is_empty());
}
#[test]
fn render_midi_tempo_min_clamped() {
let notes = vec![MidiNote {
key: 60,
velocity: 100,
start: 0,
duration: 480,
channel: 0,
}];
let result = render_midi(¬es, 0).unwrap();
assert!(!result.is_empty());
}
}