#[derive(Debug, Clone, Copy)]
pub struct AudioSyncPoint {
pub timestamp_us: u64,
pub sample_index: u64,
}
pub struct AudioSyncTracker {
pub sync_points: Vec<AudioSyncPoint>,
pub sample_rate: u32,
pub drift_us: i64,
}
impl AudioSyncTracker {
pub fn new(sample_rate: u32) -> Self {
Self {
sync_points: Vec::new(),
sample_rate,
drift_us: 0,
}
}
pub fn add_sync_point(&mut self, sp: AudioSyncPoint) {
self.sync_points.push(sp);
}
pub fn estimate_drift_us(&self) -> i64 {
if self.sync_points.len() < 2 {
return 0;
}
let first = &self.sync_points[0];
let mut total_error: i64 = 0;
let count = (self.sync_points.len() - 1) as i64;
for sp in self.sync_points.iter().skip(1) {
let elapsed_samples = sp.sample_index.saturating_sub(first.sample_index);
let expected_us = first.timestamp_us + samples_to_us(elapsed_samples, self.sample_rate);
let measured_us = sp.timestamp_us;
let error = measured_us as i64 - expected_us as i64;
total_error += error;
}
total_error / count
}
pub fn corrected_timestamp(&self, sample_index: u64) -> u64 {
let base_us = if let Some(first) = self.sync_points.first() {
let elapsed = sample_index.saturating_sub(first.sample_index);
first.timestamp_us + samples_to_us(elapsed, self.sample_rate)
} else {
samples_to_us(sample_index, self.sample_rate)
};
let drift = self.estimate_drift_us() + self.drift_us;
if drift >= 0 {
base_us.saturating_sub(drift as u64)
} else {
base_us.saturating_add((-drift) as u64)
}
}
pub fn sample_for_time(&self, timestamp_us: u64) -> u64 {
if let Some(first) = self.sync_points.first() {
let elapsed_us = timestamp_us.saturating_sub(first.timestamp_us);
first.sample_index + us_to_samples(elapsed_us, self.sample_rate)
} else {
us_to_samples(timestamp_us, self.sample_rate)
}
}
}
pub fn compute_sample_offset(a_rate: u32, b_rate: u32, a_frame: u64) -> u64 {
if a_rate == 0 {
return 0;
}
let result = a_frame as u128 * b_rate as u128 / a_rate as u128;
result as u64
}
pub fn audio_video_sync_error_ms(audio_pts_us: u64, video_pts_us: u64) -> f64 {
let diff = audio_pts_us as i64 - video_pts_us as i64;
diff as f64 / 1000.0
}
fn samples_to_us(samples: u64, sample_rate: u32) -> u64 {
if sample_rate == 0 {
return 0;
}
samples * 1_000_000 / sample_rate as u64
}
fn us_to_samples(us: u64, sample_rate: u32) -> u64 {
us * sample_rate as u64 / 1_000_000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_samples_to_us_48k() {
assert_eq!(samples_to_us(48_000, 48_000), 1_000_000);
}
#[test]
fn test_us_to_samples_48k() {
assert_eq!(us_to_samples(1_000_000, 48_000), 48_000);
}
#[test]
fn test_tracker_new() {
let t = AudioSyncTracker::new(44_100);
assert_eq!(t.sample_rate, 44_100);
assert!(t.sync_points.is_empty());
}
#[test]
fn test_estimate_drift_no_points() {
let t = AudioSyncTracker::new(48_000);
assert_eq!(t.estimate_drift_us(), 0);
}
#[test]
fn test_estimate_drift_one_point() {
let mut t = AudioSyncTracker::new(48_000);
t.add_sync_point(AudioSyncPoint {
timestamp_us: 0,
sample_index: 0,
});
assert_eq!(t.estimate_drift_us(), 0);
}
#[test]
fn test_estimate_drift_perfect_sync() {
let mut t = AudioSyncTracker::new(48_000);
t.add_sync_point(AudioSyncPoint {
timestamp_us: 0,
sample_index: 0,
});
t.add_sync_point(AudioSyncPoint {
timestamp_us: 1_000_000,
sample_index: 48_000,
});
assert_eq!(t.estimate_drift_us(), 0);
}
#[test]
fn test_estimate_drift_with_drift() {
let mut t = AudioSyncTracker::new(48_000);
t.add_sync_point(AudioSyncPoint {
timestamp_us: 0,
sample_index: 0,
});
t.add_sync_point(AudioSyncPoint {
timestamp_us: 1_000_500,
sample_index: 48_000,
});
assert_eq!(t.estimate_drift_us(), 500);
}
#[test]
fn test_corrected_timestamp_no_sync() {
let t = AudioSyncTracker::new(48_000);
assert_eq!(t.corrected_timestamp(48_000), 1_000_000);
}
#[test]
fn test_sample_for_time_no_sync() {
let t = AudioSyncTracker::new(48_000);
assert_eq!(t.sample_for_time(1_000_000), 48_000);
}
#[test]
fn test_sample_for_time_with_anchor() {
let mut t = AudioSyncTracker::new(48_000);
t.add_sync_point(AudioSyncPoint {
timestamp_us: 1_000_000,
sample_index: 48_000,
});
assert_eq!(t.sample_for_time(2_000_000), 96_000);
}
#[test]
fn test_compute_sample_offset_same_rate() {
assert_eq!(compute_sample_offset(48_000, 48_000, 1000), 1000);
}
#[test]
fn test_compute_sample_offset_rate_conversion() {
assert_eq!(compute_sample_offset(44_100, 48_000, 44_100), 48_000);
}
#[test]
fn test_audio_video_sync_error_ms_ahead() {
let err = audio_video_sync_error_ms(1_010_000, 1_000_000);
assert!((err - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_audio_video_sync_error_ms_late() {
let err = audio_video_sync_error_ms(990_000, 1_000_000);
assert!((err + 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_audio_video_sync_error_ms_zero() {
let err = audio_video_sync_error_ms(1_000_000, 1_000_000);
assert_eq!(err, 0.0);
}
}