use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
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,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Key {
pub t: RationalTime,
pub v: f64,
#[serde(default)]
pub interp: Interp,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Param {
Value(f64),
Keys(Vec<Key>),
}
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,
};
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
};
let u = match a.interp {
Interp::Linear => u,
Interp::Ease => u * u * (3.0 - 2.0 * u),
Interp::Hold => 0.0,
};
a.v + (b.v - a.v) * 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 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 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),
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 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) {
if 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"
);
}
}