#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncMethod {
Timecode,
AudioClap,
WaveformMatch,
Manual,
}
impl SyncMethod {
#[must_use]
pub fn accuracy_frames(&self) -> f32 {
match self {
Self::Timecode => 0.0,
Self::AudioClap => 1.0,
Self::WaveformMatch => 0.5,
Self::Manual => 2.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SyncPoint {
pub clip_id: String,
pub frame: u64,
pub confidence: f32,
}
impl SyncPoint {
#[must_use]
pub fn new(clip_id: &str, frame: u64, confidence: f32) -> Self {
Self {
clip_id: clip_id.to_owned(),
frame,
confidence,
}
}
#[must_use]
pub fn is_reliable(&self) -> bool {
self.confidence >= 0.7
}
}
pub struct ClapDetector;
impl ClapDetector {
#[must_use]
pub fn detect(energy: &[f32], sample_rate: u32) -> Option<u64> {
if energy.is_empty() || sample_rate == 0 {
return None;
}
let search_len = ((sample_rate as usize) * 10).min(energy.len());
let window = &energy[..search_len];
let mut best_idx = 0usize;
let mut best_val = window[0];
for (i, &v) in window.iter().enumerate() {
if v > best_val {
best_val = v;
best_idx = i;
}
}
Some(best_idx as u64)
}
}
#[derive(Debug, Clone)]
pub struct MultiClipSync {
pub reference_id: String,
pub sync_points: Vec<SyncPoint>,
}
impl MultiClipSync {
#[must_use]
pub fn new(reference_id: &str) -> Self {
Self {
reference_id: reference_id.to_owned(),
sync_points: Vec::new(),
}
}
pub fn set_reference(&mut self, id: &str) {
self.reference_id = id.to_owned();
}
pub fn add_sync(&mut self, point: SyncPoint) {
self.sync_points.push(point);
}
#[must_use]
pub fn offset_for(&self, clip_id: &str) -> Option<i64> {
let ref_frame = self
.sync_points
.iter()
.find(|sp| sp.clip_id == self.reference_id)
.map(|sp| sp.frame)?;
let clip_frame = self
.sync_points
.iter()
.find(|sp| sp.clip_id == clip_id)
.map(|sp| sp.frame)?;
Some(clip_frame as i64 - ref_frame as i64)
}
#[must_use]
pub fn synchronized_clips(&self) -> Vec<&str> {
self.sync_points
.iter()
.map(|sp| sp.clip_id.as_str())
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SyncReport {
pub success: bool,
pub method: SyncMethod,
pub max_offset_frames: f32,
pub confidence: f32,
}
impl SyncReport {
#[must_use]
pub fn new(success: bool, method: SyncMethod, max_offset_frames: f32, confidence: f32) -> Self {
Self {
success,
method,
max_offset_frames,
confidence,
}
}
#[must_use]
pub fn is_accurate(&self) -> bool {
self.success
&& self.confidence >= 0.7
&& self.max_offset_frames <= self.method.accuracy_frames() + 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timecode_accuracy() {
assert_eq!(SyncMethod::Timecode.accuracy_frames(), 0.0);
}
#[test]
fn test_audio_clap_accuracy() {
assert_eq!(SyncMethod::AudioClap.accuracy_frames(), 1.0);
}
#[test]
fn test_waveform_accuracy() {
assert_eq!(SyncMethod::WaveformMatch.accuracy_frames(), 0.5);
}
#[test]
fn test_manual_accuracy() {
assert_eq!(SyncMethod::Manual.accuracy_frames(), 2.0);
}
#[test]
fn test_sync_point_reliable_above_threshold() {
let sp = SyncPoint::new("clip_a", 100, 0.9);
assert!(sp.is_reliable());
}
#[test]
fn test_sync_point_reliable_at_threshold() {
let sp = SyncPoint::new("clip_a", 100, 0.7);
assert!(sp.is_reliable());
}
#[test]
fn test_sync_point_not_reliable() {
let sp = SyncPoint::new("clip_a", 100, 0.5);
assert!(!sp.is_reliable());
}
#[test]
fn test_clap_detector_empty() {
assert!(ClapDetector::detect(&[], 48000).is_none());
}
#[test]
fn test_clap_detector_zero_sample_rate() {
let energy = vec![0.1f32; 100];
assert!(ClapDetector::detect(&energy, 0).is_none());
}
#[test]
fn test_clap_detector_finds_peak() {
let mut energy = vec![0.1f32; 1000];
energy[300] = 10.0; let result = ClapDetector::detect(&energy, 100);
assert_eq!(result, Some(300));
}
#[test]
fn test_clap_detector_respects_10s_window() {
let mut energy = vec![0.0f32; 20_000];
energy[500] = 5.0; energy[15_000] = 100.0; let result = ClapDetector::detect(&energy, 1000);
assert_eq!(result, Some(500));
}
#[test]
fn test_multi_clip_sync_new() {
let mcs = MultiClipSync::new("ref");
assert_eq!(mcs.reference_id, "ref");
assert!(mcs.sync_points.is_empty());
}
#[test]
fn test_set_reference() {
let mut mcs = MultiClipSync::new("old");
mcs.set_reference("new");
assert_eq!(mcs.reference_id, "new");
}
#[test]
fn test_offset_for_no_reference() {
let mcs = MultiClipSync::new("ref");
assert!(mcs.offset_for("clip_a").is_none());
}
#[test]
fn test_offset_for_returns_correct_value() {
let mut mcs = MultiClipSync::new("ref");
mcs.add_sync(SyncPoint::new("ref", 100, 1.0));
mcs.add_sync(SyncPoint::new("cam2", 120, 0.9));
assert_eq!(mcs.offset_for("cam2"), Some(20));
}
#[test]
fn test_offset_for_negative() {
let mut mcs = MultiClipSync::new("ref");
mcs.add_sync(SyncPoint::new("ref", 200, 1.0));
mcs.add_sync(SyncPoint::new("cam2", 150, 0.8));
assert_eq!(mcs.offset_for("cam2"), Some(-50));
}
#[test]
fn test_synchronized_clips_list() {
let mut mcs = MultiClipSync::new("ref");
mcs.add_sync(SyncPoint::new("ref", 0, 1.0));
mcs.add_sync(SyncPoint::new("cam_a", 5, 0.9));
mcs.add_sync(SyncPoint::new("cam_b", 3, 0.8));
let clips = mcs.synchronized_clips();
assert_eq!(clips.len(), 3);
assert!(clips.contains(&"cam_a"));
assert!(clips.contains(&"cam_b"));
}
#[test]
fn test_sync_report_accurate_timecode() {
let r = SyncReport::new(true, SyncMethod::Timecode, 0.0, 1.0);
assert!(r.is_accurate());
}
#[test]
fn test_sync_report_not_accurate_low_confidence() {
let r = SyncReport::new(true, SyncMethod::Timecode, 0.0, 0.5);
assert!(!r.is_accurate());
}
#[test]
fn test_sync_report_not_accurate_large_offset() {
let r = SyncReport::new(true, SyncMethod::AudioClap, 5.0, 0.9);
assert!(!r.is_accurate());
}
#[test]
fn test_sync_report_not_accurate_failed() {
let r = SyncReport::new(false, SyncMethod::Timecode, 0.0, 1.0);
assert!(!r.is_accurate());
}
}