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}