Skip to main content

alpha2_model/
timeline.rs

1use crate::{
2    AudioProperties, Captions, ClipBlendMode, ClipComposition, ClipEffect, ClipId, Easing, Matte,
3    MediaKind, ShapeMask, TimeMap, TrackAudio, TrackId, VisualProperties,
4};
5use alpha2_media::SourceId;
6use alpha2_time::TimeRange;
7use serde::{Deserialize, Serialize};
8
9/// Timeline tracks ordered from back to front for visual tracks.
10#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(deny_unknown_fields)]
13pub struct Timeline {
14    /// Tracks in composition order.
15    pub tracks: Vec<Track>,
16}
17
18impl Timeline {
19    /// The smallest interval holding every clip and caption cue, which a whole-project render or
20    /// export covers. `None` for a timeline that shows nothing.
21    #[must_use]
22    pub fn extent(&self) -> Option<TimeRange> {
23        let mut ranges = self.tracks.iter().flat_map(|track| {
24            track.clips.iter().map(|clip| clip.timeline_range).chain(
25                track
26                    .captions
27                    .iter()
28                    .flat_map(|captions| captions.cues.iter().map(|cue| cue.timeline_range)),
29            )
30        });
31        let first = ranges.next()?;
32        let (start, end) = ranges.fold((first.start(), first.end()), |(start, end), range| {
33            (start.min(range.start()), end.max(range.end()))
34        });
35        TimeRange::new(start, end).ok()
36    }
37
38    /// Whether any track solos its bus, which silences every bus that does not
39    /// ([`TrackAudio::is_silenced`]).
40    #[must_use]
41    pub fn any_solo(&self) -> bool {
42        self.tracks.iter().any(|track| track.audio.solo)
43    }
44
45    /// The clips that are another clip's matte. Such a clip is drawn only into the mattes that
46    /// name it and never composited itself.
47    #[must_use]
48    pub fn matte_sources(&self) -> std::collections::HashSet<&ClipId> {
49        self.tracks
50            .iter()
51            .flat_map(|track| &track.clips)
52            .filter_map(|clip| clip.matte.as_ref().map(|matte| &matte.source))
53            .collect()
54    }
55}
56
57/// A homogeneous visual or audio timeline lane.
58#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(deny_unknown_fields)]
61#[non_exhaustive]
62pub struct Track {
63    /// Stable track identity.
64    pub id: TrackId,
65    /// User-visible track name.
66    pub name: String,
67    /// Controls source compatibility and evaluation behavior.
68    pub kind: TrackKind,
69    /// Clips on this track, in authoring order; evaluation sorts by time. A caption track has
70    /// none.
71    #[serde(default)]
72    pub clips: Vec<Clip>,
73    /// Explicit transitions between overlapping clips on this track.
74    #[serde(default)]
75    pub transitions: Vec<ClipTransition>,
76    /// Bus controls applied to this track's mixed audio.
77    #[serde(default)]
78    pub audio: TrackAudio,
79    /// The cues and style of a caption track; present exactly for that kind.
80    #[serde(default)]
81    pub captions: Option<Captions>,
82}
83
84impl Track {
85    /// Creates an empty track with default bus controls.
86    #[must_use]
87    pub fn new(id: TrackId, name: impl Into<String>, kind: TrackKind) -> Self {
88        Self {
89            id,
90            name: name.into(),
91            kind,
92            clips: Vec::new(),
93            transitions: Vec::new(),
94            audio: TrackAudio::default(),
95            captions: (kind == TrackKind::Caption).then(Captions::default),
96        }
97    }
98}
99
100/// The media domain accepted by a track.
101#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[serde(rename_all = "snake_case")]
104#[non_exhaustive]
105pub enum TrackKind {
106    /// A composited visual track.
107    Video,
108    /// A mixed audio track.
109    Audio,
110    /// Timed text, drawn over the tracks before it and under the ones after.
111    Caption,
112}
113
114impl TrackKind {
115    pub(crate) const fn accepts(self, source: MediaKind) -> bool {
116        matches!(
117            (self, source),
118            (
119                Self::Video,
120                MediaKind::Video | MediaKind::Image | MediaKind::Generated | MediaKind::Composition
121            ) | (Self::Audio, MediaKind::Audio)
122        )
123    }
124}
125
126/// A reference to a source over independent timeline and source ranges.
127#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129#[serde(deny_unknown_fields)]
130#[non_exhaustive]
131pub struct Clip {
132    /// Stable clip identity.
133    pub id: ClipId,
134    /// Source consumed by this clip.
135    pub source_id: SourceId,
136    /// Destination interval on the project timeline.
137    pub timeline_range: TimeRange,
138    /// Interval read from the source timeline.
139    pub source_range: TimeRange,
140    /// Resolution-independent visual presentation controls.
141    #[serde(default)]
142    pub visual: VisualProperties,
143    /// The composition graph that builds the clip's picture from its source, with the clip's
144    /// time mapping onto it. Without one the clip shows its source as is. Visual clips only.
145    #[serde(default)]
146    pub composition: Option<ClipComposition>,
147    /// Artistic operation used when this clip is composited over earlier layers.
148    #[serde(default)]
149    pub blend_mode: ClipBlendMode,
150    /// Shapes that limit where the clip is visible, applied in order after its composition and
151    /// before its transform. At most [`crate::MAX_CLIP_MASKS`]; visual clips only.
152    #[serde(default)]
153    pub masks: Vec<ShapeMask>,
154    /// Another visual clip whose picture sets where this clip is visible.
155    #[serde(default)]
156    pub matte: Option<Matte>,
157    /// How timeline time maps onto the source range.
158    #[serde(default)]
159    pub time_map: TimeMap,
160    /// Presentation controls applied to this clip's audio.
161    #[serde(default)]
162    pub audio: AudioProperties,
163    /// Whether a runtime may keep and reuse the clip's rendered picture. Never changes what
164    /// renders.
165    #[serde(default, skip_serializing_if = "CacheIntent::is_auto")]
166    pub cache: CacheIntent,
167}
168
169/// Whether a runtime may keep and reuse a clip's rendered picture.
170#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173pub enum CacheIntent {
174    /// The runtime decides, keeping pictures whose inputs it can identify.
175    #[default]
176    Auto,
177    /// Keep it ahead of other pictures, and bake its range when a host bakes render caches.
178    Pin,
179    /// Always render it live, never reading from or writing to a cache.
180    Bypass,
181}
182
183impl CacheIntent {
184    /// Whether it is [`CacheIntent::Auto`].
185    #[must_use]
186    pub const fn is_auto(&self) -> bool {
187        matches!(self, Self::Auto)
188    }
189}
190
191impl Clip {
192    /// Whether the clip's audio is time-stretched to keep its pitch: it asks to, plays forward
193    /// or reversed, and at a speed other than 1.
194    #[must_use]
195    pub fn stretches_audio(&self) -> bool {
196        self.audio.preserve_pitch
197            && matches!(self.time_map, TimeMap::Forward | TimeMap::Reverse)
198            && self.timeline_range.duration() != self.source_range.duration()
199    }
200
201    /// Creates a clip with default visual, blend, and audio controls and no composition.
202    #[must_use]
203    pub fn new(
204        id: ClipId,
205        source_id: SourceId,
206        timeline_range: TimeRange,
207        source_range: TimeRange,
208    ) -> Self {
209        Self {
210            id,
211            source_id,
212            timeline_range,
213            source_range,
214            visual: VisualProperties::default(),
215            composition: None,
216            blend_mode: ClipBlendMode::default(),
217            masks: Vec::new(),
218            matte: None,
219            time_map: TimeMap::Forward,
220            audio: AudioProperties::default(),
221            cache: CacheIntent::Auto,
222        }
223    }
224}
225
226/// A two-input transition over the overlap of two visual clips.
227#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
228#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
229#[serde(deny_unknown_fields)]
230pub struct ClipTransition {
231    /// Clip sampled as transition progress zero.
232    pub from_clip: ClipId,
233    /// Clip sampled as transition progress one.
234    pub to_clip: ClipId,
235    /// Half-open timeline interval over which both clips must be active.
236    pub timeline_range: TimeRange,
237    /// Timing curve applied to the transition's linear progress. The eased progress is clamped
238    /// to `0..=1`.
239    pub easing: Easing,
240    /// Versioned transition shader and named parameter values.
241    pub effect: ClipEffect,
242}