use crate::{
RAW_SOURCE_V1_MAX_CLIPS, SourceLoaderDispositionV1, SourceProvenanceV1, SourceSetCoverageV1,
};
pub const EXACT_SOURCE_TIMING_V1_ID: &str = "urn:animsmith:exact-source-timing:1";
pub const EXACT_SOURCE_TIMING_V1_MAX_CLIPS: usize = RAW_SOURCE_V1_MAX_CLIPS;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExactSourceTimingUnavailableReasonV1 {
Malformed,
CustomFrameRateNotExact,
UnsupportedTimeMode,
UnsupportedTimeBasis,
ParserUnavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExactSourceTimingObservationStateV1<T> {
Observed(T),
ProvenAbsent,
Unavailable(ExactSourceTimingUnavailableReasonV1),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactSourceTimingObservationV1<T> {
state: ExactSourceTimingObservationStateV1<T>,
disposition: SourceLoaderDispositionV1,
provenance: Option<SourceProvenanceV1>,
}
impl<T> ExactSourceTimingObservationV1<T> {
pub fn observed(
value: T,
provenance: SourceProvenanceV1,
disposition: SourceLoaderDispositionV1,
) -> Self {
Self {
state: ExactSourceTimingObservationStateV1::Observed(value),
disposition,
provenance: Some(provenance),
}
}
pub fn proven_absent(provenance: SourceProvenanceV1) -> Self {
Self {
state: ExactSourceTimingObservationStateV1::ProvenAbsent,
disposition: SourceLoaderDispositionV1::NotApplicable,
provenance: Some(provenance),
}
}
pub fn unavailable(
reason: ExactSourceTimingUnavailableReasonV1,
provenance: Option<SourceProvenanceV1>,
disposition: SourceLoaderDispositionV1,
) -> Self {
Self {
state: ExactSourceTimingObservationStateV1::Unavailable(reason),
disposition,
provenance,
}
}
pub const fn state(&self) -> &ExactSourceTimingObservationStateV1<T> {
&self.state
}
pub const fn disposition(&self) -> SourceLoaderDispositionV1 {
self.disposition
}
pub const fn provenance(&self) -> Option<&SourceProvenanceV1> {
self.provenance.as_ref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExactSourceTimeBasisV1(i64);
impl ExactSourceTimeBasisV1 {
pub fn new(units_per_second: i64) -> Result<Self, ExactSourceTimingContractError> {
if units_per_second <= 0 {
return Err(ExactSourceTimingContractError::InvalidTimeBasis);
}
Ok(Self(units_per_second))
}
pub const fn units_per_second(self) -> i64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceTimelineModeV1 {
Default,
Fps120,
Fps100,
Fps60,
Fps50,
Fps48,
Fps30,
Fps30Drop,
NtscDropFrame,
NtscFullFrame,
Pal,
Fps24,
Fps1000,
FilmFullFrame,
Custom,
Fps96,
Fps72,
Fps59Dot94,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SourceTimeDisplayProtocolV1 {
Smpte,
FrameCount,
Default,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ParserFrameRateProjectionV1(u64);
impl ParserFrameRateProjectionV1 {
pub fn new(value: f64) -> Result<Self, ExactSourceTimingContractError> {
if !value.is_finite() || value <= 0.0 {
return Err(ExactSourceTimingContractError::InvalidFrameRateProjection);
}
Ok(Self(value.to_bits()))
}
pub const fn binary64_bits(self) -> u64 {
self.0
}
pub const fn parser_value(self) -> f64 {
f64::from_bits(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExactSourceFramePeriodV1(i64);
impl ExactSourceFramePeriodV1 {
pub fn new(units_per_frame: i64) -> Result<Self, ExactSourceTimingContractError> {
if units_per_frame <= 0 {
return Err(ExactSourceTimingContractError::InvalidFramePeriod);
}
Ok(Self(units_per_frame))
}
pub const fn units_per_frame(self) -> i64 {
self.0
}
pub fn is_whole_frame(self, coordinate_units: i64) -> bool {
coordinate_units.rem_euclid(self.0) == 0
}
pub fn frame_index(self, coordinate_units: i64) -> Option<i64> {
self.is_whole_frame(coordinate_units)
.then_some(coordinate_units / self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExactSourceRangeSelectionV1 {
Primary,
Fallback,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExactSourceClipTimeRangeV1 {
selection: ExactSourceRangeSelectionV1,
begin_units: i64,
end_units: i64,
}
impl ExactSourceClipTimeRangeV1 {
pub fn new(
selection: ExactSourceRangeSelectionV1,
begin_units: i64,
end_units: i64,
) -> Result<Self, ExactSourceTimingContractError> {
if begin_units > end_units {
return Err(ExactSourceTimingContractError::ReversedClipRange);
}
Ok(Self {
selection,
begin_units,
end_units,
})
}
pub const fn selection(self) -> ExactSourceRangeSelectionV1 {
self.selection
}
pub const fn begin_units(self) -> i64 {
self.begin_units
}
pub const fn end_units(self) -> i64 {
self.end_units
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactSourceClipTimingV1 {
source_clip_index: usize,
source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
}
impl ExactSourceClipTimingV1 {
pub fn new(
source_clip_index: usize,
source_time_range: ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1>,
) -> Self {
Self {
source_clip_index,
source_time_range,
}
}
pub const fn source_clip_index(&self) -> usize {
self.source_clip_index
}
pub const fn source_time_range(
&self,
) -> &ExactSourceTimingObservationV1<ExactSourceClipTimeRangeV1> {
&self.source_time_range
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExactSourceTimingV1 {
time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
clip_coverage: SourceSetCoverageV1,
clips: Vec<ExactSourceClipTimingV1>,
}
impl ExactSourceTimingV1 {
#[allow(clippy::too_many_arguments)]
pub fn new(
time_basis: ExactSourceTimingObservationV1<ExactSourceTimeBasisV1>,
declared_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
effective_time_mode: ExactSourceTimingObservationV1<SourceTimelineModeV1>,
declared_custom_frame_rate: ExactSourceTimingObservationV1<ParserFrameRateProjectionV1>,
frame_period: ExactSourceTimingObservationV1<ExactSourceFramePeriodV1>,
declared_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
effective_time_protocol: ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1>,
clip_coverage: SourceSetCoverageV1,
clips: Vec<ExactSourceClipTimingV1>,
) -> Result<Self, ExactSourceTimingContractError> {
if clips.len() > EXACT_SOURCE_TIMING_V1_MAX_CLIPS {
return Err(ExactSourceTimingContractError::TooManyClips {
count: clips.len(),
limit: EXACT_SOURCE_TIMING_V1_MAX_CLIPS,
});
}
for (expected, clip) in clips.iter().enumerate() {
if clip.source_clip_index != expected {
return Err(ExactSourceTimingContractError::NonCanonicalClipIndex {
expected,
actual: clip.source_clip_index,
});
}
}
Ok(Self {
time_basis,
declared_time_mode,
effective_time_mode,
declared_custom_frame_rate,
frame_period,
declared_time_protocol,
effective_time_protocol,
clip_coverage,
clips,
})
}
pub const fn contract_id(&self) -> &'static str {
EXACT_SOURCE_TIMING_V1_ID
}
pub const fn time_basis(&self) -> &ExactSourceTimingObservationV1<ExactSourceTimeBasisV1> {
&self.time_basis
}
pub const fn declared_time_mode(
&self,
) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
&self.declared_time_mode
}
pub const fn effective_time_mode(
&self,
) -> &ExactSourceTimingObservationV1<SourceTimelineModeV1> {
&self.effective_time_mode
}
pub const fn declared_custom_frame_rate(
&self,
) -> &ExactSourceTimingObservationV1<ParserFrameRateProjectionV1> {
&self.declared_custom_frame_rate
}
pub const fn frame_period(&self) -> &ExactSourceTimingObservationV1<ExactSourceFramePeriodV1> {
&self.frame_period
}
pub const fn declared_time_protocol(
&self,
) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
&self.declared_time_protocol
}
pub const fn effective_time_protocol(
&self,
) -> &ExactSourceTimingObservationV1<SourceTimeDisplayProtocolV1> {
&self.effective_time_protocol
}
pub const fn clip_coverage(&self) -> SourceSetCoverageV1 {
self.clip_coverage
}
pub fn clips(&self) -> &[ExactSourceClipTimingV1] {
&self.clips
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExactSourceTimingContractError {
#[error("source-time units per second must be positive")]
InvalidTimeBasis,
#[error("source-time units per frame must be positive")]
InvalidFramePeriod,
#[error("parser frame-rate projection must be finite and positive")]
InvalidFrameRateProjection,
#[error("exact source clip range must satisfy begin_units <= end_units")]
ReversedClipRange,
#[error("exact source timing has {count} clips, exceeding the limit of {limit}")]
TooManyClips {
count: usize,
limit: usize,
},
#[error("exact source clip index {actual} is not expected prefix index {expected}")]
NonCanonicalClipIndex {
expected: usize,
actual: usize,
},
#[error(
"exact source clip count {exact} does not match retained raw clip count {source_count}"
)]
ClipCountMismatch {
exact: usize,
source_count: usize,
},
#[error("exact source clip coverage does not match retained raw clip coverage")]
ClipCoverageMismatch,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whole_frame_predicate_is_signed_and_unit_exact() {
let period = ExactSourceFramePeriodV1::new(100).unwrap();
for coordinate in [-300, 0, 700] {
assert!(period.is_whole_frame(coordinate));
}
for coordinate in [-301, -299, 699, 701] {
assert!(!period.is_whole_frame(coordinate));
}
assert_eq!(period.frame_index(-300), Some(-3));
assert_eq!(period.frame_index(701), None);
}
#[test]
fn long_timeline_remains_unit_exact_beyond_binary64_integer_precision() {
let period = ExactSourceFramePeriodV1::new(4_708_704).unwrap();
let whole = period.units_per_frame() * 3_000_000_000;
assert!(whole > (1i64 << 53));
assert!(period.is_whole_frame(whole));
assert!(!period.is_whole_frame(whole - 1));
assert!(!period.is_whole_frame(whole + 1));
assert_eq!(period.frame_index(whole), Some(3_000_000_000));
assert_eq!(whole as f64, (whole - 1) as f64);
assert_eq!(whole as f64, (whole + 1) as f64);
}
}