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-to-even
57/// like FFmpeg's `av_rescale_q`.
58pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 {
59    // value * (from.num/from.den) / (to.num/to.den)
60    //   = value * from.num * to.den / (from.den * to.num)
61    let num = from.num as i128 * to.den as i128;
62    let den = from.den as i128 * to.num as i128;
63    if den == 0 {
64        return 0;
65    }
66    let prod = value as i128 * num;
67    let half = den.abs() / 2;
68    let rounded = if (prod >= 0) == (den > 0) {
69        (prod + half) / den
70    } else {
71        (prod - half) / den
72    };
73    rounded as i64
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn rescale_samples_to_pts() {
82        // 48000 samples at 1/48000 base → 1 second at 1/1000 base = 1000 ticks
83        assert_eq!(
84            rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
85            1000
86        );
87    }
88
89    #[test]
90    fn timestamp_seconds() {
91        let ts = Timestamp::new(48000, TimeBase::new(1, 48000));
92        assert!((ts.seconds() - 1.0).abs() < 1e-9);
93    }
94}