Skip to main content

broadcast_common/
clock33.rs

1//! Generic 33-bit wrapping-clock helpers.
2//!
3//! ISO/IEC 13818-1 §2.4.3.7 samples a 90 kHz clock into a 33-bit PTS/DTS
4//! field; ANSI/SCTE 35 §9.2 `pts_time` reuses the identical 2^33 modulus so a
5//! splice cue can be compared against the same clock. Both wrap roughly every
6//! 26.5 hours, and any long-lived consumer that needs an ever-growing
7//! timeline, or just needs to compare two nearby samples correctly across a
8//! wrap boundary, needs the same handful of primitives. Before this module
9//! existed, four crates (`timed-metadata`, `transmux`, `media-doctor`,
10//! `compliance-probe`) each hand-rolled their own copy; an overrun or
11//! wrap-direction fix in one reached none of the others. This module is now
12//! the single owner both algorithms live in.
13//!
14//! `transmux` (a container-muxing hub) cannot take a dependency on
15//! `timed-metadata` (a DPI/timed-metadata *signalling* conversion crate several
16//! layers up the stack, pulling in `scte35-splice`/`mp4-emsg`) without an
17//! inverted, heavy dependency edge, and the primitive itself has no
18//! dependencies of its own — so it lives here, in the crate every one of the
19//! four already depends on, rather than promoting one sibling to depend on
20//! another.
21//!
22//! Two independent operations live here, because they answer different
23//! questions and must not be collapsed into one:
24//!
25//! - [`unwrap_delta`] — extend a running **unwrapped** (ever-growing, signed)
26//!   accumulator by the next raw sample, correcting for exactly one wrap in
27//!   *either* direction. Used to turn a repeating hardware counter into an
28//!   absolute timeline (PTS/DTS unrolling across a capture, including
29//!   B-frame reordering that dips slightly backward without crossing a
30//!   wrap).
31//! - [`wrapping_forward_distance`] — the modular forward distance from one
32//!   already-comparable raw value to another, with no accumulator or history
33//!   at all. Used to classify a single pair of values as "in order" vs
34//!   "wrapped/out of order" when the caller already knows the two are
35//!   supposed to be close in time (e.g. a decode-order monotonicity check,
36//!   or a splice cue's `pts_time` judged against a reference "now").
37
38/// The 33-bit modulus (2^33) shared by MPEG-2 Systems PTS/DTS (ISO/IEC
39/// 13818-1 §2.4.3.7) and SCTE-35 `pts_time` (ANSI/SCTE 35 §9.2) — both a
40/// 90 kHz clock sampled into a 33-bit field.
41pub const WRAP_33BIT: u64 = 1 << 33;
42
43/// Half of [`WRAP_33BIT`] — the threshold distinguishing a genuine backward
44/// step from a legal wrap.
45pub const WRAP_33BIT_HALF: u64 = WRAP_33BIT / 2;
46
47/// Extend a running unwrapped 33-bit clock by the delta to the next raw
48/// value, correcting for a single wrap in either direction.
49///
50/// The delta is computed on the wrapped clock (a signed value in
51/// `(-2^32, 2^32]`), then applied to the unwrapped accumulator — so an
52/// ordinary small backward step (e.g. B-frame PTS reordering) is preserved
53/// as-is, and only a near-full-range jump is treated as a wrap.
54/// `prev_unwrapped` need not itself be in `[0, 2^33)`; after the first wrap
55/// it grows (or, in a reorder that dips across the origin before any wrap
56/// has happened, can go slightly negative) without bound.
57///
58/// This is deliberately **bidirectional**: a naive "epoch counter that only
59/// ever increments" unroller (which is what this replaced in
60/// `timed-metadata`) gets a rare-but-real case wrong — a small backward
61/// reorder that happens to straddle the wrap boundary (e.g. previous raw `2`,
62/// next raw `2^33 - 3`, a legitimate 5-tick backward step) is
63/// indistinguishable, from an epoch-counter's point of view, from a huge
64/// forward jump, and it reports the latter. Computing the delta first and
65/// only then deciding whether it wrapped gets both directions right.
66#[must_use]
67pub fn unwrap_delta(prev_unwrapped: i128, prev_raw: u64, raw: u64) -> i128 {
68    let mut delta = raw as i128 - prev_raw as i128;
69    if delta > WRAP_33BIT_HALF as i128 {
70        delta -= WRAP_33BIT as i128; // wrapped backward across 2^33
71    } else if delta < -(WRAP_33BIT_HALF as i128) {
72        delta += WRAP_33BIT as i128; // wrapped forward across 2^33
73    }
74    prev_unwrapped + delta
75}
76
77/// The modular forward distance from `from` to `to` on the 33-bit clock:
78/// `(to - from) mod 2^33`, always in `[0, 2^33)`.
79///
80/// A distance greater than [`WRAP_33BIT_HALF`] means `to` is "behind" `from`
81/// on the wrapped clock, not genuinely more than `2^32` ticks ahead — the
82/// same wrap-vs-past ambiguity [`unwrap_delta`] resolves using history; this
83/// function resolves it using only the half-range convention (no state),
84/// which is enough when the caller already knows the two values are
85/// supposed to be close in time.
86#[must_use]
87pub fn wrapping_forward_distance(from: u64, to: u64) -> u64 {
88    to.wrapping_sub(from) % WRAP_33BIT
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn unwrap_delta_forward_wrap_advances_by_one_modulus() {
97        // prev near the top of the range, next small: a legitimate forward
98        // wrap of +8 ticks, not a ~2^33-tick backward jump.
99        let prev_unwrapped = (WRAP_33BIT - 10) as i128;
100        let got = unwrap_delta(prev_unwrapped, WRAP_33BIT - 10, 5);
101        assert_eq!(got, prev_unwrapped + 15);
102        assert_eq!(got, 5 + WRAP_33BIT as i128);
103    }
104
105    #[test]
106    fn unwrap_delta_small_forward_step_is_identity_shift() {
107        assert_eq!(unwrap_delta(1_000, 1_000, 2_000), 2_000);
108    }
109
110    #[test]
111    fn unwrap_delta_small_backward_step_is_preserved_not_wrapped() {
112        // Ordinary B-frame reordering: a small backward step within an
113        // epoch must NOT be treated as a wrap.
114        assert_eq!(unwrap_delta(2_000, 2_000, 1_995), 1_995);
115    }
116
117    /// MUTATION-PROOF: a reorder that straddles the origin (previous raw
118    /// value small, next raw value near the top of the range, representing a
119    /// genuine small *backward* step across 0) must unwrap to a small
120    /// negative delta, not a huge forward jump. This is exactly the case a
121    /// naive forward-only epoch counter (what `timed_metadata::Timeline`
122    /// used before this module existed) gets wrong. Verified by temporarily
123    /// deleting the `delta > WRAP_33BIT_HALF` branch below (so only forward
124    /// wraps are corrected): this test then fails with `got = 2^33 - 3`
125    /// instead of `-3`, confirming the branch is load-bearing. Restored.
126    #[test]
127    fn unwrap_delta_backward_reorder_across_origin_stays_small_and_negative() {
128        let got = unwrap_delta(2, 2, WRAP_33BIT - 3);
129        assert_eq!(got, -3);
130    }
131
132    #[test]
133    fn wrapping_forward_distance_small_forward_is_small() {
134        assert_eq!(wrapping_forward_distance(100, 105), 5);
135    }
136
137    #[test]
138    fn wrapping_forward_distance_wraps_at_modulus() {
139        assert_eq!(wrapping_forward_distance(WRAP_33BIT - 1, 0), 1);
140    }
141
142    #[test]
143    fn wrapping_forward_distance_backward_step_is_large() {
144        // A backward step of 5 reports as (modulus - 5): a huge forward
145        // distance, which callers threshold against `WRAP_33BIT_HALF` to
146        // classify as "actually behind", not "far ahead".
147        let d = wrapping_forward_distance(105, 100);
148        assert_eq!(d, WRAP_33BIT - 5);
149        assert!(d > WRAP_33BIT_HALF);
150    }
151}