Skip to main content

alpha2_eval/
retime.rs

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