Skip to main content

alpha2_model/
audio.rs

1use crate::{Animated, TrackId};
2use alpha2_time::{MediaTime, TICKS_PER_SECOND};
3use serde::{Deserialize, Serialize};
4
5/// Ticks in a millisecond.
6pub(crate) const TICKS_PER_MS: u32 = 120;
7const _: () = assert!(TICKS_PER_SECOND == 1_000 * TICKS_PER_MS as i64);
8
9/// Largest linear amplitude accepted by any authored gain.
10pub const MAX_GAIN: f32 = 16.0;
11
12/// Most audio effects a project holds state for at once: its master's, its
13/// tracks', and those of the clips holding state then (see
14/// [`AUDIO_EFFECT_STATE_MARGIN_MS`]). An equalizer is one effect, whatever its
15/// bands.
16pub const MAX_AUDIO_EFFECTS: usize = 8_192;
17
18/// Most audio a project's effects hold in delay lines at once, in
19/// milliseconds, summed over the effects [`MAX_AUDIO_EFFECTS`] counts then: a
20/// compressor holds its `lookahead_ms`, a reverb [`REVERB_DELAY_MS`]. At the
21/// highest project rate this is under 100 MB of state.
22pub const MAX_AUDIO_DELAY_MS: u32 = 120_000;
23
24/// How long before its start, beyond the lookahead of its track's and its own
25/// compressors, a clip's effects are counted as holding state, in
26/// milliseconds. A mixer builds a clip's effects for the blocks that mix the
27/// clip and drops them for the first that does not; it reads a clip early by
28/// the lookahead after it, and one block of the largest size at the lowest
29/// project rate, with the rounding of each lookahead to samples, is within
30/// this margin. A clip's effects count until its end.
31pub const AUDIO_EFFECT_STATE_MARGIN_MS: u32 = 1_100;
32
33/// What one reverb counts toward [`MAX_AUDIO_DELAY_MS`]: its comb and allpass
34/// lines hold about 157 ms of audio at any rate.
35pub const REVERB_DELAY_MS: u32 = 160;
36
37/// Largest peaking gain, in decibels either way, an EQ band renders.
38pub const MAX_EQ_GAIN_DB: f32 = 36.0;
39
40/// The resonances an EQ band renders.
41pub const EQ_Q_RANGE: std::ops::RangeInclusive<f32> = 0.05..=50.0;
42
43/// The frequencies an EQ band may name, in hertz: from 10 Hz to just under
44/// the Nyquist frequency of the highest project rate. A band renders at most
45/// at 0.49 times the project's own rate, so a band above that limit of the
46/// project it is in sounds at the limit.
47pub const EQ_FREQUENCY_RANGE_HZ: std::ops::RangeInclusive<f32> = 10.0..=47_040.0;
48
49/// Largest compressor ratio.
50pub const MAX_COMPRESSOR_RATIO: f32 = 100.0;
51
52/// Most effects one chain holds: a clip's, a track's, or the master's.
53pub const MAX_CHAIN_EFFECTS: usize = 16;
54
55/// Most bands one equalizer holds.
56pub const MAX_EQ_BANDS: usize = 16;
57
58/// The quietest level a compressor threshold or a ducking depth names, in
59/// decibels; both are at most 0 dB.
60pub const MIN_LEVEL_DB: f32 = -96.0;
61
62/// Longest compressor lookahead, in milliseconds.
63pub const MAX_LOOKAHEAD_MS: f32 = 100.0;
64
65/// Longest attack or release time, in milliseconds.
66pub const MAX_ENVELOPE_MS: f32 = 10_000.0;
67
68/// The filter shape of one equalizer band.
69#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
71#[serde(rename_all = "snake_case")]
72pub enum EqBandKind {
73    /// Bell-shaped parametric band.
74    Peaking,
75    /// Second-order low-pass.
76    LowPass,
77    /// Second-order high-pass.
78    HighPass,
79}
80
81/// A single equalizer section stored independently from its DSP implementation.
82#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(deny_unknown_fields)]
85pub struct EqBand {
86    /// Filter shape.
87    pub kind: EqBandKind,
88    /// Center or corner frequency in hertz, within [`EQ_FREQUENCY_RANGE_HZ`].
89    pub frequency_hz: f32,
90    /// Resonance, within [`EQ_Q_RANGE`]. Larger values produce narrower bands.
91    pub q: f32,
92    /// Peaking gain in decibels, at most [`MAX_EQ_GAIN_DB`] either way.
93    /// Ignored by the pass filters.
94    #[serde(default)]
95    pub gain_db: f32,
96}
97
98impl EqBand {
99    fn is_valid(self) -> bool {
100        EQ_FREQUENCY_RANGE_HZ.contains(&self.frequency_hz)
101            && EQ_Q_RANGE.contains(&self.q)
102            && self.gain_db.is_finite()
103            && self.gain_db.abs() <= MAX_EQ_GAIN_DB
104    }
105}
106
107/// A serializable single-input audio effect applied to a clip or track bus.
108///
109/// Sidechain ducking is not an entry here because it consumes a second input.
110/// It is authored per track through [`Ducking`].
111#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(deny_unknown_fields)]
114#[serde(tag = "type", rename_all = "snake_case")]
115pub enum AudioEffect {
116    /// Cascaded biquad sections.
117    Eq {
118        /// Ordered filter sections, 1 to [`MAX_EQ_BANDS`] of them.
119        bands: Vec<EqBand>,
120    },
121    /// Lookahead dynamic range compression.
122    Compressor {
123        /// Level above which gain reduction begins, in decibels from
124        /// [`MIN_LEVEL_DB`] to 0.
125        threshold_db: f32,
126        /// Downward ratio, from one (unity) to [`MAX_COMPRESSOR_RATIO`].
127        ratio: f32,
128        /// Attack time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
129        attack_ms: f32,
130        /// Release time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
131        release_ms: f32,
132        /// Lookahead in milliseconds, at most [`MAX_LOOKAHEAD_MS`]. Reported as
133        /// graph latency.
134        #[serde(default)]
135        lookahead_ms: f32,
136    },
137    /// Schroeder reverberation.
138    Reverb {
139        /// Room size, in `0..=1`.
140        room_size: f32,
141        /// High-frequency damping, in `0..=1`.
142        damping: f32,
143        /// Wet mix, in `0..=1`.
144        mix: f32,
145    },
146}
147
148impl AudioEffect {
149    /// What this effect counts toward [`MAX_AUDIO_DELAY_MS`], in ticks (a
150    /// millisecond is 120), rounded up: a compressor its `lookahead_ms`, a
151    /// reverb [`REVERB_DELAY_MS`]. A tick count is exact, so the effects of a
152    /// project add up to the same number in any order.
153    #[must_use]
154    pub fn delay_ticks(&self) -> u64 {
155        match self {
156            Self::Eq { .. } => 0,
157            Self::Compressor { .. } => self.lookahead_ticks(),
158            Self::Reverb { .. } => u64::from(REVERB_DELAY_MS) * u64::from(TICKS_PER_MS),
159        }
160    }
161
162    /// How early a mixer reads this effect's input, in ticks, rounded up: a
163    /// compressor's `lookahead_ms`.
164    #[must_use]
165    pub fn lookahead_ticks(&self) -> u64 {
166        match self {
167            // A 32-bit value times 120 is exact in 64 bits, so rounding it up is too.
168            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
169            Self::Compressor { lookahead_ms, .. } if *lookahead_ms > 0.0 => {
170                (f64::from(*lookahead_ms) * f64::from(TICKS_PER_MS)).ceil() as u64
171            }
172            _ => 0,
173        }
174    }
175
176    pub(crate) fn is_valid(&self) -> bool {
177        match self {
178            Self::Eq { bands } => {
179                !bands.is_empty()
180                    && bands.len() <= MAX_EQ_BANDS
181                    && bands.iter().copied().all(EqBand::is_valid)
182            }
183            Self::Compressor {
184                threshold_db,
185                ratio,
186                attack_ms,
187                release_ms,
188                lookahead_ms,
189            } => {
190                threshold_db.is_finite()
191                    && (MIN_LEVEL_DB..=0.0).contains(threshold_db)
192                    && (1.0..=MAX_COMPRESSOR_RATIO).contains(ratio)
193                    && positive_ms(*attack_ms)
194                    && positive_ms(*release_ms)
195                    && lookahead_ms.is_finite()
196                    && (0.0..=MAX_LOOKAHEAD_MS).contains(lookahead_ms)
197            }
198            Self::Reverb {
199                room_size,
200                damping,
201                mix,
202            } => unit(*room_size) && unit(*damping) && unit(*mix),
203        }
204    }
205}
206
207/// Sidechain ducking authored on the track that is attenuated.
208#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
210#[serde(deny_unknown_fields)]
211pub struct Ducking {
212    /// Track whose mixed output drives the envelope follower.
213    pub sidechain_track: TrackId,
214    /// Linear level above which attenuation begins. Positive.
215    pub threshold: f32,
216    /// Maximum attenuation in decibels, from [`MIN_LEVEL_DB`] to 0.
217    pub depth_db: f32,
218    /// Attack time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
219    pub attack_ms: f32,
220    /// Release time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
221    pub release_ms: f32,
222}
223
224impl Ducking {
225    fn is_valid(&self) -> bool {
226        self.threshold.is_finite()
227            && self.threshold > 0.0
228            && self.depth_db.is_finite()
229            && (MIN_LEVEL_DB..=0.0).contains(&self.depth_db)
230            && positive_ms(self.attack_ms)
231            && positive_ms(self.release_ms)
232    }
233}
234
235/// Curve applied by a clip's fade in and fade out.
236#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
238#[serde(rename_all = "snake_case")]
239pub enum FadeShape {
240    /// Amplitude moves linearly. Two linear fades over one overlap dip by 6 dB.
241    #[default]
242    Linear,
243    /// Amplitude follows a quarter sine. Two of these crossfade at constant
244    /// power, which is what overlapping clips normally want.
245    EqualPower,
246}
247
248impl FadeShape {
249    /// Maps linear fade progress in `0..=1` to an amplitude multiplier.
250    #[must_use]
251    pub fn amplitude(self, progress: f32) -> f32 {
252        let progress = progress.clamp(0.0, 1.0);
253        match self {
254            Self::Linear => progress,
255            Self::EqualPower => libm::sinf(progress * core::f32::consts::FRAC_PI_2),
256        }
257    }
258}
259
260/// Per-clip audio presentation controls.
261#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263#[serde(deny_unknown_fields)]
264pub struct AudioProperties {
265    /// Animated linear amplitude constrained to `0..=MAX_GAIN`.
266    #[serde(default = "unit_gain")]
267    pub gain: Animated<f32>,
268    /// Animated stereo position from `-1` (left) to `1` (right).
269    #[serde(default = "center_pan")]
270    pub pan: Animated<f32>,
271    /// Fade-in duration measured from the clip's timeline start. Non-negative.
272    #[serde(default)]
273    pub fade_in: MediaTime,
274    /// Fade-out duration measured back from the clip's timeline end.
275    /// Non-negative.
276    #[serde(default)]
277    pub fade_out: MediaTime,
278    /// Curve used by both fades.
279    #[serde(default)]
280    pub fade_shape: FadeShape,
281    /// Silences this clip without removing it.
282    #[serde(default)]
283    pub muted: bool,
284    /// Ordered single-input effects applied before the track bus, at most
285    /// [`MAX_CHAIN_EFFECTS`].
286    #[serde(default)]
287    pub effects: Vec<AudioEffect>,
288}
289
290impl Default for AudioProperties {
291    fn default() -> Self {
292        Self {
293            gain: unit_gain(),
294            pan: center_pan(),
295            fade_in: MediaTime::ZERO,
296            fade_out: MediaTime::ZERO,
297            fade_shape: FadeShape::Linear,
298            muted: false,
299            effects: Vec::new(),
300        }
301    }
302}
303
304impl AudioProperties {
305    /// Returns whether this clip can contribute signal at any timestamp.
306    #[must_use]
307    pub fn is_audible(&self) -> bool {
308        !self.muted
309            && match &self.gain {
310                Animated::Constant { value } => *value > 0.0,
311                Animated::Keyframes { keyframes } => {
312                    keyframes.iter().any(|keyframe| keyframe.value > 0.0)
313                }
314            }
315    }
316
317    pub(crate) fn validate(&self) -> bool {
318        valid_gain(&self.gain)
319            && valid_pan(&self.pan)
320            && self.fade_in.ticks() >= 0
321            && self.fade_out.ticks() >= 0
322            && self.effects.len() <= MAX_CHAIN_EFFECTS
323            && self.effects.iter().all(AudioEffect::is_valid)
324    }
325}
326
327/// Per-track audio bus controls.
328#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330#[serde(deny_unknown_fields)]
331pub struct TrackAudio {
332    /// Animated linear bus amplitude constrained to `0..=MAX_GAIN`.
333    #[serde(default = "unit_gain")]
334    pub gain: Animated<f32>,
335    /// Animated stereo position applied after the clip mix, from `-1` (left)
336    /// to `1` (right).
337    #[serde(default = "center_pan")]
338    pub pan: Animated<f32>,
339    /// Silences this bus.
340    #[serde(default)]
341    pub muted: bool,
342    /// Restricts the mix to soloed buses when any track sets this.
343    #[serde(default)]
344    pub solo: bool,
345    /// Ordered single-input effects applied to the summed clip signal, at
346    /// most [`MAX_CHAIN_EFFECTS`].
347    #[serde(default)]
348    pub effects: Vec<AudioEffect>,
349    /// Optional sidechain attenuation driven by another track.
350    #[serde(default)]
351    pub ducking: Option<Ducking>,
352}
353
354impl Default for TrackAudio {
355    fn default() -> Self {
356        Self {
357            gain: unit_gain(),
358            pan: center_pan(),
359            muted: false,
360            solo: false,
361            effects: Vec::new(),
362            ducking: None,
363        }
364    }
365}
366
367impl TrackAudio {
368    /// Whether this bus adds nothing to the mix: it is muted, or another
369    /// track solos (`any_solo`, [`crate::Timeline::any_solo`]) and this one
370    /// does not.
371    #[must_use]
372    pub const fn is_silenced(&self, any_solo: bool) -> bool {
373        self.muted || (any_solo && !self.solo)
374    }
375
376    pub(crate) fn validate(&self) -> bool {
377        valid_gain(&self.gain)
378            && valid_pan(&self.pan)
379            && self.effects.len() <= MAX_CHAIN_EFFECTS
380            && self.effects.iter().all(AudioEffect::is_valid)
381            && self.ducking.as_ref().is_none_or(Ducking::is_valid)
382    }
383}
384
385/// Project-wide master bus settings.
386#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
387#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
388#[serde(deny_unknown_fields)]
389pub struct MasterAudio {
390    /// Animated linear master amplitude constrained to `0..=MAX_GAIN`.
391    #[serde(default = "unit_gain")]
392    pub gain: Animated<f32>,
393    /// Ordered single-input effects applied to the summed track buses, at
394    /// most [`MAX_CHAIN_EFFECTS`].
395    #[serde(default)]
396    pub effects: Vec<AudioEffect>,
397    /// Final peak limiter. An omitted field is [`Limiter::default`]; an
398    /// explicit `null` leaves the mix unlimited.
399    #[serde(default = "default_limiter")]
400    pub limiter: Option<Limiter>,
401}
402
403impl Default for MasterAudio {
404    fn default() -> Self {
405        Self {
406            gain: unit_gain(),
407            effects: Vec::new(),
408            limiter: default_limiter(),
409        }
410    }
411}
412
413impl MasterAudio {
414    pub(crate) fn validate(&self) -> bool {
415        valid_gain(&self.gain)
416            && self.effects.len() <= MAX_CHAIN_EFFECTS
417            && self.effects.iter().all(AudioEffect::is_valid)
418            && self.limiter.is_none_or(Limiter::is_valid)
419    }
420}
421
422/// Final peak limiter placed at the end of the master bus.
423#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
424#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
425#[serde(deny_unknown_fields)]
426pub struct Limiter {
427    /// Linear ceiling, positive and at most [`MAX_GAIN`]. One is full scale.
428    pub threshold: f32,
429    /// Release time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
430    pub release_ms: f32,
431}
432
433impl Default for Limiter {
434    fn default() -> Self {
435        Self {
436            threshold: 1.0,
437            release_ms: 50.0,
438        }
439    }
440}
441
442impl Limiter {
443    fn is_valid(self) -> bool {
444        self.threshold.is_finite()
445            && self.threshold > 0.0
446            && self.threshold <= MAX_GAIN
447            && positive_ms(self.release_ms)
448    }
449}
450
451/// Serde default: a project without a `limiter` field keeps the safety ceiling.
452#[allow(clippy::unnecessary_wraps)]
453fn default_limiter() -> Option<Limiter> {
454    Some(Limiter::default())
455}
456
457fn unit_gain() -> Animated<f32> {
458    Animated::Constant { value: 1.0 }
459}
460
461fn center_pan() -> Animated<f32> {
462    Animated::Constant { value: 0.0 }
463}
464
465fn unit(value: f32) -> bool {
466    value.is_finite() && (0.0..=1.0).contains(&value)
467}
468
469fn positive_ms(value: f32) -> bool {
470    value.is_finite() && value > 0.0 && value <= MAX_ENVELOPE_MS
471}
472
473fn valid_gain(gain: &Animated<f32>) -> bool {
474    valid_range(gain, 0.0, MAX_GAIN)
475}
476
477fn valid_pan(pan: &Animated<f32>) -> bool {
478    valid_range(pan, -1.0, 1.0)
479}
480
481fn valid_range(animation: &Animated<f32>, low: f32, high: f32) -> bool {
482    let valid = |value: f32| value.is_finite() && (low..=high).contains(&value);
483    match animation {
484        Animated::Constant { value } => valid(*value),
485        Animated::Keyframes { keyframes } => {
486            crate::animation::valid_keyframes(keyframes)
487                && keyframes.iter().all(|keyframe| valid(keyframe.value))
488        }
489    }
490}