1#![cfg_attr(
17 not(any(
18 test,
19 all(feature = "aaudio", target_os = "android"),
20 all(
21 feature = "cpal-backend",
22 not(any(target_os = "android", target_arch = "wasm32"))
23 )
24 )),
25 allow(dead_code)
26)]
27
28use crate::ring::{Consumer, Producer};
29use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
30use std::sync::Arc;
31
32pub const MAX_CLIPS: usize = 256;
35
36pub const MAX_VOICES: usize = 32;
39
40pub const BUS_COUNT: usize = 2;
42
43pub const IDLE_GRACE_SECONDS: f32 = 2.0;
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum RenderStatus {
65 Continue,
67 Idle,
71}
72
73#[derive(Clone)]
75pub struct ClipData {
76 pub samples: Arc<[f32]>,
78 pub channels: u8,
80 pub sample_rate: u32,
82}
83
84impl ClipData {
85 fn frames(&self) -> usize {
86 self.samples.len() / usize::from(self.channels)
87 }
88}
89
90impl std::fmt::Debug for ClipData {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("ClipData")
93 .field("frames", &self.frames())
94 .field("channels", &self.channels)
95 .field("sample_rate", &self.sample_rate)
96 .finish()
97 }
98}
99
100#[derive(Debug)]
102pub enum Command {
103 LoadClip {
105 slot: u32,
107 clip: ClipData,
109 },
110 UnloadClip {
112 slot: u32,
114 },
115 Play {
117 voice: u64,
119 slot: u32,
121 gain_left: f32,
123 gain_right: f32,
125 rate: f32,
127 bus: u8,
129 looping: bool,
131 },
132 RetuneVoice {
134 voice: u64,
136 gain_left: f32,
138 gain_right: f32,
140 rate: f32,
142 },
143 StopVoice {
145 voice: u64,
147 },
148 StopClip {
150 slot: u32,
152 },
153 StopAll,
155 SetMaster(f32),
157 SetBusVolume {
159 bus: u8,
161 volume: f32,
163 },
164 SetBusEnabled {
166 bus: u8,
168 enabled: bool,
170 },
171}
172
173pub struct MixerSeed {
175 pub commands: Consumer<Command>,
177 pub retired: Producer<ClipData>,
179 pub leaked_clips: Arc<AtomicU32>,
181 pub underruns: Arc<AtomicU32>,
183 pub streaming: Arc<AtomicBool>,
191}
192
193#[derive(Clone, Copy)]
194struct Voice {
195 id: u64,
197 slot: usize,
198 position: f64,
200 step: f64,
202 rate: f32,
205 gain_left: f32,
206 gain_right: f32,
207 bus: usize,
208 looping: bool,
209}
210
211impl Voice {
212 const IDLE: Voice = Voice {
213 id: 0,
214 slot: 0,
215 position: 0.0,
216 step: 1.0,
217 rate: 1.0,
218 gain_left: 0.0,
219 gain_right: 0.0,
220 bus: 0,
221 looping: false,
222 };
223}
224
225pub struct Mixer {
227 commands: Consumer<Command>,
228 retired: Producer<ClipData>,
229 leaked_clips: Arc<AtomicU32>,
230 underruns: Arc<AtomicU32>,
231 streaming: Arc<AtomicBool>,
232 clips: Vec<Option<ClipData>>,
233 voices: Vec<Voice>,
234 master: f32,
235 bus_volume: [f32; BUS_COUNT],
236 bus_enabled: [bool; BUS_COUNT],
237 device_sample_rate: f32,
238 device_channels: usize,
239 idle_frames: u64,
241 idle_grace_frames: u64,
244}
245
246impl Mixer {
247 pub fn new(seed: MixerSeed, sample_rate: f32, channels: usize) -> Mixer {
250 let mut clips = Vec::with_capacity(MAX_CLIPS);
251 clips.resize_with(MAX_CLIPS, || None);
252 Mixer {
253 commands: seed.commands,
254 retired: seed.retired,
255 leaked_clips: seed.leaked_clips,
256 underruns: seed.underruns,
257 streaming: seed.streaming,
258 clips,
259 voices: vec![Voice::IDLE; MAX_VOICES],
260 master: 1.0,
261 bus_volume: [1.0; BUS_COUNT],
262 bus_enabled: [true; BUS_COUNT],
263 device_sample_rate: sample_rate.max(1.0),
264 device_channels: channels.max(1),
265 idle_frames: 0,
266 idle_grace_frames: grace_frames(sample_rate),
267 }
268 }
269
270 #[allow(dead_code)]
277 pub fn set_device_format(&mut self, sample_rate: f32, channels: usize) {
278 let sample_rate = sample_rate.max(1.0);
279 let channels = channels.max(1);
280 if sample_rate == self.device_sample_rate && channels == self.device_channels {
281 return;
282 }
283 self.device_sample_rate = sample_rate;
284 self.device_channels = channels;
285 self.idle_grace_frames = grace_frames(sample_rate);
286 for index in 0..self.voices.len() {
287 if self.voices[index].id == 0 {
288 continue;
289 }
290 let slot = self.voices[index].slot;
291 let rate = self.voices[index].rate;
292 let clip_rate = self.clips[slot]
293 .as_ref()
294 .map(|clip| clip.sample_rate)
295 .unwrap_or(0);
296 self.voices[index].step = step_for(rate, clip_rate, sample_rate);
297 }
298 }
299
300 #[allow(dead_code)]
302 pub fn device_sample_rate(&self) -> f32 {
303 self.device_sample_rate
304 }
305
306 #[allow(dead_code)]
309 pub fn active_voices(&self) -> usize {
310 self.voices.iter().filter(|voice| voice.id != 0).count()
311 }
312
313 pub fn render(&mut self, out: &mut [f32]) -> RenderStatus {
319 self.drain_commands();
320
321 for sample in out.iter_mut() {
322 *sample = 0.0;
323 }
324
325 let channels = self.device_channels;
326 if channels == 0 || out.is_empty() {
327 return RenderStatus::Continue;
328 }
329 let out_frames = out.len() / channels;
330 if out_frames == 0 {
331 self.underruns.fetch_add(1, Ordering::Relaxed);
332 return RenderStatus::Continue;
333 }
334
335 let master = self.master;
336 let bus_gain = [
337 if self.bus_enabled[0] {
338 self.bus_volume[0] * master
339 } else {
340 0.0
341 },
342 if self.bus_enabled[1] {
343 self.bus_volume[1] * master
344 } else {
345 0.0
346 },
347 ];
348
349 let mut sounding = 0usize;
354 let clips = &self.clips;
355 for voice in self.voices.iter_mut() {
356 if voice.id == 0 {
357 continue;
358 }
359 let Some(clip) = clips[voice.slot].as_ref() else {
360 voice.id = 0;
361 continue;
362 };
363 let frames = clip.frames();
364 if frames == 0 {
365 voice.id = 0;
366 continue;
367 }
368 let stereo_clip = clip.channels == 2;
369 let gain = bus_gain[voice.bus];
370 let gain_left = voice.gain_left * gain;
371 let gain_right = voice.gain_right * gain;
372 let mut position = voice.position;
373 let step = voice.step;
374 let length = frames as f64;
375 let audible = voice.looping || position < length;
382
383 for frame in 0..out_frames {
384 if position >= length {
385 if voice.looping {
386 position -= length;
389 if position < 0.0 || position >= length {
390 position = 0.0;
391 }
392 } else {
393 voice.id = 0;
394 break;
395 }
396 }
397
398 let index = position as usize;
399 let index = if index < frames { index } else { frames - 1 };
400 let fraction = (position - index as f64) as f32;
401 let next = if index + 1 < frames {
402 index + 1
403 } else if voice.looping {
404 0
405 } else {
406 index
407 };
408
409 let (left, right) = if stereo_clip {
410 let a_left = clip.samples[index * 2];
411 let a_right = clip.samples[index * 2 + 1];
412 let b_left = clip.samples[next * 2];
413 let b_right = clip.samples[next * 2 + 1];
414 (
415 a_left + (b_left - a_left) * fraction,
416 a_right + (b_right - a_right) * fraction,
417 )
418 } else {
419 let a = clip.samples[index];
420 let b = clip.samples[next];
421 let sample = a + (b - a) * fraction;
422 (sample, sample)
423 };
424
425 let base = frame * channels;
426 if channels == 1 {
427 out[base] += (left * gain_left + right * gain_right) * 0.5;
428 } else {
429 out[base] += left * gain_left;
430 out[base + 1] += right * gain_right;
431 }
432
433 position += step;
434 }
435
436 voice.position = position;
437 if audible {
438 sounding += 1;
439 }
440 }
441
442 for sample in out.iter_mut() {
443 *sample = sample.clamp(-1.0, 1.0);
444 }
445
446 self.settle(sounding, out_frames)
447 }
448
449 fn settle(&mut self, sounding: usize, frames: usize) -> RenderStatus {
460 if sounding > 0 {
461 self.idle_frames = 0;
462 return RenderStatus::Continue;
463 }
464 self.idle_frames = self.idle_frames.saturating_add(frames as u64);
465 if self.idle_frames < self.idle_grace_frames {
466 return RenderStatus::Continue;
467 }
468
469 self.streaming.store(false, Ordering::SeqCst);
470 std::sync::atomic::fence(Ordering::SeqCst);
471 if !self.commands.is_empty() {
472 self.streaming.store(true, Ordering::SeqCst);
473 self.idle_frames = 0;
474 return RenderStatus::Continue;
475 }
476 RenderStatus::Idle
477 }
478
479 fn drain_commands(&mut self) {
480 while let Some(command) = self.commands.pop() {
481 self.apply(command);
482 }
483 }
484
485 fn apply(&mut self, command: Command) {
486 match command {
487 Command::LoadClip { slot, clip } => {
488 let slot = slot as usize;
489 if slot >= self.clips.len() {
490 self.retire(clip);
491 return;
492 }
493 self.silence_slot(slot);
494 if let Some(previous) = self.clips[slot].replace(clip) {
495 self.retire(previous);
496 }
497 }
498 Command::UnloadClip { slot } => {
499 let slot = slot as usize;
500 if slot >= self.clips.len() {
501 return;
502 }
503 self.silence_slot(slot);
504 if let Some(previous) = self.clips[slot].take() {
505 self.retire(previous);
506 }
507 }
508 Command::Play {
509 voice,
510 slot,
511 gain_left,
512 gain_right,
513 rate,
514 bus,
515 looping,
516 } => {
517 let slot = slot as usize;
518 let bus = usize::from(bus).min(BUS_COUNT - 1);
519 let Some(clip) = self.clips.get(slot).and_then(|clip| clip.as_ref()) else {
520 return;
521 };
522 let step = step_for(rate, clip.sample_rate, self.device_sample_rate);
523 let index = self.claim_voice();
524 self.voices[index] = Voice {
525 id: voice,
526 slot,
527 position: 0.0,
528 step,
529 rate,
530 gain_left,
531 gain_right,
532 bus,
533 looping,
534 };
535 }
536 Command::RetuneVoice {
537 voice,
538 gain_left,
539 gain_right,
540 rate,
541 } => {
542 for index in 0..self.voices.len() {
543 if self.voices[index].id != voice {
544 continue;
545 }
546 let slot = self.voices[index].slot;
547 let clip_rate = self.clips[slot]
548 .as_ref()
549 .map(|clip| clip.sample_rate)
550 .unwrap_or(0);
551 self.voices[index].gain_left = gain_left;
552 self.voices[index].gain_right = gain_right;
553 self.voices[index].rate = rate;
554 self.voices[index].step = step_for(rate, clip_rate, self.device_sample_rate);
555 }
556 }
557 Command::StopVoice { voice } => {
558 for slot in self.voices.iter_mut() {
559 if slot.id == voice {
560 slot.id = 0;
561 }
562 }
563 }
564 Command::StopClip { slot } => {
565 self.silence_slot(slot as usize);
566 }
567 Command::StopAll => {
568 for voice in self.voices.iter_mut() {
569 voice.id = 0;
570 }
571 }
572 Command::SetMaster(volume) => self.master = sane_gain(volume),
573 Command::SetBusVolume { bus, volume } => {
574 if let Some(entry) = self.bus_volume.get_mut(usize::from(bus)) {
575 *entry = sane_gain(volume);
576 }
577 }
578 Command::SetBusEnabled { bus, enabled } => {
579 if let Some(entry) = self.bus_enabled.get_mut(usize::from(bus)) {
580 *entry = enabled;
581 }
582 }
583 }
584 }
585
586 fn silence_slot(&mut self, slot: usize) {
587 for voice in self.voices.iter_mut() {
588 if voice.id != 0 && voice.slot == slot {
589 voice.id = 0;
590 }
591 }
592 }
593
594 fn claim_voice(&mut self) -> usize {
597 let mut oldest_one_shot: Option<(usize, u64)> = None;
598 let mut oldest_any: Option<(usize, u64)> = None;
599 for (index, voice) in self.voices.iter().enumerate() {
600 if voice.id == 0 {
601 return index;
602 }
603 if oldest_any.is_none_or(|(_, id)| voice.id < id) {
604 oldest_any = Some((index, voice.id));
605 }
606 if !voice.looping && oldest_one_shot.is_none_or(|(_, id)| voice.id < id) {
607 oldest_one_shot = Some((index, voice.id));
608 }
609 }
610 oldest_one_shot
611 .or(oldest_any)
612 .map(|(index, _)| index)
613 .unwrap_or(0)
614 }
615
616 fn retire(&mut self, clip: ClipData) {
623 if let Err(clip) = self.retired.push(clip) {
624 std::mem::forget(clip);
625 self.leaked_clips.fetch_add(1, Ordering::Relaxed);
626 }
627 }
628}
629
630fn grace_frames(sample_rate: f32) -> u64 {
632 (f64::from(sample_rate.max(1.0)) * f64::from(IDLE_GRACE_SECONDS)) as u64
633}
634
635fn sane_gain(value: f32) -> f32 {
636 if value.is_finite() {
637 value.clamp(0.0, 4.0)
638 } else {
639 1.0
640 }
641}
642
643fn step_for(rate: f32, clip_sample_rate: u32, device_sample_rate: f32) -> f64 {
644 if clip_sample_rate == 0 || device_sample_rate <= 0.0 {
645 return 1.0;
646 }
647 let rate = if rate.is_finite() {
648 rate.clamp(0.05, 8.0)
649 } else {
650 1.0
651 };
652 f64::from(rate) * f64::from(clip_sample_rate) / f64::from(device_sample_rate)
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::ring;
659
660 struct Harness {
661 commands: Producer<Command>,
662 retired: Consumer<ClipData>,
663 mixer: Mixer,
664 leaked: Arc<AtomicU32>,
665 streaming: Arc<AtomicBool>,
666 }
667
668 impl Harness {
669 fn run(&mut self, frames: usize) -> RenderStatus {
672 let burst = 128;
673 let channels = self.mixer.device_channels;
674 let mut out = vec![0.0f32; burst * channels];
675 let mut status = RenderStatus::Continue;
676 let mut remaining = frames;
677 while remaining > 0 {
678 let take = remaining.min(burst);
679 status = self.mixer.render(&mut out[..take * channels]);
680 remaining -= take;
681 }
682 status
683 }
684 }
685
686 fn harness(sample_rate: f32, channels: usize) -> Harness {
687 let (command_tx, command_rx) = ring::channel::<Command>(64);
688 let (retired_tx, retired_rx) = ring::channel::<ClipData>(64);
689 let leaked = Arc::new(AtomicU32::new(0));
690 let streaming = Arc::new(AtomicBool::new(true));
691 let seed = MixerSeed {
692 commands: command_rx,
693 retired: retired_tx,
694 leaked_clips: Arc::clone(&leaked),
695 underruns: Arc::new(AtomicU32::new(0)),
696 streaming: Arc::clone(&streaming),
697 };
698 Harness {
699 commands: command_tx,
700 retired: retired_rx,
701 mixer: Mixer::new(seed, sample_rate, channels),
702 leaked,
703 streaming,
704 }
705 }
706
707 fn clip(samples: Vec<f32>, channels: u8, sample_rate: u32) -> ClipData {
708 ClipData {
709 samples: samples.into(),
710 channels,
711 sample_rate,
712 }
713 }
714
715 fn play(voice: u64, slot: u32) -> Command {
716 Command::Play {
717 voice,
718 slot,
719 gain_left: 1.0,
720 gain_right: 1.0,
721 rate: 1.0,
722 bus: 0,
723 looping: false,
724 }
725 }
726
727 #[test]
728 fn renders_silence_without_voices() {
729 let mut h = harness(48_000.0, 2);
730 let mut out = vec![1.0f32; 8];
731 h.mixer.render(&mut out);
732 assert!(out.iter().all(|sample| *sample == 0.0));
733 }
734
735 #[test]
736 fn plays_a_one_shot_and_frees_the_voice() {
737 let mut h = harness(48_000.0, 2);
738 h.commands
739 .push(Command::LoadClip {
740 slot: 0,
741 clip: clip(vec![1.0, 1.0], 1, 48_000),
742 })
743 .expect("queued");
744 h.commands.push(play(1, 0)).expect("queued");
745
746 let mut out = vec![0.0f32; 8];
747 h.mixer.render(&mut out);
748 assert_eq!(h.mixer.active_voices(), 0, "a two-frame clip ends at once");
749 assert!(out[0] > 0.0 && out[1] > 0.0);
750 assert_eq!(out[6], 0.0, "past the end of the clip is silent");
751 }
752
753 #[test]
754 fn overlapping_voices_sum() {
755 let mut h = harness(48_000.0, 2);
756 h.commands
757 .push(Command::LoadClip {
758 slot: 0,
759 clip: clip(vec![0.25; 64], 1, 48_000),
760 })
761 .expect("queued");
762 h.commands.push(play(1, 0)).expect("queued");
763 h.commands.push(play(2, 0)).expect("queued");
764 h.commands.push(play(3, 0)).expect("queued");
765
766 let mut out = vec![0.0f32; 8];
767 h.mixer.render(&mut out);
768 assert_eq!(h.mixer.active_voices(), 3);
769 assert!(out[0] > 0.5, "three voices sum, got {}", out[0]);
770 }
771
772 #[test]
773 fn output_is_clamped_to_the_nominal_range() {
774 let mut h = harness(48_000.0, 2);
775 h.commands
776 .push(Command::LoadClip {
777 slot: 0,
778 clip: clip(vec![1.0; 64], 1, 48_000),
779 })
780 .expect("queued");
781 for voice in 1..=8 {
782 h.commands.push(play(voice, 0)).expect("queued");
783 }
784 let mut out = vec![0.0f32; 8];
785 h.mixer.render(&mut out);
786 assert!(out.iter().all(|s| (-1.0..=1.0).contains(s)));
787 assert!((out[0] - 1.0).abs() < 1e-6);
788 }
789
790 #[test]
791 fn rate_shifts_the_read_position() {
792 let mut h = harness(48_000.0, 1);
793 let ramp: Vec<f32> = (0..64).map(|i| i as f32 / 64.0).collect();
794 h.commands
795 .push(Command::LoadClip {
796 slot: 0,
797 clip: clip(ramp, 1, 48_000),
798 })
799 .expect("queued");
800 h.commands
801 .push(Command::Play {
802 voice: 1,
803 slot: 0,
804 gain_left: 1.0,
805 gain_right: 1.0,
806 rate: 2.0,
807 bus: 0,
808 looping: false,
809 })
810 .expect("queued");
811
812 let mut out = vec![0.0f32; 4];
813 h.mixer.render(&mut out);
814 for (frame, sample) in out.iter().enumerate() {
817 let expected = (2 * frame) as f32 / 64.0;
818 assert!(
819 (sample - expected).abs() < 1e-6,
820 "frame {frame}: expected {expected}, got {sample}"
821 );
822 }
823 }
824
825 #[test]
826 fn clip_sample_rate_is_resampled_to_the_device_rate() {
827 let mut h = harness(48_000.0, 1);
828 h.commands
829 .push(Command::LoadClip {
830 slot: 0,
831 clip: clip(vec![0.5; 1024], 1, 24_000),
832 })
833 .expect("queued");
834 h.commands.push(play(1, 0)).expect("queued");
835 let mut out = vec![0.0f32; 8];
836 h.mixer.render(&mut out);
837 assert_eq!(h.mixer.active_voices(), 1);
838 for _ in 0..255 {
841 h.mixer.render(&mut out);
842 }
843 assert_eq!(
844 h.mixer.active_voices(),
845 1,
846 "still playing after 2048 frames"
847 );
848 }
849
850 #[test]
851 fn looping_voice_keeps_going_until_stopped() {
852 let mut h = harness(48_000.0, 2);
853 h.commands
854 .push(Command::LoadClip {
855 slot: 0,
856 clip: clip(vec![0.5, 0.5], 1, 48_000),
857 })
858 .expect("queued");
859 h.commands
860 .push(Command::Play {
861 voice: 9,
862 slot: 0,
863 gain_left: 1.0,
864 gain_right: 1.0,
865 rate: 1.0,
866 bus: 1,
867 looping: true,
868 })
869 .expect("queued");
870
871 let mut out = vec![0.0f32; 64];
872 h.mixer.render(&mut out);
873 assert_eq!(h.mixer.active_voices(), 1);
874 assert!(out[40] != 0.0, "the loop refills the whole buffer");
875
876 h.commands
877 .push(Command::StopVoice { voice: 9 })
878 .expect("queued");
879 h.mixer.render(&mut out);
880 assert_eq!(h.mixer.active_voices(), 0);
881 }
882
883 #[test]
884 fn muting_a_bus_silences_only_that_bus() {
885 let mut h = harness(48_000.0, 2);
886 h.commands
887 .push(Command::LoadClip {
888 slot: 0,
889 clip: clip(vec![1.0; 64], 1, 48_000),
890 })
891 .expect("queued");
892 h.commands
893 .push(Command::Play {
894 voice: 1,
895 slot: 0,
896 gain_left: 0.5,
897 gain_right: 0.5,
898 rate: 1.0,
899 bus: 1,
900 looping: true,
901 })
902 .expect("queued");
903 h.commands
904 .push(Command::SetBusEnabled {
905 bus: 1,
906 enabled: false,
907 })
908 .expect("queued");
909
910 let mut out = vec![0.0f32; 16];
911 h.mixer.render(&mut out);
912 assert!(out.iter().all(|sample| *sample == 0.0));
913 assert_eq!(h.mixer.active_voices(), 1, "muting does not stop the voice");
914
915 h.commands
916 .push(Command::SetBusEnabled {
917 bus: 1,
918 enabled: true,
919 })
920 .expect("queued");
921 h.mixer.render(&mut out);
922 assert!(out[0] > 0.0, "unmuting resumes mid-track");
923 }
924
925 #[test]
926 fn master_volume_scales_every_bus() {
927 let mut h = harness(48_000.0, 2);
928 h.commands
929 .push(Command::LoadClip {
930 slot: 0,
931 clip: clip(vec![1.0; 64], 1, 48_000),
932 })
933 .expect("queued");
934 h.commands
935 .push(Command::Play {
936 voice: 1,
937 slot: 0,
938 gain_left: 0.5,
939 gain_right: 0.5,
940 rate: 1.0,
941 bus: 0,
942 looping: true,
943 })
944 .expect("queued");
945 h.commands.push(Command::SetMaster(0.0)).expect("queued");
946 let mut out = vec![0.0f32; 16];
947 h.mixer.render(&mut out);
948 assert!(out.iter().all(|sample| *sample == 0.0));
949
950 h.commands.push(Command::SetMaster(1.0)).expect("queued");
951 h.mixer.render(&mut out);
952 assert!((out[0] - 0.5).abs() < 1e-6);
953 }
954
955 #[test]
956 fn stop_clip_silences_every_voice_of_that_clip() {
957 let mut h = harness(48_000.0, 2);
958 h.commands
959 .push(Command::LoadClip {
960 slot: 0,
961 clip: clip(vec![1.0; 64], 1, 48_000),
962 })
963 .expect("queued");
964 h.commands
965 .push(Command::LoadClip {
966 slot: 1,
967 clip: clip(vec![1.0; 64], 1, 48_000),
968 })
969 .expect("queued");
970 h.commands.push(play(1, 0)).expect("queued");
971 h.commands.push(play(2, 0)).expect("queued");
972 h.commands.push(play(3, 1)).expect("queued");
973 let mut out = vec![0.0f32; 8];
974 h.mixer.render(&mut out);
975 assert_eq!(h.mixer.active_voices(), 3);
976
977 h.commands
978 .push(Command::StopClip { slot: 0 })
979 .expect("queued");
980 h.mixer.render(&mut out);
981 assert_eq!(h.mixer.active_voices(), 1);
982
983 h.commands.push(Command::StopAll).expect("queued");
984 h.mixer.render(&mut out);
985 assert_eq!(h.mixer.active_voices(), 0);
986 }
987
988 #[test]
989 fn voice_stealing_prefers_one_shots_over_loops() {
990 let mut h = harness(48_000.0, 2);
991 h.commands
992 .push(Command::LoadClip {
993 slot: 0,
994 clip: clip(vec![1.0; 4096], 1, 48_000),
995 })
996 .expect("queued");
997 h.commands
999 .push(Command::Play {
1000 voice: 1,
1001 slot: 0,
1002 gain_left: 1.0,
1003 gain_right: 1.0,
1004 rate: 1.0,
1005 bus: 0,
1006 looping: true,
1007 })
1008 .expect("queued");
1009 let mut out = vec![0.0f32; 8];
1010 for voice in 2..=(MAX_VOICES as u64) {
1011 h.commands.push(play(voice, 0)).expect("queued");
1012 }
1013 h.mixer.render(&mut out);
1014 assert_eq!(h.mixer.active_voices(), MAX_VOICES);
1015
1016 h.commands
1018 .push(play(MAX_VOICES as u64 + 1, 0))
1019 .expect("queued");
1020 h.mixer.render(&mut out);
1021 assert_eq!(h.mixer.active_voices(), MAX_VOICES);
1022 assert!(
1023 h.mixer.voices.iter().any(|voice| voice.id == 1),
1024 "the looping voice is not stolen while one-shots remain"
1025 );
1026 }
1027
1028 #[test]
1029 fn unloading_a_clip_returns_it_to_the_ui_thread() {
1030 let mut h = harness(48_000.0, 2);
1031 h.commands
1032 .push(Command::LoadClip {
1033 slot: 3,
1034 clip: clip(vec![1.0; 8], 1, 48_000),
1035 })
1036 .expect("queued");
1037 h.commands.push(play(5, 3)).expect("queued");
1038 let mut out = vec![0.0f32; 4];
1039 h.mixer.render(&mut out);
1040 assert_eq!(h.mixer.active_voices(), 1);
1041
1042 h.commands
1043 .push(Command::UnloadClip { slot: 3 })
1044 .expect("queued");
1045 h.mixer.render(&mut out);
1046 assert_eq!(h.mixer.active_voices(), 0);
1047 assert!(h.retired.pop().is_some(), "the clip came back for dropping");
1048 assert_eq!(h.leaked.load(Ordering::Relaxed), 0);
1049 }
1050
1051 #[test]
1052 fn replacing_a_slot_returns_the_previous_clip() {
1053 let mut h = harness(48_000.0, 2);
1054 for _ in 0..2 {
1055 h.commands
1056 .push(Command::LoadClip {
1057 slot: 1,
1058 clip: clip(vec![1.0; 8], 1, 48_000),
1059 })
1060 .expect("queued");
1061 }
1062 let mut out = vec![0.0f32; 4];
1063 h.mixer.render(&mut out);
1064 assert!(h.retired.pop().is_some());
1065 assert!(h.retired.pop().is_none());
1066 }
1067
1068 #[test]
1069 fn out_of_range_slots_are_ignored() {
1070 let mut h = harness(48_000.0, 2);
1071 h.commands
1072 .push(Command::LoadClip {
1073 slot: MAX_CLIPS as u32 + 5,
1074 clip: clip(vec![1.0; 8], 1, 48_000),
1075 })
1076 .expect("queued");
1077 h.commands
1078 .push(Command::UnloadClip {
1079 slot: MAX_CLIPS as u32 + 5,
1080 })
1081 .expect("queued");
1082 h.commands
1083 .push(play(1, MAX_CLIPS as u32 + 5))
1084 .expect("queued");
1085 let mut out = vec![0.0f32; 4];
1086 h.mixer.render(&mut out);
1087 assert_eq!(h.mixer.active_voices(), 0);
1088 assert!(
1089 h.retired.pop().is_some(),
1090 "the rejected clip is handed back"
1091 );
1092 }
1093
1094 #[test]
1095 fn retune_changes_gain_and_rate_of_a_running_voice() {
1096 let mut h = harness(48_000.0, 2);
1097 h.commands
1098 .push(Command::LoadClip {
1099 slot: 0,
1100 clip: clip(vec![1.0; 4096], 1, 48_000),
1101 })
1102 .expect("queued");
1103 h.commands
1104 .push(Command::Play {
1105 voice: 4,
1106 slot: 0,
1107 gain_left: 1.0,
1108 gain_right: 1.0,
1109 rate: 1.0,
1110 bus: 0,
1111 looping: true,
1112 })
1113 .expect("queued");
1114 let mut out = vec![0.0f32; 8];
1115 h.mixer.render(&mut out);
1116 assert!((out[0] - 1.0).abs() < 1e-6);
1117
1118 h.commands
1119 .push(Command::RetuneVoice {
1120 voice: 4,
1121 gain_left: 0.25,
1122 gain_right: 0.25,
1123 rate: 2.0,
1124 })
1125 .expect("queued");
1126 h.mixer.render(&mut out);
1127 assert!((out[0] - 0.25).abs() < 1e-6);
1128 let voice = h.mixer.voices.iter().find(|v| v.id == 4).expect("running");
1129 assert!((voice.step - 2.0).abs() < 1e-9);
1130 }
1131
1132 #[test]
1133 fn device_format_change_keeps_voice_pitch() {
1134 let mut h = harness(48_000.0, 2);
1135 h.commands
1136 .push(Command::LoadClip {
1137 slot: 0,
1138 clip: clip(vec![1.0; 4096], 1, 48_000),
1139 })
1140 .expect("queued");
1141 h.commands
1142 .push(Command::Play {
1143 voice: 1,
1144 slot: 0,
1145 gain_left: 1.0,
1146 gain_right: 1.0,
1147 rate: 1.0,
1148 bus: 0,
1149 looping: true,
1150 })
1151 .expect("queued");
1152 let mut out = vec![0.0f32; 8];
1153 h.mixer.render(&mut out);
1154 h.mixer.set_device_format(24_000.0, 2);
1155 assert_eq!(h.mixer.device_sample_rate(), 24_000.0);
1156 let voice = h.mixer.voices.iter().find(|v| v.id == 1).expect("running");
1157 assert!((voice.step - 2.0).abs() < 1e-9);
1158 }
1159
1160 #[test]
1161 fn nan_gains_and_rates_do_not_wedge_the_mixer() {
1162 let mut h = harness(48_000.0, 2);
1163 h.commands
1164 .push(Command::LoadClip {
1165 slot: 0,
1166 clip: clip(vec![1.0; 64], 1, 48_000),
1167 })
1168 .expect("queued");
1169 h.commands
1170 .push(Command::SetMaster(f32::NAN))
1171 .expect("queued");
1172 h.commands
1173 .push(Command::SetBusVolume {
1174 bus: 0,
1175 volume: f32::INFINITY,
1176 })
1177 .expect("queued");
1178 h.commands
1179 .push(Command::Play {
1180 voice: 1,
1181 slot: 0,
1182 gain_left: 0.5,
1183 gain_right: 0.5,
1184 rate: f32::NAN,
1185 bus: 0,
1186 looping: true,
1187 })
1188 .expect("queued");
1189 let mut out = vec![0.0f32; 16];
1190 h.mixer.render(&mut out);
1191 assert!(out.iter().all(|sample| sample.is_finite()));
1192 }
1193
1194 #[test]
1195 fn silence_gives_the_device_up_after_the_grace_period() {
1196 let mut h = harness(48_000.0, 2);
1197 let grace = grace_frames(48_000.0) as usize;
1198 assert_eq!(h.run(grace - 128), RenderStatus::Continue);
1199 assert!(h.streaming.load(Ordering::SeqCst), "still inside the grace");
1200 assert_eq!(h.run(128), RenderStatus::Idle);
1201 assert!(!h.streaming.load(Ordering::SeqCst));
1202 }
1203
1204 #[test]
1205 fn the_grace_period_starts_when_the_last_voice_ends() {
1206 let mut h = harness(48_000.0, 2);
1207 h.commands
1208 .push(Command::LoadClip {
1209 slot: 0,
1210 clip: clip(vec![0.5; 48_000], 1, 48_000),
1211 })
1212 .expect("queued");
1213 h.commands.push(play(1, 0)).expect("queued");
1214
1215 let grace = grace_frames(48_000.0) as usize;
1217 assert_eq!(h.run(48_000 + grace - 128), RenderStatus::Continue);
1218 assert_eq!(h.run(128), RenderStatus::Idle);
1219 }
1220
1221 #[test]
1222 fn a_looping_voice_holds_the_device_open_indefinitely() {
1223 let mut h = harness(48_000.0, 2);
1224 h.commands
1225 .push(Command::LoadClip {
1226 slot: 0,
1227 clip: clip(vec![0.5; 64], 1, 48_000),
1228 })
1229 .expect("queued");
1230 h.commands
1231 .push(Command::Play {
1232 voice: 1,
1233 slot: 0,
1234 gain_left: 1.0,
1235 gain_right: 1.0,
1236 rate: 1.0,
1237 bus: 0,
1238 looping: true,
1239 })
1240 .expect("queued");
1241
1242 let grace = grace_frames(48_000.0) as usize;
1243 assert_eq!(h.run(grace * 2), RenderStatus::Continue);
1244 assert!(h.streaming.load(Ordering::SeqCst));
1245 }
1246
1247 #[test]
1248 fn a_muted_voice_still_counts_as_a_reason_to_run() {
1249 let mut h = harness(48_000.0, 2);
1250 h.commands
1251 .push(Command::LoadClip {
1252 slot: 0,
1253 clip: clip(vec![0.5; 64], 1, 48_000),
1254 })
1255 .expect("queued");
1256 h.commands
1257 .push(Command::Play {
1258 voice: 1,
1259 slot: 0,
1260 gain_left: 1.0,
1261 gain_right: 1.0,
1262 rate: 1.0,
1263 bus: 1,
1264 looping: true,
1265 })
1266 .expect("queued");
1267 h.commands
1268 .push(Command::SetBusEnabled {
1269 bus: 1,
1270 enabled: false,
1271 })
1272 .expect("queued");
1273
1274 let grace = grace_frames(48_000.0) as usize;
1278 assert_eq!(h.run(grace + 128), RenderStatus::Continue);
1279 }
1280
1281 #[test]
1282 fn a_command_landing_while_the_stream_stops_keeps_it_alive() {
1283 let mut h = harness(48_000.0, 2);
1284 assert_eq!(h.run(grace_frames(48_000.0) as usize), RenderStatus::Idle);
1285 assert!(!h.streaming.load(Ordering::SeqCst));
1286
1287 h.commands.push(Command::StopAll).expect("queued");
1292 assert_eq!(h.mixer.settle(0, 128), RenderStatus::Continue);
1293 assert!(h.streaming.load(Ordering::SeqCst));
1294 }
1295
1296 #[test]
1297 fn the_grace_period_is_a_duration_not_a_callback_count() {
1298 let mut h = harness(24_000.0, 2);
1299 let grace = grace_frames(24_000.0) as usize;
1300 assert_eq!(grace * 2, grace_frames(48_000.0) as usize);
1301 assert_eq!(h.run(grace - 128), RenderStatus::Continue);
1302 assert_eq!(h.run(128), RenderStatus::Idle);
1303 }
1304
1305 #[test]
1306 fn a_device_rate_change_rescales_the_grace_period() {
1307 let mut h = harness(48_000.0, 2);
1308 h.mixer.set_device_format(24_000.0, 2);
1309 assert_eq!(h.run(grace_frames(24_000.0) as usize), RenderStatus::Idle);
1310 }
1311
1312 #[test]
1313 fn mono_device_downmixes_both_channels() {
1314 let mut h = harness(48_000.0, 1);
1315 h.commands
1316 .push(Command::LoadClip {
1317 slot: 0,
1318 clip: clip(vec![1.0, -1.0, 1.0, -1.0], 2, 48_000),
1319 })
1320 .expect("queued");
1321 h.commands.push(play(1, 0)).expect("queued");
1322 let mut out = vec![0.0f32; 2];
1323 h.mixer.render(&mut out);
1324 assert!(
1325 out[0].abs() < 1e-6,
1326 "opposite channels cancel in the downmix"
1327 );
1328 }
1329}