use crate::{Animated, TrackId};
use alpha2_time::{MediaTime, TICKS_PER_SECOND};
use serde::{Deserialize, Serialize};
pub(crate) const TICKS_PER_MS: u32 = 120;
const _: () = assert!(TICKS_PER_SECOND == 1_000 * TICKS_PER_MS as i64);
pub const MAX_GAIN: f32 = 16.0;
pub const MAX_AUDIO_EFFECTS: usize = 8_192;
pub const MAX_AUDIO_DELAY_MS: u32 = 120_000;
pub const AUDIO_EFFECT_STATE_MARGIN_MS: u32 = 1_100;
pub const REVERB_DELAY_MS: u32 = 160;
pub const MAX_EQ_GAIN_DB: f32 = 36.0;
pub const EQ_Q_RANGE: std::ops::RangeInclusive<f32> = 0.05..=50.0;
pub const EQ_FREQUENCY_RANGE_HZ: std::ops::RangeInclusive<f32> = 10.0..=47_040.0;
pub const MAX_COMPRESSOR_RATIO: f32 = 100.0;
pub const MAX_CHAIN_EFFECTS: usize = 16;
pub const MAX_EQ_BANDS: usize = 16;
pub const MIN_LEVEL_DB: f32 = -96.0;
pub const MAX_LOOKAHEAD_MS: f32 = 100.0;
pub const MAX_ENVELOPE_MS: f32 = 10_000.0;
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EqBandKind {
Peaking,
LowPass,
HighPass,
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct EqBand {
pub kind: EqBandKind,
pub frequency_hz: f32,
pub q: f32,
#[serde(default)]
pub gain_db: f32,
}
impl EqBand {
fn is_valid(self) -> bool {
EQ_FREQUENCY_RANGE_HZ.contains(&self.frequency_hz)
&& EQ_Q_RANGE.contains(&self.q)
&& self.gain_db.is_finite()
&& self.gain_db.abs() <= MAX_EQ_GAIN_DB
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AudioEffect {
Eq {
bands: Vec<EqBand>,
},
Compressor {
threshold_db: f32,
ratio: f32,
attack_ms: f32,
release_ms: f32,
#[serde(default)]
lookahead_ms: f32,
},
Reverb {
room_size: f32,
damping: f32,
mix: f32,
},
}
impl AudioEffect {
#[must_use]
pub fn delay_ticks(&self) -> u64 {
match self {
Self::Eq { .. } => 0,
Self::Compressor { .. } => self.lookahead_ticks(),
Self::Reverb { .. } => u64::from(REVERB_DELAY_MS) * u64::from(TICKS_PER_MS),
}
}
#[must_use]
pub fn lookahead_ticks(&self) -> u64 {
match self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Self::Compressor { lookahead_ms, .. } if *lookahead_ms > 0.0 => {
(f64::from(*lookahead_ms) * f64::from(TICKS_PER_MS)).ceil() as u64
}
_ => 0,
}
}
pub(crate) fn is_valid(&self) -> bool {
match self {
Self::Eq { bands } => {
!bands.is_empty()
&& bands.len() <= MAX_EQ_BANDS
&& bands.iter().copied().all(EqBand::is_valid)
}
Self::Compressor {
threshold_db,
ratio,
attack_ms,
release_ms,
lookahead_ms,
} => {
threshold_db.is_finite()
&& (MIN_LEVEL_DB..=0.0).contains(threshold_db)
&& (1.0..=MAX_COMPRESSOR_RATIO).contains(ratio)
&& positive_ms(*attack_ms)
&& positive_ms(*release_ms)
&& lookahead_ms.is_finite()
&& (0.0..=MAX_LOOKAHEAD_MS).contains(lookahead_ms)
}
Self::Reverb {
room_size,
damping,
mix,
} => unit(*room_size) && unit(*damping) && unit(*mix),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Ducking {
pub sidechain_track: TrackId,
pub threshold: f32,
pub depth_db: f32,
pub attack_ms: f32,
pub release_ms: f32,
}
impl Ducking {
fn is_valid(&self) -> bool {
self.threshold.is_finite()
&& self.threshold > 0.0
&& self.depth_db.is_finite()
&& (MIN_LEVEL_DB..=0.0).contains(&self.depth_db)
&& positive_ms(self.attack_ms)
&& positive_ms(self.release_ms)
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum FadeShape {
#[default]
Linear,
EqualPower,
}
impl FadeShape {
#[must_use]
pub fn amplitude(self, progress: f32) -> f32 {
let progress = progress.clamp(0.0, 1.0);
match self {
Self::Linear => progress,
Self::EqualPower => libm::sinf(progress * core::f32::consts::FRAC_PI_2),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct AudioProperties {
#[serde(default = "unit_gain")]
pub gain: Animated<f32>,
#[serde(default = "center_pan")]
pub pan: Animated<f32>,
#[serde(default)]
pub fade_in: MediaTime,
#[serde(default)]
pub fade_out: MediaTime,
#[serde(default)]
pub fade_shape: FadeShape,
#[serde(default)]
pub muted: bool,
#[serde(default)]
pub effects: Vec<AudioEffect>,
}
impl Default for AudioProperties {
fn default() -> Self {
Self {
gain: unit_gain(),
pan: center_pan(),
fade_in: MediaTime::ZERO,
fade_out: MediaTime::ZERO,
fade_shape: FadeShape::Linear,
muted: false,
effects: Vec::new(),
}
}
}
impl AudioProperties {
#[must_use]
pub fn is_audible(&self) -> bool {
!self.muted
&& match &self.gain {
Animated::Constant { value } => *value > 0.0,
Animated::Keyframes { keyframes } => {
keyframes.iter().any(|keyframe| keyframe.value > 0.0)
}
}
}
pub(crate) fn validate(&self) -> bool {
valid_gain(&self.gain)
&& valid_pan(&self.pan)
&& self.fade_in.ticks() >= 0
&& self.fade_out.ticks() >= 0
&& self.effects.len() <= MAX_CHAIN_EFFECTS
&& self.effects.iter().all(AudioEffect::is_valid)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TrackAudio {
#[serde(default = "unit_gain")]
pub gain: Animated<f32>,
#[serde(default = "center_pan")]
pub pan: Animated<f32>,
#[serde(default)]
pub muted: bool,
#[serde(default)]
pub solo: bool,
#[serde(default)]
pub effects: Vec<AudioEffect>,
#[serde(default)]
pub ducking: Option<Ducking>,
}
impl Default for TrackAudio {
fn default() -> Self {
Self {
gain: unit_gain(),
pan: center_pan(),
muted: false,
solo: false,
effects: Vec::new(),
ducking: None,
}
}
}
impl TrackAudio {
#[must_use]
pub const fn is_silenced(&self, any_solo: bool) -> bool {
self.muted || (any_solo && !self.solo)
}
pub(crate) fn validate(&self) -> bool {
valid_gain(&self.gain)
&& valid_pan(&self.pan)
&& self.effects.len() <= MAX_CHAIN_EFFECTS
&& self.effects.iter().all(AudioEffect::is_valid)
&& self.ducking.as_ref().is_none_or(Ducking::is_valid)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MasterAudio {
#[serde(default = "unit_gain")]
pub gain: Animated<f32>,
#[serde(default)]
pub effects: Vec<AudioEffect>,
#[serde(default = "default_limiter")]
pub limiter: Option<Limiter>,
}
impl Default for MasterAudio {
fn default() -> Self {
Self {
gain: unit_gain(),
effects: Vec::new(),
limiter: default_limiter(),
}
}
}
impl MasterAudio {
pub(crate) fn validate(&self) -> bool {
valid_gain(&self.gain)
&& self.effects.len() <= MAX_CHAIN_EFFECTS
&& self.effects.iter().all(AudioEffect::is_valid)
&& self.limiter.is_none_or(Limiter::is_valid)
}
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Limiter {
pub threshold: f32,
pub release_ms: f32,
}
impl Default for Limiter {
fn default() -> Self {
Self {
threshold: 1.0,
release_ms: 50.0,
}
}
}
impl Limiter {
fn is_valid(self) -> bool {
self.threshold.is_finite()
&& self.threshold > 0.0
&& self.threshold <= MAX_GAIN
&& positive_ms(self.release_ms)
}
}
#[allow(clippy::unnecessary_wraps)]
fn default_limiter() -> Option<Limiter> {
Some(Limiter::default())
}
fn unit_gain() -> Animated<f32> {
Animated::Constant { value: 1.0 }
}
fn center_pan() -> Animated<f32> {
Animated::Constant { value: 0.0 }
}
fn unit(value: f32) -> bool {
value.is_finite() && (0.0..=1.0).contains(&value)
}
fn positive_ms(value: f32) -> bool {
value.is_finite() && value > 0.0 && value <= MAX_ENVELOPE_MS
}
fn valid_gain(gain: &Animated<f32>) -> bool {
valid_range(gain, 0.0, MAX_GAIN)
}
fn valid_pan(pan: &Animated<f32>) -> bool {
valid_range(pan, -1.0, 1.0)
}
fn valid_range(animation: &Animated<f32>, low: f32, high: f32) -> bool {
let valid = |value: f32| value.is_finite() && (low..=high).contains(&value);
match animation {
Animated::Constant { value } => valid(*value),
Animated::Keyframes { keyframes } => {
crate::animation::valid_keyframes(keyframes)
&& keyframes.iter().all(|keyframe| valid(keyframe.value))
}
}
}