Skip to main content

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}