1use std::collections::VecDeque;
8use std::f32::consts;
9use std::sync::atomic::{AtomicU32, Ordering};
10use std::sync::{Arc, Mutex};
11
12struct AudioTrack {
15 buf: Arc<Mutex<VecDeque<f32>>>,
16 volume: Arc<AtomicU32>,
17 pan: Arc<AtomicU32>,
18}
19
20#[derive(Clone)]
27pub struct AudioTrackHandle {
28 buf: Arc<Mutex<VecDeque<f32>>>,
29 volume: Arc<AtomicU32>,
30 pan: Arc<AtomicU32>,
31}
32
33impl AudioTrackHandle {
34 pub fn set_volume(&self, v: f32) {
41 self.volume.store(v.max(0.0).to_bits(), Ordering::Relaxed);
42 }
43
44 pub fn set_pan(&self, p: f32) {
46 self.pan
47 .store(p.clamp(-1.0, 1.0).to_bits(), Ordering::Relaxed);
48 }
49
50 pub fn push_samples(&self, samples: &[f32]) {
55 self.buf
56 .lock()
57 .unwrap_or_else(std::sync::PoisonError::into_inner)
58 .extend(samples.iter().copied());
59 }
60
61 #[cfg(feature = "timeline")]
65 pub(crate) fn buffered_samples(&self) -> usize {
66 self.buf
67 .lock()
68 .unwrap_or_else(std::sync::PoisonError::into_inner)
69 .len()
70 }
71
72 #[cfg(feature = "timeline")]
76 pub(crate) fn clear(&self) {
77 self.buf
78 .lock()
79 .unwrap_or_else(std::sync::PoisonError::into_inner)
80 .clear();
81 }
82}
83
84pub struct AudioMixer {
117 tracks: Vec<AudioTrack>,
118 pub sample_rate: u32,
120 pub channels: u16,
122}
123
124impl AudioMixer {
125 #[must_use]
127 pub fn new(sample_rate: u32) -> Self {
128 Self {
129 tracks: Vec::new(),
130 sample_rate,
131 channels: 2,
132 }
133 }
134
135 pub fn add_track(&mut self) -> AudioTrackHandle {
139 let buf = Arc::new(Mutex::new(VecDeque::new()));
140 let volume = Arc::new(AtomicU32::new(1.0_f32.to_bits()));
141 let pan = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
142 let handle = AudioTrackHandle {
143 buf: Arc::clone(&buf),
144 volume: Arc::clone(&volume),
145 pan: Arc::clone(&pan),
146 };
147 self.tracks.push(AudioTrack { buf, volume, pan });
148 handle
149 }
150
151 #[allow(clippy::cast_precision_loss)]
157 pub fn mix(&mut self, n_samples: usize) -> Vec<f32> {
158 let n_frames = n_samples / 2;
159 let mut out = vec![0.0_f32; n_frames * 2];
160
161 for track in &self.tracks {
162 let volume = f32::from_bits(track.volume.load(Ordering::Relaxed));
163 let pan = f32::from_bits(track.pan.load(Ordering::Relaxed));
164
165 let p_norm = (pan + 1.0) * consts::FRAC_PI_4;
167 let l_gain = volume * p_norm.cos();
168 let r_gain = volume * p_norm.sin();
169
170 let mut guard = track
171 .buf
172 .lock()
173 .unwrap_or_else(std::sync::PoisonError::into_inner);
174 for i in 0..n_frames {
175 let s = guard.pop_front().unwrap_or(0.0);
176 out[i * 2] += s * l_gain;
177 out[i * 2 + 1] += s * r_gain;
178 }
179 }
180
181 for sample in &mut out {
183 *sample = sample.clamp(-1.0, 1.0);
184 }
185
186 out
187 }
188
189 #[cfg(feature = "timeline")]
193 pub(crate) fn invalidate_all(&mut self) {
194 for track in &self.tracks {
195 track
196 .buf
197 .lock()
198 .unwrap_or_else(std::sync::PoisonError::into_inner)
199 .clear();
200 }
201 }
202}
203
204#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn audio_mixer_mix_two_tracks_should_sum_and_clip_left_channel() {
212 let mut mixer = AudioMixer::new(48_000);
215 let t1 = mixer.add_track();
216 let t2 = mixer.add_track();
217 t1.set_pan(-1.0);
218 t2.set_pan(-1.0);
219 t1.push_samples(&[0.8, 0.8]);
220 t2.push_samples(&[0.8, 0.8]);
221
222 let out = mixer.mix(4); assert_eq!(out.len(), 4);
224 assert!(
225 (out[0] - 1.0).abs() < 1e-6,
226 "L must clip to 1.0; got {}",
227 out[0]
228 );
229 assert!(
230 out[1].abs() < 1e-6,
231 "R must be 0.0 for full-left pan; got {}",
232 out[1]
233 );
234 }
235
236 #[test]
237 fn audio_mixer_pan_full_left_should_produce_zero_right_channel() {
238 let mut mixer = AudioMixer::new(48_000);
239 let track = mixer.add_track();
240 track.set_pan(-1.0);
241 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
242
243 let out = mixer.mix(8); assert_eq!(out.len(), 8);
245 for i in (1..8usize).step_by(2) {
246 assert!(
247 out[i].abs() < 1e-6,
248 "R channel must be 0.0 for full-left pan; got {} at index {i}",
249 out[i]
250 );
251 }
252 }
253
254 #[test]
255 fn audio_mixer_pan_full_right_should_produce_zero_left_channel() {
256 let mut mixer = AudioMixer::new(48_000);
257 let track = mixer.add_track();
258 track.set_pan(1.0);
259 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
260
261 let out = mixer.mix(8);
262 for i in (0..8usize).step_by(2) {
263 assert!(
264 out[i].abs() < 1e-6,
265 "L channel must be 0.0 for full-right pan; got {} at index {i}",
266 out[i]
267 );
268 }
269 }
270
271 #[test]
272 fn audio_mixer_two_tracks_volume_sum_exceeding_one_should_be_clipped() {
273 let mut mixer = AudioMixer::new(48_000);
276 let t1 = mixer.add_track();
277 let t2 = mixer.add_track();
278 t1.set_volume(0.7);
279 t2.set_volume(0.7);
280 t1.set_pan(-1.0);
281 t2.set_pan(-1.0);
282 t1.push_samples(&[0.8, 0.8]);
283 t2.push_samples(&[0.8, 0.8]);
284
285 let out = mixer.mix(4);
286 for &s in &out {
287 assert!(
288 s >= -1.0 && s <= 1.0,
289 "all output must be within [-1.0, 1.0]; got {s}"
290 );
291 }
292 }
293
294 #[test]
295 fn audio_mixer_center_pan_should_apply_constant_power_law() {
296 let mut mixer = AudioMixer::new(48_000);
298 let track = mixer.add_track();
299 track.push_samples(&[1.0]);
301
302 let out = mixer.mix(2); let expected = (std::f32::consts::FRAC_PI_4).cos(); assert!(
305 (out[0] - expected).abs() < 1e-5,
306 "L at center should be cos(π/4) ≈ {expected:.5}; got {}",
307 out[0]
308 );
309 assert!(
310 (out[1] - expected).abs() < 1e-5,
311 "R at center should be sin(π/4) ≈ {expected:.5}; got {}",
312 out[1]
313 );
314 }
315
316 #[test]
317 fn audio_mixer_underrun_should_zero_pad_remaining_frames() {
318 let mut mixer = AudioMixer::new(48_000);
319 let track = mixer.add_track();
320 track.set_pan(-1.0); track.push_samples(&[0.5]); let out = mixer.mix(8);
324 assert_eq!(out.len(), 8);
325
326 for i in 2..8 {
328 assert_eq!(out[i], 0.0, "underrun frame must be silent; got {}", out[i]);
329 }
330 }
331
332 #[test]
333 fn audio_mixer_empty_tracks_should_produce_silence() {
334 let mut mixer = AudioMixer::new(48_000);
335 let _track = mixer.add_track();
336 let out = mixer.mix(8);
337 assert_eq!(out.len(), 8);
338 assert!(
339 out.iter().all(|&s| s == 0.0),
340 "empty track must produce silence"
341 );
342 }
343
344 #[cfg(feature = "timeline")]
345 #[test]
346 fn audio_mixer_invalidate_all_should_clear_all_buffers() {
347 let mut mixer = AudioMixer::new(48_000);
348 let t1 = mixer.add_track();
349 let t2 = mixer.add_track();
350 t1.push_samples(&[0.5, 0.5]);
351 t2.push_samples(&[0.5, 0.5]);
352
353 mixer.invalidate_all();
354
355 let out = mixer.mix(4);
356 assert!(
357 out.iter().all(|&s| s == 0.0),
358 "after invalidate_all, mix must be silent"
359 );
360 }
361
362 #[test]
363 fn audio_track_handle_set_volume_above_one_should_amplify() {
364 let mut mixer = AudioMixer::new(48_000);
365 let track = mixer.add_track();
366 track.set_volume(2.0);
367 track.set_pan(-1.0); track.push_samples(&[0.4]);
369 let out = mixer.mix(2); assert!(
372 (out[0] - 0.8).abs() < 1e-5,
373 "volume 2.0 should amplify to 0.8; got {}",
374 out[0]
375 );
376 }
377
378 #[test]
379 fn audio_track_handle_set_negative_volume_should_be_silent() {
380 let mut mixer = AudioMixer::new(48_000);
381 let track = mixer.add_track();
382 track.set_volume(-1.0); track.push_samples(&[1.0]);
384 let out = mixer.mix(2);
385 assert!(
386 out.iter().all(|&s| s.abs() < 1e-6),
387 "negative volume must be silent"
388 );
389 }
390
391 #[cfg(feature = "timeline")]
392 #[test]
393 fn audio_track_handle_clear_should_drain_buffered_samples() {
394 let mut mixer = AudioMixer::new(48_000);
395 let track = mixer.add_track();
396 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
397 assert_eq!(track.buffered_samples(), 4);
398 track.clear();
399 assert_eq!(
400 track.buffered_samples(),
401 0,
402 "clear() must drain all samples"
403 );
404 }
405}