1use crate::{Animated, TrackId};
2use alpha2_time::{MediaTime, TICKS_PER_SECOND};
3use serde::{Deserialize, Serialize};
4
5pub(crate) const TICKS_PER_MS: u32 = 120;
7const _: () = assert!(TICKS_PER_SECOND == 1_000 * TICKS_PER_MS as i64);
8
9pub const MAX_GAIN: f32 = 16.0;
11
12pub const MAX_AUDIO_EFFECTS: usize = 8_192;
17
18pub const MAX_AUDIO_DELAY_MS: u32 = 120_000;
23
24pub const AUDIO_EFFECT_STATE_MARGIN_MS: u32 = 1_100;
32
33pub const REVERB_DELAY_MS: u32 = 160;
36
37pub const MAX_EQ_GAIN_DB: f32 = 36.0;
39
40pub const EQ_Q_RANGE: std::ops::RangeInclusive<f32> = 0.05..=50.0;
42
43pub const EQ_FREQUENCY_RANGE_HZ: std::ops::RangeInclusive<f32> = 10.0..=47_040.0;
48
49pub const MAX_COMPRESSOR_RATIO: f32 = 100.0;
51
52pub const MAX_CHAIN_EFFECTS: usize = 16;
54
55pub const MAX_EQ_BANDS: usize = 16;
57
58pub const MIN_LEVEL_DB: f32 = -96.0;
61
62pub const MAX_LOOKAHEAD_MS: f32 = 100.0;
64
65pub const MAX_ENVELOPE_MS: f32 = 10_000.0;
67
68#[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 Peaking,
75 LowPass,
77 HighPass,
79}
80
81#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(deny_unknown_fields)]
85pub struct EqBand {
86 pub kind: EqBandKind,
88 pub frequency_hz: f32,
90 pub q: f32,
92 #[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#[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 Eq {
118 bands: Vec<EqBand>,
120 },
121 Compressor {
123 threshold_db: f32,
126 ratio: f32,
128 attack_ms: f32,
130 release_ms: f32,
132 #[serde(default)]
135 lookahead_ms: f32,
136 },
137 Reverb {
139 room_size: f32,
141 damping: f32,
143 mix: f32,
145 },
146}
147
148impl AudioEffect {
149 #[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 #[must_use]
165 pub fn lookahead_ticks(&self) -> u64 {
166 match self {
167 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
210#[serde(deny_unknown_fields)]
211pub struct Ducking {
212 pub sidechain_track: TrackId,
214 pub threshold: f32,
216 pub depth_db: f32,
218 pub attack_ms: f32,
220 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#[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 #[default]
242 Linear,
243 EqualPower,
246}
247
248impl FadeShape {
249 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263#[serde(deny_unknown_fields)]
264pub struct AudioProperties {
265 #[serde(default = "unit_gain")]
267 pub gain: Animated<f32>,
268 #[serde(default = "center_pan")]
270 pub pan: Animated<f32>,
271 #[serde(default)]
273 pub fade_in: MediaTime,
274 #[serde(default)]
277 pub fade_out: MediaTime,
278 #[serde(default)]
280 pub fade_shape: FadeShape,
281 #[serde(default)]
283 pub muted: bool,
284 #[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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330#[serde(deny_unknown_fields)]
331pub struct TrackAudio {
332 #[serde(default = "unit_gain")]
334 pub gain: Animated<f32>,
335 #[serde(default = "center_pan")]
338 pub pan: Animated<f32>,
339 #[serde(default)]
341 pub muted: bool,
342 #[serde(default)]
344 pub solo: bool,
345 #[serde(default)]
348 pub effects: Vec<AudioEffect>,
349 #[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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
387#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
388#[serde(deny_unknown_fields)]
389pub struct MasterAudio {
390 #[serde(default = "unit_gain")]
392 pub gain: Animated<f32>,
393 #[serde(default)]
396 pub effects: Vec<AudioEffect>,
397 #[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#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
424#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
425#[serde(deny_unknown_fields)]
426pub struct Limiter {
427 pub threshold: f32,
429 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#[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}