use crate::audio::{self, AudioMix};
use crate::geometry::Vec2;
use crate::raster::{RasterImage, Resolution};
use crate::render_context::RenderContext;
use crate::time::Time;
use crate::timeline_component::{
Arrangement, AudioBuffer, Clock, Cue, NodeKind, TimelineComponent,
};
pub(super) const STUB_PROBE_SECONDS: f32 = 1.0;
#[crate::component(timeline)]
#[derive(Clone, crate::Keyable)]
pub struct VideoFile {
#[builder(into)]
pub path: String,
#[builder(into)]
pub duration: Option<f32>,
#[builder(skip)]
pub trim: Option<(f32, f32)>,
}
impl VideoFile {
pub fn trim(mut self, r: std::ops::Range<f32>) -> Self {
self.trim = Some((r.start, r.end));
self
}
fn probe(&self) -> f32 {
if let Some(d) = self.duration {
return d;
}
if let Some((a, b)) = self.trim {
return (b - a).max(0.0);
}
crate::video_decode::probe_duration(&self.path).unwrap_or(STUB_PROBE_SECONDS)
}
}
impl TimelineComponent for VideoFile {
fn duration(&self) -> Option<f32> {
Some(self.probe())
}
fn frame(
&self,
clock: Clock<'_>,
canvas: Vec2,
target: Resolution,
ctx: &mut dyn RenderContext,
) -> Option<RasterImage> {
let _ = ctx;
let _ = canvas;
crate::video_decode::decode_frame(&self.path, clock.local().seconds(), self.trim, target)
}
fn arrangement(&self, offset: f32) -> Arrangement {
Arrangement {
kind: NodeKind::Video,
label: self.path.clone(),
name: None,
source: None,
start: offset,
end: offset + self.probe(),
trim: self.trim,
triggers: Vec::new(),
children: Vec::new(),
}
}
}
#[crate::component(timeline)]
#[derive(Clone, crate::Keyable)]
pub struct AudioFile {
#[builder(into)]
pub path: String,
#[builder(default = 1.0)]
pub gain: f32,
#[builder(default = 0.0)]
pub fade_in: f32,
#[builder(default = 0.0)]
pub fade_out: f32,
#[builder(into)]
pub duration: Option<f32>,
#[builder(skip)]
pub trim: Option<(f32, f32)>,
}
impl AudioFile {
pub fn trim(mut self, r: std::ops::Range<f32>) -> Self {
self.trim = Some((r.start, r.end));
self
}
fn probe(&self) -> f32 {
if let Some(d) = self.duration {
return d;
}
match audio::decoded_duration(&self.path, self.trim) {
Ok(duration) => duration,
Err(_) => self
.trim
.map(|(a, b)| (b - a).max(0.0))
.unwrap_or(STUB_PROBE_SECONDS),
}
}
}
impl TimelineComponent for AudioFile {
fn duration(&self) -> Option<f32> {
Some(self.probe())
}
fn samples(&self, clock: Clock<'_>, window: f32) -> Option<AudioBuffer> {
let _ = (clock, window);
None
}
fn mix_into(&self, mix: &mut AudioMix, start_secs: f32, speed: f32) {
let speed = if speed.is_finite() && speed > 0.0 {
speed
} else {
1.0
};
let mix_duration = mix.duration();
let visible_start = start_secs.max(0.0);
if visible_start >= mix_duration {
return;
}
let source_offset = ((visible_start - start_secs) * speed).max(0.0);
let source_window = (mix_duration - visible_start).max(0.0) * speed;
let trim_start = self.trim.map(|(start, _)| start).unwrap_or(0.0);
let trim_end = self.trim.map(|(_, end)| end);
let decode_start = trim_start + source_offset;
let decode_end = trim_end
.map(|end| (decode_start + source_window).min(end))
.unwrap_or(decode_start + source_window);
if decode_end <= decode_start {
return;
}
if let Ok(buf) = audio::conform_file_cached(
&self.path,
self.trim,
mix.rate(),
mix.channels(),
self.gain,
speed,
) {
let output_start = source_offset / speed;
let clip_duration = self
.duration
.map(|duration| (duration / speed).max(0.0))
.unwrap_or_else(|| audio_buffer_duration(&buf));
let output_duration = (mix_duration - visible_start)
.max(0.0)
.min((clip_duration - output_start).max(0.0));
if output_duration <= 0.0 {
return;
}
let window = audio::slice_audio_buffer(
&buf,
Some((output_start, output_start + output_duration)),
);
let mut samples = window.samples;
apply_audio_fade(
&mut samples,
mix.rate(),
mix.channels(),
output_start,
clip_duration,
self.fade_in,
self.fade_out,
);
mix.add(&samples, visible_start);
}
}
fn arrangement(&self, offset: f32) -> Arrangement {
Arrangement {
kind: NodeKind::Audio,
label: self.path.clone(),
name: None,
source: None,
start: offset,
end: offset + self.probe(),
trim: self.trim,
triggers: Vec::new(),
children: Vec::new(),
}
}
}
fn audio_buffer_duration(buffer: &AudioBuffer) -> f32 {
let channels = buffer.channels.max(1) as usize;
let frames = buffer.samples.len() / channels;
frames as f32 / buffer.rate.max(1) as f32
}
fn apply_audio_fade(
samples: &mut [f32],
rate: u32,
channels: u16,
local_start: f32,
clip_duration: f32,
fade_in: f32,
fade_out: f32,
) {
let fade_in = fade_seconds(fade_in);
let fade_out = fade_seconds(fade_out);
if fade_in == 0.0 && fade_out == 0.0 {
return;
}
let channels = channels.max(1) as usize;
let frames = samples.len() / channels;
let rate = rate.max(1) as f32;
for frame in 0..frames {
let local_time = local_start + frame as f32 / rate;
let gain = audio_fade_gain(local_time, clip_duration, fade_in, fade_out);
for channel in 0..channels {
samples[frame * channels + channel] *= gain;
}
}
}
fn fade_seconds(seconds: f32) -> f32 {
if seconds.is_finite() {
seconds.max(0.0)
} else {
0.0
}
}
fn audio_fade_gain(local_time: f32, clip_duration: f32, fade_in: f32, fade_out: f32) -> f32 {
let rise = if fade_in <= 0.0 {
1.0
} else {
(local_time / fade_in).clamp(0.0, 1.0)
};
let fall = if fade_out <= 0.0 {
1.0
} else {
((clip_duration - local_time) / fade_out).clamp(0.0, 1.0)
};
rise.min(fall)
}
#[crate::component(timeline)]
#[derive(Clone, Copy, crate::Keyable)]
pub struct TimeBox {
pub duration: f32,
}
impl TimelineComponent for TimeBox {
fn duration(&self) -> Option<f32> {
Some(self.duration)
}
fn arrangement(&self, offset: f32) -> Arrangement {
Arrangement {
kind: NodeKind::Timeline,
label: "time box".to_owned(),
name: None,
source: None,
start: offset,
end: offset + self.duration,
trim: None,
triggers: Vec::new(),
children: Vec::new(),
}
}
}
#[crate::component(timeline)]
#[derive(Clone, crate::Keyable)]
pub struct Subtitle {
#[builder(into)]
pub text: String,
}
impl TimelineComponent for Subtitle {
fn cues(&self, offset: f32) -> Vec<Cue> {
let end = offset + self.duration().unwrap_or(0.0);
vec![Cue {
start: offset,
end,
text: self.text.clone(),
}]
}
fn arrangement(&self, offset: f32) -> Arrangement {
Arrangement {
kind: NodeKind::Subtitle,
label: self.text.clone(),
name: None,
source: None,
start: offset,
end: offset + self.duration().unwrap_or(0.0),
trim: None,
triggers: Vec::new(),
children: Vec::new(),
}
}
}