use super::Scene;
use crate::{
Color, Duration, Error, FrameRate, Node, NodeModifiers, SceneContext, Size, TICKS_PER_SECOND,
Time,
};
use std::{fmt, sync::Arc};
#[cfg(test)]
mod tests;
mod track;
pub use track::{Clip, Track, TrackBuilder};
const MAX_TRACKS: usize = 256;
const MAX_CLIPS: usize = 16_384;
const MAX_ADJUSTMENTS: usize = 65_536;
fn invalid(message: impl Into<String>) -> Error {
Error::Timeline {
timeline: "<native>".into(),
message: message.into(),
}
}
fn on_track(track: usize, message: impl fmt::Display) -> Error {
invalid(format!("track[{track}]: {message}"))
}
fn add_time(first: Time, second: Duration) -> crate::Result<Time> {
first
.ticks()
.checked_add(second.ticks())
.map(Time::from_ticks)
.ok_or_else(|| invalid("Scheduled time exceeds signed ticks"))
}
fn align(time: Time, rate: FrameRate) -> crate::Result<(Time, i64)> {
let quantum = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
let numerator = i128::from(rate.numerator());
let lower = (i128::from(time.ticks()) * numerator).div_euclid(quantum);
let boundary = |frame: i128| {
let ticks = frame * quantum;
ticks.div_euclid(numerator) + i128::from(ticks.rem_euclid(numerator) != 0)
};
let left = boundary(lower);
let right = boundary(lower + 1);
let frame = if i128::from(time.ticks()) - left < right - i128::from(time.ticks()) {
lower
} else {
lower + 1
};
let ticks = i64::try_from(boundary(frame))
.map_err(|_| invalid("Nearest frame boundary exceeds signed ticks"))?;
let frame =
i64::try_from(frame).map_err(|_| invalid("Nearest frame index exceeds signed range"))?;
Ok((Time::from_ticks(ticks), frame))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoundaryEdge {
Start,
End,
Cursor,
Duration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoundaryAdjustment {
pub track: Option<usize>,
pub clip: Option<usize>,
pub edge: BoundaryEdge,
pub requested: Time,
pub aligned: Time,
pub frame: i64,
}
#[derive(Clone, Debug)]
pub struct Timeline {
size: Size,
rate: FrameRate,
background: Option<Color>,
tracks: Arc<Vec<Track>>,
inferred: Duration,
explicit: Option<Duration>,
adjustments: Arc<Vec<BoundaryAdjustment>>,
audio: Arc<Vec<crate::AudioClip>>,
resolved_audio: Option<Arc<Vec<crate::AudioClip>>>,
master: crate::audio::Master,
audio_buses: Arc<Vec<crate::audio::AudioBus>>,
errors: Arc<Vec<Arc<Error>>>,
}
impl Timeline {
fn defer(self, operation: impl FnOnce(Self) -> crate::Result<Self>) -> Self {
let mut previous = self.clone();
match operation(self) {
Ok(value) => value,
Err(error) => {
let errors = Arc::make_mut(&mut previous.errors);
if errors.len() < 1024 {
match error {
Error::BuildErrors(values) => {
errors.extend(values.into_iter().take(1024 - errors.len()))
}
error => errors.push(Arc::new(error)),
}
}
previous
}
}
}
pub fn new(size: Size, rate: impl Into<FrameRate>) -> Self {
let rate = rate.into();
Self::empty(size, rate).defer(|_| Self::try_new(size, rate))
}
fn empty(size: Size, rate: FrameRate) -> Self {
Self {
size,
rate,
background: None,
tracks: Arc::new(Vec::new()),
inferred: Time::ZERO,
explicit: None,
adjustments: Arc::new(Vec::new()),
audio: Arc::new(Vec::new()),
resolved_audio: None,
master: crate::audio::Master::default(),
audio_buses: Arc::new(Vec::new()),
errors: Arc::new(Vec::new()),
}
}
pub fn validate(&self) -> crate::Result<()> {
self.master.validate()?;
if self.audio_buses.len() > 16 {
return Err(invalid("At most sixteen audio buses"));
}
for (index, bus) in self.audio_buses.iter().enumerate() {
crate::audio::Master {
gain_db: bus.gain_db,
..Default::default()
}
.validate()?;
if bus.name.is_empty()
|| bus.name.len() > 128
|| bus.effects.len() > 16
|| self.audio_buses[..index]
.iter()
.any(|other| other.name == bus.name)
{
return Err(invalid("Invalid or duplicate audio bus"));
}
}
let mut errors = self.errors.as_ref().clone();
for (track_index, track) in self.tracks.iter().enumerate() {
for clip in track.clips.iter() {
if let Err(error) = clip.scene.validate() {
errors.push(Arc::new(on_track(
track_index,
format!("clip[{}]: {error}", clip.id),
)));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(Error::BuildErrors(errors))
}
}
pub fn track(self, build: impl FnOnce(TrackBuilder) -> TrackBuilder) -> Self {
self.defer(|timeline| timeline.try_track(|builder| Ok(build(builder))))
}
pub fn audio(self, clip: crate::AudioClip) -> Self {
self.defer(|timeline| timeline.try_audio(clip))
}
pub fn master(mut self, master: crate::audio::Master) -> Self {
self.master = master;
self
}
pub fn audio_bus(self, bus: crate::audio::AudioBus) -> Self {
self.defer(|mut timeline| {
Arc::make_mut(&mut timeline.audio_buses).push(bus);
for clip in timeline.audio.as_ref().clone() {
if clip.unresolved_file() && clip.parameters().duration.is_none() {
continue;
}
timeline.inferred = timeline.inferred.max(timeline.audio_end(&clip)?);
}
Ok(timeline)
})
}
pub fn audio_master(&self) -> crate::audio::Master {
self.master
}
pub fn audio_buses(&self) -> &[crate::audio::AudioBus] {
&self.audio_buses
}
pub fn duration(self, duration: Duration) -> Self {
self.defer(|timeline| timeline.try_duration(duration))
}
pub fn try_new(size: Size, rate: impl Into<FrameRate>) -> crate::Result<Self> {
let rate = rate.into();
if !size.width.is_finite()
|| !size.height.is_finite()
|| size.width <= 0.0
|| size.height <= 0.0
{
return Err(invalid(
"Timeline design dimensions must be finite and positive",
));
}
if i128::from(rate.numerator())
> i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator())
{
return Err(invalid("Frame rate exceeds one frame per tick"));
}
Ok(Self::empty(size, rate))
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn try_audio(mut self, clip: crate::AudioClip) -> crate::Result<Self> {
if self.audio.len() >= 256 {
return Err(invalid("Timeline exceeds 256 audio clips"));
}
if clip.unresolved_file() && clip.parameters().duration.is_none() {
Arc::make_mut(&mut self.audio).push(clip);
return Ok(self);
}
let end = self.audio_end(&clip)?;
let frame = end.to_frame(self.rate);
let boundary = Time::checked_from_frame(frame, self.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
let end = if boundary < end {
Time::checked_from_frame(frame + 1, self.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?
} else {
boundary
};
self.inferred = self.inferred.max(end);
Arc::make_mut(&mut self.audio).push(clip);
Ok(self)
}
fn audio_end(&self, clip: &crate::AudioClip) -> crate::Result<Time> {
let mut end = clip.end()?;
if let Some(bus) = self
.audio_buses
.iter()
.find(|bus| Some(bus.name.as_str()) == clip.parameters().bus)
{
for effect in &bus.effects {
let tail = effect.tail();
if tail < Time::ZERO || tail.as_seconds() > 30.0 {
return Err(invalid("Bus effect tail requires 0..=30 seconds"));
}
end = Time::from_ticks(
end.ticks()
.checked_add(tail.ticks())
.ok_or_else(|| invalid("Bus tail exceeds frame clock"))?,
);
}
}
if end.as_seconds() > 86400.0 {
return Err(invalid("Audio end exceeds 24 hours"));
}
let frame = end.to_frame(self.rate);
let boundary = Time::checked_from_frame(frame, self.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
Ok(if boundary < end {
Time::checked_from_frame(frame + 1, self.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?
} else {
boundary
})
}
pub fn try_track(
mut self,
build: impl FnOnce(TrackBuilder) -> crate::Result<TrackBuilder>,
) -> crate::Result<Self> {
if self.tracks.len() >= MAX_TRACKS {
return Err(invalid("Timeline exceeds 256 tracks"));
}
let builder = TrackBuilder {
index: self.tracks.len(),
rate: self.rate,
clips: Vec::new(),
cursor: Time::ZERO,
adjustments: Vec::new(),
pending: None,
errors: Vec::new(),
};
let builder = build(builder)?;
if builder.index != self.tracks.len() || builder.rate != self.rate {
return Err(invalid(
"Track builder belongs to a different index or frame grid",
));
}
let (track, adjustments) = builder.finish()?;
let total = self
.tracks
.iter()
.map(|track| track.clips.len())
.sum::<usize>()
+ track.clips.len();
if total > MAX_CLIPS || self.adjustments.len() + adjustments.len() > MAX_ADJUSTMENTS {
return Err(invalid("Timeline exceeds clip or adjustment allowance"));
}
self.inferred = self.inferred.max(track.duration);
Arc::make_mut(&mut self.tracks).push(track);
Arc::make_mut(&mut self.adjustments).extend(adjustments);
Ok(self)
}
pub fn try_duration(mut self, duration: Duration) -> crate::Result<Self> {
if duration < Time::ZERO {
return Err(invalid("Timeline duration must be nonnegative"));
}
let (aligned, frame) = align(duration, self.rate)?;
if duration > Time::ZERO && aligned == Time::ZERO {
return Err(invalid(
"Timeline duration becomes empty after frame alignment",
));
}
let adjustments = Arc::make_mut(&mut self.adjustments);
adjustments.retain(|adjustment| adjustment.edge != BoundaryEdge::Duration);
if duration != aligned {
if adjustments.len() >= MAX_ADJUSTMENTS {
return Err(invalid("Timeline exceeds adjustment allowance"));
}
adjustments.push(BoundaryAdjustment {
track: None,
clip: None,
edge: BoundaryEdge::Duration,
requested: duration,
aligned,
frame,
});
}
self.explicit = Some(aligned);
Ok(self)
}
pub fn size(&self) -> Size {
self.size
}
pub fn frame_rate(&self) -> FrameRate {
self.rate
}
pub fn total_duration(&self) -> Duration {
self.explicit.unwrap_or(self.inferred)
}
pub fn frame_count(&self) -> u64 {
self.total_duration().to_frame(self.rate) as u64
}
pub fn tracks(&self) -> &[Track] {
&self.tracks
}
pub fn adjustments(&self) -> &[BoundaryAdjustment] {
&self.adjustments
}
pub fn resolve_audio(
&self,
mut resolve: impl FnMut(&crate::AudioClip) -> crate::Result<crate::AudioClip>,
) -> crate::Result<Self> {
let clips = self
.audio_clips()?
.iter()
.map(&mut resolve)
.collect::<crate::Result<Vec<_>>>()?;
let mut timeline = self.clone();
for clip in &clips {
let end = timeline.audio_end(clip)?;
let frame = end.to_frame(timeline.rate);
let mut boundary = Time::checked_from_frame(frame, timeline.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
if boundary < end {
boundary = Time::checked_from_frame(frame + 1, timeline.rate)
.ok_or_else(|| invalid("Audio end exceeds frame clock"))?;
}
timeline.inferred = timeline.inferred.max(boundary);
}
timeline.resolved_audio = Some(Arc::new(clips));
Ok(timeline)
}
}
impl Scene for Timeline {
fn audio_composition(&self) -> Option<&Timeline> {
Some(self)
}
fn validate(&self) -> crate::Result<()> {
Timeline::validate(self)
}
fn duration(&self) -> Option<Duration> {
Some(self.total_duration())
}
fn audio_clips(&self) -> crate::Result<Vec<crate::AudioClip>> {
self.validate()?;
if let Some(clips) = &self.resolved_audio {
return Ok(clips.as_ref().clone());
}
let mut audio = self.audio.as_ref().clone();
for track in self.tracks.iter() {
for clip in track.clips.iter() {
let sources = if let Some(child) = clip.scene.audio_composition().filter(|child| {
child.audio_master() != crate::Master::default()
|| !child.audio_buses().is_empty()
}) {
vec![crate::AudioClip::composition(child.clone()).duration(clip.duration)]
} else {
clip.scene.audio_clips()?
};
for source in sources {
if let Some(source) = source.relocated(clip.start, clip.duration)? {
audio.push(source);
}
if audio.len() > 256 {
return Err(invalid("Nested timeline exceeds 256 audio clips"));
}
}
}
}
Ok(audio)
}
fn render(&self, context: &SceneContext<'_>) -> Node {
if let Err(error) = self.validate() {
return super::transition::diagnostic(crate::ValidationError::invalid(
"timeline",
"build",
error.to_string(),
));
}
if context.time() < Time::ZERO || context.time() >= self.total_duration() {
return Node::empty();
}
let parent_size = context.size();
let nested = context.is_nested_scene();
let context = context.for_timeline(self.size, self.rate);
let background = self
.background
.map(|color| Node::from(self.size.to_rect().fill(color)));
let clips = self.tracks.iter().filter_map(|track| {
let index = track
.clips
.partition_point(|clip| clip.start <= context.time())
.checked_sub(1)?;
let clip = &track.clips[index];
if context.time() >= clip.end() {
return None;
}
let render = |clip: &Clip| {
context.during(clip.start, clip.duration, |local| clip.scene.render(local))
};
let node = if let Some(transition) = clip
.transition_in
.as_ref()
.filter(|transition| context.time() < clip.start + transition.duration)
{
let outgoing = render(&track.clips[index - 1]);
let incoming = render(clip);
let animation = crate::Tween::new(0.0f64, 1.0, transition.duration)
.easing(transition.effect.easing());
let progress = crate::Animation::sample(&animation, context.time() - clip.start);
if progress.is_finite() {
let local = context
.shifted(clip.start)
.with_clip_duration(transition.duration);
transition
.effect
.compose(progress.clamp(0.0, 1.0), outgoing, incoming, &local)
} else {
super::transition::failure(
"easing",
"Transition easing must produce finite progress",
)
}
} else {
render(clip)
};
Some(Node::group([node]))
});
let node = Node::group(background.into_iter().chain(clips));
if parent_size == self.size {
return if nested {
node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
} else {
node
};
}
let scale =
(parent_size.width / self.size.width).min(parent_size.height / self.size.height);
let scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
f64::NAN
};
node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
.pivot(crate::Pivot::Point(crate::Point::ORIGIN))
.scale(scale)
.translate(
(parent_size.width - self.size.width * scale) * 0.5,
(parent_size.height - self.size.height * scale) * 0.5,
)
}
}