1use crate::backend::{self, AudioSink};
9use crate::mixer::{ClipData, Command, MixerSeed, BUS_COUNT, MAX_CLIPS};
10use crate::ring;
11use cranpose_services::{
12 AudioBus, AudioClip, AudioError, AudioPlayer, PlaybackParams, SoundId, VoiceId,
13};
14use std::cell::{Cell, RefCell};
15use std::sync::atomic::{fence, AtomicBool, AtomicU32, Ordering};
16use std::sync::Arc;
17
18const COMMAND_CAPACITY: usize = 512;
22
23const RETIRE_CAPACITY: usize = COMMAND_CAPACITY;
27
28type SinkOpener = Box<dyn Fn(MixerSeed) -> Result<Box<dyn AudioSink>, AudioError>>;
29
30pub struct AudioEngine {
43 commands: RefCell<ring::Producer<Command>>,
44 retired: RefCell<ring::Consumer<ClipData>>,
45 seed: RefCell<Option<MixerSeed>>,
46 sink: RefCell<Option<Box<dyn AudioSink>>>,
47 open_sink: SinkOpener,
48 free_slots: RefCell<Vec<u32>>,
49 next_voice: Cell<u64>,
50 master: Cell<f32>,
51 bus_volume: Cell<[f32; BUS_COUNT]>,
52 bus_enabled: Cell<[bool; BUS_COUNT]>,
53 last_error: RefCell<Option<AudioError>>,
54 device_unavailable: Cell<bool>,
55 suspended: Cell<bool>,
56 streaming: Arc<AtomicBool>,
60 parked: Cell<bool>,
64 leaked_clips: Arc<AtomicU32>,
65 underruns: Arc<AtomicU32>,
66}
67
68impl AudioEngine {
69 pub fn new() -> AudioEngine {
71 AudioEngine::with_sink_opener(Box::new(backend::open))
72 }
73
74 pub fn with_sink_opener(open_sink: SinkOpener) -> AudioEngine {
77 let (command_tx, command_rx) = ring::channel::<Command>(COMMAND_CAPACITY);
78 let (retired_tx, retired_rx) = ring::channel::<ClipData>(RETIRE_CAPACITY);
79 let leaked_clips = Arc::new(AtomicU32::new(0));
80 let underruns = Arc::new(AtomicU32::new(0));
81 let streaming = Arc::new(AtomicBool::new(false));
82 AudioEngine {
83 commands: RefCell::new(command_tx),
84 retired: RefCell::new(retired_rx),
85 seed: RefCell::new(Some(MixerSeed {
86 commands: command_rx,
87 retired: retired_tx,
88 leaked_clips: Arc::clone(&leaked_clips),
89 underruns: Arc::clone(&underruns),
90 streaming: Arc::clone(&streaming),
91 })),
92 sink: RefCell::new(None),
93 open_sink,
94 free_slots: RefCell::new((0..MAX_CLIPS as u32).rev().collect()),
95 next_voice: Cell::new(0),
96 master: Cell::new(1.0),
97 bus_volume: Cell::new([1.0; BUS_COUNT]),
98 bus_enabled: Cell::new([true; BUS_COUNT]),
99 last_error: RefCell::new(None),
100 device_unavailable: Cell::new(false),
101 suspended: Cell::new(false),
102 streaming,
103 parked: Cell::new(false),
104 leaked_clips,
105 underruns,
106 }
107 }
108
109 pub fn take_last_error(&self) -> Option<AudioError> {
112 self.last_error.borrow_mut().take()
113 }
114
115 pub fn leaked_clips(&self) -> u32 {
119 self.leaked_clips.load(Ordering::Relaxed)
120 }
121
122 pub fn underruns(&self) -> u32 {
124 self.underruns.load(Ordering::Relaxed)
125 }
126
127 pub fn is_running(&self) -> bool {
133 self.sink.borrow().is_some()
134 }
135
136 pub fn is_streaming(&self) -> bool {
145 self.streaming.load(Ordering::Relaxed)
146 }
147
148 fn ensure_running(&self) -> bool {
151 if self.sink.borrow().is_some() {
152 return true;
153 }
154 if self.device_unavailable.get() {
155 return false;
156 }
157 let Some(seed) = self.seed.borrow_mut().take() else {
158 self.device_unavailable.set(true);
159 return false;
160 };
161 self.streaming.store(true, Ordering::SeqCst);
164 match (self.open_sink)(seed) {
165 Ok(sink) => {
166 *self.sink.borrow_mut() = Some(sink);
167 self.publish_settings();
168 true
169 }
170 Err(error) => {
171 log::warn!("cranpose audio device unavailable: {error}");
172 self.streaming.store(false, Ordering::SeqCst);
173 *self.last_error.borrow_mut() = Some(error);
174 self.device_unavailable.set(true);
175 false
176 }
177 }
178 }
179
180 fn publish_settings(&self) {
184 let master = self.master.get();
185 let volumes = self.bus_volume.get();
186 let enabled = self.bus_enabled.get();
187 self.send(Command::SetMaster(master));
188 for bus in 0..BUS_COUNT {
189 self.send(Command::SetBusVolume {
190 bus: bus as u8,
191 volume: volumes[bus],
192 });
193 self.send(Command::SetBusEnabled {
194 bus: bus as u8,
195 enabled: enabled[bus],
196 });
197 }
198 }
199
200 fn wake_stream(&self) {
211 fence(Ordering::SeqCst);
212 if self.streaming.swap(true, Ordering::SeqCst) {
213 return;
214 }
215 self.parked.set(false);
216 self.publish_settings();
221 if let Some(sink) = self.sink.borrow().as_ref() {
222 sink.resume();
223 }
224 }
225
226 fn send(&self, command: Command) {
229 self.housekeeping();
230 if self.device_unavailable.get() {
231 return;
232 }
233 if !self.streaming.load(Ordering::Relaxed) && !survives_a_stopped_stream(&command) {
240 return;
241 }
242 if self.commands.borrow_mut().push(command).is_err() {
243 log::debug!("cranpose audio command queue is full; dropped one request");
244 }
245 }
246
247 fn housekeeping(&self) {
250 self.drain_retired();
251 self.park_if_idle();
252 }
253
254 fn drain_retired(&self) {
257 let mut retired = self.retired.borrow_mut();
258 while let Some(clip) = retired.pop() {
259 drop(clip);
260 }
261 }
262
263 fn park_if_idle(&self) {
271 if self.parked.get() || self.streaming.load(Ordering::SeqCst) {
272 return;
273 }
274 let sink = self.sink.borrow();
275 let Some(sink) = sink.as_ref() else {
276 return;
277 };
278 sink.park();
279 self.parked.set(true);
280 if self.streaming.load(Ordering::SeqCst) {
281 sink.resume();
286 self.parked.set(false);
287 }
288 }
289
290 fn allocate_voice(&self) -> u64 {
291 let next = self.next_voice.get().wrapping_add(1).max(1);
292 self.next_voice.set(next);
293 next
294 }
295
296 fn slot_of(id: SoundId) -> Option<u32> {
297 id.raw()
298 .checked_sub(1)
299 .filter(|slot| (*slot as usize) < MAX_CLIPS)
300 }
301
302 fn start_voice(&self, id: SoundId, params: PlaybackParams, looping: bool) -> VoiceId {
303 let Some(slot) = Self::slot_of(id) else {
304 return VoiceId::NONE;
305 };
306 if !self.ensure_running() {
307 return VoiceId::NONE;
308 }
309 let params = params.sanitized();
310 let (gain_left, gain_right) = params.gains();
311 let voice = self.allocate_voice();
312 self.send(Command::Play {
313 voice,
314 slot,
315 gain_left,
316 gain_right,
317 rate: params.rate,
318 bus: params.bus.index() as u8,
319 looping,
320 });
321 self.wake_stream();
323 VoiceId::from_raw(voice)
324 }
325}
326
327fn survives_a_stopped_stream(command: &Command) -> bool {
332 matches!(
333 command,
334 Command::LoadClip { .. } | Command::UnloadClip { .. } | Command::Play { .. }
335 )
336}
337
338impl Default for AudioEngine {
339 fn default() -> AudioEngine {
340 AudioEngine::new()
341 }
342}
343
344impl AudioPlayer for AudioEngine {
345 fn load_clip(&self, clip: AudioClip) -> Result<SoundId, AudioError> {
365 self.housekeeping();
366 let slot = self
367 .free_slots
368 .borrow_mut()
369 .pop()
370 .ok_or(AudioError::ClipTableFull {
371 capacity: MAX_CLIPS,
372 })?;
373 self.send(Command::LoadClip {
374 slot,
375 clip: ClipData {
376 samples: clip.shared_samples(),
377 channels: clip.channels().min(2) as u8,
378 sample_rate: clip.sample_rate(),
379 },
380 });
381 Ok(SoundId::from_raw(slot + 1))
382 }
383
384 fn unload(&self, id: SoundId) {
385 let Some(slot) = Self::slot_of(id) else {
386 return;
387 };
388 self.send(Command::UnloadClip { slot });
389 let mut free = self.free_slots.borrow_mut();
390 if !free.contains(&slot) {
391 free.push(slot);
392 }
393 }
394
395 fn play(&self, id: SoundId, params: PlaybackParams) {
396 self.start_voice(id, params, false);
397 }
398
399 fn play_loop(&self, id: SoundId, params: PlaybackParams) -> VoiceId {
400 self.start_voice(id, params, true)
401 }
402
403 fn stop(&self, id: SoundId) {
404 if let Some(slot) = Self::slot_of(id) {
405 self.send(Command::StopClip { slot });
406 }
407 }
408
409 fn stop_voice(&self, voice: VoiceId) {
410 if voice.is_valid() {
411 self.send(Command::StopVoice { voice: voice.raw() });
412 }
413 }
414
415 fn stop_all(&self) {
416 self.send(Command::StopAll);
417 }
418
419 fn set_voice_params(&self, voice: VoiceId, params: PlaybackParams) {
420 if !voice.is_valid() {
421 return;
422 }
423 let params = params.sanitized();
424 let (gain_left, gain_right) = params.gains();
425 self.send(Command::RetuneVoice {
426 voice: voice.raw(),
427 gain_left,
428 gain_right,
429 rate: params.rate,
430 });
431 }
432
433 fn set_master_volume(&self, volume: f32) {
434 let volume = if volume.is_finite() {
435 volume.clamp(0.0, 1.0)
436 } else {
437 1.0
438 };
439 self.master.set(volume);
440 self.send(Command::SetMaster(volume));
441 }
442
443 fn master_volume(&self) -> f32 {
444 self.master.get()
445 }
446
447 fn set_bus_volume(&self, bus: AudioBus, volume: f32) {
448 let volume = if volume.is_finite() {
449 volume.clamp(0.0, 1.0)
450 } else {
451 1.0
452 };
453 let mut volumes = self.bus_volume.get();
454 volumes[bus.index()] = volume;
455 self.bus_volume.set(volumes);
456 self.send(Command::SetBusVolume {
457 bus: bus.index() as u8,
458 volume,
459 });
460 }
461
462 fn bus_volume(&self, bus: AudioBus) -> f32 {
463 self.bus_volume.get()[bus.index()]
464 }
465
466 fn set_bus_enabled(&self, bus: AudioBus, enabled: bool) {
467 let mut flags = self.bus_enabled.get();
468 flags[bus.index()] = enabled;
469 self.bus_enabled.set(flags);
470 self.send(Command::SetBusEnabled {
471 bus: bus.index() as u8,
472 enabled,
473 });
474 }
475
476 fn bus_enabled(&self, bus: AudioBus) -> bool {
477 self.bus_enabled.get()[bus.index()]
478 }
479
480 fn suspend(&self) {
481 self.housekeeping();
482 if !self.streaming.load(Ordering::Relaxed) {
486 return;
487 }
488 if let Some(sink) = self.sink.borrow().as_ref() {
489 sink.suspend();
490 }
491 self.suspended.set(true);
492 }
493
494 fn resume(&self) {
495 if self.suspended.replace(false) {
496 if let Some(sink) = self.sink.borrow().as_ref() {
497 sink.resume();
498 }
499 }
500 self.housekeeping();
501 }
502
503 fn is_available(&self) -> bool {
504 backend::is_compiled() && !self.device_unavailable.get()
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::mixer::{Mixer, RenderStatus, IDLE_GRACE_SECONDS, MAX_VOICES};
512 use std::rc::Rc;
513
514 const RIG_SAMPLE_RATE: f32 = 48_000.0;
517 const RIG_BURST_FRAMES: usize = 128;
518
519 #[derive(Default)]
522 struct SinkLog {
523 suspended: Cell<bool>,
524 parks: Cell<u32>,
525 resumes: Cell<u32>,
526 }
527
528 struct TestSink {
530 log: Rc<SinkLog>,
531 }
532
533 impl AudioSink for TestSink {
534 fn suspend(&self) {
535 self.log.suspended.set(true);
536 }
537 fn resume(&self) {
538 self.log.suspended.set(false);
539 self.log.resumes.set(self.log.resumes.get() + 1);
540 }
541 fn park(&self) {
542 self.log.parks.set(self.log.parks.get() + 1);
543 }
544 }
545
546 struct Rig {
547 engine: AudioEngine,
548 mixer: Rc<RefCell<Option<Mixer>>>,
549 sink: Rc<SinkLog>,
550 }
551
552 impl Rig {
553 fn new() -> Rig {
554 Rig::with_failure(false)
555 }
556
557 fn with_failure(fail: bool) -> Rig {
558 let mixer: Rc<RefCell<Option<Mixer>>> = Rc::new(RefCell::new(None));
559 let sink = Rc::new(SinkLog::default());
560 let mixer_for_opener = Rc::clone(&mixer);
561 let sink_for_opener = Rc::clone(&sink);
562 let engine = AudioEngine::with_sink_opener(Box::new(move |seed| {
563 if fail {
564 return Err(AudioError::Backend("no device in this test".into()));
565 }
566 *mixer_for_opener.borrow_mut() = Some(Mixer::new(seed, RIG_SAMPLE_RATE, 2));
567 Ok(Box::new(TestSink {
568 log: Rc::clone(&sink_for_opener),
569 }))
570 }));
571 Rig {
572 engine,
573 mixer,
574 sink,
575 }
576 }
577
578 fn render(&self, frames: usize) -> Vec<f32> {
579 let mut out = vec![0.0f32; frames * 2];
580 self.mixer
581 .borrow_mut()
582 .as_mut()
583 .expect("device opened")
584 .render(&mut out);
585 out
586 }
587
588 fn run(&self, seconds: f32) -> RenderStatus {
592 let mut out = vec![0.0f32; RIG_BURST_FRAMES * 2];
593 let mut remaining = (RIG_SAMPLE_RATE * seconds) as usize;
594 let mut status = RenderStatus::Continue;
595 while remaining > 0 {
596 let take = remaining.min(RIG_BURST_FRAMES);
597 status = self
598 .mixer
599 .borrow_mut()
600 .as_mut()
601 .expect("device opened")
602 .render(&mut out[..take * 2]);
603 remaining -= take;
604 if status == RenderStatus::Idle {
605 break;
606 }
607 }
608 status
609 }
610
611 fn go_idle(&self) -> RenderStatus {
614 self.run(IDLE_GRACE_SECONDS + 0.1)
615 }
616
617 fn active_voices(&self) -> usize {
618 self.mixer
619 .borrow()
620 .as_ref()
621 .expect("device opened")
622 .active_voices()
623 }
624
625 fn device_opened(&self) -> bool {
626 self.mixer.borrow().is_some()
627 }
628 }
629
630 fn tone(frames: usize) -> AudioClip {
631 AudioClip::from_samples(vec![0.5; frames], 1, 48_000).expect("valid clip")
632 }
633
634 #[test]
635 fn loading_a_clip_does_not_open_the_device() {
636 let rig = Rig::new();
637 for _ in 0..8 {
638 assert!(rig.engine.load_clip(tone(64)).expect("loads").is_valid());
639 }
640 assert!(
641 !rig.engine.is_running() && !rig.device_opened(),
642 "a bank loaded on the way into a screen must not cost an audio thread"
643 );
644 assert!(!rig.engine.is_streaming());
645 }
646
647 #[test]
648 fn the_first_play_opens_the_device_and_drains_the_queued_loads() {
649 let rig = Rig::new();
650 let id = rig.engine.load_clip(tone(4096)).expect("loads");
651 assert!(!rig.engine.is_running());
652
653 rig.engine.play(id, PlaybackParams::new());
654 assert!(rig.engine.is_running());
655 assert!(rig.engine.is_streaming());
656
657 let out = rig.render(16);
658 assert!(out[0] > 0.0, "the load queued before the mixer existed ran");
659 assert_eq!(rig.active_voices(), 1);
660 }
661
662 #[test]
663 fn play_reaches_the_mixer() {
664 let rig = Rig::new();
665 let id = rig.engine.load_clip(tone(4096)).expect("loads");
666 rig.engine.play(id, PlaybackParams::new());
667 let out = rig.render(16);
668 assert!(out[0] > 0.0);
669 assert_eq!(rig.active_voices(), 1);
670 }
671
672 #[test]
673 fn rapid_retriggering_layers_voices() {
674 let rig = Rig::new();
675 let id = rig.engine.load_clip(tone(4096)).expect("loads");
676 for _ in 0..5 {
677 rig.engine.play(id, PlaybackParams::new().volume(0.1));
678 }
679 rig.render(8);
680 assert_eq!(rig.active_voices(), 5);
681 }
682
683 #[test]
684 fn voice_table_saturates_instead_of_growing() {
685 let rig = Rig::new();
686 let id = rig.engine.load_clip(tone(1 << 16)).expect("loads");
687 for _ in 0..(MAX_VOICES * 3) {
688 rig.engine.play(id, PlaybackParams::new().volume(0.01));
689 }
690 rig.render(8);
691 assert_eq!(rig.active_voices(), MAX_VOICES);
692 }
693
694 #[test]
695 fn looping_voice_can_be_stopped_by_handle() {
696 let rig = Rig::new();
697 let id = rig.engine.load_clip(tone(64)).expect("loads");
698 let voice = rig.engine.play_loop(id, PlaybackParams::new());
699 assert!(voice.is_valid());
700 rig.render(8);
701 assert_eq!(rig.active_voices(), 1);
702 rig.engine.stop_voice(voice);
703 rig.render(8);
704 assert_eq!(rig.active_voices(), 0);
705 }
706
707 #[test]
708 fn stop_silences_every_voice_of_a_clip() {
709 let rig = Rig::new();
710 let id = rig.engine.load_clip(tone(4096)).expect("loads");
711 rig.engine.play(id, PlaybackParams::new());
712 rig.engine.play(id, PlaybackParams::new());
713 rig.render(8);
714 assert_eq!(rig.active_voices(), 2);
715 rig.engine.stop(id);
716 rig.render(8);
717 assert_eq!(rig.active_voices(), 0);
718 }
719
720 #[test]
721 fn bus_toggles_survive_the_device_opening_later() {
722 let rig = Rig::new();
723 rig.engine.set_bus_enabled(AudioBus::Music, false);
724 rig.engine.set_master_volume(0.5);
725 assert!(!rig.engine.bus_enabled(AudioBus::Music));
726 assert_eq!(rig.engine.master_volume(), 0.5);
727
728 let id = rig.engine.load_clip(tone(4096)).expect("loads");
729 rig.engine
730 .play(id, PlaybackParams::new().bus(AudioBus::Music));
731 let out = rig.render(8);
732 assert!(
733 out.iter().all(|sample| *sample == 0.0),
734 "settings made before the device opened are applied to it"
735 );
736
737 rig.engine.set_bus_enabled(AudioBus::Music, true);
738 let out = rig.render(8);
739 assert!(out[0] > 0.0);
740 }
741
742 #[test]
743 fn unloading_returns_the_slot_and_stops_the_sound() {
744 let rig = Rig::new();
745 let id = rig.engine.load_clip(tone(4096)).expect("loads");
746 rig.engine.play(id, PlaybackParams::new());
747 rig.render(8);
748 assert_eq!(rig.active_voices(), 1);
749 rig.engine.unload(id);
750 rig.render(8);
751 assert_eq!(rig.active_voices(), 0);
752
753 let again = rig.engine.load_clip(tone(64)).expect("loads");
754 assert_eq!(again, id, "the slot is reused");
755 assert_eq!(rig.engine.leaked_clips(), 0);
756 }
757
758 #[test]
759 fn clip_table_reports_when_it_is_full() {
760 let rig = Rig::new();
761 for _ in 0..MAX_CLIPS {
762 rig.engine.load_clip(tone(2)).expect("loads");
763 }
764 assert_eq!(
765 rig.engine.load_clip(tone(2)),
766 Err(AudioError::ClipTableFull {
767 capacity: MAX_CLIPS
768 })
769 );
770 }
771
772 #[test]
773 fn invalid_handles_are_ignored() {
774 let rig = Rig::new();
775 rig.engine.load_clip(tone(64)).expect("loads");
776 rig.engine.play(SoundId::NONE, PlaybackParams::new());
777 assert_eq!(
778 rig.engine.play_loop(SoundId::NONE, PlaybackParams::new()),
779 VoiceId::NONE
780 );
781 rig.engine.stop(SoundId::NONE);
782 rig.engine.unload(SoundId::NONE);
783 rig.engine.stop_voice(VoiceId::NONE);
784 rig.engine
785 .set_voice_params(VoiceId::NONE, PlaybackParams::new());
786 assert!(
787 !rig.engine.is_running(),
788 "nothing here could make a sound, so nothing needed a device"
789 );
790 }
791
792 #[test]
793 fn retuning_a_voice_reaches_the_mixer() {
794 let rig = Rig::new();
795 let id = rig.engine.load_clip(tone(4096)).expect("loads");
796 let voice = rig.engine.play_loop(id, PlaybackParams::new());
797 let before = rig.render(8);
798 rig.engine
799 .set_voice_params(voice, PlaybackParams::new().volume(0.0));
800 let after = rig.render(8);
801 assert!(before[0] > 0.0);
802 assert_eq!(after[0], 0.0);
803 }
804
805 #[test]
806 fn suspend_and_resume_reach_the_sink() {
807 let rig = Rig::new();
808 let id = rig.engine.load_clip(tone(1 << 16)).expect("loads");
809 rig.engine.play_loop(id, PlaybackParams::new());
810 rig.engine.suspend();
811 assert!(rig.sink.suspended.get());
812 rig.engine.resume();
813 assert!(!rig.sink.suspended.get());
814 }
815
816 #[test]
817 fn suspending_a_stream_the_mixer_already_released_touches_nothing() {
818 let rig = Rig::new();
819 let id = rig.engine.load_clip(tone(64)).expect("loads");
820 rig.engine.play(id, PlaybackParams::new());
821 assert_eq!(rig.go_idle(), RenderStatus::Idle);
822
823 rig.engine.suspend();
824 assert!(
825 !rig.sink.suspended.get(),
826 "there is nothing left to pause, and pausing a stopped AAudio \
827 stream is an error"
828 );
829 let resumes = rig.sink.resumes.get();
830 rig.engine.resume();
831 assert_eq!(
832 rig.sink.resumes.get(),
833 resumes,
834 "coming back to the foreground on a silent screen must not start a \
835 device the app has no sound for"
836 );
837 assert!(!rig.engine.is_streaming());
838 }
839
840 #[test]
841 fn a_device_that_will_not_open_degrades_to_silence() {
842 let rig = Rig::with_failure(true);
843 let id = rig
847 .engine
848 .load_clip(tone(8))
849 .expect("loads without a device");
850 assert!(id.is_valid());
851 assert!(rig.engine.take_last_error().is_none());
852
853 rig.engine.play(id, PlaybackParams::new());
855 assert!(!rig.engine.is_available());
856 assert!(!rig.engine.is_running());
857 assert!(!rig.engine.is_streaming());
858 assert!(matches!(
859 rig.engine.take_last_error(),
860 Some(AudioError::Backend(_))
861 ));
862 assert!(rig.engine.take_last_error().is_none());
863
864 assert_eq!(
866 rig.engine.play_loop(id, PlaybackParams::new()),
867 VoiceId::NONE
868 );
869 rig.engine.stop_all();
870 rig.engine.set_master_volume(0.5);
871 rig.engine.suspend();
872 rig.engine.resume();
873 assert_eq!(rig.engine.underruns(), 0);
874 }
875
876 #[test]
877 fn wav_bytes_decode_through_the_default_load() {
878 let rig = Rig::new();
879 let mut bytes = Vec::new();
880 let data = [0i16, 16_384, -16_384, 0];
881 let pcm: Vec<u8> = data.iter().flat_map(|s| s.to_le_bytes()).collect();
882 bytes.extend_from_slice(b"RIFF");
883 bytes.extend_from_slice(&(36u32 + pcm.len() as u32).to_le_bytes());
884 bytes.extend_from_slice(b"WAVE");
885 bytes.extend_from_slice(b"fmt ");
886 bytes.extend_from_slice(&16u32.to_le_bytes());
887 bytes.extend_from_slice(&1u16.to_le_bytes());
888 bytes.extend_from_slice(&1u16.to_le_bytes());
889 bytes.extend_from_slice(&48_000u32.to_le_bytes());
890 bytes.extend_from_slice(&96_000u32.to_le_bytes());
891 bytes.extend_from_slice(&2u16.to_le_bytes());
892 bytes.extend_from_slice(&16u16.to_le_bytes());
893 bytes.extend_from_slice(b"data");
894 bytes.extend_from_slice(&(pcm.len() as u32).to_le_bytes());
895 bytes.extend_from_slice(&pcm);
896
897 let id = rig.engine.load(&bytes).expect("decodes and loads");
898 rig.engine.play(id, PlaybackParams::new());
899 let out = rig.render(4);
900 assert!(out.iter().any(|sample| *sample != 0.0));
901 assert!(rig.engine.load(b"not audio").is_err());
902 }
903
904 #[test]
905 fn a_screen_that_falls_silent_releases_the_device() {
906 let rig = Rig::new();
907 let id = rig.engine.load_clip(tone(64)).expect("loads");
908 rig.engine.play(id, PlaybackParams::new());
909 assert!(rig.engine.is_streaming());
910
911 assert_eq!(rig.go_idle(), RenderStatus::Idle);
912 assert!(!rig.engine.is_streaming());
913 assert!(
914 rig.engine.is_running(),
915 "the device object and its clips outlive the stream"
916 );
917
918 assert_eq!(rig.sink.parks.get(), 0, "nothing has called the engine yet");
921 rig.engine.stop_all();
922 assert_eq!(rig.sink.parks.get(), 1);
923 rig.engine.stop_all();
924 assert_eq!(rig.sink.parks.get(), 1, "released once, not once per call");
925 }
926
927 #[test]
928 fn a_play_after_going_idle_starts_the_stream_and_is_heard() {
929 let rig = Rig::new();
930 let id = rig.engine.load_clip(tone(4096)).expect("loads");
931 rig.engine.play(id, PlaybackParams::new());
932 assert_eq!(rig.go_idle(), RenderStatus::Idle);
933 rig.engine.stop_all();
934 let resumes = rig.sink.resumes.get();
935
936 rig.engine.play(id, PlaybackParams::new());
937 assert!(rig.engine.is_streaming());
938 assert_eq!(rig.sink.resumes.get(), resumes + 1, "the stream restarted");
939
940 let out = rig.render(16);
941 assert_eq!(rig.active_voices(), 1, "the voice really is running");
942 assert!(out[0] > 0.0);
943 assert_eq!(rig.run(0.5), RenderStatus::Continue, "and it stays running");
944 }
945
946 #[test]
947 fn settings_changed_while_the_device_is_released_survive_the_restart() {
948 let rig = Rig::new();
949 let id = rig.engine.load_clip(tone(4096)).expect("loads");
950 rig.engine.play(id, PlaybackParams::new());
951 assert_eq!(rig.go_idle(), RenderStatus::Idle);
952
953 rig.engine.set_master_volume(0.0);
956 rig.engine.play(id, PlaybackParams::new());
957 let out = rig.render(16);
958 assert!(
959 out.iter().all(|sample| *sample == 0.0),
960 "the master volume set while the device was released was applied"
961 );
962
963 rig.engine.set_master_volume(1.0);
964 let out = rig.render(16);
965 assert!(out[0] > 0.0);
966 }
967
968 #[test]
969 fn a_stopped_stream_does_not_let_chatter_push_out_a_clip_load() {
970 let rig = Rig::new();
971 let id = rig.engine.load_clip(tone(64)).expect("loads");
972 rig.engine.play(id, PlaybackParams::new());
973 assert_eq!(rig.go_idle(), RenderStatus::Idle);
974
975 for step in 0..(COMMAND_CAPACITY * 4) {
977 rig.engine
978 .set_master_volume(step as f32 / (COMMAND_CAPACITY * 4) as f32);
979 }
980 let late = rig.engine.load_clip(tone(4096)).expect("loads");
981
982 rig.engine.play(late, PlaybackParams::new());
983 let out = rig.render(16);
984 assert!(
985 out[0] > 0.0,
986 "the clip loaded after the chatter still reached the mixer"
987 );
988 }
989
990 #[test]
991 fn rapid_cues_inside_the_grace_period_never_stop_the_stream() {
992 let rig = Rig::new();
993 let id = rig.engine.load_clip(tone(48_000)).expect("loads");
994
995 for _ in 0..20 {
999 rig.engine.play(id, PlaybackParams::new());
1000 assert_eq!(rig.run(0.05), RenderStatus::Continue);
1001 rig.engine.stop(id);
1002 assert_eq!(
1003 rig.run(0.25),
1004 RenderStatus::Continue,
1005 "the stream must not be given up between taps"
1006 );
1007 }
1008 assert!(rig.engine.is_streaming());
1009 assert_eq!(rig.sink.parks.get(), 0);
1010 assert_eq!(
1011 rig.sink.resumes.get(),
1012 0,
1013 "no restart means the device was never given up"
1014 );
1015 }
1016
1017 #[test]
1018 fn a_cue_shorter_than_one_callback_still_restarts_the_grace_period() {
1019 let rig = Rig::new();
1020 let id = rig.engine.load_clip(tone(96)).expect("loads");
1023 rig.engine.play(id, PlaybackParams::new());
1024
1025 assert_eq!(rig.run(IDLE_GRACE_SECONDS - 0.1), RenderStatus::Continue);
1026 rig.engine.play(id, PlaybackParams::new());
1027 assert_eq!(
1028 rig.run(IDLE_GRACE_SECONDS - 0.1),
1029 RenderStatus::Continue,
1030 "the second cue restarted the clock rather than being missed"
1031 );
1032 assert_eq!(rig.run(0.2), RenderStatus::Idle);
1033 }
1034
1035 #[test]
1036 fn a_looping_voice_keeps_the_device_for_as_long_as_it_plays() {
1037 let rig = Rig::new();
1038 let id = rig.engine.load_clip(tone(4096)).expect("loads");
1039 let voice = rig.engine.play_loop(id, PlaybackParams::new());
1040
1041 assert_eq!(rig.run(IDLE_GRACE_SECONDS * 2.0), RenderStatus::Continue);
1042 assert!(rig.engine.is_streaming());
1043
1044 rig.engine.stop_voice(voice);
1045 assert_eq!(rig.go_idle(), RenderStatus::Idle);
1046 assert!(!rig.engine.is_streaming());
1047 }
1048}