use std::time::Duration;
use midly::{Format, MetaMessage, MidiMessage, TrackEvent, TrackEventKind};
#[derive(Clone)]
pub(crate) struct TimedMidiEvent {
pub time: Duration,
pub channel: u8,
pub message: MidiMessage,
}
pub(crate) struct PrecomputedMidi {
events: Vec<TimedMidiEvent>,
}
pub(crate) struct TempoEntry {
pub(crate) tick: u64,
pub(crate) micros_per_tick: f64,
}
struct TrackResult {
events: Vec<TimedMidiEvent>,
total_duration: Duration,
}
impl PrecomputedMidi {
pub fn from_tracks(
tracks: &[Vec<TrackEvent<'_>>],
ticks_per_beat: u16,
format: Format,
) -> Self {
let tpb = ticks_per_beat as f64;
let default_micros_per_tick = 500_000.0 / tpb;
match format {
Format::SingleTrack => {
let events = if let Some(track) = tracks.first() {
Self::process_track(track, default_micros_per_tick, tpb, &[]).events
} else {
Vec::new()
};
PrecomputedMidi { events }
}
Format::Parallel => {
let tempo_map = tracks
.first()
.map(|track| Self::extract_tempo_map(track, tpb))
.unwrap_or_default();
let mut all_events = Vec::new();
for track in tracks.iter().skip(1) {
let mut track_events =
Self::process_track(track, default_micros_per_tick, tpb, &tempo_map).events;
all_events.append(&mut track_events);
}
all_events.sort_by(|a, b| a.time.cmp(&b.time));
PrecomputedMidi { events: all_events }
}
Format::Sequential => {
let mut all_events = Vec::new();
let mut cumulative_offset = Duration::ZERO;
for track in tracks {
let result = Self::process_track(track, default_micros_per_tick, tpb, &[]);
for event in result.events {
all_events.push(TimedMidiEvent {
time: event.time + cumulative_offset,
channel: event.channel,
message: event.message,
});
}
cumulative_offset += result.total_duration;
}
PrecomputedMidi { events: all_events }
}
}
}
pub(crate) fn extract_tempo_map(track: &[TrackEvent<'_>], tpb: f64) -> Vec<TempoEntry> {
let mut tempo_map = Vec::new();
let mut tick_position: u64 = 0;
for event in track {
tick_position += event.delta.as_int() as u64;
if let TrackEventKind::Meta(MetaMessage::Tempo(tempo)) = event.kind {
tempo_map.push(TempoEntry {
tick: tick_position,
micros_per_tick: tempo.as_int() as f64 / tpb,
});
}
}
tempo_map
}
fn process_track(
track: &[TrackEvent<'_>],
default_micros_per_tick: f64,
tpb: f64,
external_tempo_map: &[TempoEntry],
) -> TrackResult {
let mut events = Vec::new();
let mut tick_position: u64 = 0;
let mut elapsed_micros: f64 = 0.0;
let mut last_tick: u64 = 0;
let mut micros_per_tick = default_micros_per_tick;
let mut tempo_idx: usize = 0;
for event in track {
let delta = event.delta.as_int() as u64;
tick_position += delta;
if !external_tempo_map.is_empty() {
let mut remaining_ticks = tick_position - last_tick;
let mut cursor = last_tick;
while tempo_idx < external_tempo_map.len()
&& external_tempo_map[tempo_idx].tick <= tick_position
{
let entry = &external_tempo_map[tempo_idx];
if entry.tick > cursor {
let ticks_at_old_tempo = entry.tick - cursor;
elapsed_micros += ticks_at_old_tempo as f64 * micros_per_tick;
remaining_ticks -= ticks_at_old_tempo;
cursor = entry.tick;
}
micros_per_tick = entry.micros_per_tick;
tempo_idx += 1;
}
elapsed_micros += remaining_ticks as f64 * micros_per_tick;
} else {
let ticks_since_last = tick_position - last_tick;
elapsed_micros += ticks_since_last as f64 * micros_per_tick;
if let TrackEventKind::Meta(MetaMessage::Tempo(tempo)) = event.kind {
micros_per_tick = tempo.as_int() as f64 / tpb;
}
}
last_tick = tick_position;
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push(TimedMidiEvent {
time: Duration::from_micros(elapsed_micros.round() as u64),
channel: channel.as_int(),
message,
});
}
}
TrackResult {
events,
total_duration: Duration::from_micros(elapsed_micros.round() as u64),
}
}
pub(crate) fn build_tempo_info(
tracks: &[Vec<TrackEvent<'_>>],
ticks_per_beat: u16,
format: Format,
) -> (Vec<TempoEntry>, u16, u64) {
let tpb = ticks_per_beat as f64;
match format {
Format::SingleTrack => {
let (tempo_map, total_ticks) = if let Some(track) = tracks.first() {
let map = Self::extract_tempo_map(track, tpb);
let total = track.iter().map(|e| e.delta.as_int() as u64).sum::<u64>();
(map, total)
} else {
(Vec::new(), 0)
};
(tempo_map, ticks_per_beat, total_ticks)
}
Format::Parallel => {
let tempo_map = tracks
.first()
.map(|track| Self::extract_tempo_map(track, tpb))
.unwrap_or_default();
let total_ticks = tracks
.iter()
.map(|track| track.iter().map(|e| e.delta.as_int() as u64).sum::<u64>())
.max()
.unwrap_or(0);
(tempo_map, ticks_per_beat, total_ticks)
}
Format::Sequential => {
let mut combined_map = Vec::new();
let mut total_ticks: u64 = 0;
for track in tracks {
let map = Self::extract_tempo_map(track, tpb);
for entry in map {
combined_map.push(TempoEntry {
tick: entry.tick + total_ticks,
micros_per_tick: entry.micros_per_tick,
});
}
total_ticks += track.iter().map(|e| e.delta.as_int() as u64).sum::<u64>();
}
(combined_map, ticks_per_beat, total_ticks)
}
}
}
pub fn from_events(events: Vec<TimedMidiEvent>) -> Self {
PrecomputedMidi { events }
}
pub fn into_events(self) -> Vec<TimedMidiEvent> {
self.events
}
pub fn events_from(&self, start_time: Duration) -> &[TimedMidiEvent] {
let idx = self.events.partition_point(|e| e.time < start_time);
&self.events[idx..]
}
pub fn events(&self) -> &[TimedMidiEvent] {
&self.events
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn len(&self) -> usize {
self.events.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use midly::{num::u7, MetaMessage, MidiMessage, TrackEvent, TrackEventKind};
fn note_on(delta: u32, channel: u8, key: u8, vel: u8) -> TrackEvent<'static> {
TrackEvent {
delta: delta.into(),
kind: TrackEventKind::Midi {
channel: channel.into(),
message: MidiMessage::NoteOn {
key: u7::new(key),
vel: u7::new(vel),
},
},
}
}
fn tempo_event(delta: u32, micros_per_beat: u32) -> TrackEvent<'static> {
TrackEvent {
delta: delta.into(),
kind: TrackEventKind::Meta(MetaMessage::Tempo(micros_per_beat.into())),
}
}
fn end_of_track(delta: u32) -> TrackEvent<'static> {
TrackEvent {
delta: delta.into(),
kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
}
}
#[test]
fn test_empty_tracks() {
let midi = PrecomputedMidi::from_tracks(&[], 480, Format::Parallel);
assert!(midi.is_empty());
assert_eq!(midi.len(), 0);
}
#[test]
fn test_single_track_basic() {
let tpb = 480;
let track = vec![
note_on(0, 0, 60, 100), note_on(480, 0, 62, 100), note_on(480, 0, 64, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::SingleTrack);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(500_000));
assert_eq!(midi.events()[2].time, Duration::from_micros(1_000_000));
}
#[test]
fn test_parallel_tracks() {
let tpb = 480;
let track0 = vec![end_of_track(0)];
let track1 = vec![
note_on(0, 0, 60, 100),
note_on(960, 0, 64, 100), end_of_track(0),
];
let track2 = vec![
note_on(480, 1, 62, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track0, track1, track2], tpb, Format::Parallel);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(500_000));
assert_eq!(midi.events()[2].time, Duration::from_micros(1_000_000));
assert_eq!(midi.events()[0].channel, 0); assert_eq!(midi.events()[1].channel, 1); assert_eq!(midi.events()[2].channel, 0); }
#[test]
fn test_tempo_change() {
let tpb = 480;
let track = vec![
note_on(0, 0, 60, 100), tempo_event(480, 1_000_000), note_on(0, 0, 62, 100), note_on(480, 0, 64, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::SingleTrack);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(500_000));
assert_eq!(midi.events()[2].time, Duration::from_micros(1_500_000));
}
#[test]
fn test_tempo_change_parallel_conductor() {
let tpb = 480;
let track0 = vec![
tempo_event(480, 1_000_000), end_of_track(0),
];
let track1 = vec![
note_on(0, 0, 60, 100), note_on(480, 0, 62, 100), note_on(480, 0, 64, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track0, track1], tpb, Format::Parallel);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(500_000));
assert_eq!(midi.events()[2].time, Duration::from_micros(1_500_000));
}
#[test]
fn test_seek() {
let tpb = 480;
let track = vec![
note_on(0, 0, 60, 100), note_on(480, 0, 62, 100), note_on(480, 0, 64, 100), note_on(480, 0, 65, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::SingleTrack);
let from_zero = midi.events_from(Duration::ZERO);
assert_eq!(from_zero.len(), 4);
let from_half = midi.events_from(Duration::from_millis(500));
assert_eq!(from_half.len(), 3);
assert_eq!(from_half[0].time, Duration::from_micros(500_000));
let from_750 = midi.events_from(Duration::from_millis(750));
assert_eq!(from_750.len(), 2);
assert_eq!(from_750[0].time, Duration::from_micros(1_000_000));
let from_end = midi.events_from(Duration::from_secs(10));
assert_eq!(from_end.len(), 0);
}
#[test]
fn test_channel_preserved() {
let tpb = 480;
let track = vec![
note_on(0, 3, 60, 100),
note_on(0, 7, 62, 100),
note_on(0, 15, 64, 100),
end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::SingleTrack);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].channel, 3);
assert_eq!(midi.events()[1].channel, 7);
assert_eq!(midi.events()[2].channel, 15);
}
#[test]
fn test_sequential_tracks_with_trailing_silence() {
let tpb = 480;
let track1 = vec![
note_on(0, 0, 60, 100), note_on(480, 0, 62, 100), end_of_track(480), ];
let track2 = vec![
note_on(0, 1, 72, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track1, track2], tpb, Format::Sequential);
assert_eq!(midi.len(), 3);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(500_000));
assert_eq!(midi.events()[2].time, Duration::from_micros(1_000_000));
assert_eq!(midi.events()[2].channel, 1);
}
#[test]
fn test_from_events_and_into_events() {
let events = vec![
TimedMidiEvent {
time: Duration::from_millis(0),
channel: 0,
message: MidiMessage::NoteOn {
key: u7::new(60),
vel: u7::new(100),
},
},
TimedMidiEvent {
time: Duration::from_millis(500),
channel: 1,
message: MidiMessage::NoteOff {
key: u7::new(60),
vel: u7::new(0),
},
},
];
let midi = PrecomputedMidi::from_events(events);
assert_eq!(midi.len(), 2);
assert!(!midi.is_empty());
assert_eq!(midi.events()[0].channel, 0);
assert_eq!(midi.events()[1].channel, 1);
let recovered = midi.into_events();
assert_eq!(recovered.len(), 2);
assert_eq!(recovered[0].time, Duration::from_millis(0));
assert_eq!(recovered[1].time, Duration::from_millis(500));
}
#[test]
fn test_events_from_boundary() {
let tpb = 480;
let track = vec![
note_on(0, 0, 60, 100), note_on(480, 0, 62, 100), note_on(480, 0, 64, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::SingleTrack);
let from_exact = midi.events_from(Duration::from_micros(500_000));
assert_eq!(from_exact.len(), 2);
assert_eq!(from_exact[0].time, Duration::from_micros(500_000));
}
#[test]
fn test_sequential_with_inline_tempo() {
let tpb = 480;
let track = vec![
note_on(0, 0, 60, 100), tempo_event(480, 1_000_000), note_on(480, 0, 62, 100), end_of_track(0),
];
let midi = PrecomputedMidi::from_tracks(&[track], tpb, Format::Sequential);
assert_eq!(midi.len(), 2);
assert_eq!(midi.events()[0].time, Duration::from_micros(0));
assert_eq!(midi.events()[1].time, Duration::from_micros(1_500_000));
}
#[test]
fn test_parallel_single_track_no_conductor() {
let tpb = 480;
let track0 = vec![note_on(0, 0, 60, 100), end_of_track(0)];
let midi = PrecomputedMidi::from_tracks(&[track0], tpb, Format::Parallel);
assert_eq!(midi.len(), 0);
}
#[test]
fn test_parallel_empty_tracks() {
let midi = PrecomputedMidi::from_tracks(&[], 480, Format::Parallel);
assert!(midi.is_empty());
}
#[test]
fn test_single_track_empty() {
let midi = PrecomputedMidi::from_tracks(&[], 480, Format::SingleTrack);
assert!(midi.is_empty());
}
#[test]
fn test_sequential_empty() {
let midi = PrecomputedMidi::from_tracks(&[], 480, Format::Sequential);
assert!(midi.is_empty());
}
#[test]
fn test_build_tempo_info_no_tempo_events() {
let tpb = 480;
let track = vec![note_on(0, 0, 60, 100), end_of_track(480)];
let (tempo_map, returned_tpb, total_ticks) =
PrecomputedMidi::build_tempo_info(&[track], tpb, Format::SingleTrack);
assert!(tempo_map.is_empty());
assert_eq!(returned_tpb, tpb);
assert_eq!(total_ticks, 480);
}
#[test]
fn test_build_tempo_info_single_track_with_tempo() {
let tpb = 480;
let track = vec![
tempo_event(0, 500_000),
note_on(480, 0, 60, 100),
end_of_track(480),
];
let (tempo_map, _, total_ticks) =
PrecomputedMidi::build_tempo_info(&[track], tpb, Format::SingleTrack);
assert_eq!(tempo_map.len(), 1);
assert_eq!(tempo_map[0].tick, 0);
assert_eq!(total_ticks, 960);
}
#[test]
fn test_build_tempo_info_parallel_uses_conductor() {
let tpb = 480;
let track0 = vec![
tempo_event(0, 1_000_000),
tempo_event(480, 500_000),
end_of_track(0),
];
let track1 = vec![note_on(0, 0, 60, 100), end_of_track(960)];
let (tempo_map, _, total_ticks) =
PrecomputedMidi::build_tempo_info(&[track0, track1], tpb, Format::Parallel);
assert_eq!(tempo_map.len(), 2);
assert_eq!(tempo_map[0].tick, 0);
assert_eq!(tempo_map[1].tick, 480);
assert_eq!(total_ticks, 960);
}
#[test]
fn test_build_tempo_info_sequential_offsets_ticks() {
let tpb = 480;
let track1 = vec![tempo_event(0, 500_000), end_of_track(480)];
let track2 = vec![tempo_event(0, 1_000_000), end_of_track(480)];
let (tempo_map, _, total_ticks) =
PrecomputedMidi::build_tempo_info(&[track1, track2], tpb, Format::Sequential);
assert_eq!(tempo_map.len(), 2);
assert_eq!(tempo_map[0].tick, 0);
assert_eq!(tempo_map[1].tick, 480);
assert_eq!(total_ticks, 960);
}
#[test]
fn test_build_tempo_info_empty_tracks() {
let (tempo_map, tpb, total_ticks) =
PrecomputedMidi::build_tempo_info(&[], 480, Format::Parallel);
assert!(tempo_map.is_empty());
assert_eq!(tpb, 480);
assert_eq!(total_ticks, 0);
}
}