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)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn audio_mixer_mix_two_tracks_should_sum_and_clip_left_channel() {
210 let mut mixer = AudioMixer::new(48_000);
213 let t1 = mixer.add_track();
214 let t2 = mixer.add_track();
215 t1.set_pan(-1.0);
216 t2.set_pan(-1.0);
217 t1.push_samples(&[0.8, 0.8]);
218 t2.push_samples(&[0.8, 0.8]);
219
220 let out = mixer.mix(4); assert_eq!(out.len(), 4);
222 assert!(
223 (out[0] - 1.0).abs() < 1e-6,
224 "L must clip to 1.0; got {}",
225 out[0]
226 );
227 assert!(
228 out[1].abs() < 1e-6,
229 "R must be 0.0 for full-left pan; got {}",
230 out[1]
231 );
232 }
233
234 #[test]
235 fn audio_mixer_pan_full_left_should_produce_zero_right_channel() {
236 let mut mixer = AudioMixer::new(48_000);
237 let track = mixer.add_track();
238 track.set_pan(-1.0);
239 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
240
241 let out = mixer.mix(8); assert_eq!(out.len(), 8);
243 for i in (1..8usize).step_by(2) {
244 assert!(
245 out[i].abs() < 1e-6,
246 "R channel must be 0.0 for full-left pan; got {} at index {i}",
247 out[i]
248 );
249 }
250 }
251
252 #[test]
253 fn audio_mixer_pan_full_right_should_produce_zero_left_channel() {
254 let mut mixer = AudioMixer::new(48_000);
255 let track = mixer.add_track();
256 track.set_pan(1.0);
257 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
258
259 let out = mixer.mix(8);
260 for i in (0..8usize).step_by(2) {
261 assert!(
262 out[i].abs() < 1e-6,
263 "L channel must be 0.0 for full-right pan; got {} at index {i}",
264 out[i]
265 );
266 }
267 }
268
269 #[test]
270 fn audio_mixer_two_tracks_volume_sum_exceeding_one_should_be_clipped() {
271 let mut mixer = AudioMixer::new(48_000);
274 let t1 = mixer.add_track();
275 let t2 = mixer.add_track();
276 t1.set_volume(0.7);
277 t2.set_volume(0.7);
278 t1.set_pan(-1.0);
279 t2.set_pan(-1.0);
280 t1.push_samples(&[0.8, 0.8]);
281 t2.push_samples(&[0.8, 0.8]);
282
283 let out = mixer.mix(4);
284 for &s in &out {
285 assert!(
286 s >= -1.0 && s <= 1.0,
287 "all output must be within [-1.0, 1.0]; got {s}"
288 );
289 }
290 }
291
292 #[test]
293 fn audio_mixer_center_pan_should_apply_constant_power_law() {
294 let mut mixer = AudioMixer::new(48_000);
296 let track = mixer.add_track();
297 track.push_samples(&[1.0]);
299
300 let out = mixer.mix(2); let expected = (std::f32::consts::FRAC_PI_4).cos(); assert!(
303 (out[0] - expected).abs() < 1e-5,
304 "L at center should be cos(π/4) ≈ {expected:.5}; got {}",
305 out[0]
306 );
307 assert!(
308 (out[1] - expected).abs() < 1e-5,
309 "R at center should be sin(π/4) ≈ {expected:.5}; got {}",
310 out[1]
311 );
312 }
313
314 #[test]
315 fn audio_mixer_underrun_should_zero_pad_remaining_frames() {
316 let mut mixer = AudioMixer::new(48_000);
317 let track = mixer.add_track();
318 track.set_pan(-1.0); track.push_samples(&[0.5]); let out = mixer.mix(8);
322 assert_eq!(out.len(), 8);
323
324 for i in 2..8 {
326 assert_eq!(out[i], 0.0, "underrun frame must be silent; got {}", out[i]);
327 }
328 }
329
330 #[test]
331 fn audio_mixer_empty_tracks_should_produce_silence() {
332 let mut mixer = AudioMixer::new(48_000);
333 let _track = mixer.add_track();
334 let out = mixer.mix(8);
335 assert_eq!(out.len(), 8);
336 assert!(
337 out.iter().all(|&s| s == 0.0),
338 "empty track must produce silence"
339 );
340 }
341
342 #[cfg(feature = "timeline")]
343 #[test]
344 fn audio_mixer_invalidate_all_should_clear_all_buffers() {
345 let mut mixer = AudioMixer::new(48_000);
346 let t1 = mixer.add_track();
347 let t2 = mixer.add_track();
348 t1.push_samples(&[0.5, 0.5]);
349 t2.push_samples(&[0.5, 0.5]);
350
351 mixer.invalidate_all();
352
353 let out = mixer.mix(4);
354 assert!(
355 out.iter().all(|&s| s == 0.0),
356 "after invalidate_all, mix must be silent"
357 );
358 }
359
360 #[test]
361 fn audio_track_handle_set_volume_above_one_should_amplify() {
362 let mut mixer = AudioMixer::new(48_000);
363 let track = mixer.add_track();
364 track.set_volume(2.0);
365 track.set_pan(-1.0); track.push_samples(&[0.4]);
367 let out = mixer.mix(2); assert!(
370 (out[0] - 0.8).abs() < 1e-5,
371 "volume 2.0 should amplify to 0.8; got {}",
372 out[0]
373 );
374 }
375
376 #[test]
377 fn audio_track_handle_set_negative_volume_should_be_silent() {
378 let mut mixer = AudioMixer::new(48_000);
379 let track = mixer.add_track();
380 track.set_volume(-1.0); track.push_samples(&[1.0]);
382 let out = mixer.mix(2);
383 assert!(
384 out.iter().all(|&s| s.abs() < 1e-6),
385 "negative volume must be silent"
386 );
387 }
388
389 #[cfg(feature = "timeline")]
390 #[test]
391 fn audio_track_handle_clear_should_drain_buffered_samples() {
392 let mut mixer = AudioMixer::new(48_000);
393 let track = mixer.add_track();
394 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
395 assert_eq!(track.buffered_samples(), 4);
396 track.clear();
397 assert_eq!(
398 track.buffered_samples(),
399 0,
400 "clear() must drain all samples"
401 );
402 }
403}