audio-clock-bsd 0.1.0

PTP/NTP clock synchronization and sample-index to timestamp conversion for real-time audio
Documentation
//! [`PtpClock`] — a `PTPv2` (IEEE 1588-2008) [`ClockSource`] backend.
//!
//! [`PtpClock`] obtains PTP timestamps from an injectable [`PtpTimeProvider`].
//! FreeBSD hardware PTP timestamping support is NIC-dependent (igb/ixl/mlx5en)
//! and not guaranteed, so the default construction ([`PtpClock::unavailable`])
//! reports [`ClockError::Unavailable`] until a provider is supplied — typically
//! by a daemon-polling thread that reads `/dev/ptp*` or queries `ptpd2`.
//!
//! # Threading
//!
//! Like all [`ClockSource`] implementors, [`PtpClock::ptp_now_ns`] may block
//! (the provider might read a device or poll a daemon) and must run off the RT
//! audio thread. The anchor conversions remain allocation-free and RT-safe.

use crate::clock::{ClockAnchor, ClockSource};
use crate::error::{ClockError, Result};

/// A source of live PTP timestamps, injected into [`PtpClock`].
///
/// Implementations typically wrap a daemon-polling thread or a `/dev/ptp*`
/// device read. For tests, a deterministic stub returning a fixed value is
/// sufficient.
pub trait PtpTimeProvider: Send + Sync {
    /// Returns the current PTP timestamp in nanoseconds since the PTP epoch.
    ///
    /// # Errors
    ///
    /// - [`ClockError::Unavailable`] when no PTP source is reachable.
    /// - [`ClockError::NotSynced`] when the source exists but has not locked.
    fn ptp_now_ns(&self) -> Result<i64>;
}

/// A [`ClockSource`] backed by an injectable [`PtpTimeProvider`].
///
/// Construct with [`PtpClock::with_provider`] when a live PTP source is
/// available, or [`PtpClock::unavailable`] for the stub (returns
/// [`ClockError::Unavailable`] from [`ClockSource::ptp_now_ns`]).
pub struct PtpClock {
    anchor: ClockAnchor,
    provider: Box<dyn PtpTimeProvider>,
}

impl std::fmt::Debug for PtpClock {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PtpClock")
            .field("anchor", &self.anchor)
            .field("provider", &"<dyn PtpTimeProvider>")
            .finish()
    }
}

impl PtpClock {
    /// Creates a PTP clock with a live time provider.
    #[must_use]
    pub fn with_provider(anchor: ClockAnchor, provider: Box<dyn PtpTimeProvider>) -> Self {
        Self { anchor, provider }
    }

    /// Creates a PTP clock whose [`ClockSource::ptp_now_ns`] always reports
    /// [`ClockError::Unavailable`].
    ///
    /// The anchor conversions still work, so the clock is usable for offline
    /// timestamp mapping even without a live source.
    #[must_use]
    pub fn unavailable(anchor: ClockAnchor) -> Self {
        Self {
            anchor,
            provider: Box::new(NoPtp),
        }
    }

    /// Returns the current anchor.
    #[must_use]
    pub fn anchor(&self) -> ClockAnchor {
        self.anchor
    }
}

impl ClockSource for PtpClock {
    fn ptp_now_ns(&self) -> Result<i64> {
        self.provider.ptp_now_ns()
    }

    fn sample_to_ptp(&self, sample_idx: i64) -> i64 {
        self.anchor
            .sample_to_ptp_checked(sample_idx)
            .unwrap_or(i64::MIN)
    }

    fn ptp_to_sample(&self, ptp_ns: i64) -> i64 {
        self.anchor
            .ptp_to_sample_checked(ptp_ns)
            .unwrap_or(i64::MIN)
    }
}

/// Default provider when no PTP hardware/daemon is present.
struct NoPtp;

impl PtpTimeProvider for NoPtp {
    fn ptp_now_ns(&self) -> Result<i64> {
        Err(ClockError::Unavailable(
            "no PTP hardware timestamping available".into(),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicI64, Ordering};

    /// A deterministic PTP provider returning a controllable timestamp.
    struct StubProvider {
        t: AtomicI64,
    }

    impl PtpTimeProvider for StubProvider {
        fn ptp_now_ns(&self) -> Result<i64> {
            Ok(self.t.load(Ordering::Relaxed))
        }
    }

    #[test]
    fn unavailable_reports_unavailable() {
        let clock = PtpClock::unavailable(ClockAnchor::new(48_000, 0, 0).unwrap());
        let err = clock.ptp_now_ns().unwrap_err();
        assert!(matches!(err, ClockError::Unavailable(_)));
    }

    #[test]
    fn unavailable_conversions_still_work() {
        let clock = PtpClock::unavailable(ClockAnchor::new(48_000, 0, 0).unwrap());
        assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
        assert_eq!(clock.ptp_to_sample(1_000_000_000), 48_000);
    }

    #[test]
    fn with_provider_reads_live_timestamp() {
        let provider = StubProvider {
            t: AtomicI64::new(7_000_000_000),
        };
        let clock =
            PtpClock::with_provider(ClockAnchor::new(48_000, 0, 0).unwrap(), Box::new(provider));
        assert_eq!(clock.ptp_now_ns().unwrap(), 7_000_000_000);
    }

    #[test]
    fn provider_not_synced_propagates() {
        struct Unsynced;
        impl PtpTimeProvider for Unsynced {
            fn ptp_now_ns(&self) -> Result<i64> {
                Err(ClockError::NotSynced("ptp master not locked".into()))
            }
        }
        let clock =
            PtpClock::with_provider(ClockAnchor::new(48_000, 0, 0).unwrap(), Box::new(Unsynced));
        let err = clock.ptp_now_ns().unwrap_err();
        assert!(matches!(err, ClockError::NotSynced(_)));
    }

    #[test]
    fn dyn_clock_source_object_with_provider() {
        let clock: Box<dyn ClockSource> = Box::new(PtpClock::unavailable(
            ClockAnchor::new(48_000, 0, 0).unwrap(),
        ));
        // ptp_now_ns errors, but conversions work.
        assert!(clock.ptp_now_ns().is_err());
        assert_eq!(clock.sample_to_ptp(0), 0);
    }
}