1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
//! Time base and timestamp types.
use crate::rational::Rational;
/// A time base expressed as a rational number of seconds per tick.
///
/// A `TimeBase` of 1/48000 means each timestamp unit is 1/48000 second.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TimeBase(pub Rational);
impl TimeBase {
pub const fn new(num: i64, den: i64) -> Self {
Self(Rational::new(num, den))
}
/// Construct a `TimeBase` representing `1/rate` seconds per tick —
/// the canonical "sample-rate-style" base used by audio codecs
/// (`1/48000` for 48 kHz PCM, `1/44100` for CD audio, `1/8000` for
/// G.711) and by the common video bases (`1/90000` for MPEG-TS,
/// `1/1000000` for microsecond PTS).
///
/// Equivalent to `TimeBase::new(1, rate as i64)`, but reads more
/// clearly at call sites and documents the inverse-of-rate
/// convention so a reader doesn't have to mentally swap arguments.
pub const fn from_rate(rate: u32) -> Self {
Self(Rational::new(1, rate as i64))
}
/// `num` of the underlying [`Rational`]. Sugar over `tb.0.num` for
/// callers that don't want to reach through the tuple-struct field.
pub const fn num(&self) -> i64 {
self.0.num
}
/// `den` of the underlying [`Rational`]. Sugar over `tb.0.den`.
pub const fn den(&self) -> i64 {
self.0.den
}
pub fn as_rational(&self) -> Rational {
self.0
}
/// `true` when this time base is usable for rescaling — both terms
/// non-zero. A zero denominator denotes "no defined time base" (the
/// `1/0` placeholder some demuxers stamp on data-only streams);
/// callers that want to skip rescaling on those streams can branch
/// on `is_valid()` instead of re-doing the same `den != 0 && num != 0`
/// check at every call site.
pub const fn is_valid(&self) -> bool {
self.0.num != 0 && self.0.den != 0
}
/// Convert a tick count in this time base to seconds.
pub fn seconds_of(&self, ticks: i64) -> f64 {
ticks as f64 * self.0.as_f64()
}
/// Convert a fractional-seconds count to the nearest tick count in
/// this time base. The inverse of [`seconds_of`]: `seconds_of` goes
/// `ticks → seconds`; `ticks_of` goes `seconds → ticks`. Useful
/// for muxers and encoders that have a target wall-clock duration
/// and need to land it on the stream's time base without hand-rolling
/// the divide-and-round at every call site.
///
/// Rounds half-away-from-zero (matches [`rescale`]). On an invalid
/// time base (`is_valid() == false`) or when the result would exceed
/// `i64` range, returns `0` — pick a defaulted timestamp rather than
/// panicking, since callers are typically muxing best-effort output.
pub fn ticks_of(&self, seconds: f64) -> i64 {
// ticks = seconds / (num/den) = seconds * den / num
if !self.is_valid() || !seconds.is_finite() {
return 0;
}
let scaled = seconds * (self.0.den as f64) / (self.0.num as f64);
if !scaled.is_finite() {
return 0;
}
// Half-away-from-zero rounding, matching `rescale`.
let rounded = if scaled >= 0.0 {
(scaled + 0.5).floor()
} else {
(scaled - 0.5).ceil()
};
// Clamp to i64 range.
if rounded >= i64::MAX as f64 {
i64::MAX
} else if rounded <= i64::MIN as f64 {
i64::MIN
} else {
rounded as i64
}
}
/// Rescale a timestamp from this time base to another.
pub fn rescale(&self, ts: i64, target: TimeBase) -> i64 {
rescale(ts, self.0, target.0)
}
}
/// Common time-base constants.
///
/// These are the rates that show up over and over across the workspace:
/// MPEG-TS / RTP video at 90 kHz, microsecond PTS (most demuxers'
/// "expose-everything" base), MKV at 1 ms, and the audio sample rates
/// the codec crates spend most of their lives at. Naming them once
/// removes the magic-numbers-at-call-sites that grep-fishing has to
/// distinguish from random integer literals.
impl TimeBase {
/// 1/1 — one tick per second. The "no rescaling" identity base,
/// useful for placeholders on streams without a defined cadence
/// (e.g. one-shot SVG / image frames).
pub const SECONDS: TimeBase = TimeBase::new(1, 1);
/// 1/1000 — millisecond ticks (Matroska / WebM `Timecode` default).
pub const MILLIS: TimeBase = TimeBase::new(1, 1_000);
/// 1/1_000_000 — microsecond ticks (the base most demuxers expose
/// to consumers when they want the finest sane resolution without
/// going to nanoseconds).
pub const MICROS: TimeBase = TimeBase::new(1, 1_000_000);
/// 1/1_000_000_000 — nanosecond ticks.
pub const NANOS: TimeBase = TimeBase::new(1, 1_000_000_000);
/// 1/90000 — 90 kHz, the MPEG-TS / RTP video PTS clock.
pub const MPEG_TS: TimeBase = TimeBase::new(1, 90_000);
/// 1/48000 — 48 kHz audio sample-clock (Opus, AC-3, most modern
/// AAC, DTS).
pub const AUDIO_48K: TimeBase = TimeBase::new(1, 48_000);
/// 1/44100 — 44.1 kHz audio sample-clock (CD audio, MP3 at 44.1,
/// many FLAC streams).
pub const AUDIO_44K1: TimeBase = TimeBase::new(1, 44_100);
/// 1/8000 — 8 kHz audio sample-clock (G.711, G.722, G.729, AMR-NB).
pub const AUDIO_8K: TimeBase = TimeBase::new(1, 8_000);
}
/// A timestamp in a particular time base.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Timestamp {
pub value: i64,
pub base: TimeBase,
}
impl Timestamp {
pub const fn new(value: i64, base: TimeBase) -> Self {
Self { value, base }
}
/// Construct a timestamp at `seconds` in the given `base`, rounded
/// to the nearest tick. Sugar over `Timestamp::new(base.ticks_of(s), base)`.
pub fn from_seconds(seconds: f64, base: TimeBase) -> Self {
Self::new(base.ticks_of(seconds), base)
}
pub fn seconds(&self) -> f64 {
self.base.seconds_of(self.value)
}
pub fn rescale(&self, target: TimeBase) -> Self {
Self {
value: self.base.rescale(self.value, target),
base: target,
}
}
/// Advance the timestamp by `ticks` units in its own base. Returns
/// `None` on `i64` overflow rather than wrapping silently — muxers
/// that compute a packet-end timestamp at the edge of the
/// representable range get a clean signal instead of a wrap.
pub fn checked_add_ticks(&self, ticks: i64) -> Option<Self> {
self.value.checked_add(ticks).map(|v| Self {
value: v,
base: self.base,
})
}
/// Move the timestamp backwards by `ticks` units in its own base.
/// Returns `None` on `i64` overflow.
pub fn checked_sub_ticks(&self, ticks: i64) -> Option<Self> {
self.value.checked_sub(ticks).map(|v| Self {
value: v,
base: self.base,
})
}
/// Tick-difference `self - other` after rescaling `other` onto
/// `self`'s base. Returns `None` when the subtraction would overflow
/// `i64` (rare in practice but easy to surface cleanly).
///
/// Use this to compute the duration between two `Timestamp`s that
/// may have been produced by different sources (e.g. a packet from a
/// container demuxer minus a packet from a different demuxer in a
/// remux pipeline).
pub fn checked_diff(&self, other: Timestamp) -> Option<i64> {
let other_in_self_base = other.rescale(self.base).value;
self.value.checked_sub(other_in_self_base)
}
}
/// Rescale a value from one rational time base to another using 128-bit
/// intermediate arithmetic to avoid overflow. Rounding is half-away-from-zero:
/// a tie rounds toward the larger magnitude (e.g. `+1.5 → +2`, `-1.5 → -2`),
/// which the sign-aware `± half` adjustment below implements.
pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 {
// value * (from.num/from.den) / (to.num/to.den)
// = value * from.num * to.den / (from.den * to.num)
let num = from.num as i128 * to.den as i128;
let den = from.den as i128 * to.num as i128;
if den == 0 {
return 0;
}
let prod = value as i128 * num;
let half = den.abs() / 2;
let rounded = if (prod >= 0) == (den > 0) {
(prod + half) / den
} else {
(prod - half) / den
};
rounded as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rescale_samples_to_pts() {
// 48000 samples at 1/48000 base → 1 second at 1/1000 base = 1000 ticks
assert_eq!(
rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
1000
);
}
#[test]
fn timestamp_seconds() {
let ts = Timestamp::new(48000, TimeBase::new(1, 48000));
assert!((ts.seconds() - 1.0).abs() < 1e-9);
}
#[test]
fn rescale_rounds_half_away_from_zero() {
// 1 tick at 1/2 s/tick → 1/1 base = 0.5 → ties up to 1.
assert_eq!(rescale(1, Rational::new(1, 2), Rational::new(1, 1)), 1);
// -1 tick at 1/2 s/tick → -0.5 → ties to -1 (away from zero).
assert_eq!(rescale(-1, Rational::new(1, 2), Rational::new(1, 1)), -1);
// 3 ticks at 1/2 → 1.5 → 2.
assert_eq!(rescale(3, Rational::new(1, 2), Rational::new(1, 1)), 2);
assert_eq!(rescale(-3, Rational::new(1, 2), Rational::new(1, 1)), -2);
}
#[test]
fn from_rate_matches_long_form() {
assert_eq!(TimeBase::from_rate(48_000), TimeBase::new(1, 48_000));
assert_eq!(TimeBase::from_rate(90_000), TimeBase::new(1, 90_000));
assert_eq!(TimeBase::from_rate(1), TimeBase::new(1, 1));
}
#[test]
fn num_den_accessors() {
let tb = TimeBase::new(1, 90_000);
assert_eq!(tb.num(), 1);
assert_eq!(tb.den(), 90_000);
// Const-context callable.
const NUM: i64 = TimeBase::AUDIO_48K.num();
const DEN: i64 = TimeBase::AUDIO_48K.den();
assert_eq!(NUM, 1);
assert_eq!(DEN, 48_000);
}
#[test]
fn is_valid_rejects_zero_terms() {
assert!(TimeBase::new(1, 1000).is_valid());
// Den == 0: undefined rate.
assert!(!TimeBase::new(1, 0).is_valid());
// Num == 0: degenerate ratio (everything is zero seconds).
assert!(!TimeBase::new(0, 1).is_valid());
}
#[test]
fn ticks_of_is_inverse_of_seconds_of() {
// 1 second on a 1/48000 base = 48000 ticks.
assert_eq!(TimeBase::AUDIO_48K.ticks_of(1.0), 48_000);
// 1 second on a 1/90000 base = 90000 ticks.
assert_eq!(TimeBase::MPEG_TS.ticks_of(1.0), 90_000);
// 0.5 second on 1/1000 base = 500 ticks.
assert_eq!(TimeBase::MILLIS.ticks_of(0.5), 500);
// Round-trip on integer multiples.
let tb = TimeBase::AUDIO_44K1;
assert_eq!(tb.ticks_of(tb.seconds_of(44_100)), 44_100);
}
#[test]
fn ticks_of_rounds_half_away_from_zero() {
// 0.5 tick on 1/1 base → 1 (positive ties up).
assert_eq!(TimeBase::SECONDS.ticks_of(0.5), 1);
// -0.5 tick on 1/1 base → -1 (negative ties down).
assert_eq!(TimeBase::SECONDS.ticks_of(-0.5), -1);
// 1.5 ticks → 2.
assert_eq!(TimeBase::SECONDS.ticks_of(1.5), 2);
// -1.5 ticks → -2.
assert_eq!(TimeBase::SECONDS.ticks_of(-1.5), -2);
}
#[test]
fn ticks_of_invalid_inputs() {
// Invalid time base → 0.
assert_eq!(TimeBase::new(1, 0).ticks_of(1.0), 0);
assert_eq!(TimeBase::new(0, 1).ticks_of(1.0), 0);
// Non-finite seconds → 0.
assert_eq!(TimeBase::MILLIS.ticks_of(f64::NAN), 0);
assert_eq!(TimeBase::MILLIS.ticks_of(f64::INFINITY), 0);
assert_eq!(TimeBase::MILLIS.ticks_of(f64::NEG_INFINITY), 0);
}
#[test]
fn common_constants_match_long_form() {
assert_eq!(TimeBase::SECONDS, TimeBase::new(1, 1));
assert_eq!(TimeBase::MILLIS, TimeBase::new(1, 1_000));
assert_eq!(TimeBase::MICROS, TimeBase::new(1, 1_000_000));
assert_eq!(TimeBase::NANOS, TimeBase::new(1, 1_000_000_000));
assert_eq!(TimeBase::MPEG_TS, TimeBase::new(1, 90_000));
assert_eq!(TimeBase::AUDIO_48K, TimeBase::new(1, 48_000));
assert_eq!(TimeBase::AUDIO_44K1, TimeBase::new(1, 44_100));
assert_eq!(TimeBase::AUDIO_8K, TimeBase::new(1, 8_000));
}
#[test]
fn timestamp_from_seconds() {
let ts = Timestamp::from_seconds(1.0, TimeBase::AUDIO_48K);
assert_eq!(ts.value, 48_000);
assert_eq!(ts.base, TimeBase::AUDIO_48K);
// Round-trip.
assert!((ts.seconds() - 1.0).abs() < 1e-9);
}
#[test]
fn checked_add_sub_ticks_round_trip() {
let ts = Timestamp::new(100, TimeBase::MILLIS);
assert_eq!(ts.checked_add_ticks(50).unwrap().value, 150);
assert_eq!(ts.checked_sub_ticks(50).unwrap().value, 50);
// Base unchanged through the arithmetic.
assert_eq!(ts.checked_add_ticks(50).unwrap().base, TimeBase::MILLIS);
}
#[test]
fn checked_add_ticks_detects_overflow() {
let ts = Timestamp::new(i64::MAX - 5, TimeBase::SECONDS);
assert!(ts.checked_add_ticks(10).is_none());
// Boundary case: i64::MAX exactly is fine.
let near_max = Timestamp::new(i64::MAX - 1, TimeBase::SECONDS);
assert_eq!(near_max.checked_add_ticks(1).unwrap().value, i64::MAX);
}
#[test]
fn checked_sub_ticks_detects_overflow() {
let ts = Timestamp::new(i64::MIN + 5, TimeBase::SECONDS);
assert!(ts.checked_sub_ticks(10).is_none());
}
#[test]
fn checked_diff_rescales_other_onto_self_base() {
// 1 second at 1/48000 minus 500ms at 1/1000 = 500ms = 24000 ticks at 48k.
let a = Timestamp::new(48_000, TimeBase::AUDIO_48K); // 1.0s
let b = Timestamp::new(500, TimeBase::MILLIS); // 0.5s
assert_eq!(a.checked_diff(b), Some(24_000));
}
#[test]
fn checked_diff_same_base() {
let a = Timestamp::new(1000, TimeBase::MILLIS);
let b = Timestamp::new(250, TimeBase::MILLIS);
assert_eq!(a.checked_diff(b), Some(750));
assert_eq!(b.checked_diff(a), Some(-750));
}
}