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 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub stream: Option<u32>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub channels: Option<Vec<u32>>,
285}
286
287pub 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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342#[serde(deny_unknown_fields)]
343pub struct TrackAudio {
344 #[serde(default = "unit_gain")]
346 pub gain: Animated<f32>,
347 #[serde(default = "center_pan")]
349 pub pan: Animated<f32>,
350 #[serde(default)]
352 pub muted: bool,
353 #[serde(default)]
355 pub solo: bool,
356 #[serde(default)]
359 pub effects: Vec<AudioEffect>,
360 #[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 #[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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
398#[serde(deny_unknown_fields)]
399pub struct MasterAudio {
400 #[serde(default = "unit_gain")]
402 pub gain: Animated<f32>,
403 #[serde(default)]
406 pub effects: Vec<AudioEffect>,
407 #[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#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(deny_unknown_fields)]
436pub struct Limiter {
437 pub threshold: f32,
439 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#[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}