Skip to main content

oxideav_core/
time.rs

1//! Time base and timestamp types.
2
3use crate::rational::Rational;
4
5/// A time base expressed as a rational number of seconds per tick.
6///
7/// A `TimeBase` of 1/48000 means each timestamp unit is 1/48000 second.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct TimeBase(pub Rational);
10
11impl TimeBase {
12    pub const fn new(num: i64, den: i64) -> Self {
13        Self(Rational::new(num, den))
14    }
15
16    pub fn as_rational(&self) -> Rational {
17        self.0
18    }
19
20    /// Convert a tick count in this time base to seconds.
21    pub fn seconds_of(&self, ticks: i64) -> f64 {
22        ticks as f64 * self.0.as_f64()
23    }
24
25    /// Rescale a timestamp from this time base to another.
26    pub fn rescale(&self, ts: i64, target: TimeBase) -> i64 {
27        rescale(ts, self.0, target.0)
28    }
29}
30
31/// A timestamp in a particular time base.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33pub struct Timestamp {
34    pub value: i64,
35    pub base: TimeBase,
36}
37
38impl Timestamp {
39    pub const fn new(value: i64, base: TimeBase) -> Self {
40        Self { value, base }
41    }
42
43    pub fn seconds(&self) -> f64 {
44        self.base.seconds_of(self.value)
45    }
46
47    pub fn rescale(&self, target: TimeBase) -> Self {
48        Self {
49            value: self.base.rescale(self.value, target),
50            base: target,
51        }
52    }
53}
54
55/// Rescale a value from one rational time base to another using 128-bit
56/// intermediate arithmetic to avoid overflow. Rounding is half-away-from-zero:
57/// a tie rounds toward the larger magnitude (e.g. `+1.5 → +2`, `-1.5 → -2`),
58/// which the sign-aware `± half` adjustment below implements.
59pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 {
60    // value * (from.num/from.den) / (to.num/to.den)
61    //   = value * from.num * to.den / (from.den * to.num)
62    let num = from.num as i128 * to.den as i128;
63    let den = from.den as i128 * to.num as i128;
64    if den == 0 {
65        return 0;
66    }
67    let prod = value as i128 * num;
68    let half = den.abs() / 2;
69    let rounded = if (prod >= 0) == (den > 0) {
70        (prod + half) / den
71    } else {
72        (prod - half) / den
73    };
74    rounded as i64
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn rescale_samples_to_pts() {
83        // 48000 samples at 1/48000 base → 1 second at 1/1000 base = 1000 ticks
84        assert_eq!(
85            rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
86            1000
87        );
88    }
89
90    #[test]
91    fn timestamp_seconds() {
92        let ts = Timestamp::new(48000, TimeBase::new(1, 48000));
93        assert!((ts.seconds() - 1.0).abs() < 1e-9);
94    }
95
96    #[test]
97    fn rescale_rounds_half_away_from_zero() {
98        // 1 tick at 1/2 s/tick → 1/1 base = 0.5 → ties up to 1.
99        assert_eq!(rescale(1, Rational::new(1, 2), Rational::new(1, 1)), 1);
100        // -1 tick at 1/2 s/tick → -0.5 → ties to -1 (away from zero).
101        assert_eq!(rescale(-1, Rational::new(1, 2), Rational::new(1, 1)), -1);
102        // 3 ticks at 1/2 → 1.5 → 2.
103        assert_eq!(rescale(3, Rational::new(1, 2), Rational::new(1, 1)), 2);
104        assert_eq!(rescale(-3, Rational::new(1, 2), Rational::new(1, 1)), -2);
105    }
106}