use std::time::Duration;
use super::playback::TempoEntry;
const CLOCKS_PER_BEAT: u64 = 24;
pub(crate) struct PrecomputedBeatClock {
ticks: Vec<Duration>,
}
impl PrecomputedBeatClock {
pub(crate) fn from_tempo_info(
tempo_map: &[TempoEntry],
ticks_per_beat: u16,
total_ticks: u64,
) -> Self {
let tpb = ticks_per_beat as f64;
let default_micros_per_tick = 500_000.0 / tpb;
let clock_interval_ticks_f = tpb / CLOCKS_PER_BEAT as f64;
let mut ticks = Vec::new();
let mut micros_per_tick = default_micros_per_tick;
let mut elapsed_micros: f64 = 0.0;
let mut current_tick: f64 = 0.0;
let mut tempo_idx: usize = 0;
while (current_tick as u64) < total_ticks {
let next_clock_tick = current_tick + clock_interval_ticks_f;
while tempo_idx < tempo_map.len()
&& (tempo_map[tempo_idx].tick as f64) <= next_clock_tick
{
let entry = &tempo_map[tempo_idx];
if (entry.tick as f64) > current_tick {
elapsed_micros += (entry.tick as f64 - current_tick) * micros_per_tick;
current_tick = entry.tick as f64;
}
micros_per_tick = entry.micros_per_tick;
tempo_idx += 1;
}
if next_clock_tick > current_tick {
elapsed_micros += (next_clock_tick - current_tick) * micros_per_tick;
}
current_tick = next_clock_tick;
if (current_tick as u64) <= total_ticks {
ticks.push(Duration::from_micros(elapsed_micros.round() as u64));
}
}
PrecomputedBeatClock { ticks }
}
pub(crate) fn ticks_from(&self, start_time: Duration) -> &[Duration] {
let idx = self.ticks.partition_point(|t| *t < start_time);
&self.ticks[idx..]
}
#[cfg(test)]
pub(crate) fn ticks(&self) -> &[Duration] {
&self.ticks
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_tempo_tick_count() {
let beat_clock = PrecomputedBeatClock::from_tempo_info(&[], 480, 1920);
assert_eq!(beat_clock.ticks().len(), 96);
}
#[test]
fn constant_tempo_tick_spacing() {
let beat_clock = PrecomputedBeatClock::from_tempo_info(&[], 480, 480);
assert_eq!(beat_clock.ticks().len(), 24);
let expected_interval = Duration::from_micros((500_000.0_f64 / 24.0).round() as u64);
assert_eq!(beat_clock.ticks()[0], expected_interval);
let last = beat_clock.ticks()[23];
assert!(
last >= Duration::from_micros(499_000) && last <= Duration::from_micros(501_000),
"last tick at {:?}",
last
);
}
#[test]
fn tempo_change_adjusts_spacing() {
let tpb = 480u16;
let tempo_map = vec![TempoEntry {
tick: 480,
micros_per_tick: 1_000_000.0 / 480.0,
}];
let beat_clock = PrecomputedBeatClock::from_tempo_info(&tempo_map, tpb, 960);
assert_eq!(beat_clock.ticks().len(), 48);
let first_beat_interval =
beat_clock.ticks()[1].as_micros() - beat_clock.ticks()[0].as_micros();
assert!(
(first_beat_interval as i64 - 20_833).unsigned_abs() < 10,
"first beat interval: {}",
first_beat_interval
);
let second_beat_interval =
beat_clock.ticks()[25].as_micros() - beat_clock.ticks()[24].as_micros();
assert!(
(second_beat_interval as i64 - 41_667).unsigned_abs() < 10,
"second beat interval: {}",
second_beat_interval
);
}
#[test]
fn ticks_from_seeks_correctly() {
let beat_clock = PrecomputedBeatClock::from_tempo_info(&[], 480, 960);
assert_eq!(beat_clock.ticks().len(), 48);
assert_eq!(beat_clock.ticks_from(Duration::ZERO).len(), 48);
let from_half = beat_clock.ticks_from(Duration::from_micros(500_001));
assert_eq!(from_half.len(), 24);
}
#[test]
fn empty_song_produces_no_ticks() {
let beat_clock = PrecomputedBeatClock::from_tempo_info(&[], 480, 0);
assert_eq!(beat_clock.ticks().len(), 0);
}
#[test]
fn multiple_tempo_changes() {
let tpb = 480u16;
let tempo_map = vec![
TempoEntry {
tick: 480,
micros_per_tick: 1_000_000.0 / 480.0, },
TempoEntry {
tick: 960,
micros_per_tick: 250_000.0 / 480.0, },
];
let beat_clock = PrecomputedBeatClock::from_tempo_info(&tempo_map, tpb, 1440);
assert_eq!(beat_clock.ticks().len(), 72);
}
#[test]
fn tempo_change_at_tick_zero() {
let tpb = 480u16;
let tempo_map = vec![TempoEntry {
tick: 0,
micros_per_tick: 1_000_000.0 / 480.0, }];
let beat_clock = PrecomputedBeatClock::from_tempo_info(&tempo_map, tpb, 480);
assert_eq!(beat_clock.ticks().len(), 24);
let interval = beat_clock.ticks()[1].as_micros() - beat_clock.ticks()[0].as_micros();
assert!(
(interval as i64 - 41_667).unsigned_abs() < 10,
"interval: {}",
interval
);
}
#[test]
fn non_standard_ticks_per_beat() {
let tpb = 100u16;
let tempo_map = vec![TempoEntry {
tick: 0,
micros_per_tick: 500_000.0 / 100.0, }];
let beat_clock = PrecomputedBeatClock::from_tempo_info(&tempo_map, tpb, 100);
assert_eq!(beat_clock.ticks().len(), 24);
}
#[test]
fn ticks_from_past_end_returns_empty() {
let beat_clock = PrecomputedBeatClock::from_tempo_info(&[], 480, 480);
let from_far = beat_clock.ticks_from(Duration::from_secs(100));
assert_eq!(from_far.len(), 0);
}
}