use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use ffcine_core::anim::{Clock, Ease, Track as ParamTrack};
use ffcine_core::{Error, Result};
use crate::mix::ClipDefaults;
use ffcine_timeline::{Clip, Effect, GenericEffect, HasMetadata, RationalTime, Track};
pub const CLEAN: &str = "ffcine.audio.clean";
pub const LEVEL: &str = "ffcine.audio.level";
pub const SYNC: &str = "ffcine.audio.sync";
pub const PAN: &str = "ffcine.audio.pan";
pub const LOOP: &str = "ffcine.audio.loop";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Interp {
#[default]
Linear,
Ease,
Hold,
}
impl Interp {
pub fn ease(self) -> Ease {
match self {
Self::Linear => Ease::Linear,
Self::Ease => Ease::Smoothstep,
Self::Hold => Ease::Hold,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Key {
pub t: RationalTime,
pub v: f64,
#[serde(default, alias = "ease")]
pub interp: Interp,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Param {
Value(f64),
Keys(Vec<Key>),
Expr(ExprParam),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExprParam {
src: Value,
track: ParamTrack<f64>,
}
impl ExprParam {
pub fn parse(v: &Value) -> Result<Self> {
let mut norm = v.clone();
if let Some(Value::Array(keys)) = norm.get_mut("keys") {
for k in keys.iter_mut().filter_map(Value::as_object_mut) {
for name in ["interp", "ease"] {
if k.get(name).and_then(Value::as_str) == Some("ease") {
k.insert(name.into(), "smoothstep".into());
}
}
}
}
Ok(Self {
track: ParamTrack::parse(&norm, Clock::Source, "audio param")?,
src: v.clone(),
})
}
}
impl Serialize for ExprParam {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
self.src.serialize(s)
}
}
impl<'de> Deserialize<'de> for ExprParam {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let v = Value::deserialize(d)?;
Self::parse(&v).map_err(serde::de::Error::custom)
}
}
impl<'de> Deserialize<'de> for Param {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let v = Value::deserialize(d)?;
match &v {
Value::Number(n) => n
.as_f64()
.map(Param::Value)
.ok_or_else(|| D::Error::custom(format!("invalid number {n}"))),
Value::Array(a) if a.is_empty() || a.iter().any(Value::is_object) => {
Vec::<Key>::deserialize(v)
.map(Param::Keys)
.map_err(|e| D::Error::custom(format!("audio param keyframes: {e}")))
}
_ => ExprParam::parse(&v)
.map(Param::Expr)
.map_err(D::Error::custom),
}
}
}
impl Default for Param {
fn default() -> Self {
Param::Value(0.0)
}
}
impl From<f64> for Param {
fn from(v: f64) -> Self {
Param::Value(v)
}
}
impl Param {
pub fn is_constant(&self) -> bool {
matches!(self, Param::Value(_)) || matches!(self, Param::Keys(k) if k.len() < 2)
}
pub fn at(&self, t: f64) -> f64 {
let keys = match self {
Param::Value(v) => return *v,
Param::Keys(k) => k,
Param::Expr(e) => return e.track.at(Clock::Source, t, 0.0).unwrap_or(0.0),
};
let Some(first) = keys.first() else {
return 0.0;
};
let i = keys.partition_point(|k| k.t.to_seconds() <= t);
if i == 0 {
return first.v;
}
let a = &keys[i - 1];
let Some(b) = keys.get(i) else { return a.v };
let (ta, tb) = (a.t.to_seconds(), b.t.to_seconds());
let u = if tb > ta {
((t - ta) / (tb - ta)).clamp(0.0, 1.0)
} else {
1.0
};
a.v + (b.v - a.v) * a.interp.ease().at(u)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Pan {
pub pan: Param,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Bus {
#[default]
Dialogue,
Music,
Ambience,
Sfx,
}
impl Bus {
pub const ALL: [Bus; 4] = [Bus::Dialogue, Bus::Music, Bus::Ambience, Bus::Sfx];
pub fn of_track(track: &Track) -> Self {
if let Some(Ok(b)) = track.ffcine_get::<Bus>("bus") {
return b;
}
let n = track.name.to_ascii_lowercase();
if n.contains("music") {
Bus::Music
} else if n.contains("amb") {
Bus::Ambience
} else if n.contains("sfx") || n.contains("fx") {
Bus::Sfx
} else {
Bus::Dialogue
}
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|b| b.name() == name)
}
pub fn name(self) -> &'static str {
match self {
Bus::Dialogue => "dialogue",
Bus::Music => "music",
Bus::Ambience => "ambience",
Bus::Sfx => "sfx",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Denoise {
Off,
#[default]
Afftdn,
Arnndn,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Clean {
#[serde(alias = "strength")]
pub amount: f64,
pub highpass: Option<f64>,
pub lowpass: Option<f64>,
pub wind: bool,
pub declick: bool,
pub denoise: Denoise,
pub model: Option<String>,
pub dialogue: Option<bool>,
}
impl Default for Clean {
fn default() -> Self {
Self {
amount: 0.6,
highpass: Some(80.0),
lowpass: None,
wind: false,
declick: false,
denoise: Denoise::Afftdn,
model: None,
dialogue: None,
}
}
}
impl Clean {
pub fn for_mode(mode: LevelMode) -> Self {
Self::for_mode_in(mode, &ClipDefaults::default())
}
pub fn for_mode_in(mode: LevelMode, d: &ClipDefaults) -> Self {
match mode {
LevelMode::Dialogue => Self {
amount: d.clean,
highpass: d.highpass,
..Self::default()
},
_ => Self {
amount: if mode == LevelMode::Nat {
d.clean_nat
} else {
0.0
},
highpass: d.highpass,
dialogue: Some(false),
..Self::default()
},
}
}
pub fn filter_spec(&self, mode: LevelMode) -> String {
let c = self.amount.clamp(0.0, 1.0);
let mut f: Vec<String> = Vec::new();
if let Some(hz) = self.highpass {
f.push(format!("highpass=f={hz}"));
}
if let Some(hz) = self.lowpass {
f.push(format!("lowpass=f={hz}"));
}
if self.wind {
f.push("highpass=f=150:p=2,highpass=f=150:p=2,lowshelf=f=300:g=-6".into());
}
if self.declick || self.wind {
f.push("adeclick".into());
}
let afftdn = format!(
"afftdn=nr={:.1}:nf={:.0}:tn=1",
6.0 + 10.0 * c,
-34.0 + 6.0 * c
);
if c > 0.0 {
match (&self.denoise, &self.model) {
(Denoise::Off, _) => {}
(Denoise::Arnndn, Some(m)) => {
f.push(format!("arnndn=m='{}':mix={c:.2}", m.replace('\'', "\\'")));
}
_ => f.push(afftdn),
}
}
if self.dialogue.unwrap_or(mode == LevelMode::Dialogue) {
if c > 0.0 {
f.push(format!("equalizer=f=3200:t=q:w=1.1:g={:.2}", 1.8 * c));
f.push(format!("deesser=i={:.2}:m=0.5:f=0.55", 0.35 * c));
}
f.push("acompressor=threshold=-24dB:ratio=2.5:attack=8:release=160:makeup=1".into());
}
f.join(",")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LevelMode {
#[default]
#[serde(alias = "orig")]
Dialogue,
Nat,
Music,
Ambience,
Sfx,
Off,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Level {
pub mode: Option<LevelMode>,
pub lufs: Option<f64>,
pub gain_db: Param,
pub ref_dbfs: Option<f64>,
pub nat_db: Option<f64>,
pub duck_db: Option<f64>,
pub fade_in: Option<f64>,
pub fade_out: Option<f64>,
}
impl Level {
pub fn mode(&self, bus: Bus) -> LevelMode {
self.mode.unwrap_or(match bus {
Bus::Dialogue => LevelMode::Dialogue,
Bus::Music => LevelMode::Music,
Bus::Ambience => LevelMode::Ambience,
Bus::Sfx => LevelMode::Sfx,
})
}
pub fn audible(&self, bus: Bus) -> bool {
!(bus == Bus::Dialogue && self.mode(bus) == LevelMode::Music && self.nat_db.is_none())
}
pub fn target(&self, bus: Bus) -> Option<f64> {
self.target_in(bus, &ClipDefaults::default())
}
pub fn target_in(&self, bus: Bus, d: &ClipDefaults) -> Option<f64> {
if let Some(l) = self.lufs {
return Some(l);
}
match self.mode(bus) {
LevelMode::Dialogue => Some(d.dialogue_lufs),
LevelMode::Nat => Some(d.nat_lufs + self.nat_db.unwrap_or(0.0)),
LevelMode::Music if bus == Bus::Dialogue => {
Some(d.nat_lufs + self.nat_db.unwrap_or(0.0))
}
LevelMode::Music => Some(-20.0),
LevelMode::Ambience => Some(-36.0),
LevelMode::Sfx | LevelMode::Off => None,
}
}
pub fn duck(&self, bus: Bus) -> f64 {
self.duck_in(bus, &ClipDefaults::default())
}
pub fn duck_in(&self, bus: Bus, d: &ClipDefaults) -> f64 {
self.duck_db.unwrap_or(match (bus, self.mode(bus)) {
(Bus::Dialogue, LevelMode::Dialogue) => d.duck_db,
(Bus::Dialogue, LevelMode::Nat) => d.nat_duck_db,
_ => 0.0,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Loop {
pub r#in: Option<f64>,
pub out: Option<f64>,
pub crossfade: f64,
}
impl Default for Loop {
fn default() -> Self {
Self {
r#in: None,
out: None,
crossfade: 1.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SyncInfo {
pub schema: String,
pub video: String,
pub offset: f64,
pub drift_ppm: f64,
pub confidence: f64,
}
pub fn clip_effect<T: for<'de> Deserialize<'de>>(clip: &Clip, name: &str) -> Result<Option<T>> {
for e in &clip.effects {
let Effect::Effect(g) = e else { continue };
if g.effect_name != name {
continue;
}
let Some(params) = g.ffcine() else {
return Ok(Some(serde_json::from_value(Value::Object(Map::new()))?));
};
if let Some(schema) = params.get("schema").and_then(Value::as_str)
&& schema != format!("{name}/1")
{
return Err(Error::Invalid {
what: "audio effect schema",
value: schema.to_owned(),
});
}
return Ok(Some(serde_json::from_value(Value::Object(params.clone()))?));
}
Ok(None)
}
pub fn make_effect<T: Serialize>(label: &str, name: &str, params: &T) -> Result<Effect> {
let mut e = GenericEffect::new(label, name);
let Value::Object(mut m) = serde_json::to_value(params)? else {
return Err(Error::Invalid {
what: "effect parameters",
value: name.to_owned(),
});
};
m.insert("schema".into(), format!("{name}/1").into());
e.metadata
.insert(ffcine_timeline::FFCINE.into(), Value::Object(m));
Ok(Effect::Effect(e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_reads_resolve_keyframes() {
let v = serde_json::json!({
"schema": "ffcine.audio.level/1",
"gain_db": [
{ "t": { "OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": 0.0 }, "v": -6.0 },
{ "t": { "OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": 48.0 }, "v": 0.0, "interp": "hold" }
],
"fade_in": 0.5
});
let l: Level = serde_json::from_value(v).expect("level");
assert!((l.gain_db.at(1.0) + 3.0).abs() < 1e-9);
assert!((l.gain_db.at(5.0)).abs() < 1e-9);
assert_eq!(l.fade_in, Some(0.5));
assert_eq!(l.target(Bus::Music), Some(-20.0));
let orig: Level =
serde_json::from_value(serde_json::json!({"mode": "orig"})).expect("orig");
assert_eq!(
(orig.target(Bus::Dialogue), orig.duck(Bus::Dialogue)),
(Some(-18.0), -14.0)
);
let muted: Level =
serde_json::from_value(serde_json::json!({"mode": "music"})).expect("music");
assert!(!muted.audible(Bus::Dialogue) && muted.audible(Bus::Music));
}
#[test]
fn clean_spec_matches_prototype() {
assert_eq!(
Clean::default().filter_spec(LevelMode::Dialogue),
"highpass=f=80,afftdn=nr=12.0:nf=-30:tn=1,equalizer=f=3200:t=q:w=1.1:g=1.08,\
deesser=i=0.21:m=0.5:f=0.55,\
acompressor=threshold=-24dB:ratio=2.5:attack=8:release=160:makeup=1"
);
assert_eq!(
Clean::for_mode(LevelMode::Nat).filter_spec(LevelMode::Nat),
"highpass=f=80"
);
}
#[test]
fn gain_takes_expressions() {
let v = serde_json::json!({"gain_db": "-6 + 6 * clip(st - 1, 0, 1)"});
let l: Level = serde_json::from_value(v.clone()).expect("level");
assert!(!l.gain_db.is_constant());
assert!((l.gain_db.at(0.5) + 6.0).abs() < 1e-9 && (l.gain_db.at(1.5) + 3.0).abs() < 1e-9);
let back = serde_json::to_value(&l).expect("ser");
assert_eq!(back["gain_db"], v["gain_db"]);
assert!(serde_json::from_value::<Level>(serde_json::json!({"gain_db": "st +"})).is_err());
assert!((Interp::Ease.ease().at(0.25) - 0.156_25).abs() < 1e-12);
}
#[test]
fn bad_keys_are_an_error_not_an_expression() {
let e = serde_json::from_value::<Level>(serde_json::json!({
"gain_db": [{"t": 1, "v": -6, "interp": "ease"}]
}))
.expect_err("bad key");
assert!(e.to_string().contains("audio param keyframes"), "{e}");
let l: Level = serde_json::from_value(serde_json::json!({"gain_db": []})).expect("[]");
assert_eq!(l.gain_db, Param::Keys(Vec::new()));
let l: Level = serde_json::from_value(serde_json::json!({"gain_db": {
"keys": [{"t": 0, "v": 0, "interp": "ease"}, {"t": 1, "v": 1}], "expr": "value"
}}))
.expect("keys expr");
assert!((l.gain_db.at(0.25) - 0.156_25).abs() < 1e-12);
}
#[test]
fn key_ease_is_an_alias_of_interp() {
let l: Level = serde_json::from_value(serde_json::json!({
"gain_db": [
{"t": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": 0.0}, "v": 0.0, "ease": "ease"},
{"t": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": 24.0}, "v": 1.0}
]
}))
.expect("ease alias");
let Param::Keys(k) = &l.gain_db else {
panic!("keys: {:?}", l.gain_db)
};
assert_eq!(k[0].interp, Interp::Ease);
assert!((l.gain_db.at(0.25) - 0.156_25).abs() < 1e-12);
}
}