use crate::phase::Phase;
use crate::window::Window;
use crate::Keyable;
pub trait Time: Copy + Sized {
fn seconds(&self) -> f64;
fn from_seconds(seconds: f64) -> Self;
fn during(&self, start: f64, end: f64) -> Option<Self> {
let s = self.seconds();
if s >= start && s < end {
Some(*self)
} else {
None
}
}
fn fps(&self, fps: u32) -> Self {
if fps == 0 {
return *self;
}
let step = 1.0 / fps as f64;
Self::from_seconds((self.seconds() / step).floor() * step)
}
fn cycle(&self, period: f64) -> Phase {
assert_valid_period(period, "Time::cycle");
Phase::saturating((self.seconds().rem_euclid(period) / period) as f32)
}
fn bounce(&self, period: f64) -> Phase {
assert_valid_period(period, "Time::bounce");
let p = self.cycle(period).get();
Phase::saturating(1.0 - (2.0 * p - 1.0).abs())
}
fn wave(&self, period: f64) -> Phase {
assert_valid_period(period, "Time::wave");
let u = self.seconds() / period;
Phase::saturating((0.5 - 0.5 * (std::f64::consts::TAU * u).cos()) as f32)
}
fn phase(&self, start: f64, end: f64) -> Phase {
assert_valid_span(start, end, "Time::phase");
Phase::saturating(((self.seconds() - start) / (end - start)) as f32)
}
fn window(&self, start: f64, end: f64) -> Window {
Window::new(start, end, self.seconds())
}
}
fn assert_valid_span(start: f64, end: f64, caller: &str) {
assert!(
start.is_finite() && end.is_finite() && end > start,
"{caller} requires finite start/end with end > start"
);
}
fn assert_valid_period(period: f64, caller: &str) {
assert!(
period.is_finite() && period > 0.0,
"{caller} requires a finite positive period"
);
}
#[derive(Debug, Clone, Copy, Keyable)]
pub struct TimelineTime {
seconds: f64,
}
impl TimelineTime {
pub const fn new(seconds: f64) -> Self {
Self { seconds }
}
}
impl Time for TimelineTime {
fn seconds(&self) -> f64 {
self.seconds
}
fn from_seconds(seconds: f64) -> Self {
Self { seconds }
}
}
#[derive(Debug, Clone, Copy, Keyable)]
pub struct LocalTime {
seconds: f64,
}
impl LocalTime {
pub const fn new(seconds: f64) -> Self {
Self { seconds }
}
}
impl Time for LocalTime {
fn seconds(&self) -> f64 {
self.seconds
}
fn from_seconds(seconds: f64) -> Self {
Self { seconds }
}
}
impl From<TimelineTime> for LocalTime {
fn from(t: TimelineTime) -> Self {
Self { seconds: t.seconds }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn during_gates_without_rebase_and_preserves_type() {
let t = TimelineTime::new(4.2);
let gated = t.during(3.0, 5.0).expect("in range");
assert_eq!(gated, TimelineTime::new(4.2));
}
#[test]
fn during_out_of_range_is_none() {
let t = TimelineTime::new(2.0);
assert!(t.during(3.0, 5.0).is_none());
let t = TimelineTime::new(5.0);
assert!(t.during(3.0, 5.0).is_none());
}
#[test]
fn fps_floors_and_preserves_type() {
let t = TimelineTime::new(1.234);
let q = t.fps(24);
let expected = 29.0_f64 / 24.0;
assert!((q.seconds() - expected).abs() < 1e-5);
let _is_timeline_time: TimelineTime = q;
}
#[test]
fn fps_zero_is_identity() {
let t = TimelineTime::new(1.234);
assert_eq!(t.fps(0), t);
}
#[test]
fn local_time_supports_same_ops() {
let t = LocalTime::new(0.8);
let gated: LocalTime = t.during(0.0, 1.0).expect("in range");
assert_eq!(gated, LocalTime::new(0.8));
}
#[test]
fn timeline_time_distinguishes_adjacent_48khz_samples_at_large_offsets() {
let base = 512.0;
let next_sample = base + 1.0 / 48_000.0;
assert_ne!(TimelineTime::new(base), TimelineTime::new(next_sample));
assert_eq!(TimelineTime::new(next_sample).seconds(), next_sample);
}
#[test]
fn cycle_rises_across_each_period() {
let p = TimelineTime::new(2.5).cycle(1.0);
assert!((p.get() - 0.5).abs() < 1e-6);
}
#[test]
fn cycle_resets_at_period_boundary() {
assert_eq!(TimelineTime::new(1.0).cycle(1.0).get(), 0.0);
}
#[test]
fn cycle_handles_negative_time() {
let p = LocalTime::new(-0.3).cycle(1.0);
assert!((p.get() - 0.7).abs() < 1e-6);
}
#[test]
fn bounce_peaks_at_half_period() {
let p = TimelineTime::new(0.5).bounce(1.0);
assert!((p.get() - 1.0).abs() < 1e-6);
}
#[test]
fn bounce_is_zero_at_period_boundaries() {
assert_eq!(TimelineTime::new(0.0).bounce(1.0).get(), 0.0);
assert_eq!(TimelineTime::new(1.0).bounce(1.0).get(), 0.0);
assert_eq!(TimelineTime::new(2.0).bounce(1.0).get(), 0.0);
}
#[test]
fn wave_matches_bounce_at_the_landmarks() {
assert!(TimelineTime::new(0.0).wave(2.0).get() < 1e-6);
assert!((TimelineTime::new(1.0).wave(2.0).get() - 1.0).abs() < 1e-6);
assert!(TimelineTime::new(2.0).wave(2.0).get() < 1e-5);
}
#[test]
fn wave_is_smooth_not_linear() {
let triangle = TimelineTime::new(0.25).bounce(2.0).get();
let sine = TimelineTime::new(0.25).wave(2.0).get();
assert!((triangle - 0.25).abs() < 1e-6);
assert!(sine < triangle);
}
#[test]
#[should_panic(expected = "Time::cycle requires a finite positive period")]
fn cycle_rejects_zero_period() {
let _ = TimelineTime::new(1.0).cycle(0.0);
}
#[test]
fn phase_maps_range_to_unit_interval() {
let t = TimelineTime::new(4.0);
assert_eq!(t.phase(3.0, 5.0).get(), 0.5);
}
#[test]
fn phase_clamps_outside_range() {
let before = TimelineTime::new(2.0);
assert_eq!(before.phase(3.0, 5.0).get(), Phase::ZERO.get());
let after = TimelineTime::new(6.0);
assert_eq!(after.phase(3.0, 5.0).get(), Phase::ONE.get());
}
#[test]
#[should_panic(expected = "Time::phase requires finite start/end with end > start")]
fn phase_rejects_equal_bounds() {
let _ = TimelineTime::new(5.0).phase(5.0, 5.0);
}
#[test]
#[should_panic(expected = "Time::phase requires finite start/end with end > start")]
fn phase_rejects_reversed_bounds() {
let _ = TimelineTime::new(1.0).phase(2.0, 1.0);
}
#[test]
fn window_carries_the_interval_and_cursor() {
let w = TimelineTime::new(4.0).window(3.0, 5.0);
assert_eq!(w.start(), 3.0);
assert_eq!(w.end(), 5.0);
assert_eq!(w.current(), 4.0);
assert_eq!(w.phase().get(), 0.5);
}
}