Skip to main content

alpha2_model/
audio.rs

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