#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone)]
pub struct AudioClock {
sample_rate: u32,
position: u64,
}
impl AudioClock {
pub fn new(sample_rate: u32) -> Self {
Self {
sample_rate,
position: 0,
}
}
pub fn tick(&mut self, frames: u64) {
self.position += frames;
}
pub fn sample_position(&self) -> u64 {
self.position
}
#[allow(clippy::cast_precision_loss)]
pub fn pts_us(&self) -> i64 {
if self.sample_rate == 0 {
return 0;
}
let us = (self.position as f64 / self.sample_rate as f64) * 1_000_000.0;
us as i64
}
pub fn reset(&mut self) {
self.position = 0;
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
#[derive(Debug, Clone)]
pub struct AudioClockSync {
reference_rate: u32,
measured_rate: u32,
ref_samples: u64,
meas_samples: u64,
}
impl AudioClockSync {
pub fn new(reference_rate: u32, measured_rate: u32) -> Self {
Self {
reference_rate,
measured_rate,
ref_samples: 0,
meas_samples: 0,
}
}
pub fn advance_reference(&mut self, frames: u64) {
self.ref_samples += frames;
}
pub fn advance_measured(&mut self, frames: u64) {
self.meas_samples += frames;
}
#[allow(clippy::cast_precision_loss)]
pub fn drift_samples(&self) -> i64 {
if self.reference_rate == 0 || self.measured_rate == 0 {
return 0;
}
let ref_us = (self.ref_samples as f64 / self.reference_rate as f64) * 1_000_000.0;
let meas_us = (self.meas_samples as f64 / self.measured_rate as f64) * 1_000_000.0;
let drift_us = meas_us - ref_us;
((drift_us / 1_000_000.0) * self.reference_rate as f64) as i64
}
#[allow(clippy::cast_precision_loss)]
pub fn compensate(&self) -> f64 {
if self.meas_samples == 0 || self.measured_rate == 0 || self.reference_rate == 0 {
return 0.0;
}
let ref_dur_s = self.ref_samples as f64 / self.reference_rate as f64;
let ideal_meas = ref_dur_s * self.measured_rate as f64;
let actual_meas = self.meas_samples as f64;
if actual_meas == 0.0 {
return 0.0;
}
(ideal_meas - actual_meas) / actual_meas * 1_000_000.0
}
pub fn reset(&mut self) {
self.ref_samples = 0;
self.meas_samples = 0;
}
}
#[derive(Debug, Clone, Copy)]
pub struct AudioTimestampCalc {
sample_rate: u32,
}
impl AudioTimestampCalc {
pub fn new(sample_rate: u32) -> Self {
Self { sample_rate }
}
#[allow(clippy::cast_precision_loss)]
pub fn frames_to_pts_us(&self, frames: u64) -> i64 {
if self.sample_rate == 0 {
return 0;
}
let us = (frames as f64 / self.sample_rate as f64) * 1_000_000.0;
us as i64
}
#[allow(clippy::cast_precision_loss)]
pub fn pts_us_to_frames(&self, pts_us: i64) -> u64 {
if self.sample_rate == 0 {
return 0;
}
let frames = (pts_us as f64 / 1_000_000.0) * self.sample_rate as f64;
frames.round() as u64
}
#[allow(clippy::cast_precision_loss)]
pub fn frames_to_ms(&self, frames: u64) -> f64 {
if self.sample_rate == 0 {
return 0.0;
}
(frames as f64 / self.sample_rate as f64) * 1_000.0
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clock_initial_position() {
let clock = AudioClock::new(48_000);
assert_eq!(clock.sample_position(), 0);
}
#[test]
fn test_clock_tick_advances_position() {
let mut clock = AudioClock::new(48_000);
clock.tick(1024);
assert_eq!(clock.sample_position(), 1024);
}
#[test]
fn test_clock_pts_us_zero_at_start() {
let clock = AudioClock::new(48_000);
assert_eq!(clock.pts_us(), 0);
}
#[test]
fn test_clock_pts_us_one_second() {
let mut clock = AudioClock::new(48_000);
clock.tick(48_000);
assert!((clock.pts_us() - 1_000_000).abs() < 10);
}
#[test]
fn test_clock_reset() {
let mut clock = AudioClock::new(48_000);
clock.tick(9999);
clock.reset();
assert_eq!(clock.sample_position(), 0);
}
#[test]
fn test_clock_sample_rate() {
let clock = AudioClock::new(44_100);
assert_eq!(clock.sample_rate(), 44_100);
}
#[test]
fn test_sync_no_drift_when_equal() {
let mut sync = AudioClockSync::new(48_000, 48_000);
sync.advance_reference(48_000);
sync.advance_measured(48_000);
assert_eq!(sync.drift_samples(), 0);
}
#[test]
fn test_sync_drift_positive_when_measured_ahead() {
let mut sync = AudioClockSync::new(48_000, 48_000);
sync.advance_reference(48_000);
sync.advance_measured(48_001);
let drift = sync.drift_samples();
assert!(drift > 0, "Expected positive drift, got {drift}");
}
#[test]
fn test_sync_compensate_zero_when_equal() {
let mut sync = AudioClockSync::new(48_000, 48_000);
sync.advance_reference(48_000);
sync.advance_measured(48_000);
let ppm = sync.compensate();
assert!(ppm.abs() < 1.0, "Expected near-zero PPM, got {ppm}");
}
#[test]
fn test_sync_reset() {
let mut sync = AudioClockSync::new(48_000, 48_000);
sync.advance_reference(1000);
sync.advance_measured(1000);
sync.reset();
assert_eq!(sync.drift_samples(), 0);
}
#[test]
fn test_timestamp_calc_frames_to_pts_us() {
let calc = AudioTimestampCalc::new(48_000);
assert!((calc.frames_to_pts_us(48_000) - 1_000_000).abs() < 10);
}
#[test]
fn test_timestamp_calc_pts_us_to_frames() {
let calc = AudioTimestampCalc::new(48_000);
assert_eq!(calc.pts_us_to_frames(1_000_000), 48_000);
}
#[test]
fn test_timestamp_calc_frames_to_ms() {
let calc = AudioTimestampCalc::new(48_000);
let ms = calc.frames_to_ms(48_000);
assert!((ms - 1000.0).abs() < 0.01);
}
#[test]
fn test_timestamp_calc_zero_sample_rate() {
let calc = AudioTimestampCalc::new(0);
assert_eq!(calc.frames_to_pts_us(1000), 0);
assert_eq!(calc.pts_us_to_frames(1_000_000), 0);
}
}