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: Duration,
13 duration: Option<Duration>,
14 volume: f64,
15 fade_in: Duration,
16 fade_out: Duration,
17 pan: f64,
18 envelope: Option<Arc<dyn crate::Signal<f64>>>,
19 limit: Option<Duration>,
20 effects: Arc<Vec<Arc<dyn AudioEffect>>>,
21 pan_animation: Option<Arc<dyn crate::Signal<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: Duration,
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::Signal<f64>>,
54 pub pan: f64,
56 pub pan_animation: Option<&'a dyn crate::Signal<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: Duration::ZERO,
134 duration: None,
135 volume: 1.0,
136 fade_in: Duration::ZERO,
137 fade_out: Duration::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: Duration) -> 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::Signal<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::Signal<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::Signal<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::Signal<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: Duration, end: Duration) -> Self {
281 self.offset(start).duration(end.saturating_sub(start))
282 }
283
284 pub fn resolved_duration(&self) -> Result<Duration, ValidationError> {
286 let mut duration = self
287 .duration
288 .or_else(|| {
289 self.source
290 .duration()
291 .map(|duration| duration.saturating_sub(self.offset))
292 })
293 .ok_or_else(|| invalid("duration", "Unbounded audio requires an explicit duration"))?;
294 if duration <= Duration::ZERO {
295 return Err(invalid(
296 "duration",
297 "Clip source duration must be positive before effect tails",
298 ));
299 }
300 if self.effects.len() > 16 {
301 return Err(invalid("effects", "At most sixteen effects per clip"));
302 }
303 if self.ducking.len() > 4 {
304 return Err(invalid("ducking", "At most four ducking triggers per clip"));
305 }
306 for ducking in self.ducking.iter() {
307 ducking.duck.validate()?;
308 match &ducking.trigger {
309 DuckTrigger::Clip(trigger) if !trigger.unresolved_file() => {
310 trigger.resolved_duration()?;
311 }
312 DuckTrigger::Clip(_) => {}
313 DuckTrigger::Bus(_) => {
314 return Err(invalid("ducking", "Clip ducking requires a clip trigger"));
315 }
316 }
317 }
318 for effect in self.effects.iter() {
319 effect.validate(self.source.sample_rate())?;
320 let tail = effect.tail();
321 if tail.as_seconds() > 30.0 {
322 return Err(invalid("tail", "Effect tail must be in 0..=30 seconds"));
323 }
324 duration = Duration::from_ticks(
325 duration
326 .ticks()
327 .checked_add(tail.ticks())
328 .ok_or_else(|| invalid("tail", "Effect duration overflow"))?,
329 );
330 }
331 if self.start < Time::ZERO
332 || duration <= Duration::ZERO
333 || duration.as_seconds() > 86400.0
334 || self
335 .start
336 .checked_add(duration)
337 .is_none_or(|end| end.ticks() > 86400 * crate::TICKS_PER_SECOND)
338 || !self.volume.is_finite()
339 || !(0.0..=16.0).contains(&self.volume)
340 || !self.pan.is_finite()
341 || !(-1.0..=1.0).contains(&self.pan)
342 || !self.stereo_width.is_finite()
343 || !(0.0..=2.0).contains(&self.stereo_width)
344 || self
345 .bus
346 .as_ref()
347 .is_some_and(|name| name.is_empty() || name.len() > 128)
348 || self.fade_in > duration
349 || self.fade_out > duration
350 || !(1..=192_000).contains(&self.source.sample_rate())
351 || !(1..=2).contains(&self.source.channels())
352 {
353 return Err(invalid(
354 "clip",
355 "Invalid audio timing, format, volume, pan or fade",
356 ));
357 }
358 Ok(duration)
359 }
360
361 pub fn end(&self) -> Result<Time, ValidationError> {
363 let duration = self.resolved_duration()?;
364 Ok(self.start + self.limit.map_or(duration, |limit| limit.min(duration)))
365 }
366
367 pub fn relocated(
370 &self,
371 offset: Duration,
372 window: Duration,
373 ) -> Result<Option<Self>, ValidationError> {
374 if !self.unresolved_file() {
375 self.resolved_duration()?;
376 }
377 let start = self
378 .start
379 .duration_since(Time::ZERO)
380 .ok_or_else(|| invalid("clip", "Audio start must be nonnegative"))?;
381 if start >= window {
382 return Ok(None);
383 }
384 let mut clip = self.clone();
385 let mut ducking = Vec::new();
386 for item in self.ducking.iter() {
387 if let DuckTrigger::Clip(trigger) = &item.trigger
388 && let Some(trigger) = trigger.relocated(offset, window)?
389 {
390 ducking.push(Ducking {
391 trigger: DuckTrigger::Clip(Arc::new(trigger)),
392 duck: item.duck,
393 });
394 }
395 }
396 clip.ducking = Arc::new(ducking);
397 let remaining = window - start;
398 clip.limit = Some(clip.limit.map_or(remaining, |limit| limit.min(remaining)));
399 clip.start = Time::ZERO
400 .checked_add(offset)
401 .and_then(|time| time.checked_add(start))
402 .ok_or_else(|| invalid("nested_audio", "Nested audio placement overflow"))?;
403 if !clip.unresolved_file() {
404 clip.resolved_duration()?;
405 }
406 Ok(Some(clip))
407 }
408}
409
410struct CompositionSource(crate::Timeline);
411
412struct SpatialPan {
413 position: Arc<dyn crate::Signal<crate::Point>>,
414 listener: crate::Point,
415 width: f64,
416}
417
418impl crate::Signal<f64> for SpatialPan {
419 fn sample(&self, time: Time) -> f64 {
420 let point = self.position.sample(time);
421 if !point.x.is_finite() || !point.y.is_finite() {
422 return f64::NAN;
423 }
424 ((point.x - self.listener.x) / self.width).clamp(-1.0, 1.0)
425 }
426}
427
428struct SpatialGain {
429 position: Arc<dyn crate::Signal<crate::Point>>,
430 listener: crate::Point,
431 width: f64,
432 rolloff: f64,
433 previous: Option<Arc<dyn crate::Signal<f64>>>,
434}
435
436impl crate::Signal<f64> for SpatialGain {
437 fn sample(&self, time: Time) -> f64 {
438 let point = self.position.sample(time);
439 if !point.x.is_finite() || !point.y.is_finite() {
440 return f64::NAN;
441 }
442 let distance =
443 libm::hypot(point.x - self.listener.x, point.y - self.listener.y) / self.width;
444 self.previous
445 .as_ref()
446 .map_or(1.0, |previous| previous.sample(time))
447 / (1.0 + self.rolloff * distance)
448 }
449}
450
451impl AudioSource for CompositionSource {
452 fn composition(&self) -> Option<&crate::Timeline> {
453 Some(&self.0)
454 }
455
456 fn sample_rate(&self) -> u32 {
457 48000
458 }
459
460 fn channels(&self) -> u16 {
461 2
462 }
463
464 fn duration(&self) -> Option<Duration> {
465 Some(self.0.duration())
466 }
467
468 fn read(&self, _: u64, output: &mut [f32]) -> usize {
469 output.fill(0.0);
470 0
471 }
472
473 fn try_read(&self, _: u64, _: &mut [f32]) -> crate::Result<usize> {
474 Err(std::io::Error::new(
475 std::io::ErrorKind::Unsupported,
476 "Deferred composition requires audio preparation",
477 )
478 .into())
479 }
480}
481
482struct FileReference(std::path::PathBuf);
483
484impl AudioSource for FileReference {
485 fn file_path(&self) -> Option<&std::path::Path> {
486 Some(&self.0)
487 }
488
489 fn sample_rate(&self) -> u32 {
490 48_000
491 }
492
493 fn channels(&self) -> u16 {
494 2
495 }
496
497 fn read(&self, _: u64, output: &mut [f32]) -> usize {
498 output.fill(0.0);
499 0
500 }
501
502 fn try_read(&self, _: u64, _: &mut [f32]) -> crate::Result<usize> {
503 Err(crate::Error::asset(
504 "audio",
505 self.0.display().to_string(),
506 std::io::Error::new(
507 std::io::ErrorKind::InvalidInput,
508 "File audio must resolve at an execution boundary",
509 ),
510 ))
511 }
512
513 fn duration(&self) -> Option<Duration> {
514 None
515 }
516}