use crate::collections::sort_multi;
use crate::{
math::Easing,
rhythm::{
SameUnitTimeVec,
time::{BeatTime, ClockTime, SameUnitTime, Time},
track::EventTracks,
},
};
use std::collections::HashMap;
const INTEGRATE_SAMPES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Tempo {
bpm: f64,
}
impl Tempo {
pub fn from_bpm(bpm: f64) -> Option<Self> {
(bpm.is_finite() && bpm >= 1.8973891924711e-06).then_some(Self { bpm })
}
pub fn beats_per_min(self) -> f64 {
self.bpm
}
pub fn mins_per_beat(self) -> f64 {
1.0 / self.bpm
}
pub fn beats_per_sec(self) -> f64 {
self.bpm / 60.0
}
pub fn secs_per_beat(self) -> f64 {
60.0 / self.bpm
}
pub fn beat_duration(self) -> std::time::Duration {
std::time::Duration::from_secs_f64(self.secs_per_beat())
}
fn beats_to_clock(&self, beats: BeatTime) -> ClockTime {
ClockTime::from_seconds(self.secs_per_beat() * beats.beats())
.expect("secs_per_beat is finite, and beats is not NaN. The product cannot be NaN")
}
fn clock_to_beats(&self, clock: ClockTime) -> BeatTime {
BeatTime::new(self.beats_per_sec() * clock.seconds())
.expect("beats_per_sec is finite, and clocktime is not NaN. The product cannot be NaN")
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TempoChangeEvent {
pub time: Time,
pub tempo: Tempo,
pub ease: Easing,
}
impl TempoChangeEvent {
pub fn time(&self) -> Time {
self.time
}
fn initial_cumulate(first_time: Time, event: &Self) -> (ClockTime, BeatTime) {
match first_time {
Time::Clock(t) => (t, event.tempo.clock_to_beats(t)),
Time::Beat(b) => (event.tempo.beats_to_clock(b), b),
}
}
fn cumulate<'id>(
current_clock: ClockTime,
current_beat: BeatTime,
delta: SameUnitTime<'id>,
full_duration: SameUnitTime<'id>,
current: &Self,
next: &Self,
) -> (ClockTime, BeatTime) {
let full_duration_f64 = match full_duration.as_time() {
Time::Clock(clock_time) => clock_time.seconds(),
Time::Beat(beat_time) => beat_time.beats(),
};
if full_duration_f64 <= 1e-6 {
return (current_clock, current_beat);
}
let p = delta / full_duration;
if !p.is_finite() {
return (ClockTime::INF, BeatTime::INF);
}
match Time::from(delta) {
Time::Clock(t) => {
let delta_beats = current.ease.integrate(
current.tempo.beats_per_sec(),
next.tempo.beats_per_sec(),
p,
) * full_duration_f64;
(
current_clock + t,
current_beat
+ BeatTime::new(delta_beats)
.expect("Integration with finite input can't be NaN"),
)
}
Time::Beat(t) => {
let delta_secs = full_duration_f64
* current
.ease
.integrate_reciprocal(
current.tempo.beats_per_sec(),
next.tempo.beats_per_sec(),
p,
INTEGRATE_SAMPES, )
.expect("All input is finite, and both tempo must be positive");
(
current_clock
+ ClockTime::from_seconds(delta_secs)
.expect("integrate_reciprocal never returns non-finite float"),
current_beat + t,
)
}
}
}
}
pub type TempoTracks = EventTracks<TempoTrack>;
#[derive(Default, Debug, Clone)]
pub struct TempoTrack {
id_counter: u64,
id_lookup: HashMap<u64, usize>,
ids: Vec<u64>,
time: SameUnitTimeVec,
content: Vec<TempoChangeEvent>,
cumulated: Vec<(ClockTime, BeatTime)>,
}
impl TempoTrack {
pub fn new() -> Self {
Self {
id_counter: 0,
id_lookup: Default::default(),
ids: Default::default(),
time: Default::default(),
content: Default::default(),
cumulated: Default::default(),
}
}
pub fn with_events(mut events: Vec<TempoChangeEvent>) -> Option<Self> {
let len = events.len();
let mut time = SameUnitTimeVec::try_from_iter(events.iter().map(|event| event.time()))?;
match &mut time {
SameUnitTimeVec::Clock(times) => {
sort_multi(times, |i, j| events.swap(i, j));
}
SameUnitTimeVec::Beat(times) => {
sort_multi(times, |i, j| events.swap(i, j));
}
}
let mut s = Self {
id_counter: len as u64,
ids: (0..len as u64).collect(),
time,
content: events,
id_lookup: Default::default(),
cumulated: Default::default(),
};
s.recalc_cumulative_and_lookup();
Some(s)
}
pub fn beat_to_clock(&self, source: BeatTime) -> ClockTime {
if self.is_empty() {
return ClockTime::default();
}
if source == BeatTime::INF {
return ClockTime::INF;
}
let index = self
.cumulated
.partition_point(|(_, cbeat)| cbeat <= &source);
if index == 0 {
let first = self.content[0];
return match first.time {
Time::Clock(t) => t - first.tempo.beats_to_clock(self.cumulated[0].1 - source),
Time::Beat(b) => self.cumulated[0].0 - first.tempo.beats_to_clock(b - source),
};
}
if index == self.len() {
let last_index = self.len() - 1;
let last = self.content[last_index];
let last_cumulated = self.cumulated[last_index];
return match last.time {
Time::Clock(t) => t + last.tempo.beats_to_clock(source - last_cumulated.1),
Time::Beat(b) => last_cumulated.0 + last.tempo.beats_to_clock(source - b),
};
}
let base = self.cumulated[index - 1].0;
let elapsed_beats_since = source - self.cumulated[index - 1].1;
let bps0 = self.content[index - 1].tempo.beats_per_sec();
let bps1 = self.content[index].tempo.beats_per_sec();
let ease = self.content[index - 1].ease;
match self.content[index - 1].time {
Time::Clock(_) => {
let segment_clock = self.cumulated[index].0 - self.cumulated[index - 1].0;
let val = elapsed_beats_since.beats() / segment_clock.seconds();
if !val.is_finite() {
return ClockTime::INF;
}
let t = ease
.solve_integrate(bps0, bps1, val)
.expect("All input are finite, and bpms are positive, so this can't be None");
base + segment_clock
.scale(t)
.expect("solve_integrate can't return NaN")
}
Time::Beat(_) => {
let segment_beats = self.cumulated[index].1 - self.cumulated[index - 1].1;
let beats_total = segment_beats.beats();
if !beats_total.is_finite() || beats_total == 0.0 {
return ClockTime::INF;
}
let p_b = elapsed_beats_since.beats() / beats_total;
if !p_b.is_finite() {
return ClockTime::INF;
}
let delta_secs = ease
.integrate_reciprocal(bps0, bps1, p_b, INTEGRATE_SAMPES) .expect("Both bps are positive, so no pole")
* beats_total;
base + ClockTime::from_seconds(delta_secs)
.expect("integrate_reciprocal with finite input can't be NaN")
}
}
}
pub fn clock_to_beats(&self, source: ClockTime) -> BeatTime {
if self.is_empty() {
return BeatTime::default();
}
if source == ClockTime::INF {
return BeatTime::INF;
}
let index = self.cumulated.partition_point(|(cms, _)| cms <= &source);
if index == 0 {
let first = self.content[0];
return match first.time {
Time::Clock(t) => self.cumulated[0].1 - first.tempo.clock_to_beats(t - source),
Time::Beat(b) => b - first.tempo.clock_to_beats(self.cumulated[0].0 - source),
};
}
if index == self.len() {
let last_index = self.len() - 1;
let last = self.content[last_index];
let last_cumulated = self.cumulated[last_index];
return match last.time {
Time::Clock(t) => last_cumulated.1 + last.tempo.clock_to_beats(source - t),
Time::Beat(b) => b + last.tempo.clock_to_beats(source - last_cumulated.0),
};
}
let base = self.cumulated[index - 1].1;
let elapsed_ms_since = source - self.cumulated[index - 1].0;
let bps0 = self.content[index - 1].tempo.beats_per_sec();
let bps1 = self.content[index].tempo.beats_per_sec();
let ease = self.content[index - 1].ease;
match self.content[index - 1].time {
Time::Clock(_) => {
let segment_clock = self.cumulated[index].0 - self.cumulated[index - 1].0;
let p = elapsed_ms_since / segment_clock;
if !p.is_finite() {
return BeatTime::INF;
}
let delta_beats = ease.integrate(bps0, bps1, p) * segment_clock.seconds();
base + BeatTime::new(delta_beats)
.expect("All input to integrate is finite, so this can't be NaN")
}
Time::Beat(_) => {
let segment_beats = self.cumulated[index].1 - self.cumulated[index - 1].1;
let beats_total = segment_beats.beats();
if !beats_total.is_finite() || beats_total == 0.0 {
return BeatTime::INF;
}
let val = elapsed_ms_since.seconds() / beats_total;
if !val.is_finite() {
return BeatTime::INF;
}
let p_b = ease
.solve_integrate_reciprocal(bps0, bps1, val, 1024)
.expect("Both bps are positive, so no pole and monotonic");
let delta_beats = p_b * beats_total;
base + BeatTime::new(delta_beats)
.expect("solve_integrate_reciprocal returns finite value in [0,1]")
}
}
}
pub fn add_event(&mut self, event: TempoChangeEvent) -> bool {
if !self.time.unit_match(event.time()) {
return false;
}
self.add_event_impl(event);
self.recalc_cumulative_and_lookup();
self.health_check();
true
}
pub fn add_events(&mut self, events: impl Iterator<Item = TempoChangeEvent> + Clone) -> bool {
if events.clone().any(|ev| !self.time.unit_match(ev.time())) {
return false;
}
for ev in events {
self.add_event_impl(ev);
}
self.recalc_cumulative_and_lookup();
self.health_check();
true
}
pub fn remove_event(&mut self, id: u64) {
let Some(&index) = self.id_lookup.get(&id) else {
return;
};
self.ids.remove(index);
self.time.remove(index);
self.content.remove(index);
self.recalc_cumulative_and_lookup();
self.health_check();
}
pub fn remove_events(&mut self, ids: impl IntoIterator<Item = u64>) {
let mut removing_indices = ids
.into_iter()
.flat_map(|id| self.id_lookup.get(&id))
.copied()
.collect::<Vec<_>>();
removing_indices.sort_unstable();
removing_indices.dedup();
for i in removing_indices.into_iter().rev() {
self.ids.remove(i);
self.time.remove(i);
self.content.remove(i);
}
self.recalc_cumulative_and_lookup();
self.health_check();
}
pub fn replace_event(&mut self, id: u64, replace_with: TempoChangeEvent) -> bool {
let time = replace_with.time();
if !self.time.unit_match(time) {
return false;
}
let Some(&index) = self.id_lookup.get(&id) else {
return false;
};
let old_time = self.time.index(index);
self.time
.try_set(index, time)
.expect("Checked that unit matches");
self.content[index] = replace_with;
let len = self.ids.len();
let mut j = index;
if self.time.index(j) > old_time {
while j + 1 < len && self.time.index(j) > self.time.index(j + 1) {
self.ids.swap(j, j + 1);
self.time.swap(j, j + 1);
self.content.swap(j, j + 1);
j += 1;
}
} else {
while j > 0 && self.time.index(j - 1) > self.time.index(j) {
self.ids.swap(j - 1, j);
self.time.swap(j - 1, j);
self.content.swap(j - 1, j);
j -= 1;
}
}
self.recalc_cumulative_and_lookup();
self.health_check();
true
}
pub fn replace_events(
&mut self,
events: impl Iterator<Item = (u64, TempoChangeEvent)> + Clone,
) -> bool {
if events
.clone()
.any(|(_, ev)| !self.time.unit_match(ev.time()))
{
return false;
}
for (id, ev) in events {
let Some(&index) = self.id_lookup.get(&id) else {
continue;
};
self.time
.try_set(index, ev.time())
.expect("Checked that unit matches");
self.content[index] = ev;
}
let swap = |i, j| {
self.ids.swap(i, j);
self.content.swap(i, j);
};
match &mut self.time {
SameUnitTimeVec::Clock(times) => {
sort_multi(times, swap);
}
SameUnitTimeVec::Beat(times) => {
sort_multi(times, swap);
}
}
self.recalc_cumulative_and_lookup();
self.health_check();
true
}
#[inline]
pub fn change_times(&self) -> &SameUnitTimeVec {
&self.time
}
#[inline]
pub fn tempo_changes(&self) -> &[TempoChangeEvent] {
&self.content
}
#[inline]
pub fn cumulative_times(&self) -> &[(ClockTime, BeatTime)] {
&self.cumulated
}
#[inline]
pub fn len(&self) -> usize {
self.ids.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
fn recalc_cumulative_and_lookup(&mut self) {
self.id_lookup.clear();
while self.cumulated.len() > self.ids.len() {
self.cumulated.pop();
}
while self.cumulated.len() < self.ids.len() {
self.cumulated.push((ClockTime::ZERO, BeatTime::ZERO));
}
if self.ids.is_empty() {
return;
}
let mut cumulated =
TempoChangeEvent::initial_cumulate(self.time.index(0), &self.content[0]);
self.cumulated[0] = cumulated;
self.id_lookup.insert(self.ids[0], 0);
for i in 1..self.ids.len() {
self.id_lookup.insert(self.ids[i], i);
generativity::make_guard!(g);
let v = self.time.read_with_guard(g);
let delta = v.index(i) - v.index(i - 1);
let current = &self.content[i - 1];
let next = &self.content[i];
cumulated = TempoChangeEvent::cumulate(
cumulated.0,
cumulated.1,
delta,
v.index(i) - v.index(i - 1),
current,
next,
);
self.cumulated[i] = cumulated;
}
}
fn add_event_impl(&mut self, event: TempoChangeEvent) {
let time = event.time();
assert!(self.time.unit_match(time));
let insert_index = {
match &self.time {
SameUnitTimeVec::Clock(times) => times.partition_point(|x| Time::Clock(*x) <= time),
SameUnitTimeVec::Beat(times) => times.partition_point(|x| Time::Beat(*x) <= time),
}
};
let id = self.id_counter;
self.id_counter += 1;
self.ids.insert(insert_index, id);
self.time
.try_insert(insert_index, time)
.expect("Checked that unit matches");
self.content.insert(insert_index, event);
}
pub(crate) fn health_check(&self) {
match &self.time {
SameUnitTimeVec::Clock(l) => debug_assert!(l.is_sorted()),
SameUnitTimeVec::Beat(l) => debug_assert!(l.is_sorted()),
}
let len = self.ids.len();
debug_assert_eq!(self.time.len(), len);
debug_assert_eq!(self.content.len(), len);
debug_assert_eq!(self.cumulated.len(), len);
debug_assert_eq!(self.id_lookup.len(), len);
for i in 0..len {
if i == 0 {
debug_assert_eq!(
self.cumulated[0],
TempoChangeEvent::initial_cumulate(self.time.index(0), &self.content[0])
);
} else {
generativity::make_guard!(g);
let v = self.time.read_with_guard(g);
let delta = v.index(i) - v.index(i - 1);
let expected = TempoChangeEvent::cumulate(
self.cumulated[i - 1].0,
self.cumulated[i - 1].1,
delta,
v.index(i) - v.index(i - 1),
&self.content[i - 1],
&self.content[i],
);
debug_assert_eq!(self.cumulated[i], expected);
}
}
}
}
#[cfg(test)]
mod test {
use crate::rhythm::ClockTime;
use proptest::prelude::*;
use super::*;
fn arb_clock_event() -> impl Strategy<Value = TempoChangeEvent> {
(any::<u16>(), any::<u32>()).prop_map(|(t, s)| TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(t as f64 / 1000.0).unwrap()),
tempo: Tempo::from_bpm(s as f64 / 1000.0).unwrap(),
ease: Easing::Linear,
})
}
fn arb_clock_events(max_len: usize) -> impl Strategy<Value = Vec<TempoChangeEvent>> {
prop::collection::vec(arb_clock_event(), 0..=max_len)
}
fn arb_clock_track(max_len: usize) -> impl Strategy<Value = TempoTrack> {
arb_clock_events(max_len).prop_map(|v| TempoTrack::with_events(v).expect("Same unit"))
}
fn arb_clock_track_select(max_len: usize) -> impl Strategy<Value = (TempoTrack, u64)> {
prop::collection::vec(arb_clock_event(), 1..=max_len)
.prop_map(|events| TempoTrack::with_events(events).expect("Same unit"))
.prop_flat_map(|track| {
let len = track.ids.len();
let ids = track.ids.clone();
(Just(track), (0..len).prop_map(move |index| ids[index]))
})
}
fn arb_clock_track_select_multi(
max_len: usize,
max_select_len: usize,
) -> impl Strategy<Value = (TempoTrack, Vec<u64>)> {
prop::collection::vec(arb_clock_event(), 1..=max_len)
.prop_map(|events| TempoTrack::with_events(events).expect("Same unit"))
.prop_flat_map(move |track| {
let len = track.ids.len();
let ids = track.ids.clone();
(
Just(track),
proptest::collection::hash_set(0..len, 0..=(max_select_len.min(len)))
.prop_map(move |indices| indices.into_iter().map(|i| ids[i]).collect()),
)
})
}
fn arb_clock_track_replace_multi(
max_len: usize,
max_replace_len: usize,
) -> impl Strategy<Value = (TempoTrack, Vec<u64>, Vec<TempoChangeEvent>)> {
arb_clock_track_select_multi(max_len, max_replace_len).prop_flat_map(|(track, ids)| {
let len = ids.len();
(
Just(track),
Just(ids),
proptest::collection::vec(arb_clock_event(), len),
)
})
}
proptest! {
#[test]
fn add_track_stays_consistent(mut track in arb_clock_track(100), event in arb_clock_event()) {
let len = track.ids.len();
track.add_event(event);
assert_eq!(track.ids.len(), len + 1);
}
}
proptest! {
#[test]
fn add_multi_track_stays_consistent(mut track in arb_clock_track(100), events in arb_clock_events(100)) {
let len = track.ids.len();
let add_len = events.len();
track.add_events(events.into_iter());
assert_eq!(track.ids.len(), len + add_len);
}
}
proptest! {
#[test]
fn remove_track_stays_consistent((mut track, index) in arb_clock_track_select(100)) {
let len = track.ids.len();
track.remove_event(index);
assert_eq!(track.ids.len(), len - 1);
}
}
proptest! {
#[test]
fn remove_multi_track_stays_consistent((mut track, ids) in arb_clock_track_select_multi(100, 50)) {
let len = track.ids.len();
let remove_len = ids.len();
track.remove_events(ids);
assert_eq!(track.ids.len(), len - remove_len);
}
}
proptest! {
#[test]
fn replace_track_stays_consistent((mut track, index) in arb_clock_track_select(100), repl in arb_clock_event()) {
let len = track.ids.len();
track.replace_event(index, repl);
assert_eq!(track.ids.len(), len);
}
}
proptest! {
#[test]
fn replace_multi_track_stays_consistent((mut track, ids, events) in arb_clock_track_replace_multi(100, 50)) {
let len = track.ids.len();
track.replace_events(std::iter::zip(ids, events));
assert_eq!(track.ids.len(), len);
}
}
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn convert_from_const_tempo_clock_time() {
let tempo = Tempo::from_bpm(120.0).unwrap();
let tempo_track = TempoTrack::with_events(vec![TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(0.0).unwrap()),
tempo,
ease: Easing::InConst,
}])
.unwrap();
assert_eq!(
tempo_track.beat_to_clock(BeatTime::new(6.0).unwrap()),
ClockTime::from_seconds(3.0).unwrap()
);
assert_eq!(
tempo_track.clock_to_beats(ClockTime::from_seconds(3.0).unwrap()),
BeatTime::new(6.0).unwrap()
);
}
#[test]
fn convert_from_const_tempo_beat_time() {
let tempo = Tempo::from_bpm(120.0).unwrap();
let tempo_track = TempoTrack::with_events(vec![TempoChangeEvent {
time: Time::Beat(BeatTime::new(0.0).unwrap()),
tempo,
ease: Easing::InConst,
}])
.unwrap();
assert_eq!(
tempo_track.beat_to_clock(BeatTime::new(6.0).unwrap()),
ClockTime::from_seconds(3.0).unwrap()
);
assert_eq!(
tempo_track.clock_to_beats(ClockTime::from_seconds(3.0).unwrap()),
BeatTime::new(6.0).unwrap()
);
}
#[test]
fn convert_from_linear_tempo_clock_time() {
let t120 = Tempo::from_bpm(120.0).unwrap();
let t240 = Tempo::from_bpm(240.0).unwrap();
let tempo_track = TempoTrack::with_events(vec![
TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(0.0).unwrap()),
tempo: t120,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(4.0).unwrap()),
tempo: t240,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(8.0).unwrap()),
tempo: t120,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Clock(ClockTime::from_seconds(12.0).unwrap()),
tempo: t240,
ease: Easing::Linear,
},
])
.unwrap();
assert_eq!(
tempo_track.beat_to_clock(BeatTime::new(5.0).unwrap()),
ClockTime::from_seconds(2.0).unwrap()
);
assert_eq!(
tempo_track.clock_to_beats(ClockTime::from_seconds(2.0).unwrap()),
BeatTime::new(5.0).unwrap()
);
}
#[test]
fn convert_from_linear_tempo_beat_time() {
let t120 = Tempo::from_bpm(120.0).unwrap();
let t240 = Tempo::from_bpm(240.0).unwrap();
let tempo_track = TempoTrack::with_events(vec![
TempoChangeEvent {
time: Time::Beat(BeatTime::new(0.0).unwrap()),
tempo: t120,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Beat(BeatTime::new(12.0).unwrap()),
tempo: t240,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Beat(BeatTime::new(24.0).unwrap()),
tempo: t120,
ease: Easing::Linear,
},
TempoChangeEvent {
time: Time::Beat(BeatTime::new(36.0).unwrap()),
tempo: t240,
ease: Easing::Linear,
},
])
.unwrap();
let expected_secs = 6.0 * f64::ln(3.0 / 2.0);
let clock = tempo_track.beat_to_clock(BeatTime::new(6.0).unwrap());
assert!(
approx_eq(clock.seconds(), expected_secs, 1e-9),
"beat_to_clock(6) = {}, expected {}",
clock.seconds(),
expected_secs
);
let beats = tempo_track.clock_to_beats(ClockTime::from_seconds(expected_secs).unwrap());
assert!(
approx_eq(beats.beats(), 6.0, 1e-9),
"clock_to_beats({}) = {}, expected 6.0",
expected_secs,
beats.beats()
);
}
}