use crate::error::{ClockError, Result};
const NS_PER_SEC: i64 = 1_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClockAnchor {
pub sample_rate: u32,
pub ref_sample: i64,
pub ref_ptp_ns: i64,
}
impl ClockAnchor {
pub fn new(sample_rate: u32, ref_sample: i64, ref_ptp_ns: i64) -> Result<Self> {
if sample_rate == 0 {
return Err(ClockError::InvalidSampleRate(0));
}
Ok(Self {
sample_rate,
ref_sample,
ref_ptp_ns,
})
}
#[must_use]
pub fn at_origin(sample_rate: u32, ref_ptp_ns: i64) -> Option<Self> {
if sample_rate == 0 {
return None;
}
Some(Self {
sample_rate,
ref_sample: 0,
ref_ptp_ns,
})
}
pub fn sample_to_ptp_checked(&self, sample_idx: i64) -> Result<i64> {
let delta_samples = i128::from(sample_idx) - i128::from(self.ref_sample);
let delta_ns = delta_samples
.checked_mul(i128::from(NS_PER_SEC))
.ok_or(ClockError::ConversionOverflow)?
/ i128::from(self.sample_rate);
let ptp_ns = i128::from(self.ref_ptp_ns)
.checked_add(delta_ns)
.ok_or(ClockError::ConversionOverflow)?;
i64::try_from(ptp_ns).map_err(|_| ClockError::ConversionOverflow)
}
pub fn ptp_to_sample_checked(&self, ptp_ns: i64) -> Result<i64> {
let delta_ns = i128::from(ptp_ns) - i128::from(self.ref_ptp_ns);
let delta_samples = delta_ns
.checked_mul(i128::from(self.sample_rate))
.ok_or(ClockError::ConversionOverflow)?
/ i128::from(NS_PER_SEC);
let sample = i128::from(self.ref_sample)
.checked_add(delta_samples)
.ok_or(ClockError::ConversionOverflow)?;
i64::try_from(sample).map_err(|_| ClockError::ConversionOverflow)
}
}
pub trait ClockSource: Send {
fn ptp_now_ns(&self) -> Result<i64>;
#[must_use]
fn sample_to_ptp(&self, sample_idx: i64) -> i64;
#[must_use]
fn ptp_to_sample(&self, ptp_ns: i64) -> i64;
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: i64, b: i64) -> bool {
(a - b).abs() <= 1
}
#[test]
fn anchor_new_rejects_zero_sample_rate() {
let err = ClockAnchor::new(0, 0, 0).unwrap_err();
assert!(matches!(err, ClockError::InvalidSampleRate(0)));
}
#[test]
fn anchor_at_origin_none_for_zero_rate() {
assert!(ClockAnchor::at_origin(0, 1_000).is_none());
assert!(ClockAnchor::at_origin(48_000, 1_000).is_some());
}
#[test]
fn sample_to_ptp_at_anchor_returns_ref() {
let a = ClockAnchor::new(48_000, 1_000, 5_000_000_000).unwrap();
assert_eq!(a.sample_to_ptp_checked(1_000).unwrap(), 5_000_000_000);
}
#[test]
fn sample_to_ptp_one_second_advances_by_rate() {
let a = ClockAnchor::new(48_000, 0, 0).unwrap();
assert_eq!(a.sample_to_ptp_checked(48_000).unwrap(), NS_PER_SEC);
}
#[test]
fn sample_to_ptp_negative_index_goes_backward() {
let a = ClockAnchor::new(48_000, 48_000, NS_PER_SEC).unwrap();
assert_eq!(a.sample_to_ptp_checked(0).unwrap(), 0);
}
#[test]
fn ptp_to_sample_at_anchor_returns_ref() {
let a = ClockAnchor::new(48_000, 1_000, 5_000_000_000).unwrap();
assert_eq!(a.ptp_to_sample_checked(5_000_000_000).unwrap(), 1_000);
}
#[test]
fn ptp_to_sample_one_second_advances_by_rate() {
let a = ClockAnchor::new(48_000, 0, 0).unwrap();
assert_eq!(a.ptp_to_sample_checked(NS_PER_SEC).unwrap(), 48_000);
}
#[test]
fn round_trip_sample_to_ptp_to_sample_is_identity() {
let a = ClockAnchor::new(48_000, 123_456, 9_000_000_000).unwrap();
for s in [0i64, 1, 123_456, 200_000, -50_000] {
let ptp = a.sample_to_ptp_checked(s).unwrap();
let back = a.ptp_to_sample_checked(ptp).unwrap();
assert!(approx(back, s), "round trip {s} -> {ptp} -> {back}");
}
}
#[test]
fn round_trip_ptp_to_sample_to_ptp_is_identity() {
let a = ClockAnchor::new(44_100, 0, 1_000_000_000).unwrap();
for p in [0i64, 1_000_000_000, 2_500_000_000, -3_000_000_000] {
let s = a.ptp_to_sample_checked(p).unwrap();
let back = a.sample_to_ptp_checked(s).unwrap();
assert!(approx(back, p), "round trip {p} -> {s} -> {back}");
}
}
#[test]
fn non_integer_rate_still_round_trips_within_tolerance() {
let a = ClockAnchor::new(44_100, 0, 0).unwrap();
let ptp = a.sample_to_ptp_checked(44_100).unwrap();
assert_eq!(ptp, NS_PER_SEC);
let back = a.ptp_to_sample_checked(ptp).unwrap();
assert_eq!(back, 44_100);
}
#[test]
fn extreme_sample_does_not_panic() {
let a = ClockAnchor::new(48_000, 0, 0).unwrap();
let _ = a.sample_to_ptp_checked(i64::MAX);
let _ = a.ptp_to_sample_checked(i64::MAX);
}
}