use ff_filter::{AnimationTrack, FilterStep};
use crate::clip::Clip;
use crate::ids::TrackId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoProperty {
X,
Y,
ScaleX,
ScaleY,
Rotation,
Opacity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioProperty {
Volume,
Pan,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrackAutomation {
pub opacity: Option<AnimationTrack<f64>>,
pub x: Option<AnimationTrack<f64>>,
pub y: Option<AnimationTrack<f64>>,
pub scale_x: Option<AnimationTrack<f64>>,
pub scale_y: Option<AnimationTrack<f64>>,
pub rotation: Option<AnimationTrack<f64>>,
pub volume: Option<AnimationTrack<f64>>,
pub pan: Option<AnimationTrack<f64>>,
}
impl TrackAutomation {
pub fn set_video(&mut self, property: VideoProperty, animation: AnimationTrack<f64>) {
let slot = match property {
VideoProperty::X => &mut self.x,
VideoProperty::Y => &mut self.y,
VideoProperty::ScaleX => &mut self.scale_x,
VideoProperty::ScaleY => &mut self.scale_y,
VideoProperty::Rotation => &mut self.rotation,
VideoProperty::Opacity => &mut self.opacity,
};
*slot = Some(animation);
}
pub fn set_audio(&mut self, property: AudioProperty, animation: AnimationTrack<f64>) {
let slot = match property {
AudioProperty::Volume => &mut self.volume,
AudioProperty::Pan => &mut self.pan,
};
*slot = Some(animation);
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Track {
pub id: TrackId,
pub name: String,
pub mute: bool,
pub solo: bool,
pub enabled: bool,
pub lock: bool,
pub clips: Vec<Clip>,
pub audio_effects: Vec<FilterStep>,
pub automation: TrackAutomation,
}
impl Track {
#[must_use]
pub fn new(clips: Vec<Clip>) -> Self {
Self {
id: TrackId::UNSET,
name: String::new(),
mute: false,
solo: false,
enabled: true,
lock: false,
clips,
audio_effects: Vec::new(),
automation: TrackAutomation::default(),
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn muted(mut self, mute: bool) -> Self {
self.mute = mute;
self
}
#[must_use]
pub fn soloed(mut self, solo: bool) -> Self {
self.solo = solo;
self
}
#[must_use]
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub fn locked(mut self, lock: bool) -> Self {
self.lock = lock;
self
}
#[must_use]
pub fn audio_effects(mut self, steps: Vec<FilterStep>) -> Self {
self.audio_effects = steps;
self
}
#[must_use]
pub fn with_opacity_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.opacity = Some(animation);
self
}
#[must_use]
pub fn with_x_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.x = Some(animation);
self
}
#[must_use]
pub fn with_y_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.y = Some(animation);
self
}
#[must_use]
pub fn with_scale_x_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.scale_x = Some(animation);
self
}
#[must_use]
pub fn with_scale_y_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.scale_y = Some(animation);
self
}
#[must_use]
pub fn with_rotation_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.rotation = Some(animation);
self
}
#[must_use]
pub fn with_volume_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.volume = Some(animation);
self
}
#[must_use]
pub fn with_pan_animation(mut self, animation: AnimationTrack<f64>) -> Self {
self.automation.pan = Some(animation);
self
}
pub(crate) fn is_active(&self, any_solo_in_list: bool) -> bool {
self.enabled && !self.mute && (!any_solo_in_list || self.solo)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_active_should_reflect_enabled_mute_solo() {
assert!(Track::new(vec![]).is_active(false));
assert!(!Track::new(vec![]).enabled(false).is_active(false));
assert!(!Track::new(vec![]).muted(true).is_active(false));
assert!(!Track::new(vec![]).is_active(true));
assert!(Track::new(vec![]).soloed(true).is_active(true));
assert!(
!Track::new(vec![])
.soloed(true)
.enabled(false)
.is_active(true)
);
}
#[test]
fn new_track_should_have_empty_audio_effects() {
assert!(Track::new(vec![]).audio_effects.is_empty());
}
#[test]
fn audio_effects_builder_should_set_chain() {
let track = Track::new(vec![]).audio_effects(vec![FilterStep::Volume(-6.0)]);
assert_eq!(track.audio_effects.len(), 1);
assert!(matches!(
track.audio_effects[0],
FilterStep::Volume(v) if (v - (-6.0)).abs() < 1e-9
));
}
#[test]
fn new_track_should_have_empty_automation() {
let a = TrackAutomation::default();
assert!(a.opacity.is_none() && a.x.is_none() && a.volume.is_none() && a.pan.is_none());
}
#[test]
fn with_animation_builders_should_set_each_property() {
let track = Track::new(vec![])
.with_opacity_animation(AnimationTrack::new())
.with_x_animation(AnimationTrack::new())
.with_y_animation(AnimationTrack::new())
.with_scale_x_animation(AnimationTrack::new())
.with_scale_y_animation(AnimationTrack::new())
.with_rotation_animation(AnimationTrack::new())
.with_volume_animation(AnimationTrack::new())
.with_pan_animation(AnimationTrack::new());
let a = &track.automation;
assert!(a.opacity.is_some(), "opacity set");
assert!(a.x.is_some() && a.y.is_some(), "position set");
assert!(a.scale_x.is_some() && a.scale_y.is_some(), "scale set");
assert!(a.rotation.is_some(), "rotation set");
assert!(a.volume.is_some() && a.pan.is_some(), "audio set");
}
#[test]
fn set_video_and_set_audio_should_target_the_right_slot() {
let mut a = TrackAutomation::default();
a.set_video(VideoProperty::ScaleY, AnimationTrack::new());
a.set_audio(AudioProperty::Pan, AnimationTrack::new());
assert!(a.scale_y.is_some(), "set_video hits scale_y");
assert!(a.pan.is_some(), "set_audio hits pan");
assert!(
a.scale_x.is_none() && a.volume.is_none(),
"no other slot touched"
);
}
}