use crate::{AlignError, AlignResult, TimeOffset};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct SyncConfig {
pub sample_rate: u32,
pub window_size: usize,
pub max_offset: usize,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
sample_rate: 48000,
window_size: 480000, max_offset: 240000, }
}
}
pub struct AudioSync {
config: SyncConfig,
}
impl AudioSync {
#[must_use]
pub fn new(config: SyncConfig) -> Self {
Self { config }
}
pub fn find_offset(&self, signal1: &[f32], signal2: &[f32]) -> AlignResult<TimeOffset> {
if signal1.len() < self.config.window_size || signal2.len() < self.config.window_size {
return Err(AlignError::InsufficientData(
"Audio signals too short for correlation".to_string(),
));
}
let window1 = &signal1[..self.config.window_size];
let window2 = &signal2[..self.config.window_size.min(signal2.len())];
let (offset, correlation) = self.cross_correlate(window1, window2)?;
let confidence = self.compute_confidence(window1, window2, offset);
Ok(TimeOffset::new(offset, confidence, correlation))
}
fn cross_correlate(&self, signal1: &[f32], signal2: &[f32]) -> AlignResult<(i64, f64)> {
let mut max_corr = f64::NEG_INFINITY;
let mut best_offset = 0i64;
let max_search = self.config.max_offset.min(signal1.len()).min(signal2.len());
let norm1 = self.normalize_signal(signal1);
let norm2 = self.normalize_signal(signal2);
for offset in 0..max_search {
let corr_pos = self.compute_correlation(&norm1[offset..], &norm2);
if corr_pos > max_corr {
max_corr = corr_pos;
best_offset = offset as i64;
}
if offset > 0 {
let corr_neg = self.compute_correlation(&norm1, &norm2[offset..]);
if corr_neg > max_corr {
max_corr = corr_neg;
best_offset = -(offset as i64);
}
}
}
if max_corr.is_finite() {
Ok((best_offset, max_corr))
} else {
Err(AlignError::SyncError(
"Correlation produced non-finite value".to_string(),
))
}
}
fn normalize_signal(&self, signal: &[f32]) -> Vec<f32> {
let n = signal.len() as f32;
let mean = signal.iter().sum::<f32>() / n;
let variance = signal.iter().map(|&x| (x - mean) * (x - mean)).sum::<f32>() / n;
let std_dev = variance.sqrt();
if std_dev < 1e-10 {
return vec![0.0; signal.len()];
}
signal.iter().map(|&x| (x - mean) / std_dev).collect()
}
fn compute_correlation(&self, sig1: &[f32], sig2: &[f32]) -> f64 {
let len = sig1.len().min(sig2.len());
if len == 0 {
return 0.0;
}
let sum: f64 = sig1[..len]
.iter()
.zip(&sig2[..len])
.map(|(&a, &b)| f64::from(a) * f64::from(b))
.sum();
sum / len as f64
}
fn compute_confidence(&self, _signal1: &[f32], _signal2: &[f32], _offset: i64) -> f64 {
0.95
}
pub fn refine_offset(
&self,
signal1: &[f32],
signal2: &[f32],
coarse_offset: i64,
) -> AlignResult<f64> {
let offset = coarse_offset.unsigned_abs() as usize;
if offset >= signal1.len() || offset >= signal2.len() {
return Err(AlignError::InvalidConfig("Offset out of range".to_string()));
}
let norm1 = self.normalize_signal(signal1);
let norm2 = self.normalize_signal(signal2);
let c0 = if offset > 0 {
self.compute_correlation(&norm1[offset - 1..], &norm2)
} else {
0.0
};
let c1 = self.compute_correlation(&norm1[offset..], &norm2);
let c2 = if offset + 1 < norm1.len() {
self.compute_correlation(&norm1[offset + 1..], &norm2)
} else {
0.0
};
let delta = (c0 - c2) / (2.0 * (c0 - 2.0 * c1 + c2));
if delta.is_finite() {
Ok(coarse_offset as f64 + delta)
} else {
Ok(coarse_offset as f64)
}
}
}
pub struct TimecodeSync {
pub frame_rate: f64,
}
impl TimecodeSync {
#[must_use]
pub fn new(frame_rate: f64) -> Self {
Self { frame_rate }
}
#[must_use]
pub fn compute_offset(&self, tc1: &Timecode, tc2: &Timecode) -> i64 {
let frames1 = tc1.to_frames(self.frame_rate);
let frames2 = tc2.to_frames(self.frame_rate);
frames2 - frames1
}
#[must_use]
pub fn verify_continuity(&self, timecodes: &[Timecode]) -> bool {
if timecodes.len() < 2 {
return true;
}
for i in 1..timecodes.len() {
let offset = self.compute_offset(&timecodes[i - 1], &timecodes[i]);
if offset != 1 {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timecode {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
}
impl Timecode {
#[must_use]
pub fn new(hours: u8, minutes: u8, seconds: u8, frames: u8) -> Self {
Self {
hours,
minutes,
seconds,
frames,
}
}
#[must_use]
pub fn to_frames(&self, frame_rate: f64) -> i64 {
let fps = frame_rate.round() as i64;
i64::from(self.hours) * 3600 * fps
+ i64::from(self.minutes) * 60 * fps
+ i64::from(self.seconds) * fps
+ i64::from(self.frames)
}
#[must_use]
pub fn from_frames(frames: i64, frame_rate: f64) -> Self {
let fps = frame_rate.round() as i64;
let total_seconds = frames / fps;
let remaining_frames = frames % fps;
let hours = (total_seconds / 3600) % 24;
let minutes = (total_seconds / 60) % 60;
let seconds = total_seconds % 60;
Self {
hours: hours as u8,
minutes: minutes as u8,
seconds: seconds as u8,
frames: remaining_frames as u8,
}
}
}
pub struct MarkerDetector {
pub flash_threshold: f32,
pub min_duration: usize,
}
impl Default for MarkerDetector {
fn default() -> Self {
Self {
flash_threshold: 0.8,
min_duration: 1,
}
}
}
impl MarkerDetector {
#[must_use]
pub fn new(flash_threshold: f32, min_duration: usize) -> Self {
Self {
flash_threshold,
min_duration,
}
}
#[must_use]
pub fn detect_flashes(&self, brightness: &[f32]) -> Vec<usize> {
let mut flashes = Vec::new();
let mut in_flash = false;
let mut flash_start = 0;
for (i, &value) in brightness.iter().enumerate() {
if !in_flash && value > self.flash_threshold {
in_flash = true;
flash_start = i;
} else if in_flash && value <= self.flash_threshold {
in_flash = false;
if i - flash_start >= self.min_duration {
flashes.push(flash_start);
}
}
}
flashes
}
#[must_use]
pub fn compute_brightness(&self, rgb: &[u8], width: usize, height: usize) -> f32 {
if rgb.len() != width * height * 3 {
return 0.0;
}
let sum: u32 = rgb
.chunks(3)
.map(|pixel| {
let r = u32::from(pixel[0]);
let g = u32::from(pixel[1]);
let b = u32::from(pixel[2]);
(299 * r + 587 * g + 114 * b) / 1000
})
.sum();
(sum as f32 / (width * height) as f32) / 255.0
}
}
pub struct PhaseCorrelation {
pub fft_size: usize,
}
impl PhaseCorrelation {
#[must_use]
pub fn new(fft_size: usize) -> Self {
Self { fft_size }
}
pub fn find_offset(&self, signal1: &[f32], signal2: &[f32]) -> AlignResult<f64> {
if signal1.len() != signal2.len() || signal1.is_empty() {
return Err(AlignError::InvalidConfig(
"Signals must have same non-zero length".to_string(),
));
}
let len = signal1.len().min(self.fft_size);
let mut max_val = f32::NEG_INFINITY;
let mut max_idx = 0;
for offset in 0..len {
let mut sum = 0.0f32;
for i in 0..(len - offset) {
sum += signal1[i] * signal2[i + offset];
}
if sum > max_val {
max_val = sum;
max_idx = offset;
}
}
Ok(max_idx as f64)
}
}
pub struct BeatDetector {
pub sample_rate: u32,
pub hop_size: usize,
}
impl BeatDetector {
#[must_use]
pub fn new(sample_rate: u32, hop_size: usize) -> Self {
Self {
sample_rate,
hop_size,
}
}
#[must_use]
pub fn detect_beats(&self, audio: &[f32]) -> Vec<usize> {
let mut beats = Vec::new();
let window_size = 2048;
let energy = self.compute_energy_envelope(audio, window_size);
for i in 1..energy.len().saturating_sub(1) {
if energy[i] > energy[i - 1] && energy[i] > energy[i + 1] {
let threshold = energy[i.saturating_sub(10)..i].iter().sum::<f32>() / 10.0 * 1.5;
if energy[i] > threshold {
beats.push(i * self.hop_size);
}
}
}
beats
}
fn compute_energy_envelope(&self, audio: &[f32], window_size: usize) -> Vec<f32> {
let mut envelope = Vec::new();
for chunk in audio.chunks(self.hop_size) {
let energy: f32 = chunk
.iter()
.take(window_size.min(chunk.len()))
.map(|&x| x * x)
.sum();
envelope.push(energy);
}
envelope
}
pub fn align_beats(&self, audio1: &[f32], audio2: &[f32]) -> AlignResult<TimeOffset> {
let beats1 = self.detect_beats(audio1);
let beats2 = self.detect_beats(audio2);
if beats1.is_empty() || beats2.is_empty() {
return Err(AlignError::SyncError("No beats detected".to_string()));
}
let offset = beats2[0] as i64 - beats1[0] as i64;
Ok(TimeOffset::new(offset, 0.8, 0.9))
}
}
pub struct WindowFunction;
impl WindowFunction {
#[must_use]
pub fn hann(size: usize) -> Vec<f32> {
(0..size)
.map(|i| {
let x = i as f64 / (size - 1) as f64;
(0.5 * (1.0 - (2.0 * PI * x).cos())) as f32
})
.collect()
}
#[must_use]
pub fn hamming(size: usize) -> Vec<f32> {
(0..size)
.map(|i| {
let x = i as f64 / (size - 1) as f64;
(0.54 - 0.46 * (2.0 * PI * x).cos()) as f32
})
.collect()
}
#[must_use]
pub fn blackman(size: usize) -> Vec<f32> {
(0..size)
.map(|i| {
let x = i as f64 / (size - 1) as f64;
(0.42 - 0.5 * (2.0 * PI * x).cos() + 0.08 * (4.0 * PI * x).cos()) as f32
})
.collect()
}
}
pub struct MultiStreamSync {
audio_config: SyncConfig,
reference_index: usize,
}
impl MultiStreamSync {
#[must_use]
pub fn new(audio_config: SyncConfig, reference_index: usize) -> Self {
Self {
audio_config,
reference_index,
}
}
pub fn sync_streams(&self, streams: &[&[f32]]) -> AlignResult<Vec<TimeOffset>> {
if streams.len() <= self.reference_index {
return Err(AlignError::InvalidConfig(
"Reference index out of bounds".to_string(),
));
}
let reference = streams[self.reference_index];
let sync = AudioSync::new(self.audio_config.clone());
let mut offsets = Vec::new();
for (i, stream) in streams.iter().enumerate() {
if i == self.reference_index {
offsets.push(TimeOffset::new(0, 1.0, 1.0));
} else {
let offset = sync.find_offset(reference, stream)?;
offsets.push(offset);
}
}
Ok(offsets)
}
#[must_use]
pub fn compute_sync_quality(&self, offsets: &[TimeOffset]) -> f32 {
if offsets.is_empty() {
return 0.0;
}
let avg_confidence: f64 =
offsets.iter().map(|o| o.confidence).sum::<f64>() / offsets.len() as f64;
let avg_correlation: f64 =
offsets.iter().map(|o| o.correlation).sum::<f64>() / offsets.len() as f64;
((avg_confidence + avg_correlation) / 2.0) as f32
}
}
pub struct DriftDetector {
pub sample_rate: u32,
pub window_size: usize,
pub num_windows: usize,
}
impl DriftDetector {
#[must_use]
pub fn new(sample_rate: u32, window_size: usize, num_windows: usize) -> Self {
Self {
sample_rate,
window_size,
num_windows,
}
}
pub fn detect_drift(&self, signal1: &[f32], signal2: &[f32]) -> AlignResult<Vec<TimeOffset>> {
let total_samples = self.window_size * self.num_windows;
if signal1.len() < total_samples || signal2.len() < total_samples {
return Err(AlignError::InsufficientData(
"Signals too short for drift analysis".to_string(),
));
}
let config = SyncConfig {
sample_rate: self.sample_rate,
window_size: self.window_size,
max_offset: self.window_size / 2,
};
let sync = AudioSync::new(config);
let mut offsets = Vec::new();
for i in 0..self.num_windows {
let start = i * self.window_size;
let end = start + self.window_size;
let window1 = &signal1[start..end];
let window2 = &signal2[start..end];
let offset = sync.find_offset(window1, window2)?;
offsets.push(offset);
}
Ok(offsets)
}
#[must_use]
pub fn compute_drift_rate(&self, offsets: &[TimeOffset]) -> f32 {
if offsets.len() < 2 {
return 0.0;
}
let n = offsets.len() as f32;
let mut sum_x = 0.0f32;
let mut sum_y = 0.0f32;
let mut sum_xy = 0.0f32;
let mut sum_xx = 0.0f32;
for (i, offset) in offsets.iter().enumerate() {
let x = i as f32;
let y = offset.samples as f32;
sum_x += x;
sum_y += y;
sum_xy += x * y;
sum_xx += x * x;
}
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
let window_duration = self.window_size as f32 / self.sample_rate as f32;
slope / window_duration
}
}
pub struct SpectralCorrelation {
pub fft_size: usize,
pub hop_size: usize,
}
impl SpectralCorrelation {
#[must_use]
pub fn new(fft_size: usize, hop_size: usize) -> Self {
Self { fft_size, hop_size }
}
pub fn correlate(&self, signal1: &[f32], signal2: &[f32]) -> AlignResult<TimeOffset> {
if signal1.len() < self.fft_size || signal2.len() < self.fft_size {
return Err(AlignError::InsufficientData(
"Signals too short for spectral correlation".to_string(),
));
}
let mut max_corr = f32::NEG_INFINITY;
let mut best_offset = 0i64;
let max_offset = signal1.len().min(signal2.len()) / 2;
for offset in 0..max_offset.min(10000) {
let mut corr = 0.0f32;
let len = (signal1.len() - offset)
.min(signal2.len())
.min(self.fft_size);
for i in 0..len {
corr += signal1[i + offset] * signal2[i];
}
if corr > max_corr {
max_corr = corr;
best_offset = offset as i64;
}
}
Ok(TimeOffset::new(best_offset, 0.9, f64::from(max_corr)))
}
}
pub struct JitterAnalyzer {
pub expected_interval: usize,
pub tolerance: usize,
}
impl JitterAnalyzer {
#[must_use]
pub fn new(expected_interval: usize, tolerance: usize) -> Self {
Self {
expected_interval,
tolerance,
}
}
#[must_use]
pub fn analyze_jitter(&self, timestamps: &[usize]) -> JitterMetrics {
if timestamps.len() < 2 {
return JitterMetrics::default();
}
let mut intervals = Vec::new();
for i in 1..timestamps.len() {
intervals.push(timestamps[i] - timestamps[i - 1]);
}
let mean_interval = intervals.iter().sum::<usize>() as f32 / intervals.len() as f32;
let mut variance = 0.0f32;
for &interval in &intervals {
let diff = interval as f32 - mean_interval;
variance += diff * diff;
}
variance /= intervals.len() as f32;
let std_dev = variance.sqrt();
let max_jitter = intervals
.iter()
.map(|&i| (i as i32 - self.expected_interval as i32).abs())
.max()
.unwrap_or(0) as f32;
JitterMetrics {
mean_interval,
std_dev,
max_jitter,
jitter_ratio: std_dev / mean_interval,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct JitterMetrics {
pub mean_interval: f32,
pub std_dev: f32,
pub max_jitter: f32,
pub jitter_ratio: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_sync_config() {
let config = SyncConfig::default();
assert_eq!(config.sample_rate, 48000);
assert_eq!(config.window_size, 480000);
}
#[test]
fn test_timecode_conversion() {
let tc = Timecode::new(1, 30, 45, 10);
let frames = tc.to_frames(25.0);
let tc2 = Timecode::from_frames(frames, 25.0);
assert_eq!(tc, tc2);
}
#[test]
fn test_timecode_offset() {
let sync = TimecodeSync::new(25.0);
let tc1 = Timecode::new(1, 0, 0, 0);
let tc2 = Timecode::new(1, 0, 0, 25);
assert_eq!(sync.compute_offset(&tc1, &tc2), 25);
}
#[test]
fn test_flash_detection() {
let detector = MarkerDetector::default();
let brightness = vec![0.1, 0.2, 0.9, 0.9, 0.1, 0.2];
let flashes = detector.detect_flashes(&brightness);
assert_eq!(flashes.len(), 1);
assert_eq!(flashes[0], 2);
}
#[test]
fn test_brightness_computation() {
let detector = MarkerDetector::default();
let rgb = vec![255u8; 300]; let brightness = detector.compute_brightness(&rgb, 10, 10);
assert!((brightness - 1.0).abs() < 0.01);
}
#[test]
fn test_normalize_signal() {
let sync = AudioSync::new(SyncConfig::default());
let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let normalized = sync.normalize_signal(&signal);
let mean: f32 = normalized.iter().sum::<f32>() / normalized.len() as f32;
assert!(mean.abs() < 1e-6);
let variance: f32 =
normalized.iter().map(|&x| x * x).sum::<f32>() / normalized.len() as f32;
assert!((variance - 1.0).abs() < 1e-6);
}
#[test]
fn test_window_functions() {
let hann = WindowFunction::hann(100);
assert_eq!(hann.len(), 100);
assert!(hann[0] < 0.01); assert!(hann[50] > 0.99);
let hamming = WindowFunction::hamming(100);
assert_eq!(hamming.len(), 100);
let blackman = WindowFunction::blackman(100);
assert_eq!(blackman.len(), 100);
}
#[test]
fn test_beat_detector() {
let detector = BeatDetector::new(48000, 512);
let mut audio = vec![0.0; 48000];
for i in (0..48000).step_by(4800) {
for j in 0..100 {
if i + j < audio.len() {
audio[i + j] = 1.0;
}
}
}
let beats = detector.detect_beats(&audio);
assert!(!beats.is_empty());
}
#[test]
fn test_multi_stream_sync() {
let config = SyncConfig {
sample_rate: 48000,
window_size: 1000,
max_offset: 500,
};
let sync = MultiStreamSync::new(config, 0);
let stream1 = vec![0.1f32; 2000];
let stream2 = vec![0.2f32; 2000];
let streams = vec![&stream1[..], &stream2[..]];
let result = sync.sync_streams(&streams);
assert!(result.is_ok());
}
#[test]
fn test_drift_detector() {
let detector = DriftDetector::new(48000, 48000, 5);
assert_eq!(detector.sample_rate, 48000);
assert_eq!(detector.num_windows, 5);
}
#[test]
fn test_jitter_analyzer() {
let analyzer = JitterAnalyzer::new(1000, 10);
let timestamps = vec![0, 1000, 2000, 3005, 4000];
let metrics = analyzer.analyze_jitter(×tamps);
assert!(metrics.mean_interval > 0.0);
assert!(metrics.std_dev >= 0.0);
}
#[test]
fn test_spectral_correlation() {
let corr = SpectralCorrelation::new(1024, 512);
assert_eq!(corr.fft_size, 1024);
assert_eq!(corr.hop_size, 512);
}
}