oximedia_timecode/
subframe.rs1use crate::{Timecode, TimecodeError};
23
24#[derive(Debug, Clone, Copy)]
26pub struct SubframeTimestamp {
27 pub timecode: Timecode,
29 pub sample: u32,
31 pub sample_rate: u32,
33}
34
35impl SubframeTimestamp {
36 pub fn new(tc: Timecode, sample: u32, sample_rate: u32) -> Self {
45 SubframeTimestamp {
46 timecode: tc,
47 sample,
48 sample_rate: sample_rate.max(1),
49 }
50 }
51
52 pub fn to_nanos(&self) -> u64 {
66 let fps_info = self.timecode.frame_rate;
67 let fps = crate::frame_rate_from_info(&fps_info);
68 let (num, den) = fps.as_rational();
69
70 let total_frames = self.timecode.to_frames() as u128;
73 let ns_per_frame_num = 1_000_000_000_u128 * den as u128;
74 let ns_per_frame_den = num as u128;
75
76 let tc_nanos = total_frames * ns_per_frame_num / ns_per_frame_den;
77
78 let sub_nanos = self.sample as u128 * 1_000_000_000 / self.sample_rate as u128;
80
81 (tc_nanos + sub_nanos) as u64
82 }
83
84 pub fn subframe_fraction(&self) -> f64 {
91 let fps_info = self.timecode.frame_rate;
92 let fps = crate::frame_rate_from_info(&fps_info);
93 let frame_rate = fps.as_float();
94 let samples_per_frame = self.sample_rate as f64 / frame_rate;
95 if samples_per_frame <= 0.0 {
96 return 0.0;
97 }
98 (self.sample as f64 / samples_per_frame).clamp(0.0, 1.0)
99 }
100
101 pub fn to_seconds_f64(&self) -> f64 {
103 self.to_nanos() as f64 / 1_000_000_000.0
104 }
105
106 pub fn advance_samples(&self, samples: u32) -> Result<Self, TimecodeError> {
110 let fps_info = self.timecode.frame_rate;
111 let fps = crate::frame_rate_from_info(&fps_info);
112 let samples_per_frame = (self.sample_rate as f64 / fps.as_float()).round() as u32;
113
114 let total_samples = self.sample + samples;
115 let extra_frames = total_samples / samples_per_frame.max(1);
116 let new_sample = total_samples % samples_per_frame.max(1);
117
118 let mut new_tc = self.timecode;
119 for _ in 0..extra_frames {
120 new_tc.increment()?;
121 }
122
123 Ok(SubframeTimestamp::new(new_tc, new_sample, self.sample_rate))
124 }
125}
126
127impl PartialEq for SubframeTimestamp {
128 fn eq(&self, other: &Self) -> bool {
129 self.to_nanos() == other.to_nanos()
130 }
131}
132
133impl Eq for SubframeTimestamp {}
134
135impl PartialOrd for SubframeTimestamp {
136 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
137 Some(self.cmp(other))
138 }
139}
140
141impl Ord for SubframeTimestamp {
142 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
143 self.to_nanos().cmp(&other.to_nanos())
144 }
145}
146
147impl std::fmt::Display for SubframeTimestamp {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(
150 f,
151 "{} +{}/{} samples",
152 self.timecode, self.sample, self.sample_rate
153 )
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::FrameRate;
161
162 fn tc(h: u8, m: u8, s: u8, fr: u8, rate: FrameRate) -> Timecode {
163 Timecode::new(h, m, s, fr, rate).expect("valid timecode")
164 }
165
166 #[test]
167 fn zero_subframe_matches_tc_nanos() {
168 let t = tc(0, 0, 1, 0, FrameRate::Fps25);
169 let sub = SubframeTimestamp::new(t, 0, 48000);
170 assert_eq!(sub.to_nanos(), 1_000_000_000);
172 }
173
174 #[test]
175 fn half_frame_offset_at_25fps() {
176 let t = tc(0, 0, 1, 0, FrameRate::Fps25);
179 let sub = SubframeTimestamp::new(t, 24000, 48000);
180 assert_eq!(sub.to_nanos(), 1_500_000_000);
181 }
182
183 #[test]
184 fn subframe_fraction_zero() {
185 let t = tc(0, 0, 0, 0, FrameRate::Fps25);
186 let sub = SubframeTimestamp::new(t, 0, 48000);
187 assert!((sub.subframe_fraction() - 0.0).abs() < 1e-9);
188 }
189
190 #[test]
191 fn to_seconds_f64_is_consistent() {
192 let t = tc(0, 0, 2, 0, FrameRate::Fps25);
193 let sub = SubframeTimestamp::new(t, 0, 48000);
194 assert!((sub.to_seconds_f64() - 2.0).abs() < 1e-6);
195 }
196
197 #[test]
198 fn ordering() {
199 let t0 = tc(0, 0, 0, 0, FrameRate::Fps25);
200 let t1 = tc(0, 0, 0, 1, FrameRate::Fps25);
201 let sub0 = SubframeTimestamp::new(t0, 0, 48000);
202 let sub1 = SubframeTimestamp::new(t1, 0, 48000);
203 assert!(sub0 < sub1);
204 }
205
206 #[test]
207 fn sample_rate_zero_clamped() {
208 let t = tc(0, 0, 0, 0, FrameRate::Fps25);
209 let sub = SubframeTimestamp::new(t, 0, 0);
210 assert_eq!(sub.sample_rate, 1);
211 }
212}