1use super::Scene;
30use crate::{
31 Color, Duration, Error, FrameRate, Node, NodeModifiers, SceneContext, Size, TICKS_PER_SECOND,
32 Time,
33};
34use std::{fmt, sync::Arc};
35
36#[cfg(test)]
37mod tests;
38mod track;
39
40pub use track::{Clip, Track, TrackBuilder};
41
42const MAX_TRACKS: usize = 256;
43
44const MAX_CLIPS: usize = 16_384;
45
46const MAX_ADJUSTMENTS: usize = 65_536;
47
48fn invalid(message: impl Into<String>) -> Error {
49 Error::Timeline {
50 timeline: "<native>".into(),
51 message: message.into(),
52 }
53}
54
55fn on_track(track: usize, message: impl fmt::Display) -> Error {
56 invalid(format!("track[{track}]: {message}"))
57}
58
59fn add_time(first: Time, second: Duration) -> crate::Result<Time> {
60 first
61 .ticks()
62 .checked_add(second.ticks())
63 .map(Time::from_ticks)
64 .ok_or_else(|| invalid("Scheduled time exceeds signed ticks"))
65}
66
67fn align(time: Time, rate: FrameRate) -> crate::Result<(Time, i64)> {
68 let quantum = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
69 let numerator = i128::from(rate.numerator());
70 let lower = (i128::from(time.ticks()) * numerator).div_euclid(quantum);
71 let boundary = |frame: i128| {
72 let ticks = frame * quantum;
73 ticks.div_euclid(numerator) + i128::from(ticks.rem_euclid(numerator) != 0)
74 };
75 let left = boundary(lower);
76 let right = boundary(lower + 1);
77 let frame = if i128::from(time.ticks()) - left < right - i128::from(time.ticks()) {
78 lower
79 } else {
80 lower + 1
81 };
82 let ticks = i64::try_from(boundary(frame))
83 .map_err(|_| invalid("Nearest frame boundary exceeds signed ticks"))?;
84 let frame =
85 i64::try_from(frame).map_err(|_| invalid("Nearest frame index exceeds signed range"))?;
86 Ok((Time::from_ticks(ticks), frame))
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum BoundaryEdge {
92 Start,
94 End,
96 Cursor,
98 Duration,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct BoundaryAdjustment {
105 pub track: Option<usize>,
107 pub clip: Option<usize>,
109 pub edge: BoundaryEdge,
111 pub requested: Time,
113 pub aligned: Time,
115 pub frame: i64,
117}
118
119#[derive(Clone, Debug)]
128pub struct Timeline {
129 size: Size,
130 rate: FrameRate,
131 background: Option<Color>,
132 tracks: Arc<Vec<Track>>,
133 inferred: Duration,
134 explicit: Option<Duration>,
135 adjustments: Arc<Vec<BoundaryAdjustment>>,
136 audio: Arc<Vec<crate::AudioClip>>,
137 resolved_audio: Option<Arc<Vec<crate::AudioClip>>>,
138 master: crate::audio::Master,
139 audio_buses: Arc<Vec<crate::audio::AudioBus>>,
140 errors: Arc<Vec<Arc<Error>>>,
141}
142
143impl Timeline {
144 fn defer(self, operation: impl FnOnce(Self) -> crate::Result<Self>) -> Self {
145 let mut previous = self.clone();
146 match operation(self) {
147 Ok(value) => value,
148 Err(error) => {
149 let errors = Arc::make_mut(&mut previous.errors);
150 if errors.len() < 1024 {
151 match error {
152 Error::BuildErrors(values) => {
153 errors.extend(values.into_iter().take(1024 - errors.len()))
154 }
155 error => errors.push(Arc::new(error)),
156 }
157 }
158 previous
159 }
160 }
161 }
162
163 pub fn new(size: Size, rate: impl Into<FrameRate>) -> Self {
165 let rate = rate.into();
166 Self::empty(size, rate).defer(|_| Self::try_new(size, rate))
167 }
168
169 fn empty(size: Size, rate: FrameRate) -> Self {
170 Self {
171 size,
172 rate,
173 background: None,
174 tracks: Arc::new(Vec::new()),
175 inferred: Time::ZERO,
176 explicit: None,
177 adjustments: Arc::new(Vec::new()),
178 audio: Arc::new(Vec::new()),
179 resolved_audio: None,
180 master: crate::audio::Master::default(),
181 audio_buses: Arc::new(Vec::new()),
182 errors: Arc::new(Vec::new()),
183 }
184 }
185
186 pub fn validate(&self) -> crate::Result<()> {
188 self.master.validate()?;
189 if self.audio_buses.len() > 16 {
190 return Err(invalid("At most sixteen audio buses"));
191 }
192 for (index, bus) in self.audio_buses.iter().enumerate() {
193 crate::audio::Master {
194 gain_db: bus.gain_db,
195 ..Default::default()
196 }
197 .validate()?;
198 if bus.name.is_empty()
199 || bus.name.len() > 128
200 || bus.effects.len() > 16
201 || self.audio_buses[..index]
202 .iter()
203 .any(|other| other.name == bus.name)
204 {
205 return Err(invalid("Invalid or duplicate audio bus"));
206 }
207 }
208 let mut errors = self.errors.as_ref().clone();
209 for (track_index, track) in self.tracks.iter().enumerate() {
210 for clip in track.clips.iter() {
211 if let Err(error) = clip.scene.validate() {
212 errors.push(Arc::new(on_track(
213 track_index,
214 format!("clip[{}]: {error}", clip.id),
215 )));
216 }
217 }
218 }
219 if errors.is_empty() {
220 Ok(())
221 } else {
222 Err(Error::BuildErrors(errors))
223 }
224 }
225
226 pub fn track(self, build: impl FnOnce(TrackBuilder) -> TrackBuilder) -> Self {
228 self.defer(|timeline| timeline.try_track(|builder| Ok(build(builder))))
229 }
230
231 pub fn audio(self, clip: crate::AudioClip) -> Self {
233 self.defer(|timeline| timeline.try_audio(clip))
234 }
235
236 pub fn master(mut self, master: crate::audio::Master) -> Self {
238 self.master = master;
239 self
240 }
241
242 pub fn audio_bus(self, bus: crate::audio::AudioBus) -> Self {
244 self.defer(|mut timeline| {
245 Arc::make_mut(&mut timeline.audio_buses).push(bus);
246 for clip in timeline.audio.as_ref().clone() {
247 if clip.unresolved_file() && clip.parameters().duration.is_none() {
248 continue;
249 }
250 timeline.inferred = timeline.inferred.max(timeline.audio_end(&clip)?);
251 }
252 Ok(timeline)
253 })
254 }
255
256 pub fn audio_master(&self) -> crate::audio::Master {
258 self.master
259 }
260
261 pub fn audio_buses(&self) -> &[crate::audio::AudioBus] {
263 &self.audio_buses
264 }
265
266 pub fn duration(self, duration: Duration) -> Self {
268 self.defer(|timeline| timeline.try_duration(duration))
269 }
270
271 pub fn try_new(size: Size, rate: impl Into<FrameRate>) -> crate::Result<Self> {
274 let rate = rate.into();
275 if !size.width.is_finite()
276 || !size.height.is_finite()
277 || size.width <= 0.0
278 || size.height <= 0.0
279 {
280 return Err(invalid(
281 "Timeline design dimensions must be finite and positive",
282 ));
283 }
284 if i128::from(rate.numerator())
285 > i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator())
286 {
287 return Err(invalid("Frame rate exceeds one frame per tick"));
288 }
289 Ok(Self::empty(size, rate))
290 }
291
292 pub fn background(mut self, color: Color) -> Self {
294 self.background = Some(color);
295 self
296 }
297
298 pub fn try_audio(mut self, clip: crate::AudioClip) -> crate::Result<Self> {
301 if self.audio.len() >= 256 {
302 return Err(invalid("Timeline exceeds 256 audio clips"));
303 }
304 if clip.unresolved_file() && clip.parameters().duration.is_none() {
305 Arc::make_mut(&mut self.audio).push(clip);
306 return Ok(self);
307 }
308 let end = self.audio_end(&clip)?;
309 let frame = end.to_frame(self.rate);
310 let boundary = Time::checked_from_frame(frame, self.rate)
311 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
312 let end = if boundary < end {
313 Time::checked_from_frame(frame + 1, self.rate)
314 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?
315 } else {
316 boundary
317 };
318 self.inferred = self.inferred.max(end);
319 Arc::make_mut(&mut self.audio).push(clip);
320 Ok(self)
321 }
322
323 fn audio_end(&self, clip: &crate::AudioClip) -> crate::Result<Time> {
324 let mut end = clip.end()?;
325 if let Some(bus) = self
326 .audio_buses
327 .iter()
328 .find(|bus| Some(bus.name.as_str()) == clip.parameters().bus)
329 {
330 for effect in &bus.effects {
331 let tail = effect.tail();
332 if tail < Time::ZERO || tail.as_seconds() > 30.0 {
333 return Err(invalid("Bus effect tail requires 0..=30 seconds"));
334 }
335 end = Time::from_ticks(
336 end.ticks()
337 .checked_add(tail.ticks())
338 .ok_or_else(|| invalid("Bus tail exceeds frame clock"))?,
339 );
340 }
341 }
342 if end.as_seconds() > 86400.0 {
343 return Err(invalid("Audio end exceeds 24 hours"));
344 }
345 let frame = end.to_frame(self.rate);
346 let boundary = Time::checked_from_frame(frame, self.rate)
347 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
348 Ok(if boundary < end {
349 Time::checked_from_frame(frame + 1, self.rate)
350 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?
351 } else {
352 boundary
353 })
354 }
355
356 pub fn try_track(
359 mut self,
360 build: impl FnOnce(TrackBuilder) -> crate::Result<TrackBuilder>,
361 ) -> crate::Result<Self> {
362 if self.tracks.len() >= MAX_TRACKS {
363 return Err(invalid("Timeline exceeds 256 tracks"));
364 }
365 let builder = TrackBuilder {
366 index: self.tracks.len(),
367 rate: self.rate,
368 clips: Vec::new(),
369 cursor: Time::ZERO,
370 adjustments: Vec::new(),
371 pending: None,
372 errors: Vec::new(),
373 };
374 let builder = build(builder)?;
375 if builder.index != self.tracks.len() || builder.rate != self.rate {
376 return Err(invalid(
377 "Track builder belongs to a different index or frame grid",
378 ));
379 }
380 let (track, adjustments) = builder.finish()?;
381 let total = self
382 .tracks
383 .iter()
384 .map(|track| track.clips.len())
385 .sum::<usize>()
386 + track.clips.len();
387 if total > MAX_CLIPS || self.adjustments.len() + adjustments.len() > MAX_ADJUSTMENTS {
388 return Err(invalid("Timeline exceeds clip or adjustment allowance"));
389 }
390 self.inferred = self.inferred.max(track.duration);
391 Arc::make_mut(&mut self.tracks).push(track);
392 Arc::make_mut(&mut self.adjustments).extend(adjustments);
393 Ok(self)
394 }
395
396 pub fn try_duration(mut self, duration: Duration) -> crate::Result<Self> {
398 if duration < Time::ZERO {
399 return Err(invalid("Timeline duration must be nonnegative"));
400 }
401 let (aligned, frame) = align(duration, self.rate)?;
402 if duration > Time::ZERO && aligned == Time::ZERO {
403 return Err(invalid(
404 "Timeline duration becomes empty after frame alignment",
405 ));
406 }
407 let adjustments = Arc::make_mut(&mut self.adjustments);
408 adjustments.retain(|adjustment| adjustment.edge != BoundaryEdge::Duration);
409 if duration != aligned {
410 if adjustments.len() >= MAX_ADJUSTMENTS {
411 return Err(invalid("Timeline exceeds adjustment allowance"));
412 }
413 adjustments.push(BoundaryAdjustment {
414 track: None,
415 clip: None,
416 edge: BoundaryEdge::Duration,
417 requested: duration,
418 aligned,
419 frame,
420 });
421 }
422 self.explicit = Some(aligned);
423 Ok(self)
424 }
425
426 pub fn size(&self) -> Size {
428 self.size
429 }
430
431 pub fn frame_rate(&self) -> FrameRate {
433 self.rate
434 }
435
436 pub fn total_duration(&self) -> Duration {
438 self.explicit.unwrap_or(self.inferred)
439 }
440
441 pub fn frame_count(&self) -> u64 {
443 self.total_duration().to_frame(self.rate) as u64
444 }
445
446 pub fn tracks(&self) -> &[Track] {
448 &self.tracks
449 }
450
451 pub fn adjustments(&self) -> &[BoundaryAdjustment] {
453 &self.adjustments
454 }
455
456 pub fn resolve_audio(
458 &self,
459 mut resolve: impl FnMut(&crate::AudioClip) -> crate::Result<crate::AudioClip>,
460 ) -> crate::Result<Self> {
461 let clips = self
462 .audio_clips()?
463 .iter()
464 .map(&mut resolve)
465 .collect::<crate::Result<Vec<_>>>()?;
466 let mut timeline = self.clone();
467 for clip in &clips {
468 let end = timeline.audio_end(clip)?;
469 let frame = end.to_frame(timeline.rate);
470 let mut boundary = Time::checked_from_frame(frame, timeline.rate)
471 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
472 if boundary < end {
473 boundary = Time::checked_from_frame(frame + 1, timeline.rate)
474 .ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
475 }
476 timeline.inferred = timeline.inferred.max(boundary);
477 }
478 timeline.resolved_audio = Some(Arc::new(clips));
479 Ok(timeline)
480 }
481}
482
483impl Scene for Timeline {
484 fn audio_composition(&self) -> Option<&Timeline> {
485 Some(self)
486 }
487
488 fn validate(&self) -> crate::Result<()> {
489 Timeline::validate(self)
490 }
491
492 fn duration(&self) -> Option<Duration> {
493 Some(self.total_duration())
494 }
495
496 fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
497 self.validate()?;
498 if let Some(clips) = &self.resolved_audio {
499 return Ok(clips.as_ref().clone());
500 }
501 let mut audio = self.audio.as_ref().clone();
502 for track in self.tracks.iter() {
503 for clip in track.clips.iter() {
504 let sources = if let Some(child) = clip.scene.audio_composition().filter(|child| {
505 child.audio_master() != crate::Master::default()
506 || !child.audio_buses().is_empty()
507 }) {
508 vec![crate::AudioClip::composition(child.clone()).duration(clip.duration)]
509 } else {
510 clip.scene.audio_clips()?
511 };
512 for source in sources {
513 if let Some(source) = source.relocated(clip.start, clip.duration)? {
514 audio.push(source);
515 }
516 if audio.len() > 256 {
517 return Err(invalid("Nested timeline exceeds 256 audio clips"));
518 }
519 }
520 }
521 }
522 Ok(audio)
523 }
524
525 fn render(&self, context: &SceneContext<'_>) -> Node {
526 if let Err(error) = self.validate() {
527 return super::transition::diagnostic(crate::ValidationError::invalid(
528 "timeline",
529 "build",
530 error.to_string(),
531 ));
532 }
533 if context.time() < Time::ZERO || context.time() >= self.total_duration() {
534 return Node::empty();
535 }
536 let parent_size = context.size();
537 let nested = context.is_nested_scene();
538 let context = context.for_timeline(self.size, self.rate);
539 let background = self
540 .background
541 .map(|color| Node::from(self.size.to_rect().fill(color)));
542 let clips = self.tracks.iter().filter_map(|track| {
543 let index = track
544 .clips
545 .partition_point(|clip| clip.start <= context.time())
546 .checked_sub(1)?;
547 let clip = &track.clips[index];
548 if context.time() >= clip.end() {
549 return None;
550 }
551 let render = |clip: &Clip| {
552 context.during(clip.start, clip.duration, |local| clip.scene.render(local))
553 };
554 let node = if let Some(transition) = clip
555 .transition_in
556 .as_ref()
557 .filter(|transition| context.time() < clip.start + transition.duration)
558 {
559 let outgoing = render(&track.clips[index - 1]);
560 let incoming = render(clip);
561 let animation = crate::Tween::new(0.0f64, 1.0, transition.duration)
562 .easing(transition.effect.easing());
563 let progress = crate::Animation::sample(&animation, context.time() - clip.start);
564 if progress.is_finite() {
565 let local = context
566 .shifted(clip.start)
567 .with_clip_duration(transition.duration);
568 transition
569 .effect
570 .compose(progress.clamp(0.0, 1.0), outgoing, incoming, &local)
571 } else {
572 super::transition::failure(
573 "easing",
574 "Transition easing must produce finite progress",
575 )
576 }
577 } else {
578 render(clip)
579 };
580 Some(Node::group([node]))
581 });
582 let node = Node::group(background.into_iter().chain(clips));
583 if parent_size == self.size {
584 return if nested {
585 node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
586 } else {
587 node
588 };
589 }
590 let scale =
591 (parent_size.width / self.size.width).min(parent_size.height / self.size.height);
592 let scale = if scale.is_finite() && scale > 0.0 {
593 scale
594 } else {
595 f64::NAN
596 };
597 node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
598 .pivot(crate::Pivot::Point(crate::Point::ORIGIN))
599 .scale(scale)
600 .translate(
601 (parent_size.width - self.size.width * scale) * 0.5,
602 (parent_size.height - self.size.height * scale) * 0.5,
603 )
604 }
605}