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
8pub const MAX_GAIN: f32 = 16.0;
10
11pub const MAX_AUDIO_EFFECTS: usize = 8_192;
15
16pub const MAX_AUDIO_DELAY_MS: u32 = 120_000;
20
21pub const AUDIO_EFFECT_STATE_MARGIN_MS: u32 = 1_100;
26
27pub const REVERB_DELAY_MS: u32 = 160;
30
31pub const MAX_EQ_GAIN_DB: f32 = 36.0;
33
34pub const EQ_Q_RANGE: std::ops::RangeInclusive<f32> = 0.05..=50.0;
36
37pub const EQ_FREQUENCY_RANGE_HZ: std::ops::RangeInclusive<f32> = 10.0..=47_040.0;
41
42pub const MAX_COMPRESSOR_RATIO: f32 = 100.0;
44
45pub const MAX_CHAIN_EFFECTS: usize = 16;
47
48pub const MAX_EQ_BANDS: usize = 16;
50
51pub const MIN_LEVEL_DB: f32 = -96.0;
53
54pub const MAX_LOOKAHEAD_MS: f32 = 100.0;
56
57pub const MAX_ENVELOPE_MS: f32 = 10_000.0;
59
60#[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 Peaking,
67 LowPass,
69 HighPass,
71}
72
73#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
75#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
76#[serde(deny_unknown_fields)]
77pub struct EqBand {
78 pub kind: EqBandKind,
80 pub frequency_hz: f32,
82 pub q: f32,
84 #[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#[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 Eq {
109 bands: Vec<EqBand>,
111 },
112 Compressor {
114 threshold_db: f32,
116 ratio: f32,
118 attack_ms: f32,
120 release_ms: f32,
122 #[serde(default)]
124 lookahead_ms: f32,
125 },
126 Reverb {
128 room_size: f32,
130 damping: f32,
132 mix: f32,
134 },
135}
136
137impl AudioEffect {
138 #[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 #[must_use]
152 pub fn lookahead_ticks(&self) -> u64 {
153 match self {
154 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[serde(deny_unknown_fields)]
198pub struct Ducking {
199 pub sidechain_track: TrackId,
201 pub threshold: f32,
203 pub depth_db: f32,
205 pub attack_ms: f32,
207 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#[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 #[default]
229 Linear,
230 EqualPower,
232}
233
234impl FadeShape {
235 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(deny_unknown_fields)]
250pub struct AudioProperties {
251 #[serde(default = "unit_gain")]
253 pub gain: Animated<f32>,
254 #[serde(default = "center_pan")]
256 pub pan: Animated<f32>,
257 #[serde(default)]
259 pub fade_in: MediaTime,
260 #[serde(default)]
262 pub fade_out: MediaTime,
263 #[serde(default)]
265 pub fade_shape: FadeShape,
266 #[serde(default)]
268 pub muted: bool,
269 #[serde(default)]
271 pub effects: Vec<AudioEffect>,
272 #[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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
318#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
319#[serde(deny_unknown_fields)]
320pub struct TrackAudio {
321 #[serde(default = "unit_gain")]
323 pub gain: Animated<f32>,
324 #[serde(default = "center_pan")]
326 pub pan: Animated<f32>,
327 #[serde(default)]
329 pub muted: bool,
330 #[serde(default)]
332 pub solo: bool,
333 #[serde(default)]
336 pub effects: Vec<AudioEffect>,
337 #[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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
375#[serde(deny_unknown_fields)]
376pub struct MasterAudio {
377 #[serde(default = "unit_gain")]
379 pub gain: Animated<f32>,
380 #[serde(default)]
383 pub effects: Vec<AudioEffect>,
384 #[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#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
412#[serde(deny_unknown_fields)]
413pub struct Limiter {
414 pub threshold: f32,
416 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#[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}