use crate::{Duration, Time, ValidationError};
use std::sync::Arc;
mod clip;
mod metadata;
pub use clip::{AudioClip, AudioClipParameters};
pub use metadata::{AudioChapter, AudioMetadata};
fn invalid(field: &str, message: &str) -> ValidationError {
ValidationError::invalid("audio", field, message)
}
pub trait AudioEffect: Send + Sync + std::fmt::Debug {
fn validate(&self, rate: u32) -> Result<(), ValidationError>;
fn tail(&self) -> Duration {
Time::ZERO
}
fn process(&self, input: &mut [f32], channels: u16, rate: u32) -> crate::Result;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PanLaw {
Linear,
#[default]
ConstantPower,
Minus4_5dB,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Limiter {
pub ceiling_db: f64,
pub lookahead: Duration,
pub release: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Loudness {
pub target_lufs: f64,
pub true_peak_db: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Master {
pub gain_db: f64,
pub limiter: Option<Limiter>,
pub normalize: Option<Loudness>,
}
impl Master {
pub fn safe() -> Self {
Self {
limiter: Some(Limiter {
ceiling_db: -1.0,
lookahead: Time::from_ticks(crate::TICKS_PER_SECOND / 200),
release: Time::from_ticks(crate::TICKS_PER_SECOND / 10),
}),
..Self::default()
}
}
pub fn validate(self) -> Result<(), ValidationError> {
if !self.gain_db.is_finite() || !(-96.0..=24.0).contains(&self.gain_db) {
return Err(invalid("master_gain", "Require finite -96..=24 dB"));
}
if self.limiter.is_some_and(|limit| {
!limit.ceiling_db.is_finite()
|| !(-24.0..=0.0).contains(&limit.ceiling_db)
|| limit.lookahead < Time::ZERO
|| limit.lookahead.as_seconds() > 0.02
|| limit.release <= Time::ZERO
|| limit.release.as_seconds() > 2.0
}) {
return Err(invalid("limiter", "Invalid ceiling, lookahead or release"));
}
if self.normalize.is_some_and(|normal| {
!normal.target_lufs.is_finite()
|| !(-70.0..=-5.0).contains(&normal.target_lufs)
|| !normal.true_peak_db.is_finite()
|| !(-24.0..=0.0).contains(&normal.true_peak_db)
}) {
return Err(invalid(
"normalize",
"Invalid LUFS target or true peak ceiling",
));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Duck {
pub threshold_db: f64,
pub depth_db: f64,
pub ratio: f64,
pub attack: Duration,
pub release: Duration,
pub lookahead: Duration,
}
impl Default for Duck {
fn default() -> Self {
Self {
threshold_db: -45.0,
depth_db: 12.0,
ratio: 100.0,
attack: Time::from_ticks(crate::TICKS_PER_SECOND / 1000 * 80),
release: Time::from_ticks(crate::TICKS_PER_SECOND / 1000 * 400),
lookahead: Time::from_ticks(crate::TICKS_PER_SECOND / 100),
}
}
}
impl Duck {
pub fn new(threshold_db: f64, depth_db: f64) -> Self {
Self {
threshold_db,
depth_db,
..Self::default()
}
}
pub fn ratio(mut self, ratio: f64) -> Self {
self.ratio = ratio;
self
}
pub fn timing(mut self, attack: Duration, release: Duration) -> Self {
self.attack = attack;
self.release = release;
self
}
pub fn lookahead(mut self, lookahead: Duration) -> Self {
self.lookahead = lookahead;
self
}
pub fn validate(self) -> Result<(), ValidationError> {
let seconds = |time: Duration, max: f64| {
time >= Time::ZERO && time.as_seconds().is_finite() && time.as_seconds() <= max
};
if !self.threshold_db.is_finite()
|| !(-96.0..=0.0).contains(&self.threshold_db)
|| !self.depth_db.is_finite()
|| !(0.0..=96.0).contains(&self.depth_db)
|| !self.ratio.is_finite()
|| !(1.0..=100.0).contains(&self.ratio)
|| !seconds(self.attack, 5.0)
|| !seconds(self.release, 10.0)
|| !seconds(self.lookahead, 0.5)
{
return Err(invalid(
"duck",
"Require threshold -96..=0 dB, depth 0..=96 dB, ratio 1..=100, attack <=5 s, release <=10 s and lookahead <=0.5 s",
));
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub enum DuckTrigger {
Clip(Arc<AudioClip>),
Bus(String),
}
#[derive(Clone, Debug)]
pub struct Ducking {
pub trigger: DuckTrigger,
pub duck: Duck,
}
#[derive(Clone, Debug)]
pub struct AudioBus {
pub name: String,
pub gain_db: f64,
pub effects: Vec<Arc<dyn AudioEffect>>,
pub ducking: Vec<Ducking>,
}
impl AudioBus {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
gain_db: 0.0,
effects: Vec::new(),
ducking: Vec::new(),
}
}
pub fn ducked_by(mut self, trigger: DuckTrigger, duck: Duck) -> Self {
self.ducking.push(Ducking { trigger, duck });
self
}
}
pub trait AudioSource: Send + Sync {
fn composition(&self) -> Option<&crate::Timeline> {
None
}
fn file_path(&self) -> Option<&std::path::Path> {
None
}
fn try_read(&self, start_frame: u64, output: &mut [f32]) -> crate::Result<usize> {
Ok(self.read(start_frame, output))
}
fn sample_rate(&self) -> u32;
fn channels(&self) -> u16;
fn read(&self, start_frame: u64, output: &mut [f32]) -> usize;
fn duration(&self) -> Option<Duration>;
}
impl<Source: AudioSource + ?Sized> AudioSource for Arc<Source> {
fn composition(&self) -> Option<&crate::Timeline> {
self.as_ref().composition()
}
fn file_path(&self) -> Option<&std::path::Path> {
self.as_ref().file_path()
}
fn try_read(&self, start: u64, output: &mut [f32]) -> crate::Result<usize> {
self.as_ref().try_read(start, output)
}
fn sample_rate(&self) -> u32 {
self.as_ref().sample_rate()
}
fn channels(&self) -> u16 {
self.as_ref().channels()
}
fn read(&self, start: u64, output: &mut [f32]) -> usize {
self.as_ref().read(start, output)
}
fn duration(&self) -> Option<Duration> {
self.as_ref().duration()
}
}
pub fn frames(time: Time, rate: u32) -> Result<u64, ValidationError> {
if time < Time::ZERO {
return Err(invalid("time", "Audio time must be nonnegative"));
}
Ok(
((u128::from(time.ticks() as u64) * u128::from(rate) + crate::TICKS_PER_SECOND as u128 / 2)
/ crate::TICKS_PER_SECOND as u128) as u64,
)
}
pub fn ceil_frames(time: Time, rate: u32) -> Result<u64, ValidationError> {
if time < Time::ZERO {
return Err(invalid("duration", "Audio duration must be nonnegative"));
}
Ok((u128::from(time.ticks() as u64) * u128::from(rate))
.div_ceil(crate::TICKS_PER_SECOND as u128) as u64)
}