1use crate::rational::Rational;
4
5#[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 const fn from_rate(rate: u32) -> Self {
26 Self(Rational::new(1, rate as i64))
27 }
28
29 pub const fn num(&self) -> i64 {
32 self.0.num
33 }
34
35 pub const fn den(&self) -> i64 {
37 self.0.den
38 }
39
40 pub fn as_rational(&self) -> Rational {
41 self.0
42 }
43
44 pub const fn is_valid(&self) -> bool {
51 self.0.num != 0 && self.0.den != 0
52 }
53
54 pub fn seconds_of(&self, ticks: i64) -> f64 {
56 ticks as f64 * self.0.as_f64()
57 }
58
59 pub fn ticks_of(&self, seconds: f64) -> i64 {
71 if !self.is_valid() || !seconds.is_finite() {
73 return 0;
74 }
75 let scaled = seconds * (self.0.den as f64) / (self.0.num as f64);
76 if !scaled.is_finite() {
77 return 0;
78 }
79 let rounded = if scaled >= 0.0 {
81 (scaled + 0.5).floor()
82 } else {
83 (scaled - 0.5).ceil()
84 };
85 if rounded >= i64::MAX as f64 {
87 i64::MAX
88 } else if rounded <= i64::MIN as f64 {
89 i64::MIN
90 } else {
91 rounded as i64
92 }
93 }
94
95 pub fn rescale(&self, ts: i64, target: TimeBase) -> i64 {
97 rescale(ts, self.0, target.0)
98 }
99}
100
101impl TimeBase {
110 pub const SECONDS: TimeBase = TimeBase::new(1, 1);
114
115 pub const MILLIS: TimeBase = TimeBase::new(1, 1_000);
117
118 pub const MICROS: TimeBase = TimeBase::new(1, 1_000_000);
122
123 pub const NANOS: TimeBase = TimeBase::new(1, 1_000_000_000);
125
126 pub const MPEG_TS: TimeBase = TimeBase::new(1, 90_000);
128
129 pub const AUDIO_48K: TimeBase = TimeBase::new(1, 48_000);
132
133 pub const AUDIO_44K1: TimeBase = TimeBase::new(1, 44_100);
136
137 pub const AUDIO_8K: TimeBase = TimeBase::new(1, 8_000);
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
143pub struct Timestamp {
144 pub value: i64,
145 pub base: TimeBase,
146}
147
148impl Timestamp {
149 pub const fn new(value: i64, base: TimeBase) -> Self {
150 Self { value, base }
151 }
152
153 pub fn from_seconds(seconds: f64, base: TimeBase) -> Self {
156 Self::new(base.ticks_of(seconds), base)
157 }
158
159 pub fn seconds(&self) -> f64 {
160 self.base.seconds_of(self.value)
161 }
162
163 pub fn rescale(&self, target: TimeBase) -> Self {
164 Self {
165 value: self.base.rescale(self.value, target),
166 base: target,
167 }
168 }
169
170 pub fn checked_add_ticks(&self, ticks: i64) -> Option<Self> {
175 self.value.checked_add(ticks).map(|v| Self {
176 value: v,
177 base: self.base,
178 })
179 }
180
181 pub fn checked_sub_ticks(&self, ticks: i64) -> Option<Self> {
184 self.value.checked_sub(ticks).map(|v| Self {
185 value: v,
186 base: self.base,
187 })
188 }
189
190 pub fn checked_diff(&self, other: Timestamp) -> Option<i64> {
199 let other_in_self_base = other.rescale(self.base).value;
200 self.value.checked_sub(other_in_self_base)
201 }
202}
203
204pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 {
209 let num = from.num as i128 * to.den as i128;
212 let den = from.den as i128 * to.num as i128;
213 if den == 0 {
214 return 0;
215 }
216 let prod = value as i128 * num;
217 let half = den.abs() / 2;
218 let rounded = if (prod >= 0) == (den > 0) {
219 (prod + half) / den
220 } else {
221 (prod - half) / den
222 };
223 rounded as i64
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn rescale_samples_to_pts() {
232 assert_eq!(
234 rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
235 1000
236 );
237 }
238
239 #[test]
240 fn timestamp_seconds() {
241 let ts = Timestamp::new(48000, TimeBase::new(1, 48000));
242 assert!((ts.seconds() - 1.0).abs() < 1e-9);
243 }
244
245 #[test]
246 fn rescale_rounds_half_away_from_zero() {
247 assert_eq!(rescale(1, Rational::new(1, 2), Rational::new(1, 1)), 1);
249 assert_eq!(rescale(-1, Rational::new(1, 2), Rational::new(1, 1)), -1);
251 assert_eq!(rescale(3, Rational::new(1, 2), Rational::new(1, 1)), 2);
253 assert_eq!(rescale(-3, Rational::new(1, 2), Rational::new(1, 1)), -2);
254 }
255
256 #[test]
257 fn from_rate_matches_long_form() {
258 assert_eq!(TimeBase::from_rate(48_000), TimeBase::new(1, 48_000));
259 assert_eq!(TimeBase::from_rate(90_000), TimeBase::new(1, 90_000));
260 assert_eq!(TimeBase::from_rate(1), TimeBase::new(1, 1));
261 }
262
263 #[test]
264 fn num_den_accessors() {
265 let tb = TimeBase::new(1, 90_000);
266 assert_eq!(tb.num(), 1);
267 assert_eq!(tb.den(), 90_000);
268 const NUM: i64 = TimeBase::AUDIO_48K.num();
270 const DEN: i64 = TimeBase::AUDIO_48K.den();
271 assert_eq!(NUM, 1);
272 assert_eq!(DEN, 48_000);
273 }
274
275 #[test]
276 fn is_valid_rejects_zero_terms() {
277 assert!(TimeBase::new(1, 1000).is_valid());
278 assert!(!TimeBase::new(1, 0).is_valid());
280 assert!(!TimeBase::new(0, 1).is_valid());
282 }
283
284 #[test]
285 fn ticks_of_is_inverse_of_seconds_of() {
286 assert_eq!(TimeBase::AUDIO_48K.ticks_of(1.0), 48_000);
288 assert_eq!(TimeBase::MPEG_TS.ticks_of(1.0), 90_000);
290 assert_eq!(TimeBase::MILLIS.ticks_of(0.5), 500);
292 let tb = TimeBase::AUDIO_44K1;
294 assert_eq!(tb.ticks_of(tb.seconds_of(44_100)), 44_100);
295 }
296
297 #[test]
298 fn ticks_of_rounds_half_away_from_zero() {
299 assert_eq!(TimeBase::SECONDS.ticks_of(0.5), 1);
301 assert_eq!(TimeBase::SECONDS.ticks_of(-0.5), -1);
303 assert_eq!(TimeBase::SECONDS.ticks_of(1.5), 2);
305 assert_eq!(TimeBase::SECONDS.ticks_of(-1.5), -2);
307 }
308
309 #[test]
310 fn ticks_of_invalid_inputs() {
311 assert_eq!(TimeBase::new(1, 0).ticks_of(1.0), 0);
313 assert_eq!(TimeBase::new(0, 1).ticks_of(1.0), 0);
314 assert_eq!(TimeBase::MILLIS.ticks_of(f64::NAN), 0);
316 assert_eq!(TimeBase::MILLIS.ticks_of(f64::INFINITY), 0);
317 assert_eq!(TimeBase::MILLIS.ticks_of(f64::NEG_INFINITY), 0);
318 }
319
320 #[test]
321 fn common_constants_match_long_form() {
322 assert_eq!(TimeBase::SECONDS, TimeBase::new(1, 1));
323 assert_eq!(TimeBase::MILLIS, TimeBase::new(1, 1_000));
324 assert_eq!(TimeBase::MICROS, TimeBase::new(1, 1_000_000));
325 assert_eq!(TimeBase::NANOS, TimeBase::new(1, 1_000_000_000));
326 assert_eq!(TimeBase::MPEG_TS, TimeBase::new(1, 90_000));
327 assert_eq!(TimeBase::AUDIO_48K, TimeBase::new(1, 48_000));
328 assert_eq!(TimeBase::AUDIO_44K1, TimeBase::new(1, 44_100));
329 assert_eq!(TimeBase::AUDIO_8K, TimeBase::new(1, 8_000));
330 }
331
332 #[test]
333 fn timestamp_from_seconds() {
334 let ts = Timestamp::from_seconds(1.0, TimeBase::AUDIO_48K);
335 assert_eq!(ts.value, 48_000);
336 assert_eq!(ts.base, TimeBase::AUDIO_48K);
337 assert!((ts.seconds() - 1.0).abs() < 1e-9);
339 }
340
341 #[test]
342 fn checked_add_sub_ticks_round_trip() {
343 let ts = Timestamp::new(100, TimeBase::MILLIS);
344 assert_eq!(ts.checked_add_ticks(50).unwrap().value, 150);
345 assert_eq!(ts.checked_sub_ticks(50).unwrap().value, 50);
346 assert_eq!(ts.checked_add_ticks(50).unwrap().base, TimeBase::MILLIS);
348 }
349
350 #[test]
351 fn checked_add_ticks_detects_overflow() {
352 let ts = Timestamp::new(i64::MAX - 5, TimeBase::SECONDS);
353 assert!(ts.checked_add_ticks(10).is_none());
354 let near_max = Timestamp::new(i64::MAX - 1, TimeBase::SECONDS);
356 assert_eq!(near_max.checked_add_ticks(1).unwrap().value, i64::MAX);
357 }
358
359 #[test]
360 fn checked_sub_ticks_detects_overflow() {
361 let ts = Timestamp::new(i64::MIN + 5, TimeBase::SECONDS);
362 assert!(ts.checked_sub_ticks(10).is_none());
363 }
364
365 #[test]
366 fn checked_diff_rescales_other_onto_self_base() {
367 let a = Timestamp::new(48_000, TimeBase::AUDIO_48K); let b = Timestamp::new(500, TimeBase::MILLIS); assert_eq!(a.checked_diff(b), Some(24_000));
371 }
372
373 #[test]
374 fn checked_diff_same_base() {
375 let a = Timestamp::new(1000, TimeBase::MILLIS);
376 let b = Timestamp::new(250, TimeBase::MILLIS);
377 assert_eq!(a.checked_diff(b), Some(750));
378 assert_eq!(b.checked_diff(a), Some(-750));
379 }
380}