Skip to main content

flexaudio_core/
clock.rs

1//! Helpers for translating timestamps to process-monotonic `i64` nanoseconds.
2//!
3//! Built-in microphone and loopback backends on every OS pass monotonic arrival times
4//! to `RawSink::push`, which discards them: the raw ring carries samples only. The
5//! facade assigns a fresh arrival time at ingest and translates it with
6//! [`ClockNormalizer`]. Device timestamps are not used by the capture pipeline;
7//! these labels do not provide media-clock or cross-stream synchronization.
8
9use std::time::Instant;
10
11/// Returns the current process monotonic clock value in nanoseconds.
12///
13/// A monotonic value based on [`Instant`]. It is not an absolute wall-clock time and
14/// is meaningful only for differences and ordering within the same process.
15pub fn monotonic_now_ns() -> i64 {
16    monotonic_base().elapsed().as_nanos() as i64
17}
18
19/// Process-wide monotonic clock origin, initialized on first use.
20fn monotonic_base() -> Instant {
21    use std::sync::OnceLock;
22    static BASE: OnceLock<Instant> = OnceLock::new();
23    *BASE.get_or_init(Instant::now)
24}
25
26/// Translates input timestamps to the process-monotonic clock.
27///
28/// Records the input timestamp and monotonic clock origins on the first call.
29/// Later calls return `device_pts - device_origin + monotonic_origin`, preserving
30/// input deltas. Despite the parameter names, the facade supplies ingest arrival
31/// times ([`monotonic_now_ns`]) for both microphone and loopback, not device PTS.
32#[derive(Debug, Clone)]
33pub struct ClockNormalizer {
34    /// Input timestamp origin recorded on the first call (ns).
35    device_origin_ns: Option<i64>,
36    /// Monotonic clock origin recorded on the first sample (ns).
37    monotonic_origin_ns: i64,
38}
39
40impl ClockNormalizer {
41    /// Creates a normalizer with unset origins, which are fixed by the first [`normalize`](Self::normalize).
42    pub fn new() -> Self {
43        Self {
44            device_origin_ns: None,
45            monotonic_origin_ns: 0,
46        }
47    }
48
49    /// Returns whether the origins are still unset (the first sample has not arrived).
50    pub fn is_unset(&self) -> bool {
51        self.device_origin_ns.is_none()
52    }
53
54    /// Maps an input timestamp (ns) to the process-monotonic timeline (ns).
55    ///
56    /// The first call fixes the origins and uses the current [`monotonic_now_ns`] as
57    /// the monotonic origin. Later calls preserve input timestamp deltas.
58    /// Input timestamp rollover uses wrapping subtraction; a translated value outside i64
59    /// saturates at the nearest boundary rather than panicking or wrapping the timeline.
60    pub fn normalize(&mut self, device_pts_ns: i64) -> i64 {
61        match self.device_origin_ns {
62            Some(origin) => self
63                .monotonic_origin_ns
64                .saturating_add(device_pts_ns.wrapping_sub(origin)),
65            None => {
66                self.device_origin_ns = Some(device_pts_ns);
67                self.monotonic_origin_ns = monotonic_now_ns();
68                self.monotonic_origin_ns
69            }
70        }
71    }
72}
73
74impl Default for ClockNormalizer {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn monotonic_is_non_decreasing() {
86        let a = monotonic_now_ns();
87        let b = monotonic_now_ns();
88        assert!(b >= a);
89    }
90
91    #[test]
92    fn first_sample_sets_origin_and_preserves_deltas() {
93        let mut n = ClockNormalizer::new();
94        assert!(n.is_unset());
95        // On the first call, fix the device origin and return the monotonic origin.
96        let t0 = n.normalize(1_000_000); // device pts 1ms
97        assert!(!n.is_unset());
98        // Preserve device deltas: +5ms produces a normalized value that is also +5ms.
99        let t1 = n.normalize(6_000_000);
100        assert_eq!(t1 - t0, 5_000_000);
101        // Preserve backward movement as a negative delta for gap detection by callers.
102        let t2 = n.normalize(4_000_000);
103        assert_eq!(t2 - t0, 3_000_000);
104    }
105
106    /// `normalize` computes deltas with `wrapping_sub`. If `device_pts` overflows near
107    /// `i64::MAX`, it does not panic; the wrapped delta is added to the monotonic origin.
108    /// Verify that +1 from origin `i64::MAX` wraps to `i64::MIN`, and `wrapping_sub`
109    /// yields `i64::MIN - i64::MAX = +1` (wrapped).
110    #[test]
111    fn normalize_wrapping_sub_handles_i64_boundary() {
112        let mut n = ClockNormalizer::new();
113        // Fix the origin at i64::MAX (the monotonic origin is the current monotonic_now_ns).
114        let base = n.normalize(i64::MAX);
115        // Set device_pts to i64::MIN to simulate clock overflow.
116        // wrapping_sub(i64::MAX) wraps to +1 (i64::MIN - i64::MAX = 1 mod 2^64).
117        let next = n.normalize(i64::MIN);
118        assert_eq!(
119            next.wrapping_sub(base),
120            1,
121            "wrapping_sub boundary: MIN - MAX should wrap to +1"
122        );
123    }
124
125    /// Verify that the first normalize changes `is_unset` to false on a fresh normalizer.
126    /// Even for a large negative device_pts, the first call returns the current monotonic
127    /// origin without calculating a delta.
128    #[test]
129    fn first_normalize_ignores_device_value_for_origin() {
130        let mut n = ClockNormalizer::new();
131        // The first call returns monotonic_now_ns regardless of the device value; it only fixes the origins.
132        let before = monotonic_now_ns();
133        let t0 = n.normalize(i64::MIN);
134        let after = monotonic_now_ns();
135        assert!(
136            t0 >= before && t0 <= after,
137            "first call should return the monotonic origin: {before} <= {t0} <= {after}"
138        );
139        assert!(!n.is_unset());
140    }
141
142    /// `Default` creates the same unset state as `new`.
143    #[test]
144    fn default_is_unset_like_new() {
145        let n = ClockNormalizer::default();
146        assert!(n.is_unset());
147    }
148}
149
150#[cfg(test)]
151mod saturation_tests {
152    use super::*;
153    #[test]
154    fn translated_clock_saturates_instead_of_panicking_or_wrapping() {
155        let mut clock = ClockNormalizer {
156            device_origin_ns: Some(0),
157            monotonic_origin_ns: 10,
158        };
159        assert_eq!(clock.normalize(i64::MAX), i64::MAX);
160        clock.monotonic_origin_ns = -10;
161        assert_eq!(clock.normalize(i64::MIN), i64::MIN);
162    }
163}