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) {
36 self.volume
37 .store(v.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
38 }
39
40 pub fn set_pan(&self, p: f32) {
42 self.pan
43 .store(p.clamp(-1.0, 1.0).to_bits(), Ordering::Relaxed);
44 }
45
46 pub fn push_samples(&self, samples: &[f32]) {
51 self.buf
52 .lock()
53 .unwrap_or_else(std::sync::PoisonError::into_inner)
54 .extend(samples.iter().copied());
55 }
56
57 #[cfg(feature = "timeline")]
61 pub(crate) fn buffered_samples(&self) -> usize {
62 self.buf
63 .lock()
64 .unwrap_or_else(std::sync::PoisonError::into_inner)
65 .len()
66 }
67
68 #[cfg(feature = "timeline")]
72 pub(crate) fn clear(&self) {
73 self.buf
74 .lock()
75 .unwrap_or_else(std::sync::PoisonError::into_inner)
76 .clear();
77 }
78}
79
80pub struct AudioMixer {
113 tracks: Vec<AudioTrack>,
114 pub sample_rate: u32,
116 pub channels: u16,
118}
119
120impl AudioMixer {
121 #[must_use]
123 pub fn new(sample_rate: u32) -> Self {
124 Self {
125 tracks: Vec::new(),
126 sample_rate,
127 channels: 2,
128 }
129 }
130
131 pub fn add_track(&mut self) -> AudioTrackHandle {
135 let buf = Arc::new(Mutex::new(VecDeque::new()));
136 let volume = Arc::new(AtomicU32::new(1.0_f32.to_bits()));
137 let pan = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
138 let handle = AudioTrackHandle {
139 buf: Arc::clone(&buf),
140 volume: Arc::clone(&volume),
141 pan: Arc::clone(&pan),
142 };
143 self.tracks.push(AudioTrack { buf, volume, pan });
144 handle
145 }
146
147 #[allow(clippy::cast_precision_loss)]
153 pub fn mix(&mut self, n_samples: usize) -> Vec<f32> {
154 let n_frames = n_samples / 2;
155 let mut out = vec![0.0_f32; n_frames * 2];
156
157 for track in &self.tracks {
158 let volume = f32::from_bits(track.volume.load(Ordering::Relaxed));
159 let pan = f32::from_bits(track.pan.load(Ordering::Relaxed));
160
161 let p_norm = (pan + 1.0) * consts::FRAC_PI_4;
163 let l_gain = volume * p_norm.cos();
164 let r_gain = volume * p_norm.sin();
165
166 let mut guard = track
167 .buf
168 .lock()
169 .unwrap_or_else(std::sync::PoisonError::into_inner);
170 for i in 0..n_frames {
171 let s = guard.pop_front().unwrap_or(0.0);
172 out[i * 2] += s * l_gain;
173 out[i * 2 + 1] += s * r_gain;
174 }
175 }
176
177 for sample in &mut out {
179 *sample = sample.clamp(-1.0, 1.0);
180 }
181
182 out
183 }
184
185 #[cfg(feature = "timeline")]
189 pub(crate) fn invalidate_all(&mut self) {
190 for track in &self.tracks {
191 track
192 .buf
193 .lock()
194 .unwrap_or_else(std::sync::PoisonError::into_inner)
195 .clear();
196 }
197 }
198}
199
200#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn audio_mixer_mix_two_tracks_should_sum_and_clip_left_channel() {
208 let mut mixer = AudioMixer::new(48_000);
211 let t1 = mixer.add_track();
212 let t2 = mixer.add_track();
213 t1.set_pan(-1.0);
214 t2.set_pan(-1.0);
215 t1.push_samples(&[0.8, 0.8]);
216 t2.push_samples(&[0.8, 0.8]);
217
218 let out = mixer.mix(4); assert_eq!(out.len(), 4);
220 assert!(
221 (out[0] - 1.0).abs() < 1e-6,
222 "L must clip to 1.0; got {}",
223 out[0]
224 );
225 assert!(
226 out[1].abs() < 1e-6,
227 "R must be 0.0 for full-left pan; got {}",
228 out[1]
229 );
230 }
231
232 #[test]
233 fn audio_mixer_pan_full_left_should_produce_zero_right_channel() {
234 let mut mixer = AudioMixer::new(48_000);
235 let track = mixer.add_track();
236 track.set_pan(-1.0);
237 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
238
239 let out = mixer.mix(8); assert_eq!(out.len(), 8);
241 for i in (1..8usize).step_by(2) {
242 assert!(
243 out[i].abs() < 1e-6,
244 "R channel must be 0.0 for full-left pan; got {} at index {i}",
245 out[i]
246 );
247 }
248 }
249
250 #[test]
251 fn audio_mixer_pan_full_right_should_produce_zero_left_channel() {
252 let mut mixer = AudioMixer::new(48_000);
253 let track = mixer.add_track();
254 track.set_pan(1.0);
255 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
256
257 let out = mixer.mix(8);
258 for i in (0..8usize).step_by(2) {
259 assert!(
260 out[i].abs() < 1e-6,
261 "L channel must be 0.0 for full-right pan; got {} at index {i}",
262 out[i]
263 );
264 }
265 }
266
267 #[test]
268 fn audio_mixer_two_tracks_volume_sum_exceeding_one_should_be_clipped() {
269 let mut mixer = AudioMixer::new(48_000);
272 let t1 = mixer.add_track();
273 let t2 = mixer.add_track();
274 t1.set_volume(0.7);
275 t2.set_volume(0.7);
276 t1.set_pan(-1.0);
277 t2.set_pan(-1.0);
278 t1.push_samples(&[0.8, 0.8]);
279 t2.push_samples(&[0.8, 0.8]);
280
281 let out = mixer.mix(4);
282 for &s in &out {
283 assert!(
284 s >= -1.0 && s <= 1.0,
285 "all output must be within [-1.0, 1.0]; got {s}"
286 );
287 }
288 }
289
290 #[test]
291 fn audio_mixer_center_pan_should_apply_constant_power_law() {
292 let mut mixer = AudioMixer::new(48_000);
294 let track = mixer.add_track();
295 track.push_samples(&[1.0]);
297
298 let out = mixer.mix(2); let expected = (std::f32::consts::FRAC_PI_4).cos(); assert!(
301 (out[0] - expected).abs() < 1e-5,
302 "L at center should be cos(π/4) ≈ {expected:.5}; got {}",
303 out[0]
304 );
305 assert!(
306 (out[1] - expected).abs() < 1e-5,
307 "R at center should be sin(π/4) ≈ {expected:.5}; got {}",
308 out[1]
309 );
310 }
311
312 #[test]
313 fn audio_mixer_underrun_should_zero_pad_remaining_frames() {
314 let mut mixer = AudioMixer::new(48_000);
315 let track = mixer.add_track();
316 track.set_pan(-1.0); track.push_samples(&[0.5]); let out = mixer.mix(8);
320 assert_eq!(out.len(), 8);
321
322 for i in 2..8 {
324 assert_eq!(out[i], 0.0, "underrun frame must be silent; got {}", out[i]);
325 }
326 }
327
328 #[test]
329 fn audio_mixer_empty_tracks_should_produce_silence() {
330 let mut mixer = AudioMixer::new(48_000);
331 let _track = mixer.add_track();
332 let out = mixer.mix(8);
333 assert_eq!(out.len(), 8);
334 assert!(
335 out.iter().all(|&s| s == 0.0),
336 "empty track must produce silence"
337 );
338 }
339
340 #[cfg(feature = "timeline")]
341 #[test]
342 fn audio_mixer_invalidate_all_should_clear_all_buffers() {
343 let mut mixer = AudioMixer::new(48_000);
344 let t1 = mixer.add_track();
345 let t2 = mixer.add_track();
346 t1.push_samples(&[0.5, 0.5]);
347 t2.push_samples(&[0.5, 0.5]);
348
349 mixer.invalidate_all();
350
351 let out = mixer.mix(4);
352 assert!(
353 out.iter().all(|&s| s == 0.0),
354 "after invalidate_all, mix must be silent"
355 );
356 }
357
358 #[test]
359 fn audio_track_handle_set_volume_should_clamp_to_zero_one() {
360 let mut mixer = AudioMixer::new(48_000);
361 let track = mixer.add_track();
362 track.set_volume(2.0); track.push_samples(&[1.0]);
364 let out = mixer.mix(2);
365 assert!(
367 out[0] <= 1.0,
368 "volume clamped to 1.0 must not exceed gain 1.0"
369 );
370 }
371
372 #[cfg(feature = "timeline")]
373 #[test]
374 fn audio_track_handle_clear_should_drain_buffered_samples() {
375 let mut mixer = AudioMixer::new(48_000);
376 let track = mixer.add_track();
377 track.push_samples(&[0.5, 0.5, 0.5, 0.5]);
378 assert_eq!(track.buffered_samples(), 4);
379 track.clear();
380 assert_eq!(
381 track.buffered_samples(),
382 0,
383 "clear() must drain all samples"
384 );
385 }
386}