Skip to main content

audio_clock_bsd/
ntp.rs

1//! [`NtpClock`] — the default system-clock fallback for [`ClockSource`].
2//!
3//! [`NtpClock`] anchors the sample-index↔timestamp map to the host wall clock
4//! via [`std::time::SystemTime`]. It needs no daemon and no hardware: it is the
5//! always-available fallback used when no PTP hardware clock is present.
6//!
7//! # Epoch note
8//!
9//! Timestamps are reported as **nanoseconds since the UNIX epoch**
10//! (1970-01-01T00:00:00Z), obtained from [`SystemTime::now`]. This is a
11//! pragmatic "PTP-like" time for the fallback path. A real PTP clock reports
12//! TAI nanoseconds since the PTP epoch; the ~37 s leap-second offset between
13//! TAI and UTC, and the 1970-vs-1900 epoch difference, are a deployment
14//! concern handled by the PTP backend — `NtpClock` deliberately does not model
15//! them.
16
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use crate::clock::{ClockAnchor, ClockSource};
20use crate::error::{ClockError, Result};
21
22/// Reads the current wall-clock time as nanoseconds since the UNIX epoch.
23///
24/// Returns [`ClockError::NotSynced`] if the system clock reports a time before
25/// the epoch, and [`ClockError::ConversionOverflow`] if the nanosecond count
26/// does not fit in `i64` (only possible after roughly the year 2262).
27pub(crate) fn system_now_ns() -> Result<i64> {
28    let duration = SystemTime::now()
29        .duration_since(UNIX_EPOCH)
30        .map_err(|e| ClockError::NotSynced(format!("system clock before epoch: {e}")))?;
31    i64::try_from(duration.as_nanos()).map_err(|_| ClockError::ConversionOverflow)
32}
33
34/// A [`ClockSource`] anchored to the host wall clock (NTP fallback).
35///
36/// On construction, sample index `0` is anchored to the current wall-clock
37/// time. The anchor may be moved later with [`NtpClock::reanchor`] (e.g. when
38/// an audio device reports its actual start time) and inspected with
39/// [`NtpClock::anchor`].
40pub struct NtpClock {
41    anchor: ClockAnchor,
42}
43
44impl std::fmt::Debug for NtpClock {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("NtpClock")
47            .field("anchor", &self.anchor)
48            .finish()
49    }
50}
51
52impl NtpClock {
53    /// Creates a clock anchored at sample `0` to the current wall-clock time.
54    ///
55    /// # Errors
56    ///
57    /// - [`ClockError::InvalidSampleRate`] when `sample_rate == 0`.
58    /// - [`ClockError::NotSynced`] when the system clock is before the epoch.
59    pub fn new(sample_rate: u32) -> Result<Self> {
60        let now_ns = system_now_ns()?;
61        Ok(Self {
62            anchor: ClockAnchor::new(sample_rate, 0, now_ns)?,
63        })
64    }
65
66    /// Creates a clock over a pre-built anchor (e.g. restored from a session).
67    #[must_use]
68    pub fn with_anchor(anchor: ClockAnchor) -> Self {
69        Self { anchor }
70    }
71
72    /// Returns the current anchor.
73    #[must_use]
74    pub fn anchor(&self) -> ClockAnchor {
75        self.anchor
76    }
77
78    /// Re-anchors so that `ref_sample` maps to the *current* wall-clock time.
79    ///
80    /// Useful when an audio device reports the actual start sample after open.
81    ///
82    /// # Errors
83    ///
84    /// - [`ClockError::NotSynced`] when the system clock is before the epoch.
85    pub fn reanchor(&mut self, ref_sample: i64) -> Result<()> {
86        let now_ns = system_now_ns()?;
87        self.anchor = ClockAnchor::new(self.anchor.sample_rate, ref_sample, now_ns)?;
88        Ok(())
89    }
90}
91
92impl ClockSource for NtpClock {
93    fn ptp_now_ns(&self) -> Result<i64> {
94        system_now_ns()
95    }
96
97    fn sample_to_ptp(&self, sample_idx: i64) -> i64 {
98        self.anchor
99            .sample_to_ptp_checked(sample_idx)
100            .unwrap_or(i64::MIN)
101    }
102
103    fn ptp_to_sample(&self, ptp_ns: i64) -> i64 {
104        self.anchor
105            .ptp_to_sample_checked(ptp_ns)
106            .unwrap_or(i64::MIN)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn new_anchors_sample_zero_to_now() {
116        let before = system_now_ns().unwrap();
117        let clock = NtpClock::new(48_000).unwrap();
118        let after = system_now_ns().unwrap();
119        // Sample 0 must map to within the construction window.
120        let t0 = clock.sample_to_ptp(0);
121        assert!(
122            t0 >= before && t0 <= after,
123            "t0={t0} not in [{before},{after}]"
124        );
125    }
126
127    #[test]
128    fn new_rejects_zero_rate() {
129        let err = NtpClock::new(0).unwrap_err();
130        assert!(matches!(err, ClockError::InvalidSampleRate(0)));
131    }
132
133    #[test]
134    fn ptp_now_ns_is_monotonic_within_window() {
135        let clock = NtpClock::new(48_000).unwrap();
136        let a = clock.ptp_now_ns().unwrap();
137        let b = clock.ptp_now_ns().unwrap();
138        assert!(b >= a, "wall clock went backwards: {a} -> {b}");
139    }
140
141    #[test]
142    fn sample_to_ptp_advances_with_rate() {
143        let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
144        // 48000 samples == 1 s == 1e9 ns.
145        assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
146    }
147
148    #[test]
149    fn ptp_to_sample_inverse_of_anchor() {
150        let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 100, 2_000_000_000).unwrap());
151        assert_eq!(clock.ptp_to_sample(2_000_000_000), 100);
152    }
153
154    #[test]
155    fn reanchor_moves_the_reference_sample() {
156        let mut clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 0).unwrap());
157        let before = system_now_ns().unwrap();
158        clock.reanchor(1_000).unwrap();
159        // After reanchor, sample 1000 maps to the reanchor moment.
160        let t = clock.sample_to_ptp(1_000);
161        let after = system_now_ns().unwrap();
162        assert!(t >= before && t <= after, "t={t} not in [{before},{after}]");
163    }
164
165    #[test]
166    fn dyn_clock_source_object_compiles() {
167        let clock: Box<dyn ClockSource> = Box::new(NtpClock::new(48_000).unwrap());
168        assert!(clock.ptp_now_ns().is_ok());
169    }
170}