oximedia_timecode/
sync_map.rs1#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct TcOffset {
10 pub offset_frames: i64,
12 pub description: String,
14}
15
16impl TcOffset {
17 #[must_use]
19 pub fn new(offset_frames: i64, description: String) -> Self {
20 Self {
21 offset_frames,
22 description,
23 }
24 }
25
26 #[must_use]
28 pub fn is_zero(&self) -> bool {
29 self.offset_frames == 0
30 }
31
32 #[must_use]
34 pub fn negate(&self) -> Self {
35 Self {
36 offset_frames: -self.offset_frames,
37 description: self.description.clone(),
38 }
39 }
40}
41
42#[allow(dead_code)]
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct TcSyncPoint {
46 pub source_frame: i64,
48 pub target_frame: i64,
50}
51
52impl TcSyncPoint {
53 #[must_use]
55 pub fn new(source_frame: i64, target_frame: i64) -> Self {
56 Self {
57 source_frame,
58 target_frame,
59 }
60 }
61
62 #[must_use]
64 pub fn offset(&self) -> i64 {
65 self.target_frame - self.source_frame
66 }
67}
68
69#[allow(dead_code)]
70#[derive(Debug, Clone, Default)]
73pub struct SyncMap {
74 pub sync_points: Vec<TcSyncPoint>,
76}
77
78impl SyncMap {
79 #[must_use]
81 pub fn new() -> Self {
82 Self {
83 sync_points: Vec::new(),
84 }
85 }
86
87 pub fn add_sync_point(&mut self, point: TcSyncPoint) {
89 self.sync_points.push(point);
90 self.sync_points.sort_by_key(|p| p.source_frame);
91 }
92
93 #[must_use]
95 pub fn point_count(&self) -> usize {
96 self.sync_points.len()
97 }
98
99 #[allow(clippy::cast_precision_loss)]
106 #[must_use]
107 pub fn source_to_target(&self, frame: i64) -> i64 {
108 if self.sync_points.is_empty() {
109 return frame;
110 }
111 if self.sync_points.len() == 1 {
112 return frame + self.sync_points[0].offset();
113 }
114
115 let first = &self.sync_points[0];
116 let last = &self.sync_points[self.sync_points.len() - 1];
117
118 if frame <= first.source_frame {
119 return frame + first.offset();
120 }
121 if frame >= last.source_frame {
122 return frame + last.offset();
123 }
124
125 let idx = self
127 .sync_points
128 .partition_point(|p| p.source_frame <= frame);
129 let lo = &self.sync_points[idx - 1];
130 let hi = &self.sync_points[idx];
131
132 let span = (hi.source_frame - lo.source_frame) as f64;
133 let t = (frame - lo.source_frame) as f64 / span;
134 let interp_target = lo.target_frame as f64 + t * (hi.target_frame - lo.target_frame) as f64;
135 interp_target.round() as i64
136 }
137
138 #[allow(clippy::cast_precision_loss)]
142 #[must_use]
143 pub fn target_to_source(&self, frame: i64) -> i64 {
144 if self.sync_points.is_empty() {
145 return frame;
146 }
147 if self.sync_points.len() == 1 {
148 return frame - self.sync_points[0].offset();
149 }
150
151 let mut by_target = self.sync_points.clone();
153 by_target.sort_by_key(|p| p.target_frame);
154
155 let first = &by_target[0];
156 let last = &by_target[by_target.len() - 1];
157
158 if frame <= first.target_frame {
159 return frame - first.offset();
160 }
161 if frame >= last.target_frame {
162 return frame - last.offset();
163 }
164
165 let idx = by_target.partition_point(|p| p.target_frame <= frame);
166 let lo = &by_target[idx - 1];
167 let hi = &by_target[idx];
168
169 let span = (hi.target_frame - lo.target_frame) as f64;
170 let t = (frame - lo.target_frame) as f64 / span;
171 let interp_source = lo.source_frame as f64 + t * (hi.source_frame - lo.source_frame) as f64;
172 interp_source.round() as i64
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_tc_offset_is_zero_true() {
182 let o = TcOffset::new(0, "zero".into());
183 assert!(o.is_zero());
184 }
185
186 #[test]
187 fn test_tc_offset_is_zero_false() {
188 let o = TcOffset::new(10, "shift".into());
189 assert!(!o.is_zero());
190 }
191
192 #[test]
193 fn test_tc_offset_negate() {
194 let o = TcOffset::new(25, "shift".into());
195 let neg = o.negate();
196 assert_eq!(neg.offset_frames, -25);
197 }
198
199 #[test]
200 fn test_tc_offset_negate_zero() {
201 let o = TcOffset::new(0, "zero".into());
202 assert_eq!(o.negate().offset_frames, 0);
203 }
204
205 #[test]
206 fn test_tc_sync_point_offset() {
207 let p = TcSyncPoint::new(100, 150);
208 assert_eq!(p.offset(), 50);
209 }
210
211 #[test]
212 fn test_tc_sync_point_negative_offset() {
213 let p = TcSyncPoint::new(200, 100);
214 assert_eq!(p.offset(), -100);
215 }
216
217 #[test]
218 fn test_sync_map_empty_passthrough() {
219 let map = SyncMap::new();
220 assert_eq!(map.source_to_target(42), 42);
221 assert_eq!(map.target_to_source(42), 42);
222 }
223
224 #[test]
225 fn test_sync_map_single_point() {
226 let mut map = SyncMap::new();
227 map.add_sync_point(TcSyncPoint::new(0, 100));
228 assert_eq!(map.source_to_target(0), 100);
230 assert_eq!(map.source_to_target(50), 150);
231 }
232
233 #[test]
234 fn test_sync_map_two_points_interpolation() {
235 let mut map = SyncMap::new();
236 map.add_sync_point(TcSyncPoint::new(0, 0));
237 map.add_sync_point(TcSyncPoint::new(100, 200));
238 assert_eq!(map.source_to_target(50), 100);
240 }
241
242 #[test]
243 fn test_sync_map_extrapolate_before_first() {
244 let mut map = SyncMap::new();
245 map.add_sync_point(TcSyncPoint::new(100, 110));
246 map.add_sync_point(TcSyncPoint::new(200, 220));
247 assert_eq!(map.source_to_target(50), 60);
249 }
250
251 #[test]
252 fn test_sync_map_extrapolate_after_last() {
253 let mut map = SyncMap::new();
254 map.add_sync_point(TcSyncPoint::new(0, 0));
255 map.add_sync_point(TcSyncPoint::new(100, 200));
256 assert_eq!(map.source_to_target(150), 250);
258 }
259
260 #[test]
261 fn test_sync_map_target_to_source_single_point() {
262 let mut map = SyncMap::new();
263 map.add_sync_point(TcSyncPoint::new(0, 100));
264 assert_eq!(map.target_to_source(100), 0);
265 assert_eq!(map.target_to_source(150), 50);
266 }
267
268 #[test]
269 fn test_sync_map_point_count() {
270 let mut map = SyncMap::new();
271 assert_eq!(map.point_count(), 0);
272 map.add_sync_point(TcSyncPoint::new(0, 0));
273 assert_eq!(map.point_count(), 1);
274 map.add_sync_point(TcSyncPoint::new(100, 100));
275 assert_eq!(map.point_count(), 2);
276 }
277
278 #[test]
279 fn test_sync_map_sorted_after_add() {
280 let mut map = SyncMap::new();
281 map.add_sync_point(TcSyncPoint::new(200, 200));
282 map.add_sync_point(TcSyncPoint::new(0, 0));
283 map.add_sync_point(TcSyncPoint::new(100, 100));
284 assert_eq!(map.sync_points[0].source_frame, 0);
285 assert_eq!(map.sync_points[1].source_frame, 100);
286 assert_eq!(map.sync_points[2].source_frame, 200);
287 }
288}