#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use crate::{FrameRate, Timecode, TimecodeError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FrameOffset {
frames: u64,
}
impl FrameOffset {
pub fn new(frames: u64) -> Self {
Self { frames }
}
pub fn as_frames(&self) -> u64 {
self.frames
}
pub fn add_frames(self, n: u64) -> Self {
Self {
frames: self.frames + n,
}
}
pub fn sub_frames(self, n: u64) -> Self {
Self {
frames: self.frames.saturating_sub(n),
}
}
pub fn diff(self, other: FrameOffset) -> i64 {
self.frames as i64 - other.frames as i64
}
pub fn to_timecode(self, frame_rate: FrameRate) -> Result<Timecode, TimecodeError> {
Timecode::from_frames(self.frames, frame_rate)
}
pub fn to_seconds(self, frame_rate: FrameRate) -> f64 {
let (num, den) = frame_rate.as_rational();
self.frames as f64 * den as f64 / num as f64
}
pub fn from_seconds(seconds: f64, frame_rate: FrameRate) -> Self {
let (num, den) = frame_rate.as_rational();
let frames = (seconds * num as f64 / den as f64).round() as u64;
Self { frames }
}
pub fn from_timecode(tc: &Timecode) -> Self {
Self {
frames: tc.to_frames(),
}
}
}
impl From<u64> for FrameOffset {
fn from(n: u64) -> Self {
Self::new(n)
}
}
#[derive(Debug, Clone)]
pub struct CrossRateConverter {
src_rate: FrameRate,
dst_rate: FrameRate,
}
impl CrossRateConverter {
pub fn new(src_rate: FrameRate, dst_rate: FrameRate) -> Self {
Self { src_rate, dst_rate }
}
pub fn convert(&self, offset: FrameOffset) -> FrameOffset {
let (src_num, src_den) = self.src_rate.as_rational();
let (dst_num, dst_den) = self.dst_rate.as_rational();
let numerator = offset.frames as u128 * src_den as u128 * dst_num as u128;
let denominator = src_num as u128 * dst_den as u128;
let dst_frames = (numerator + denominator / 2) / denominator;
FrameOffset::new(dst_frames as u64)
}
pub fn seconds_to_offset(&self, seconds: f64) -> FrameOffset {
FrameOffset::from_seconds(seconds, self.dst_rate)
}
}
#[derive(Debug, Clone)]
pub struct OffsetTable {
frame_rate: FrameRate,
entries: Vec<OffsetEntry>,
}
#[derive(Debug, Clone, Copy)]
pub struct OffsetEntry {
pub src_frame: u64,
pub dst_frame: u64,
pub edit_type: EditType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditType {
Continuous,
Cut,
Dissolve,
Discontinuity,
}
impl OffsetTable {
pub fn new(frame_rate: FrameRate) -> Self {
Self {
frame_rate,
entries: Vec::new(),
}
}
pub fn add_entry(&mut self, src_frame: u64, dst_frame: u64, edit_type: EditType) {
self.entries.push(OffsetEntry {
src_frame,
dst_frame,
edit_type,
});
self.entries.sort_by_key(|e| e.src_frame);
}
pub fn lookup(&self, src_frame: u64) -> Option<&OffsetEntry> {
let pos = self.entries.partition_point(|e| e.src_frame <= src_frame);
if pos == 0 {
None
} else {
Some(&self.entries[pos - 1])
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn frame_rate(&self) -> FrameRate {
self.frame_rate
}
pub fn offset_at(&self, src_frame: u64) -> Option<i64> {
let entry = self.lookup(src_frame)?;
Some(entry.dst_frame as i64 - entry.src_frame as i64)
}
}
pub fn frame_duration(start: FrameOffset, end: FrameOffset, frame_rate: FrameRate) -> f64 {
let frames = end.as_frames().saturating_sub(start.as_frames());
let (num, den) = frame_rate.as_rational();
frames as f64 * den as f64 / num as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_offset_arithmetic() {
let a = FrameOffset::new(100);
let b = a.add_frames(50);
assert_eq!(b.as_frames(), 150);
let c = b.sub_frames(30);
assert_eq!(c.as_frames(), 120);
}
#[test]
fn test_frame_offset_diff() {
let a = FrameOffset::new(200);
let b = FrameOffset::new(150);
assert_eq!(a.diff(b), 50);
assert_eq!(b.diff(a), -50);
}
#[test]
fn test_frame_offset_to_seconds_25fps() {
let offset = FrameOffset::new(25);
let secs = offset.to_seconds(FrameRate::Fps25);
assert!((secs - 1.0).abs() < 1e-9);
}
#[test]
fn test_frame_offset_from_seconds_25fps() {
let offset = FrameOffset::from_seconds(2.0, FrameRate::Fps25);
assert_eq!(offset.as_frames(), 50);
}
#[test]
fn test_frame_offset_from_timecode() {
let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).expect("valid timecode");
let offset = FrameOffset::from_timecode(&tc);
assert_eq!(offset.as_frames(), 25);
}
#[test]
fn test_frame_offset_to_timecode() {
let offset = FrameOffset::new(25);
let tc = offset
.to_timecode(FrameRate::Fps25)
.expect("to_timecode should succeed");
assert_eq!(tc.seconds, 1);
assert_eq!(tc.frames, 0);
}
#[test]
fn test_cross_rate_same_rate() {
let conv = CrossRateConverter::new(FrameRate::Fps25, FrameRate::Fps25);
let offset = FrameOffset::new(100);
let converted = conv.convert(offset);
assert_eq!(converted.as_frames(), 100);
}
#[test]
fn test_cross_rate_25_to_50() {
let conv = CrossRateConverter::new(FrameRate::Fps25, FrameRate::Fps50);
let offset = FrameOffset::new(25);
let converted = conv.convert(offset);
assert_eq!(converted.as_frames(), 50);
}
#[test]
fn test_cross_rate_50_to_25() {
let conv = CrossRateConverter::new(FrameRate::Fps50, FrameRate::Fps25);
let offset = FrameOffset::new(50);
let converted = conv.convert(offset);
assert_eq!(converted.as_frames(), 25);
}
#[test]
fn test_offset_table_lookup() {
let mut table = OffsetTable::new(FrameRate::Fps25);
table.add_entry(0, 0, EditType::Continuous);
table.add_entry(100, 200, EditType::Cut);
table.add_entry(300, 400, EditType::Dissolve);
assert!(table.lookup(0).is_some());
let entry = table.lookup(150).expect("lookup should succeed");
assert_eq!(entry.src_frame, 100);
assert_eq!(entry.dst_frame, 200);
}
#[test]
fn test_offset_table_offset_at() {
let mut table = OffsetTable::new(FrameRate::Fps25);
table.add_entry(0, 10, EditType::Continuous);
assert_eq!(table.offset_at(0), Some(10));
assert_eq!(table.offset_at(50), Some(10));
}
#[test]
fn test_offset_table_empty_lookup() {
let table = OffsetTable::new(FrameRate::Fps25);
assert!(table.lookup(0).is_none());
assert!(table.is_empty());
}
#[test]
fn test_frame_duration() {
let start = FrameOffset::new(0);
let end = FrameOffset::new(25);
let dur = frame_duration(start, end, FrameRate::Fps25);
assert!((dur - 1.0).abs() < 1e-9);
}
#[test]
fn test_frame_offset_sub_saturate() {
let a = FrameOffset::new(5);
let b = a.sub_frames(100);
assert_eq!(b.as_frames(), 0);
}
}