1use super::{AudioEffect, AudioSource, Duck, DuckTrigger, Ducking, PanLaw, invalid};
4use crate::{Duration, Time, ValidationError};
5use std::sync::Arc;
6
7#[derive(Clone)]
9pub struct AudioClip {
10 source: Arc<dyn AudioSource>,
11 start: Time,
12 offset: Time,
13 duration: Option<Duration>,
14 volume: f64,
15 fade_in: Duration,
16 fade_out: Duration,
17 pan: f64,
18 envelope: Option<Arc<dyn crate::Animation<f64>>>,
19 limit: Option<Duration>,
20 effects: Arc<Vec<Arc<dyn AudioEffect>>>,
21 pan_animation: Option<Arc<dyn crate::Animation<f64>>>,
22 pan_law: PanLaw,
23 stereo_width: f64,
24 bus: Option<String>,
25 ducking: Arc<Vec<Ducking>>,
26}
27
28impl<Source: AudioSource + ?Sized + 'static> From<Arc<Source>> for AudioClip {
29 fn from(source: Arc<Source>) -> Self {
30 Self::source(source)
31 }
32}
33
34pub struct AudioClipParameters<'a> {
36 pub source: &'a dyn AudioSource,
38 pub start: Time,
40 pub offset: Time,
42 pub duration: Option<Duration>,
44 pub limit: Option<Duration>,
46 pub volume: f64,
48 pub fade_in: Duration,
50 pub fade_out: Duration,
52 pub envelope: Option<&'a dyn crate::Animation<f64>>,
54 pub pan: f64,
56 pub pan_animation: Option<&'a dyn crate::Animation<f64>>,
58 pub pan_law: PanLaw,
60 pub stereo_width: f64,
62 pub effects: &'a [Arc<dyn AudioEffect>],
64 pub bus: Option<&'a str>,
66 pub ducking: &'a [Ducking],
68}
69
70impl std::fmt::Debug for AudioClip {
71 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 formatter
73 .debug_struct("AudioClip")
74 .field("start", &self.start)
75 .field("offset", &self.offset)
76 .field("duration", &self.duration)
77 .field("volume", &self.volume)
78 .field("pan", &self.pan)
79 .finish_non_exhaustive()
80 }
81}
82
83impl AudioClip {
84 pub fn composition(timeline: crate::Timeline) -> Self {
86 Self::source(CompositionSource(timeline))
87 }
88
89 pub fn file(path: impl Into<std::path::PathBuf>) -> Self {
91 Self::source(FileReference(path.into()))
92 }
93
94 pub fn with_resolved_source(&self, source: Arc<dyn AudioSource>) -> Self {
96 let mut clip = self.clone();
97 clip.source = source;
98 clip
99 }
100
101 pub fn unresolved_file(&self) -> bool {
103 self.source.file_path().is_some() && self.source.duration().is_none()
104 }
105
106 pub fn parameters(&self) -> AudioClipParameters<'_> {
108 AudioClipParameters {
109 source: self.source.as_ref(),
110 start: self.start,
111 offset: self.offset,
112 duration: self.duration,
113 limit: self.limit,
114 volume: self.volume,
115 fade_in: self.fade_in,
116 fade_out: self.fade_out,
117 envelope: self.envelope.as_deref(),
118 pan: self.pan,
119 pan_animation: self.pan_animation.as_deref(),
120 pan_law: self.pan_law,
121 stereo_width: self.stereo_width,
122 effects: &self.effects,
123 bus: self.bus.as_deref(),
124 ducking: &self.ducking,
125 }
126 }
127
128 pub fn source(source: impl AudioSource + 'static) -> Self {
130 Self {
131 source: Arc::new(source),
132 start: Time::ZERO,
133 offset: Time::ZERO,
134 duration: None,
135 volume: 1.0,
136 fade_in: Time::ZERO,
137 fade_out: Time::ZERO,
138 pan: 0.0,
139 envelope: None,
140 limit: None,
141 effects: Arc::new(Vec::new()),
142 pan_animation: None,
143 pan_law: PanLaw::ConstantPower,
144 stereo_width: 1.0,
145 bus: None,
146 ducking: Arc::new(Vec::new()),
147 }
148 }
149
150 pub fn start(mut self, start: Time) -> Self {
152 self.start = start;
153 self
154 }
155
156 pub fn offset(mut self, offset: Time) -> Self {
158 self.offset = offset;
159 self
160 }
161
162 pub fn duration(mut self, duration: Duration) -> Self {
164 self.duration = Some(duration);
165 self
166 }
167
168 pub fn volume(mut self, volume: f64) -> Self {
170 self.volume = volume;
171 self
172 }
173
174 pub fn volume_envelope(mut self, envelope: impl crate::Animation<f64> + 'static) -> Self {
176 self.envelope = Some(Arc::new(envelope));
177 self
178 }
179
180 pub fn fade_in(mut self, duration: Duration) -> Self {
182 self.fade_in = duration;
183 self
184 }
185
186 pub fn fade_out(mut self, duration: Duration) -> Self {
188 self.fade_out = duration;
189 self
190 }
191
192 pub fn pan(mut self, pan: f64) -> Self {
194 self.pan = pan;
195 self
196 }
197
198 pub fn effect(mut self, effect: impl AudioEffect + 'static) -> Self {
200 Arc::make_mut(&mut self.effects).push(Arc::new(effect));
201 self
202 }
203
204 pub fn ducked_by(mut self, trigger: &AudioClip, duck: Duck) -> Self {
208 let mut trigger = trigger.clone();
209 trigger.ducking = Arc::new(Vec::new());
210 Arc::make_mut(&mut self.ducking).push(Ducking {
211 trigger: DuckTrigger::Clip(Arc::new(trigger)),
212 duck,
213 });
214 self
215 }
216
217 pub fn bus(mut self, name: impl Into<String>) -> Self {
219 self.bus = Some(name.into());
220 self
221 }
222
223 pub fn pan_law(mut self, law: PanLaw) -> Self {
225 self.pan_law = law;
226 self
227 }
228
229 pub fn pan_animation(mut self, animation: impl crate::Animation<f64> + 'static) -> Self {
231 self.pan_animation = Some(Arc::new(animation));
232 self
233 }
234
235 pub fn stereo_width(mut self, width: f64) -> Self {
237 self.stereo_width = width;
238 self
239 }
240
241 pub fn spatial(
244 mut self,
245 position: impl crate::Animation<crate::Point> + 'static,
246 listener: crate::Point,
247 width: f64,
248 rolloff: f64,
249 ) -> Result<Self, ValidationError> {
250 if !listener.x.is_finite()
251 || !listener.y.is_finite()
252 || !width.is_finite()
253 || width <= 0.0
254 || !rolloff.is_finite()
255 || !(0.0..=1000.0).contains(&rolloff)
256 {
257 return Err(invalid(
258 "spatial",
259 "Invalid listener, width or distance rolloff",
260 ));
261 }
262 let position: Arc<dyn crate::Animation<crate::Point>> = Arc::new(position);
263 self.pan_animation = Some(Arc::new(SpatialPan {
264 position: position.clone(),
265 listener,
266 width,
267 }));
268 self.envelope = Some(Arc::new(SpatialGain {
269 position,
270 listener,
271 width,
272 rolloff,
273 previous: self.envelope.take(),
274 }));
275 Ok(self)
276 }
277
278 pub fn trim(self, start: Time, end: Time) -> Self {
280 self.offset(start).duration(end - start)
281 }
282
283 pub fn resolved_duration(&self) -> Result<Duration, ValidationError> {
285 let mut duration = self
286 .duration
287 .or_else(|| {
288 self.source.duration().and_then(|duration| {
289 duration
290 .ticks()
291 .checked_sub(self.offset.ticks())
292 .map(Time::from_ticks)
293 })
294 })
295 .ok_or_else(|| invalid("duration", "Unbounded audio requires an explicit duration"))?;
296 if duration <= Time::ZERO {
297 return Err(invalid(
298 "duration",
299 "Clip source duration must be positive before effect tails",
300 ));
301 }
302 if self.effects.len() > 16 {
303 return Err(invalid("effects", "At most sixteen effects per clip"));
304 }
305 if self.ducking.len() > 4 {
306 return Err(invalid("ducking", "At most four ducking triggers per clip"));
307 }
308 for ducking in self.ducking.iter() {
309 ducking.duck.validate()?;
310 match &ducking.trigger {
311 DuckTrigger::Clip(trigger) if !trigger.unresolved_file() => {
312 trigger.resolved_duration()?;
313 }
314 DuckTrigger::Clip(_) => {}
315 DuckTrigger::Bus(_) => {
316 return Err(invalid("ducking", "Clip ducking requires a clip trigger"));
317 }
318 }
319 }
320 for effect in self.effects.iter() {
321 effect.validate(self.source.sample_rate())?;
322 let tail = effect.tail();
323 if tail < Time::ZERO || tail.as_seconds() > 30.0 {
324 return Err(invalid("tail", "Effect tail must be in 0..=30 seconds"));
325 }
326 duration = Time::from_ticks(
327 duration
328 .ticks()
329 .checked_add(tail.ticks())
330 .ok_or_else(|| invalid("tail", "Effect duration overflow"))?,
331 );
332 }
333 if self.start < Time::ZERO
334 || self.offset < Time::ZERO
335 || duration <= Time::ZERO
336 || duration.as_seconds() > 86400.0
337 || self
338 .start
339 .ticks()
340 .checked_add(duration.ticks())
341 .is_none_or(|end| end > 86400 * crate::TICKS_PER_SECOND)
342 || !self.volume.is_finite()
343 || !(0.0..=16.0).contains(&self.volume)
344 || !self.pan.is_finite()
345 || !(-1.0..=1.0).contains(&self.pan)
346 || !self.stereo_width.is_finite()
347 || !(0.0..=2.0).contains(&self.stereo_width)
348 || self
349 .bus
350 .as_ref()
351 .is_some_and(|name| name.is_empty() || name.len() > 128)
352 || self.fade_in < Time::ZERO
353 || self.fade_out < Time::ZERO
354 || self.fade_in > duration
355 || self.fade_out > duration
356 || !(1..=192_000).contains(&self.source.sample_rate())
357 || !(1..=2).contains(&self.source.channels())
358 {
359 return Err(invalid(
360 "clip",
361 "Invalid audio timing, format, volume, pan or fade",
362 ));
363 }
364 Ok(duration)
365 }
366
367 pub fn end(&self) -> Result<Time, ValidationError> {
369 let duration = self.resolved_duration()?;
370 Ok(self.start + self.limit.map_or(duration, |limit| limit.min(duration)))
371 }
372
373 pub fn relocated(
376 &self,
377 offset: Time,
378 window: Duration,
379 ) -> Result<Option<Self>, ValidationError> {
380 if !self.unresolved_file() {
381 self.resolved_duration()?;
382 }
383 if self.start >= window {
384 return Ok(None);
385 }
386 let mut clip = self.clone();
387 let mut ducking = Vec::new();
388 for item in self.ducking.iter() {
389 if let DuckTrigger::Clip(trigger) = &item.trigger
390 && let Some(trigger) = trigger.relocated(offset, window)?
391 {
392 ducking.push(Ducking {
393 trigger: DuckTrigger::Clip(Arc::new(trigger)),
394 duck: item.duck,
395 });
396 }
397 }
398 clip.ducking = Arc::new(ducking);
399 let remaining = window - clip.start;
400 clip.limit = Some(clip.limit.map_or(remaining, |limit| limit.min(remaining)));
401 clip.start = Time::from_ticks(
402 offset
403 .ticks()
404 .checked_add(clip.start.ticks())
405 .ok_or_else(|| invalid("nested_audio", "Nested audio placement overflow"))?,
406 );
407 if !clip.unresolved_file() {
408 clip.resolved_duration()?;
409 }
410 Ok(Some(clip))
411 }
412}
413
414struct CompositionSource(crate::Timeline);
415
416struct SpatialPan {
417 position: Arc<dyn crate::Animation<crate::Point>>,
418 listener: crate::Point,
419 width: f64,
420}
421
422impl crate::Animation<f64> for SpatialPan {
423 fn sample(&self, time: Time) -> f64 {
424 let point = self.position.sample(time);
425 if !point.x.is_finite() || !point.y.is_finite() {
426 return f64::NAN;
427 }
428 ((point.x - self.listener.x) / self.width).clamp(-1.0, 1.0)
429 }
430
431 fn duration(&self) -> Option<Duration> {
432 self.position.duration()
433 }
434}
435
436struct SpatialGain {
437 position: Arc<dyn crate::Animation<crate::Point>>,
438 listener: crate::Point,
439 width: f64,
440 rolloff: f64,
441 previous: Option<Arc<dyn crate::Animation<f64>>>,
442}
443
444impl crate::Animation<f64> for SpatialGain {
445 fn sample(&self, time: Time) -> f64 {
446 let point = self.position.sample(time);
447 if !point.x.is_finite() || !point.y.is_finite() {
448 return f64::NAN;
449 }
450 let distance =
451 libm::hypot(point.x - self.listener.x, point.y - self.listener.y) / self.width;
452 self.previous
453 .as_ref()
454 .map_or(1.0, |previous| previous.sample(time))
455 / (1.0 + self.rolloff * distance)
456 }
457
458 fn duration(&self) -> Option<Duration> {
459 self.position.duration()
460 }
461}
462
463impl AudioSource for CompositionSource {
464 fn composition(&self) -> Option<&crate::Timeline> {
465 Some(&self.0)
466 }
467
468 fn sample_rate(&self) -> u32 {
469 48000
470 }
471
472 fn channels(&self) -> u16 {
473 2
474 }
475
476 fn duration(&self) -> Option<Duration> {
477 Some(self.0.total_duration())
478 }
479
480 fn read(&self, _: u64, output: &mut [f32]) -> usize {
481 output.fill(0.0);
482 0
483 }
484
485 fn try_read(&self, _: u64, _: &mut [f32]) -> crate::Result<usize> {
486 Err(std::io::Error::new(
487 std::io::ErrorKind::Unsupported,
488 "Deferred composition requires audio preparation",
489 )
490 .into())
491 }
492}
493
494struct FileReference(std::path::PathBuf);
495
496impl AudioSource for FileReference {
497 fn file_path(&self) -> Option<&std::path::Path> {
498 Some(&self.0)
499 }
500
501 fn sample_rate(&self) -> u32 {
502 48_000
503 }
504
505 fn channels(&self) -> u16 {
506 2
507 }
508
509 fn read(&self, _: u64, output: &mut [f32]) -> usize {
510 output.fill(0.0);
511 0
512 }
513
514 fn try_read(&self, _: u64, _: &mut [f32]) -> crate::Result<usize> {
515 Err(crate::Error::asset(
516 "audio",
517 self.0.display().to_string(),
518 std::io::Error::new(
519 std::io::ErrorKind::InvalidInput,
520 "File audio must resolve at an execution boundary",
521 ),
522 ))
523 }
524
525 fn duration(&self) -> Option<Duration> {
526 None
527 }
528}