use alloc::vec::Vec;
use crate::{Division, SmfEvent, SmfFile};
#[derive(Debug, Clone)]
pub struct TempoEntry {
pub tick: u64,
pub tempo_us: u32,
}
#[derive(Debug, Clone)]
pub struct TempoMap {
entries: Vec<TempoEntry>,
ticks_per_beat: u16,
}
impl TempoMap {
pub fn from_file(file: &SmfFile) -> Self {
let ticks_per_beat = match file.division {
Division::TicksPerBeat(t) => t,
Division::Smpte { .. } => {
return Self {
entries: Vec::new(),
ticks_per_beat: 0,
};
}
};
let mut raw: Vec<(u64, u32)> = Vec::new();
for track in &file.tracks {
let mut abs_tick: u64 = 0;
for ev in &track.events {
abs_tick += ev.delta_ticks as u64;
if let SmfEvent::Tempo(us) = ev.event {
raw.push((abs_tick, us));
}
}
}
raw.sort_by_key(|(tick, _)| *tick);
raw.dedup_by(|later, earlier| {
if later.0 == earlier.0 {
earlier.1 = later.1; true } else {
false
}
});
let mut entries: Vec<TempoEntry> = Vec::with_capacity(raw.len() + 1);
if raw.first().is_none_or(|(tick, _)| *tick != 0) {
entries.push(TempoEntry {
tick: 0,
tempo_us: 500_000,
});
}
for (tick, tempo_us) in raw {
entries.push(TempoEntry { tick, tempo_us });
}
Self {
entries,
ticks_per_beat,
}
}
pub fn tick_to_secs(&self, tick: u64) -> f64 {
if self.ticks_per_beat == 0 || self.entries.is_empty() {
return 0.0;
}
let mut elapsed_secs = 0.0f64;
let mut prev_tick: u64 = 0;
let mut prev_tempo_us: u32 = 500_000;
for entry in &self.entries {
if entry.tick >= tick {
break;
}
let span = entry.tick - prev_tick;
elapsed_secs += ticks_to_seconds(span, prev_tempo_us, self.ticks_per_beat);
prev_tick = entry.tick;
prev_tempo_us = entry.tempo_us;
}
let span = tick - prev_tick;
elapsed_secs += ticks_to_seconds(span, prev_tempo_us, self.ticks_per_beat);
elapsed_secs
}
}
#[inline]
pub fn ticks_to_seconds(ticks: u64, tempo_us: u32, ticks_per_beat: u16) -> f64 {
if ticks_per_beat == 0 {
return 0.0;
}
ticks as f64 * tempo_us as f64 / (ticks_per_beat as f64 * 1_000_000.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Division, SmfEvent, SmfFile, SmfFormat, SmfTrack, TrackEvent};
fn make_smf_with_tempos(ticks_per_beat: u16, tempos: &[(u32, u32)]) -> SmfFile {
let events: Vec<TrackEvent> = tempos
.iter()
.map(|(delta, tempo)| TrackEvent {
delta_ticks: *delta,
event: SmfEvent::Tempo(*tempo),
})
.chain(core::iter::once(TrackEvent {
delta_ticks: 0,
event: SmfEvent::EndOfTrack,
}))
.collect();
SmfFile {
format: SmfFormat::SingleTrack,
division: Division::TicksPerBeat(ticks_per_beat),
tracks: vec![SmfTrack { name: None, events }],
}
}
#[test]
fn test_ticks_to_seconds_basic() {
let secs = ticks_to_seconds(480, 500_000, 480);
let diff = (secs - 0.5).abs();
assert!(diff < 1e-9, "expected 0.5 s, got {}", secs);
}
#[test]
fn test_tempo_map_default_120bpm() {
let file = make_smf_with_tempos(480, &[]);
let map = TempoMap::from_file(&file);
let secs = map.tick_to_secs(480);
let diff = (secs - 0.5).abs();
assert!(diff < 1e-9, "expected 0.5 s, got {}", secs);
}
#[test]
fn test_tempo_map_single_change() {
let file = make_smf_with_tempos(480, &[(0, 500_000), (480, 1_000_000)]);
let map = TempoMap::from_file(&file);
let at_beat1 = map.tick_to_secs(480);
let diff1 = (at_beat1 - 0.5).abs();
assert!(diff1 < 1e-9, "at beat 1 expected 0.5 s, got {}", at_beat1);
let at_beat2 = map.tick_to_secs(960);
let diff2 = (at_beat2 - 1.5).abs();
assert!(diff2 < 1e-9, "at beat 2 expected 1.5 s, got {}", at_beat2);
}
#[test]
fn test_tempo_map_tick_zero() {
let file = make_smf_with_tempos(480, &[]);
let map = TempoMap::from_file(&file);
assert_eq!(map.tick_to_secs(0), 0.0);
}
#[test]
fn test_ticks_to_seconds_zero_tpb() {
assert_eq!(ticks_to_seconds(100, 500_000, 0), 0.0);
}
}