use std::time::Duration;
use super::codec::{OpusDecoder, OpusEncoder, CHANNELS, SAMPLES_PER_FRAME, SAMPLE_RATE};
pub fn tone_burst() -> Vec<f32> {
let mut pcm = vec![0f32; SAMPLES_PER_FRAME];
let start = SAMPLES_PER_FRAME / 4;
for i in 0..(SAMPLES_PER_FRAME / 2) {
let s = (i as f32) * 2.0 * std::f32::consts::PI * 1000.0 / SAMPLE_RATE as f32;
let v = s.sin() * 0.8;
pcm[(start + i) % SAMPLES_PER_FRAME] = v;
pcm[(start + i) % SAMPLES_PER_FRAME + CHANNELS] = v;
}
pcm
}
pub fn peak_index(samples: &[f32]) -> Option<usize> {
samples
.iter()
.enumerate()
.max_by(|a, b| a.1.abs().total_cmp(&b.1.abs()))
.filter(|(_, v)| v.abs() > 0.1)
.map(|(i, _)| i)
}
pub fn playout_ms(samples_per_frame: usize) -> f64 {
samples_per_frame as f64 / CHANNELS as f64 / SAMPLE_RATE as f64 * 1000.0
}
pub fn measure_tone_delay() -> Result<Duration, anyhow::Error> {
let pcm = tone_burst();
let mut encoder = OpusEncoder::new()?;
let encoded = encoder.encode(&pcm)?;
let mut decoder = OpusDecoder::new()?;
let mut out = vec![0f32; SAMPLES_PER_FRAME];
decoder.decode(&encoded, &mut out)?;
let peak = peak_index(&out)
.ok_or_else(|| anyhow::anyhow!("the tone did not survive the round trip"))?;
Ok(Duration::from_micros(
(peak as u64 * 1_000_000) / SAMPLE_RATE as u64,
))
}
pub fn video_release_time(pts_us: u64, audio_offset: Duration) -> Duration {
Duration::from_micros(pts_us).saturating_add(audio_offset)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::sync::{Estimator, OffsetSample};
#[test]
fn a_tone_survives_the_codec_with_a_bounded_delay() {
let delay = measure_tone_delay().expect("the tone must round trip");
assert!(
delay >= Duration::from_millis(10),
"the peak cannot precede the burst that produced it, took {delay:?}"
);
assert!(
delay < Duration::from_millis(40),
"a 20 ms burst must land within two frames, took {delay:?}"
);
}
#[test]
fn the_burst_is_actually_a_burst() {
let pcm = tone_burst();
let peak = peak_index(&pcm).expect("a burst needs a peak");
assert!(peak > SAMPLES_PER_FRAME / 4, "peak at {peak} is too early");
assert!(
peak < SAMPLES_PER_FRAME * 3 / 4,
"peak at {peak} is too late"
);
}
#[test]
fn twenty_milliseconds_is_twenty_milliseconds() {
assert!((playout_ms(SAMPLES_PER_FRAME) - 20.0).abs() < 0.001);
}
#[test]
fn a_video_frame_is_released_at_its_timestamp_plus_the_audio_offset() {
assert_eq!(
video_release_time(100_000, Duration::from_millis(30)),
Duration::from_millis(130)
);
}
#[test]
fn an_unconverged_estimator_refuses_to_slave_anything() {
let mut est = Estimator::new();
est.observe(OffsetSample::new(
Duration::from_millis(40),
Duration::from_millis(0),
));
assert!(!est.convergence());
assert_eq!(est.offset(), None);
}
#[test]
fn a_steady_stream_converges_and_then_reports_its_offset() {
let mut est = Estimator::new();
for _ in 0..64 {
est.observe(OffsetSample::new(
Duration::from_millis(42),
Duration::from_millis(0),
));
}
assert!(est.convergence());
let offset = est.offset().expect("converged means an answer");
assert!(
offset >= Duration::from_millis(41) && offset <= Duration::from_millis(43),
"offset {offset:?} is not the 42 ms that was observed"
);
}
}