1use std::{collections::HashMap, sync::Arc};
4
5use crate::error::MediaError;
6
7pub const AUDIO_SAMPLE_RATE: u32 = 48_000;
8pub const AUDIO_CHANNELS: usize = 2;
9const CLIP_EDGE_FADE_SAMPLES: u64 = 384;
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct AudioTimeline {
13 pub tracks: Vec<AudioTrack>,
14 pub clips: Vec<AudioClip>,
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct AudioTrack {
19 pub id: String,
20 pub name: String,
21 pub muted: bool,
22 pub solo: bool,
23 pub volume: f32,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct AudioClip {
28 pub id: String,
29 pub source_id: String,
30 pub track_id: String,
31 pub name: String,
32 pub start_ms: u64,
33 pub duration_ms: u64,
34 pub source_start_ms: u64,
35 pub volume: f32,
36}
37
38#[derive(Debug, Clone, Copy, Default, PartialEq)]
39pub struct AudioMetadata {
40 pub sample_rate: u32,
41 pub channels: u16,
42 pub duration_samples: u64,
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct AudioBuffer {
47 channels: Vec<Vec<f32>>,
48 sample_rate: u32,
49}
50
51impl AudioBuffer {
52 pub fn silent(sample_rate: u32, channel_count: usize, frames: usize) -> Self {
53 Self {
54 channels: vec![vec![0.0; frames]; channel_count],
55 sample_rate,
56 }
57 }
58
59 pub fn from_channels(sample_rate: u32, channels: Vec<Vec<f32>>) -> Self {
60 Self {
61 sample_rate,
62 channels,
63 }
64 }
65
66 pub fn sample_rate(&self) -> u32 {
67 self.sample_rate
68 }
69
70 pub fn channel_count(&self) -> usize {
71 self.channels.len()
72 }
73
74 pub fn frames(&self) -> usize {
75 self.channels.first().map_or(0, Vec::len)
76 }
77
78 pub fn channels(&self) -> &[Vec<f32>] {
79 &self.channels
80 }
81
82 pub fn channels_mut(&mut self) -> &mut [Vec<f32>] {
83 &mut self.channels
84 }
85
86 pub fn interleaved_f32(&self) -> Vec<f32> {
87 let frames = self.frames();
88 let channels = self.channel_count();
89 let mut output = Vec::with_capacity(frames.saturating_mul(channels));
90 for frame in 0..frames {
91 for channel in 0..channels {
92 output.push(self.channels[channel][frame]);
93 }
94 }
95 output
96 }
97}
98
99pub trait AudioResolver: Send + Sync {
100 fn id(&self) -> &str;
101
102 fn metadata(&self) -> AudioMetadata;
103
104 fn resolve_range(
105 &self,
106 start_sample: u64,
107 frames: usize,
108 ) -> Result<Arc<AudioBuffer>, MediaError>;
109}
110
111pub trait AudioSourceProvider {
112 fn get_audio_resolver(&self, source_id: &str) -> Option<Box<dyn AudioResolver>>;
113}
114
115pub struct AudioMixer<'a, P: AudioSourceProvider> {
116 timeline: &'a AudioTimeline,
117 provider: &'a P,
118}
119
120impl<'a, P: AudioSourceProvider> AudioMixer<'a, P> {
121 pub fn new(timeline: &'a AudioTimeline, provider: &'a P) -> Self {
122 Self { timeline, provider }
123 }
124
125 pub fn mix_range(&self, start_sample: u64, frames: usize) -> Result<AudioBuffer, MediaError> {
126 let mut output = AudioBuffer::silent(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, frames);
127 self.mix_into(start_sample, output.channels_mut())?;
128 Ok(output)
129 }
130
131 pub fn mix_into(&self, start_sample: u64, output: &mut [Vec<f32>]) -> Result<(), MediaError> {
132 let output_frames = output.first().map_or(0, Vec::len);
133 if output_frames == 0 || output.is_empty() {
134 return Ok(());
135 }
136
137 for channel in output.iter_mut() {
138 channel.fill(0.0);
139 }
140
141 let has_solo = self.timeline.tracks.iter().any(|track| track.solo);
142 let tracks = self
143 .timeline
144 .tracks
145 .iter()
146 .map(|track| (track.id.as_str(), track))
147 .collect::<HashMap<_, _>>();
148
149 for clip in &self.timeline.clips {
150 let Some(track) = tracks.get(clip.track_id.as_str()) else {
151 continue;
152 };
153 if track.muted || (has_solo && !track.solo) || track.volume <= 0.0 || clip.volume <= 0.0
154 {
155 continue;
156 }
157
158 let clip_start = ms_to_sample(clip.start_ms);
159 let clip_duration = ms_to_sample(clip.duration_ms).max(1);
160 let clip_end = clip_start.saturating_add(clip_duration);
161 let output_end = start_sample.saturating_add(output_frames as u64);
162 let overlap_start = start_sample.max(clip_start);
163 let overlap_end = output_end.min(clip_end);
164 if overlap_end <= overlap_start {
165 continue;
166 }
167
168 let frames_to_mix = usize::try_from(overlap_end - overlap_start).unwrap_or(0);
169 let output_offset = usize::try_from(overlap_start - start_sample).unwrap_or(0);
170 let clip_offset = overlap_start - clip_start;
171 let source_start = ms_to_sample(clip.source_start_ms).saturating_add(clip_offset);
172 let source = self
173 .provider
174 .get_audio_resolver(&clip.source_id)
175 .ok_or_else(|| MediaError::SourceNotFound {
176 media_source: clip.source_id.clone(),
177 })?;
178 let source_buffer = source.resolve_range(source_start, frames_to_mix)?;
179 let gain = track.volume * clip.volume;
180
181 for i in 0..frames_to_mix.min(source_buffer.frames()) {
182 let clip_sample = clip_offset.saturating_add(i as u64);
183 let envelope = clip_envelope(clip_sample, clip_duration);
184 let sample_gain = gain * envelope;
185 for (out_channel_index, output_channel) in output.iter_mut().enumerate() {
186 let source_sample =
187 sample_for_output_channel(&source_buffer, out_channel_index, i);
188 let Some(output_sample) = output_channel.get_mut(output_offset + i) else {
189 continue;
190 };
191 *output_sample += source_sample * sample_gain;
192 }
193 }
194 }
195
196 for channel in output {
197 for sample in channel {
198 *sample = sample.clamp(-1.0, 1.0);
199 }
200 }
201
202 Ok(())
203 }
204}
205
206pub fn ms_to_sample(ms: u64) -> u64 {
207 ((ms as u128 * u128::from(AUDIO_SAMPLE_RATE)) / 1_000) as u64
208}
209
210pub fn duration_samples(duration_frames: u32, fps: f32) -> u64 {
211 if fps <= 0.0 {
212 return 0;
213 }
214 ((duration_frames as f64 / fps as f64) * f64::from(AUDIO_SAMPLE_RATE)).round() as u64
215}
216
217fn sample_for_output_channel(buffer: &AudioBuffer, output_channel: usize, frame: usize) -> f32 {
218 match buffer.channel_count() {
219 0 => 0.0,
220 1 => buffer.channels()[0].get(frame).copied().unwrap_or(0.0),
221 count => buffer.channels()[output_channel.min(count - 1)]
222 .get(frame)
223 .copied()
224 .unwrap_or(0.0),
225 }
226}
227
228fn clip_envelope(clip_sample: u64, clip_duration: u64) -> f32 {
229 let fade = CLIP_EDGE_FADE_SAMPLES.min(clip_duration / 2);
230 if fade == 0 {
231 return 1.0;
232 }
233 if clip_sample < fade {
234 return clip_sample as f32 / fade as f32;
235 }
236 let remaining = clip_duration.saturating_sub(clip_sample);
237 if remaining < fade {
238 return remaining as f32 / fade as f32;
239 }
240 1.0
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[derive(Debug)]
248 struct TestProvider {
249 sources: HashMap<String, Arc<AudioBuffer>>,
250 }
251
252 impl AudioSourceProvider for TestProvider {
253 fn get_audio_resolver(&self, source_id: &str) -> Option<Box<dyn AudioResolver>> {
254 self.sources.get(source_id).cloned().map(|buffer| {
255 Box::new(TestResolver {
256 id: source_id.to_string(),
257 buffer,
258 }) as Box<dyn AudioResolver>
259 })
260 }
261 }
262
263 struct TestResolver {
264 id: String,
265 buffer: Arc<AudioBuffer>,
266 }
267
268 impl AudioResolver for TestResolver {
269 fn id(&self) -> &str {
270 &self.id
271 }
272
273 fn metadata(&self) -> AudioMetadata {
274 AudioMetadata {
275 sample_rate: self.buffer.sample_rate(),
276 channels: self.buffer.channel_count() as u16,
277 duration_samples: self.buffer.frames() as u64,
278 }
279 }
280
281 fn resolve_range(
282 &self,
283 start_sample: u64,
284 frames: usize,
285 ) -> Result<Arc<AudioBuffer>, MediaError> {
286 let start = usize::try_from(start_sample).unwrap_or(usize::MAX);
287 let end = start.saturating_add(frames).min(self.buffer.frames());
288 let mut channels = Vec::new();
289 for channel in self.buffer.channels() {
290 let mut out = vec![0.0; frames];
291 if start < channel.len() {
292 let src = &channel[start..end];
293 out[..src.len()].copy_from_slice(src);
294 }
295 channels.push(out);
296 }
297 Ok(Arc::new(AudioBuffer::from_channels(
298 self.buffer.sample_rate(),
299 channels,
300 )))
301 }
302 }
303
304 fn track(id: &str) -> AudioTrack {
305 AudioTrack {
306 id: id.to_string(),
307 name: id.to_string(),
308 muted: false,
309 solo: false,
310 volume: 1.0,
311 }
312 }
313
314 #[test]
315 fn mixes_single_clip_with_source_offset() {
316 let provider = TestProvider {
317 sources: HashMap::from([(
318 "tone".to_string(),
319 Arc::new(AudioBuffer::from_channels(
320 AUDIO_SAMPLE_RATE,
321 vec![vec![0.5; AUDIO_SAMPLE_RATE as usize]],
322 )),
323 )]),
324 };
325 let timeline = AudioTimeline {
326 tracks: vec![track("main")],
327 clips: vec![AudioClip {
328 id: "clip".to_string(),
329 source_id: "tone".to_string(),
330 track_id: "main".to_string(),
331 name: "tone".to_string(),
332 start_ms: 0,
333 duration_ms: 1_000,
334 source_start_ms: 0,
335 volume: 1.0,
336 }],
337 };
338
339 let mixed = AudioMixer::new(&timeline, &provider)
340 .mix_range(ms_to_sample(500), 5)
341 .unwrap();
342 assert_eq!(mixed.channel_count(), 2);
343 assert!(mixed.channels()[0][2] > 0.49);
344 assert_eq!(mixed.channels()[0], mixed.channels()[1]);
345 }
346
347 #[test]
348 fn applies_mute_solo_and_gain() {
349 let provider = TestProvider {
350 sources: HashMap::from([(
351 "tone".to_string(),
352 Arc::new(AudioBuffer::from_channels(
353 AUDIO_SAMPLE_RATE,
354 vec![
355 vec![1.0; AUDIO_SAMPLE_RATE as usize],
356 vec![0.5; AUDIO_SAMPLE_RATE as usize],
357 ],
358 )),
359 )]),
360 };
361 let mut solo = track("solo");
362 solo.solo = true;
363 solo.volume = 0.5;
364 let mut muted = track("muted");
365 muted.muted = true;
366 let timeline = AudioTimeline {
367 tracks: vec![solo, muted],
368 clips: vec![
369 AudioClip {
370 id: "a".to_string(),
371 source_id: "tone".to_string(),
372 track_id: "solo".to_string(),
373 name: "a".to_string(),
374 start_ms: 0,
375 duration_ms: 1_000,
376 source_start_ms: 0,
377 volume: 0.5,
378 },
379 AudioClip {
380 id: "b".to_string(),
381 source_id: "tone".to_string(),
382 track_id: "muted".to_string(),
383 name: "b".to_string(),
384 start_ms: 0,
385 duration_ms: 1_000,
386 source_start_ms: 0,
387 volume: 1.0,
388 },
389 ],
390 };
391
392 let mixed = AudioMixer::new(&timeline, &provider)
393 .mix_range(500, 4)
394 .unwrap();
395 assert_eq!(mixed.channels()[0], vec![0.25; 4]);
396 assert_eq!(mixed.channels()[1], vec![0.125; 4]);
397 }
398
399 #[test]
400 fn overlaps_clips_and_clamps_output() {
401 let provider = TestProvider {
402 sources: HashMap::from([(
403 "tone".to_string(),
404 Arc::new(AudioBuffer::from_channels(
405 AUDIO_SAMPLE_RATE,
406 vec![vec![0.75; AUDIO_SAMPLE_RATE as usize]],
407 )),
408 )]),
409 };
410 let timeline = AudioTimeline {
411 tracks: vec![track("main")],
412 clips: vec![
413 AudioClip {
414 id: "a".to_string(),
415 source_id: "tone".to_string(),
416 track_id: "main".to_string(),
417 name: "a".to_string(),
418 start_ms: 0,
419 duration_ms: 1_000,
420 source_start_ms: 0,
421 volume: 1.0,
422 },
423 AudioClip {
424 id: "b".to_string(),
425 source_id: "tone".to_string(),
426 track_id: "main".to_string(),
427 name: "b".to_string(),
428 start_ms: 0,
429 duration_ms: 1_000,
430 source_start_ms: 0,
431 volume: 1.0,
432 },
433 ],
434 };
435
436 let mixed = AudioMixer::new(&timeline, &provider)
437 .mix_range(500, 4)
438 .unwrap();
439 assert_eq!(mixed.channels()[0], vec![1.0; 4]);
440 assert_eq!(mixed.channels()[1], vec![1.0; 4]);
441 }
442}