#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TcOffset {
pub offset_frames: i64,
pub description: String,
}
impl TcOffset {
#[must_use]
pub fn new(offset_frames: i64, description: String) -> Self {
Self {
offset_frames,
description,
}
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.offset_frames == 0
}
#[must_use]
pub fn negate(&self) -> Self {
Self {
offset_frames: -self.offset_frames,
description: self.description.clone(),
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TcSyncPoint {
pub source_frame: i64,
pub target_frame: i64,
}
impl TcSyncPoint {
#[must_use]
pub fn new(source_frame: i64, target_frame: i64) -> Self {
Self {
source_frame,
target_frame,
}
}
#[must_use]
pub fn offset(&self) -> i64 {
self.target_frame - self.source_frame
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct SyncMap {
pub sync_points: Vec<TcSyncPoint>,
}
impl SyncMap {
#[must_use]
pub fn new() -> Self {
Self {
sync_points: Vec::new(),
}
}
pub fn add_sync_point(&mut self, point: TcSyncPoint) {
self.sync_points.push(point);
self.sync_points.sort_by_key(|p| p.source_frame);
}
#[must_use]
pub fn point_count(&self) -> usize {
self.sync_points.len()
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn source_to_target(&self, frame: i64) -> i64 {
if self.sync_points.is_empty() {
return frame;
}
if self.sync_points.len() == 1 {
return frame + self.sync_points[0].offset();
}
let first = &self.sync_points[0];
let last = &self.sync_points[self.sync_points.len() - 1];
if frame <= first.source_frame {
return frame + first.offset();
}
if frame >= last.source_frame {
return frame + last.offset();
}
let idx = self
.sync_points
.partition_point(|p| p.source_frame <= frame);
let lo = &self.sync_points[idx - 1];
let hi = &self.sync_points[idx];
let span = (hi.source_frame - lo.source_frame) as f64;
let t = (frame - lo.source_frame) as f64 / span;
let interp_target = lo.target_frame as f64 + t * (hi.target_frame - lo.target_frame) as f64;
interp_target.round() as i64
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn target_to_source(&self, frame: i64) -> i64 {
if self.sync_points.is_empty() {
return frame;
}
if self.sync_points.len() == 1 {
return frame - self.sync_points[0].offset();
}
let mut by_target = self.sync_points.clone();
by_target.sort_by_key(|p| p.target_frame);
let first = &by_target[0];
let last = &by_target[by_target.len() - 1];
if frame <= first.target_frame {
return frame - first.offset();
}
if frame >= last.target_frame {
return frame - last.offset();
}
let idx = by_target.partition_point(|p| p.target_frame <= frame);
let lo = &by_target[idx - 1];
let hi = &by_target[idx];
let span = (hi.target_frame - lo.target_frame) as f64;
let t = (frame - lo.target_frame) as f64 / span;
let interp_source = lo.source_frame as f64 + t * (hi.source_frame - lo.source_frame) as f64;
interp_source.round() as i64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tc_offset_is_zero_true() {
let o = TcOffset::new(0, "zero".into());
assert!(o.is_zero());
}
#[test]
fn test_tc_offset_is_zero_false() {
let o = TcOffset::new(10, "shift".into());
assert!(!o.is_zero());
}
#[test]
fn test_tc_offset_negate() {
let o = TcOffset::new(25, "shift".into());
let neg = o.negate();
assert_eq!(neg.offset_frames, -25);
}
#[test]
fn test_tc_offset_negate_zero() {
let o = TcOffset::new(0, "zero".into());
assert_eq!(o.negate().offset_frames, 0);
}
#[test]
fn test_tc_sync_point_offset() {
let p = TcSyncPoint::new(100, 150);
assert_eq!(p.offset(), 50);
}
#[test]
fn test_tc_sync_point_negative_offset() {
let p = TcSyncPoint::new(200, 100);
assert_eq!(p.offset(), -100);
}
#[test]
fn test_sync_map_empty_passthrough() {
let map = SyncMap::new();
assert_eq!(map.source_to_target(42), 42);
assert_eq!(map.target_to_source(42), 42);
}
#[test]
fn test_sync_map_single_point() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(0, 100));
assert_eq!(map.source_to_target(0), 100);
assert_eq!(map.source_to_target(50), 150);
}
#[test]
fn test_sync_map_two_points_interpolation() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(0, 0));
map.add_sync_point(TcSyncPoint::new(100, 200));
assert_eq!(map.source_to_target(50), 100);
}
#[test]
fn test_sync_map_extrapolate_before_first() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(100, 110));
map.add_sync_point(TcSyncPoint::new(200, 220));
assert_eq!(map.source_to_target(50), 60);
}
#[test]
fn test_sync_map_extrapolate_after_last() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(0, 0));
map.add_sync_point(TcSyncPoint::new(100, 200));
assert_eq!(map.source_to_target(150), 250);
}
#[test]
fn test_sync_map_target_to_source_single_point() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(0, 100));
assert_eq!(map.target_to_source(100), 0);
assert_eq!(map.target_to_source(150), 50);
}
#[test]
fn test_sync_map_point_count() {
let mut map = SyncMap::new();
assert_eq!(map.point_count(), 0);
map.add_sync_point(TcSyncPoint::new(0, 0));
assert_eq!(map.point_count(), 1);
map.add_sync_point(TcSyncPoint::new(100, 100));
assert_eq!(map.point_count(), 2);
}
#[test]
fn test_sync_map_sorted_after_add() {
let mut map = SyncMap::new();
map.add_sync_point(TcSyncPoint::new(200, 200));
map.add_sync_point(TcSyncPoint::new(0, 0));
map.add_sync_point(TcSyncPoint::new(100, 100));
assert_eq!(map.sync_points[0].source_frame, 0);
assert_eq!(map.sync_points[1].source_frame, 100);
assert_eq!(map.sync_points[2].source_frame, 200);
}
}