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    /// The source's audio stream the clip plays, counted from 0 over the file's audio streams in
277    /// container order. Absent plays the stream the host picks by default.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub stream: Option<u32>,
280    /// The stream's channels the clip plays, by index. One index plays that channel as a mono
281    /// source; two play a stereo pair, left then right. Absent plays every channel, folded by
282    /// speaker position. At most two distinct indices.
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub channels: Option<Vec<u32>>,
285}
286
287/// The most source channels [`AudioProperties::channels`] selects.
288pub const MAX_SELECTED_CHANNELS: usize = 2;
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            preserve_pitch: false,
301            stream: None,
302            channels: None,
303        }
304    }
305}
306
307impl AudioProperties {
308    /// Returns whether this clip can contribute signal at any timestamp.
309    #[must_use]
310    pub fn is_audible(&self) -> bool {
311        !self.muted
312            && match &self.gain {
313                Animated::Constant { value } => *value > 0.0,
314                Animated::Keyframes { keyframes } => {
315                    keyframes.iter().any(|keyframe| keyframe.value > 0.0)
316                }
317            }
318    }
319
320    pub(crate) fn validate(&self) -> bool {
321        valid_gain(&self.gain)
322            && valid_pan(&self.pan)
323            && self.fade_in.ticks() >= 0
324            && self.fade_out.ticks() >= 0
325            && self.effects.len() <= MAX_CHAIN_EFFECTS
326            && self.effects.iter().all(AudioEffect::is_valid)
327            && self.channels.as_deref().is_none_or(valid_channel_selection)
328    }
329}
330
331fn valid_channel_selection(channels: &[u32]) -> bool {
332    (1..=MAX_SELECTED_CHANNELS).contains(&channels.len())
333        && channels
334            .iter()
335            .all(|&channel| (channel as usize) < alpha2_media::MAX_AUDIO_CHANNELS)
336        && channels.first() != channels.get(1)
337}
338
339/// Per-track audio bus controls.
340#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342#[serde(deny_unknown_fields)]
343pub struct TrackAudio {
344    /// Animated linear bus amplitude constrained to `0..=MAX_GAIN`.
345    #[serde(default = "unit_gain")]
346    pub gain: Animated<f32>,
347    /// Animated stereo position applied after the clip mix, from `-1` (left) to `1` (right).
348    #[serde(default = "center_pan")]
349    pub pan: Animated<f32>,
350    /// Silences this bus.
351    #[serde(default)]
352    pub muted: bool,
353    /// Restricts the mix to soloed buses when any track sets this.
354    #[serde(default)]
355    pub solo: bool,
356    /// Ordered single-input effects applied to the summed clip signal, at most
357    /// [`MAX_CHAIN_EFFECTS`].
358    #[serde(default)]
359    pub effects: Vec<AudioEffect>,
360    /// Optional sidechain attenuation driven by another track.
361    #[serde(default)]
362    pub ducking: Option<Ducking>,
363}
364
365impl Default for TrackAudio {
366    fn default() -> Self {
367        Self {
368            gain: unit_gain(),
369            pan: center_pan(),
370            muted: false,
371            solo: false,
372            effects: Vec::new(),
373            ducking: None,
374        }
375    }
376}
377
378impl TrackAudio {
379    /// Whether this bus adds nothing to the mix: it is muted, or another track solos
380    /// (`any_solo`, [`crate::Timeline::any_solo`]) and this one does not.
381    #[must_use]
382    pub const fn is_silenced(&self, any_solo: bool) -> bool {
383        self.muted || (any_solo && !self.solo)
384    }
385
386    pub(crate) fn validate(&self) -> bool {
387        valid_gain(&self.gain)
388            && valid_pan(&self.pan)
389            && self.effects.len() <= MAX_CHAIN_EFFECTS
390            && self.effects.iter().all(AudioEffect::is_valid)
391            && self.ducking.as_ref().is_none_or(Ducking::is_valid)
392    }
393}
394
395/// Project-wide master bus settings.
396#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
398#[serde(deny_unknown_fields)]
399pub struct MasterAudio {
400    /// Animated linear master amplitude constrained to `0..=MAX_GAIN`.
401    #[serde(default = "unit_gain")]
402    pub gain: Animated<f32>,
403    /// Ordered single-input effects applied to the summed track buses, at most
404    /// [`MAX_CHAIN_EFFECTS`].
405    #[serde(default)]
406    pub effects: Vec<AudioEffect>,
407    /// Final peak limiter. Omitted means [`Limiter::default`]; an explicit `null` leaves the mix
408    /// unlimited.
409    #[serde(default = "default_limiter")]
410    pub limiter: Option<Limiter>,
411}
412
413impl Default for MasterAudio {
414    fn default() -> Self {
415        Self {
416            gain: unit_gain(),
417            effects: Vec::new(),
418            limiter: default_limiter(),
419        }
420    }
421}
422
423impl MasterAudio {
424    pub(crate) fn validate(&self) -> bool {
425        valid_gain(&self.gain)
426            && self.effects.len() <= MAX_CHAIN_EFFECTS
427            && self.effects.iter().all(AudioEffect::is_valid)
428            && self.limiter.is_none_or(Limiter::is_valid)
429    }
430}
431
432/// Final peak limiter placed at the end of the master bus.
433#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(deny_unknown_fields)]
436pub struct Limiter {
437    /// Linear ceiling, positive and at most [`MAX_GAIN`]. One is full scale.
438    pub threshold: f32,
439    /// Release time in milliseconds, positive and at most [`MAX_ENVELOPE_MS`].
440    pub release_ms: f32,
441}
442
443impl Default for Limiter {
444    fn default() -> Self {
445        Self {
446            threshold: 1.0,
447            release_ms: 50.0,
448        }
449    }
450}
451
452impl Limiter {
453    fn is_valid(self) -> bool {
454        self.threshold.is_finite()
455            && self.threshold > 0.0
456            && self.threshold <= MAX_GAIN
457            && positive_ms(self.release_ms)
458    }
459}
460
461/// Serde default: a project without a `limiter` field keeps the safety ceiling.
462#[allow(clippy::unnecessary_wraps)]
463fn default_limiter() -> Option<Limiter> {
464    Some(Limiter::default())
465}
466
467fn unit_gain() -> Animated<f32> {
468    Animated::Constant { value: 1.0 }
469}
470
471fn center_pan() -> Animated<f32> {
472    Animated::Constant { value: 0.0 }
473}
474
475fn unit(value: f32) -> bool {
476    value.is_finite() && (0.0..=1.0).contains(&value)
477}
478
479fn positive_ms(value: f32) -> bool {
480    value.is_finite() && value > 0.0 && value <= MAX_ENVELOPE_MS
481}
482
483fn valid_gain(gain: &Animated<f32>) -> bool {
484    valid_range(gain, 0.0, MAX_GAIN)
485}
486
487fn valid_pan(pan: &Animated<f32>) -> bool {
488    valid_range(pan, -1.0, 1.0)
489}
490
491fn valid_range(animation: &Animated<f32>, low: f32, high: f32) -> bool {
492    let valid = |value: f32| value.is_finite() && (low..=high).contains(&value);
493    match animation {
494        Animated::Constant { value } => valid(*value),
495        Animated::Keyframes { keyframes } => {
496            crate::animation::valid_keyframes(keyframes)
497                && keyframes.iter().all(|keyframe| valid(keyframe.value))
498        }
499    }
500}