1use crate::{
8 RAW_SOURCE_V1_MAX_CLIPS, SourceLoaderDispositionV1, SourceProvenanceV1, SourceSetCoverageV1,
9};
10
11pub const EXACT_SOURCE_TIMING_V1_ID: &str = "urn:animsmith:exact-source-timing:1";
13
14pub const EXACT_SOURCE_TIMING_V1_MAX_CLIPS: usize = RAW_SOURCE_V1_MAX_CLIPS;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub enum ExactSourceTimingUnavailableReasonV1 {
20 Malformed,
22 CustomFrameRateNotExact,
24 UnsupportedTimeMode,
26 UnsupportedTimeBasis,
28 ParserUnavailable,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum ExactSourceTimingObservationStateV1<T> {
35 Observed(T),
37 ProvenAbsent,
39 Unavailable(ExactSourceTimingUnavailableReasonV1),
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ExactSourceTimingObservationV1<T> {
46 state: ExactSourceTimingObservationStateV1<T>,
47 disposition: SourceLoaderDispositionV1,
48 provenance: Option<SourceProvenanceV1>,
49}
50
51impl<T> ExactSourceTimingObservationV1<T> {
52 pub fn observed(
54 value: T,
55 provenance: SourceProvenanceV1,
56 disposition: SourceLoaderDispositionV1,
57 ) -> Self {
58 Self {
59 state: ExactSourceTimingObservationStateV1::Observed(value),
60 disposition,
61 provenance: Some(provenance),
62 }
63 }
64
65 pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
67 Self {
68 state: ExactSourceTimingObservationStateV1::ProvenAbsent,
69 disposition: SourceLoaderDispositionV1::NotApplicable,
70 provenance: Some(provenance),
71 }
72 }
73
74 pub fn unavailable(
76 reason: ExactSourceTimingUnavailableReasonV1,
77 provenance: Option<SourceProvenanceV1>,
78 disposition: SourceLoaderDispositionV1,
79 ) -> Self {
80 Self {
81 state: ExactSourceTimingObservationStateV1::Unavailable(reason),
82 disposition,
83 provenance,
84 }
85 }
86
87 pub const fn state(&self) -> &ExactSourceTimingObservationStateV1<T> {
89 &self.state
90 }
91
92 pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
94 self.disposition
95 }
96
97 pub const fn provenance(&self) -> Option<&SourceProvenanceV1> {
99 self.provenance.as_ref()
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
105pub struct ExactSourceTimeBasisV1(i64);
106
107impl ExactSourceTimeBasisV1 {
108 pub fn new(units_per_second: i64) -> Result<Self, ExactSourceTimingContractError> {
110 if units_per_second <= 0 {
111 return Err(ExactSourceTimingContractError::InvalidTimeBasis);
112 }
113 Ok(Self(units_per_second))
114 }
115
116 pub const fn units_per_second(self) -> i64 {
118 self.0
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
124pub enum SourceTimelineModeV1 {
125 Default,
127 Fps120,
129 Fps100,
131 Fps60,
133 Fps50,
135 Fps48,
137 Fps30,
139 Fps30Drop,
141 NtscDropFrame,
143 NtscFullFrame,
145 Pal,
147 Fps24,
149 Fps1000,
151 FilmFullFrame,
153 Custom,
155 Fps96,
157 Fps72,
159 Fps59Dot94,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
165pub enum SourceTimeDisplayProtocolV1 {
166 Smpte,
168 FrameCount,
170 Default,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
179pub struct ParserFrameRateProjectionV1(u64);
180
181impl ParserFrameRateProjectionV1 {
182 pub fn new(value: f64) -> Result<Self, ExactSourceTimingContractError> {
184 if !value.is_finite() || value <= 0.0 {
185 return Err(ExactSourceTimingContractError::InvalidFrameRateProjection);
186 }
187 Ok(Self(value.to_bits()))
188 }
189
190 pub const fn binary64_bits(self) -> u64 {
192 self.0
193 }
194
195 pub const fn parser_value(self) -> f64 {
197 f64::from_bits(self.0)
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
203pub struct ExactSourceFramePeriodV1(i64);
204
205impl ExactSourceFramePeriodV1 {
206 pub fn new(units_per_frame: i64) -> Result<Self, ExactSourceTimingContractError> {
208 if units_per_frame <= 0 {
209 return Err(ExactSourceTimingContractError::InvalidFramePeriod);
210 }
211 Ok(Self(units_per_frame))
212 }
213
214 pub const fn units_per_frame(self) -> i64 {
216 self.0
217 }
218
219 pub fn is_whole_frame(self, coordinate_units: i64) -> bool {
221 coordinate_units.rem_euclid(self.0) == 0
222 }
223
224 pub fn frame_index(self, coordinate_units: i64) -> Option<i64> {
226 self.is_whole_frame(coordinate_units)
227 .then_some(coordinate_units / self.0)
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
233pub enum ExactSourceRangeSelectionV1 {
234 Primary,
236 Fallback,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct ExactSourceClipTimeRangeV1 {
243 selection: ExactSourceRangeSelectionV1,
244 begin_units: i64,
245 end_units: i64,
246}
247
248impl ExactSourceClipTimeRangeV1 {
249 pub fn new(
251 selection: ExactSourceRangeSelectionV1,
252 begin_units: i64,
253 end_units: i64,
254 ) -> Result<Self, ExactSourceTimingContractError> {
255 if begin_units > end_units {
256 return Err(ExactSourceTimingContractError::ReversedClipRange);
257 }
258 Ok(Self {
259 selection,
260 begin_units,
261 end_units,
262 })
263 }
264
265 pub const fn selection(self) -> ExactSourceRangeSelectionV1 {
267 self.selection
268 }
269
270 pub const fn begin_units(self) -> i64 {
272 self.begin_units
273 }
274
275 pub const fn end_units(self) -> i64 {
277 self.end_units
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct ExactSourceClipTimingV1 {
284 source_clip_index: usize,
285 source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
286}
287
288impl ExactSourceClipTimingV1 {
289 pub fn new(
291 source_clip_index: usize,
292 source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
293 ) -> Self {
294 Self {
295 source_clip_index,
296 source_time_range,
297 }
298 }
299
300 pub const fn source_clip_index(&self) -> usize {
302 self.source_clip_index
303 }
304
305 pub const fn source_time_range(
307 &self,
308 ) -> &ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1> {
309 &self.source_time_range
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct ExactSourceTimingV1 {
316 time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
317 declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
318 effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
319 declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
320 frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
321 declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
322 effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
323 clip_coverage: SourceSetCoverageV1,
324 clips: Vec<ExactSourceClipTimingV1>,
325}
326
327impl ExactSourceTimingV1 {
328 #[allow(clippy::too_many_arguments)]
330 pub fn new(
331 time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
332 declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
333 effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
334 declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
335 frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
336 declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
337 effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
338 clip_coverage: SourceSetCoverageV1,
339 clips: Vec<ExactSourceClipTimingV1>,
340 ) -> Result<Self, ExactSourceTimingContractError> {
341 if clips.len() > EXACT_SOURCE_TIMING_V1_MAX_CLIPS {
342 return Err(ExactSourceTimingContractError::TooManyClips {
343 count: clips.len(),
344 limit: EXACT_SOURCE_TIMING_V1_MAX_CLIPS,
345 });
346 }
347 for (expected, clip) in clips.iter().enumerate() {
348 if clip.source_clip_index != expected {
349 return Err(ExactSourceTimingContractError::NonCanonicalClipIndex {
350 expected,
351 actual: clip.source_clip_index,
352 });
353 }
354 }
355 Ok(Self {
356 time_basis,
357 declared_time_mode,
358 effective_time_mode,
359 declared_custom_frame_rate,
360 frame_period,
361 declared_time_protocol,
362 effective_time_protocol,
363 clip_coverage,
364 clips,
365 })
366 }
367
368 pub const fn contract_id(&self) -> &'static str {
370 EXACT_SOURCE_TIMING_V1_ID
371 }
372
373 pub const fn time_basis(&self) -> &ExactSourceTimingObservationV1<ExactSourceTimeBasisV1> {
375 &self.time_basis
376 }
377
378 pub const fn declared_time_mode(
380 &self,
381 ) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
382 &self.declared_time_mode
383 }
384
385 pub const fn effective_time_mode(
387 &self,
388 ) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
389 &self.effective_time_mode
390 }
391
392 pub const fn declared_custom_frame_rate(
394 &self,
395 ) -> &ExactSourceTimingObservationV1<ParserFrameRateProjectionV1> {
396 &self.declared_custom_frame_rate
397 }
398
399 pub const fn frame_period(&self) -> &ExactSourceTimingObservationV1<ExactSourceFramePeriodV1> {
401 &self.frame_period
402 }
403
404 pub const fn declared_time_protocol(
406 &self,
407 ) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
408 &self.declared_time_protocol
409 }
410
411 pub const fn effective_time_protocol(
413 &self,
414 ) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
415 &self.effective_time_protocol
416 }
417
418 pub const fn clip_coverage(&self) -> SourceSetCoverageV1 {
420 self.clip_coverage
421 }
422
423 pub fn clips(&self) -> &[ExactSourceClipTimingV1] {
425 &self.clips
426 }
427}
428
429#[derive(Debug, thiserror::Error, PartialEq, Eq)]
431#[non_exhaustive]
432pub enum ExactSourceTimingContractError {
433 #[error("source-time units per second must be positive")]
435 InvalidTimeBasis,
436 #[error("source-time units per frame must be positive")]
438 InvalidFramePeriod,
439 #[error("parser frame-rate projection must be finite and positive")]
441 InvalidFrameRateProjection,
442 #[error("exact source clip range must satisfy begin_units <= end_units")]
444 ReversedClipRange,
445 #[error("exact source timing has {count} clips, exceeding the limit of {limit}")]
447 TooManyClips {
448 count: usize,
450 limit: usize,
452 },
453 #[error("exact source clip index {actual} is not expected prefix index {expected}")]
455 NonCanonicalClipIndex {
456 expected: usize,
458 actual: usize,
460 },
461 #[error(
463 "exact source clip count {exact} does not match retained raw clip count {source_count}"
464 )]
465 ClipCountMismatch {
466 exact: usize,
468 source_count: usize,
470 },
471 #[error("exact source clip coverage does not match retained raw clip coverage")]
473 ClipCoverageMismatch,
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn whole_frame_predicate_is_signed_and_unit_exact() {
482 let period = ExactSourceFramePeriodV1::new(100).unwrap();
483 for coordinate in [-300, 0, 700] {
484 assert!(period.is_whole_frame(coordinate));
485 }
486 for coordinate in [-301, -299, 699, 701] {
487 assert!(!period.is_whole_frame(coordinate));
488 }
489 assert_eq!(period.frame_index(-300), Some(-3));
490 assert_eq!(period.frame_index(701), None);
491 }
492
493 #[test]
494 fn long_timeline_remains_unit_exact_beyond_binary64_integer_precision() {
495 let period = ExactSourceFramePeriodV1::new(4_708_704).unwrap();
496 let whole = period.units_per_frame() * 3_000_000_000;
497 assert!(whole > (1i64 << 53));
498 assert!(period.is_whole_frame(whole));
499 assert!(!period.is_whole_frame(whole - 1));
500 assert!(!period.is_whole_frame(whole + 1));
501 assert_eq!(period.frame_index(whole), Some(3_000_000_000));
502
503 assert_eq!(whole as f64, (whole - 1) as f64);
504 assert_eq!(whole as f64, (whole + 1) as f64);
505 }
506}