use chrono::Duration;
use super::beats::Beats;
use super::timeline::Timeline;
pub fn phase(beats: Beats, quantum: Beats) -> Beats {
if quantum.value == 0 {
Beats { value: 0 }
} else {
let quantum_micros = quantum.micro_beats();
let quantum_bins = (beats.micro_beats().abs() + quantum_micros) / quantum_micros;
let quantum_beats = quantum_bins * quantum_micros;
(beats + Beats::from_microbeats(quantum_beats)).r#mod(quantum)
}
}
pub fn next_phase_match(x: Beats, target: Beats, quantum: Beats) -> Beats {
let desired_phase = phase(target, quantum);
let x_phase = phase(x, quantum);
let phase_diff = (desired_phase - x_phase + quantum).r#mod(quantum);
x + phase_diff
}
pub fn closest_phase_match(x: Beats, target: Beats, quantum: Beats) -> Beats {
next_phase_match(x - Beats::new(0.5 * quantum.floating()), target, quantum)
}
pub fn to_phase_encoded_beats(tl: &Timeline, time: Duration, quantum: Beats) -> Beats {
let beat = tl.to_beats(time);
closest_phase_match(beat, beat - tl.beat_origin, quantum)
}
pub fn from_phase_encoded_beats(tl: &Timeline, beat: Beats, quantum: Beats) -> Duration {
let from_origin = beat - tl.beat_origin;
let origin_offset = from_origin - phase(from_origin, quantum);
let inverse_phase_offset = closest_phase_match(
quantum - phase(from_origin, quantum),
quantum - phase(beat, quantum),
quantum,
);
tl.from_beats(tl.beat_origin + origin_offset + quantum - inverse_phase_offset)
}
pub fn force_beat_at_time_impl(
timeline: &mut Timeline,
beat: Beats,
time: Duration,
quantum: Beats,
) {
let cur_beat_at_time = to_phase_encoded_beats(timeline, time, quantum);
let closest_in_phase = closest_phase_match(cur_beat_at_time, beat, quantum);
*timeline = shift_client_timeline(*timeline, closest_in_phase - cur_beat_at_time);
timeline.beat_origin = timeline.beat_origin + beat - closest_in_phase;
}
pub fn shift_client_timeline(mut timeline: Timeline, shift: Beats) -> Timeline {
let time_delta = timeline.from_beats(shift) - timeline.from_beats(Beats { value: 0 });
timeline.time_origin -= time_delta;
timeline
}
#[cfg(test)]
mod tests {
use super::*;
use crate::link::tempo::Tempo;
#[test]
fn test_phase_positive() {
let beats = Beats::new(5.5);
let quantum = Beats::new(4.0);
let result = phase(beats, quantum);
assert!((result.floating() - 1.5).abs() < 1e-10);
}
#[test]
fn test_phase_negative() {
let beats = Beats::new(-2.5);
let quantum = Beats::new(4.0);
let result = phase(beats, quantum);
assert!((result.floating() - 1.5).abs() < 1e-10);
}
#[test]
fn test_phase_zero_quantum() {
let beats = Beats::new(5.5);
let quantum = Beats::new(0.0);
let result = phase(beats, quantum);
assert_eq!(result.floating(), 0.0);
}
#[test]
fn test_phase_exact_quantum() {
let beats = Beats::new(8.0);
let quantum = Beats::new(4.0);
let result = phase(beats, quantum);
assert!((result.floating() - 0.0).abs() < 1e-10);
}
#[test]
fn test_next_phase_match() {
let x = Beats::new(1.0);
let target = Beats::new(2.5);
let quantum = Beats::new(4.0);
let result = next_phase_match(x, target, quantum);
assert!((result.floating() - 2.5).abs() < 1e-10);
}
#[test]
fn test_next_phase_match_wrap_around() {
let x = Beats::new(3.0);
let target = Beats::new(1.5);
let quantum = Beats::new(4.0);
let result = next_phase_match(x, target, quantum);
assert!((result.floating() - 5.5).abs() < 1e-10);
}
#[test]
fn test_closest_phase_match() {
let x = Beats::new(2.8);
let target = Beats::new(1.0);
let quantum = Beats::new(4.0);
let result = closest_phase_match(x, target, quantum);
assert!((result.floating() - 1.0).abs() < 1e-10);
}
#[test]
fn test_to_phase_encoded_beats() {
let timeline = Timeline {
tempo: Tempo::new(120.0),
beat_origin: Beats::new(0.0),
time_origin: Duration::zero(),
};
let time = Duration::seconds(1);
let quantum = Beats::new(4.0);
let result = to_phase_encoded_beats(&timeline, time, quantum);
let expected_beats = timeline.to_beats(time);
let phase_encoded = closest_phase_match(expected_beats, expected_beats, quantum);
assert!((result.floating() - phase_encoded.floating()).abs() < 1e-10);
}
#[test]
fn test_from_phase_encoded_beats() {
let timeline = Timeline {
tempo: Tempo::new(120.0),
beat_origin: Beats::new(0.0),
time_origin: Duration::zero(),
};
let beat = Beats::new(2.0);
let quantum = Beats::new(4.0);
let result = from_phase_encoded_beats(&timeline, beat, quantum);
assert!(
(result - Duration::seconds(1))
.num_microseconds()
.unwrap()
.abs()
< 1000
);
}
#[test]
fn test_phase_roundtrip() {
let timeline = Timeline {
tempo: Tempo::new(140.0),
beat_origin: Beats::new(1.5),
time_origin: Duration::milliseconds(500),
};
let original_time = Duration::seconds(2);
let quantum = Beats::new(4.0);
let beats = to_phase_encoded_beats(&timeline, original_time, quantum);
let result_time = from_phase_encoded_beats(&timeline, beats, quantum);
let diff = (result_time - original_time)
.num_microseconds()
.unwrap()
.abs();
assert!(diff < 10000); }
#[test]
fn test_force_beat_at_time_impl() {
let mut timeline = Timeline {
tempo: Tempo::new(120.0),
beat_origin: Beats::new(0.0),
time_origin: Duration::zero(),
};
let target_beat = Beats::new(4.0);
let target_time = Duration::seconds(1);
let quantum = Beats::new(4.0);
force_beat_at_time_impl(&mut timeline, target_beat, target_time, quantum);
let result_beat = to_phase_encoded_beats(&timeline, target_time, quantum);
assert!((result_beat.floating() - target_beat.floating()).abs() < 1e-6);
}
#[test]
fn test_shift_client_timeline() {
let original_timeline = Timeline {
tempo: Tempo::new(120.0),
beat_origin: Beats::new(1.0),
time_origin: Duration::seconds(1),
};
let shift = Beats::new(2.0);
let shifted_timeline = shift_client_timeline(original_timeline, shift);
assert_eq!(shifted_timeline.tempo.bpm(), original_timeline.tempo.bpm());
assert_eq!(shifted_timeline.beat_origin, original_timeline.beat_origin);
let expected_time_shift = original_timeline.tempo.beats_to_micros(shift);
assert_eq!(
shifted_timeline.time_origin,
original_timeline.time_origin - expected_time_shift
);
}
}