audio-clock-bsd 0.1.1

PTP/NTP clock synchronization and sample-index to timestamp conversion for real-time audio
Documentation
//! Integration test: the [`ClockSource`] contract exercised end-to-end through
//! the always-available [`NtpClock`] backend, plus the [`PtpClock`] stub.

use audio_clock_bsd::{ClockAnchor, ClockError, ClockSource, NtpClock, PtpClock};
use audio_core_bsd::AudioFrame;

/// A live NtpClock reports a timestamp consistent with its anchor.
#[test]
fn ntp_clock_now_matches_anchor_window() {
    let clock = NtpClock::new(48_000).expect("construct NtpClock");
    let before = clock.ptp_now_ns().expect("ptp_now_ns");
    // The anchor pinned sample 0 to construction time, so sample_to_ptp(0) is
    // close to a freshly-read now() (within the same test window).
    let anchored = clock.sample_to_ptp(0);
    let after = clock.ptp_now_ns().expect("ptp_now_ns");
    assert!(
        anchored >= before - 1_000_000 && anchored <= after + 1_000_000,
        "anchored t0={anchored} drifted outside [{before},{after}]"
    );
}

/// sample_to_ptp and ptp_to_sample are exact inverses at the anchor point.
#[test]
fn conversions_are_exact_at_anchor() {
    let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 9_999, 42_000_000_000).unwrap());
    assert_eq!(clock.sample_to_ptp(9_999), 42_000_000_000);
    assert_eq!(clock.ptp_to_sample(42_000_000_000), 9_999);
}

/// One second of samples maps to exactly 1e9 ns at any rate, and back.
#[test]
fn one_second_round_trip_any_rate() {
    for rate in [44_100u32, 48_000, 96_000] {
        let clock = NtpClock::with_anchor(ClockAnchor::new(rate, 0, 0).unwrap());
        let ptp = clock.sample_to_ptp(i64::from(rate));
        assert_eq!(ptp, 1_000_000_000, "rate {rate}: 1s of samples != 1e9 ns");
        assert_eq!(clock.ptp_to_sample(ptp), i64::from(rate));
    }
}

/// The PtpClock stub reports Unavailable but still converts.
#[test]
fn ptp_stub_unavailable_but_converts() {
    let clock = PtpClock::unavailable(ClockAnchor::new(48_000, 0, 0).unwrap());
    let err = clock.ptp_now_ns().unwrap_err();
    assert!(matches!(err, ClockError::Unavailable(_)));
    // Conversions are anchor-only and independent of the live source.
    assert_eq!(clock.sample_to_ptp(48_000), 1_000_000_000);
}

/// A dyn ClockSource object routes both backends.
#[test]
fn dyn_clock_source_routes_backends() {
    let ntp: Box<dyn ClockSource> = Box::new(NtpClock::new(48_000).unwrap());
    let ptp: Box<dyn ClockSource> = Box::new(PtpClock::unavailable(
        ClockAnchor::new(48_000, 0, 0).unwrap(),
    ));
    assert!(ntp.ptp_now_ns().is_ok());
    assert!(ptp.ptp_now_ns().is_err());
    // Both convert identically with the same anchor shape.
    assert_eq!(ntp.sample_to_ptp(0), ntp.sample_to_ptp(0));
}

/// Timestamps can be attached to an audio frame's worth of samples — the
/// real-world use case for this crate.
#[test]
fn timestamp_an_audio_frame() {
    let clock = NtpClock::with_anchor(ClockAnchor::new(48_000, 0, 1_000_000_000).unwrap());
    // A 256-frame buffer at 48 kHz starts at sample N; its PTP start/finish:
    let frame = AudioFrame::silence(2, 256, 48_000);
    let n = 48_000; // one second in
    let start_ns = clock.sample_to_ptp(n);
    let end_ns = clock.sample_to_ptp(n + i64::try_from(frame.num_frames()).unwrap());
    // 256 frames @ 48 kHz = 5.333 ms = 5_333_333 ns (truncated).
    assert!(end_ns - start_ns >= 5_000_000 && end_ns - start_ns <= 6_000_000);
}