alpha2_eval/retime.rs
1use alpha2_model::{LoopMode, TimeMap};
2use alpha2_time::{FrameRate, MediaTime, TICKS_PER_SECOND, TimeRange};
3
4/// The frames a reversed clip, or a ping-pong loop's backward pass, mirrors
5/// by: `ticks / per` timeline ticks each, frame `k` starting at
6/// `floor(k * ticks / per)` as the project's frames do.
7///
8/// Pictures mirror by project frame, so a reversed clip shows the forward
9/// clip's frames in the opposite order: at the start of each frame, the
10/// instant the forward clip shows at the start of the mirrored frame, at any
11/// rate and wherever the clip starts and ends. Sound mirrors by tick, as
12/// close as ticks come to the sample the mixer mirrors by.
13#[derive(Copy, Clone, Debug, Eq, PartialEq)]
14pub(crate) struct Mirror {
15 ticks: i128,
16 per: i128,
17}
18
19impl Mirror {
20 /// One tick.
21 pub(crate) const TICK: Self = Self { ticks: 1, per: 1 };
22
23 /// One frame at `rate`.
24 pub(crate) fn frame(rate: FrameRate) -> Self {
25 Self {
26 ticks: i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator()),
27 per: i128::from(rate.numerator()),
28 }
29 }
30
31 /// The tick frame `frame` starts at.
32 fn start_of(self, frame: i128) -> Option<i128> {
33 Some(frame.checked_mul(self.ticks)?.div_euclid(self.per))
34 }
35
36 /// The frame containing tick `tick`: the last starting at or before it.
37 fn frame_of(self, tick: i128) -> Option<i128> {
38 Some(
39 tick.checked_add(1)?
40 .checked_mul(self.per)?
41 .checked_sub(1)?
42 .div_euclid(self.ticks),
43 )
44 }
45
46 /// The forward offset `floor(e' * scale / length)` the mirror of
47 /// `elapsed` shows, within a window of `length` ticks starting at
48 /// timeline tick `start`.
49 ///
50 /// With `k0` and `k1` the first and last frames starting in the window
51 /// and `k` the frame containing `start + elapsed` (`k0` before it, with
52 /// no time into it), `e'` is `start_of(k0 + k1 - k)` less the time into
53 /// `k`, from the window's start, or zero where that falls before it. A
54 /// window no frame starts in shows its start.
55 fn offset(self, start: i128, length: i128, elapsed: i128, scale: i128) -> Option<i128> {
56 let first = self.frame_of(start.checked_sub(1)?)?.checked_add(1)?;
57 let last = self.frame_of(start.checked_add(length)?.checked_sub(1)?)?;
58 if first > last {
59 return Some(0);
60 }
61 let tick = start.checked_add(elapsed)?;
62 let frame = self.frame_of(tick)?;
63 let (frame, into) = if frame < first {
64 (first, 0)
65 } else {
66 (frame, tick.checked_sub(self.start_of(frame)?)?)
67 };
68 let mirrored = self
69 .start_of(first.checked_add(last)?.checked_sub(frame)?)?
70 .checked_sub(into)?
71 .checked_sub(start)?;
72 if mirrored <= 0 {
73 return Some(0);
74 }
75 Some(mirrored.checked_mul(scale)?.div_euclid(length))
76 }
77}
78
79/// The source instant a clip shows at timeline `time`, in whole ticks.
80///
81/// With `e = time - timeline.start`, `T` the timeline duration, `S` the
82/// source duration, and `mirror` the frames reversing mirrors by:
83///
84/// - forward: `source.start + floor(e * S / T)`;
85/// - reverse: the forward instant at the mirrored frame, less the time into
86/// the frame ([`Mirror`]), and the source start where that falls before
87/// zero;
88/// - freeze: its instant;
89/// - a repeating loop: `source.start + (e mod S)`;
90/// - a ping-pong loop: the same on even passes, and on odd ones the pass
91/// mirrored in the same way.
92///
93/// Returns `None` for an empty range or a value that does not fit in ticks.
94pub(crate) fn map_source_time(
95 timeline: TimeRange,
96 source: TimeRange,
97 time_map: TimeMap,
98 mirror: Mirror,
99 time: MediaTime,
100) -> Option<MediaTime> {
101 let timeline_duration = i128::from(timeline.duration()?.ticks());
102 let source_duration = i128::from(source.duration()?.ticks());
103 if timeline_duration == 0 || source_duration == 0 {
104 return None;
105 }
106 let elapsed = i128::from(time.ticks()) - i128::from(timeline.start().ticks());
107 let offset = match time_map {
108 TimeMap::Forward => elapsed
109 .checked_mul(source_duration)?
110 .div_euclid(timeline_duration),
111 TimeMap::Reverse => mirror.offset(
112 i128::from(timeline.start().ticks()),
113 timeline_duration,
114 elapsed,
115 source_duration,
116 )?,
117 TimeMap::Freeze { source_time } => return Some(source_time),
118 TimeMap::Loop { mode } => {
119 let within = elapsed.rem_euclid(source_duration);
120 let pass = elapsed.div_euclid(source_duration);
121 match mode {
122 LoopMode::PingPong if pass.rem_euclid(2) == 1 => mirror.offset(
123 i128::from(timeline.start().ticks())
124 .checked_add(pass.checked_mul(source_duration)?)?,
125 source_duration,
126 within,
127 source_duration,
128 )?,
129 LoopMode::Repeat | LoopMode::PingPong => within,
130 }
131 }
132 };
133 let mapped = i128::from(source.start().ticks()).checked_add(offset)?;
134 i64::try_from(mapped).ok().map(MediaTime::from_ticks)
135}
136
137/// The source instant a clip's picture shows at timeline `time`: its time
138/// map read with the reverse and ping-pong mirror of one project frame at
139/// `frame_rate`, exactly as evaluation draws it.
140///
141/// Editors that key a clip's source composition, or show where a clip reads
142/// its source, take the time from here. `tests/golden/time-rules-corpus.json` pins
143/// it for the TypeScript port.
144///
145/// Returns `None` for an empty range or a value that does not fit in ticks.
146#[must_use]
147pub fn picture_source_time(
148 timeline: TimeRange,
149 source: TimeRange,
150 time_map: TimeMap,
151 frame_rate: FrameRate,
152 time: MediaTime,
153) -> Option<MediaTime> {
154 map_source_time(timeline, source, time_map, Mirror::frame(frame_rate), time)
155}