Skip to main content

cranpose_audio/
engine.rs

1//! The UI-thread half of the engine: the [`AudioPlayer`] an app actually calls.
2//!
3//! Every method here is bounded work on the calling thread — decode, a little
4//! arithmetic, one queue push — and never waits on the audio thread. Handle
5//! bookkeeping (clip slots, voice ids) lives here so the mixer never has to
6//! search for a free identifier inside its real-time budget.
7
8use 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
18/// How many commands can be in flight. The audio thread drains the whole queue
19/// every callback (a few milliseconds), so this is deep enough that a frame
20/// firing every cue it owns at once still fits.
21const COMMAND_CAPACITY: usize = 512;
22
23/// Retired clips are produced at most one per command, and the UI thread drains
24/// them on every call, so matching the command depth makes overflow impossible
25/// short of an app that stops calling the engine entirely.
26const RETIRE_CAPACITY: usize = COMMAND_CAPACITY;
27
28type SinkOpener = Box<dyn Fn(MixerSeed) -> Result<Box<dyn AudioSink>, AudioError>>;
29
30/// A mixing audio player backed by a platform output device.
31///
32/// The device is opened lazily, on the first call that actually makes sound,
33/// so an app that installs the engine but never plays anything costs no audio
34/// thread and no battery. Loading clips is not such a call: a clip load is a
35/// queue push, and the queue exists from construction, so a title screen can
36/// have its whole sound bank resident with the output device still shut.
37///
38/// The device does not stay open either. When nothing has sounded for
39/// [`IDLE_GRACE_SECONDS`](crate::mixer::IDLE_GRACE_SECONDS) the mixer gives the
40/// stream up and the next [`play`](AudioPlayer::play) starts it again, so a
41/// silent screen costs nothing however it was reached.
42pub 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    /// Whether the output stream is producing audio. Written by both threads:
57    /// the mixer clears it when it goes idle, this side sets it when it starts
58    /// the stream. See [`AudioEngine::wake_stream`].
59    streaming: Arc<AtomicBool>,
60    /// Whether this side has already released the device for the current idle
61    /// stretch, so it is released once rather than on every call the app makes
62    /// while nothing is playing.
63    parked: Cell<bool>,
64    leaked_clips: Arc<AtomicU32>,
65    underruns: Arc<AtomicU32>,
66}
67
68impl AudioEngine {
69    /// Creates an engine that opens the platform output device on first use.
70    pub fn new() -> AudioEngine {
71        AudioEngine::with_sink_opener(Box::new(backend::open))
72    }
73
74    /// Creates an engine over a caller-supplied device opener. The platform
75    /// backends and the crate's own tests both go through this.
76    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    /// The most recent failure, if the device refused to open or a call was
110    /// rejected. Cleared by reading it.
111    pub fn take_last_error(&self) -> Option<AudioError> {
112        self.last_error.borrow_mut().take()
113    }
114
115    /// How many clips the mixer could not hand back for dropping. Any value
116    /// above zero means the app stopped calling the engine while clips were
117    /// being replaced; it is reported rather than hidden.
118    pub fn leaked_clips(&self) -> u32 {
119        self.leaked_clips.load(Ordering::Relaxed)
120    }
121
122    /// How many times the device asked for a buffer the mixer could not fill.
123    pub fn underruns(&self) -> u32 {
124        self.underruns.load(Ordering::Relaxed)
125    }
126
127    /// Whether the output device is open.
128    ///
129    /// Open is not the same as running: a device that has been open for a while
130    /// spends most of a quiet screen stopped. See
131    /// [`is_streaming`](AudioEngine::is_streaming).
132    pub fn is_running(&self) -> bool {
133        self.sink.borrow().is_some()
134    }
135
136    /// Whether the output stream is live rather than given up as idle.
137    ///
138    /// `false` with [`is_running`](AudioEngine::is_running) `true` is the
139    /// steady state of a silent screen: the device object and every loaded clip
140    /// are still there, the stream is not, and the next play starts it again.
141    /// A stream paused by [`suspend`](AudioPlayer::suspend) still counts as
142    /// live — the app took it away, not the mixer, and it comes back on
143    /// [`resume`](AudioPlayer::resume).
144    pub fn is_streaming(&self) -> bool {
145        self.streaming.load(Ordering::Relaxed)
146    }
147
148    /// Opens the output device if it is not open yet. Returns whether a device
149    /// is running afterwards.
150    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        // Set before the opener runs, not after: the backend starts the stream
162        // inside it, and the first callback can land before it returns.
163        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    /// Sends the engine's mix settings to a mixer that does not have them: a
181    /// fresh one, which starts from its own defaults, or one that was stopped
182    /// while the app changed a volume (see [`AudioEngine::send`]).
183    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    /// Starts a stream the mixer gave up, once a command is already queued for
201    /// it.
202    ///
203    /// The order is the whole point and is not an accident of layout: the
204    /// caller pushes first and this reads the flag afterwards, while the mixer
205    /// publishes the stop first and re-reads the queue afterwards. The fence
206    /// puts both pairs into one order, so at least one side always sees the
207    /// other's work — see `Mixer::settle` for the matching half. Reading the
208    /// flag before the push instead would let a command land in a queue that
209    /// nothing will ever drain.
210    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        // Anything the app set while the stream was stopped was kept in this
217        // struct rather than queued, so the mixer is told about it now — and
218        // before the stream starts, so the settings and the sound that woke it
219        // arrive in the same drain rather than a buffer apart.
220        self.publish_settings();
221        if let Some(sink) = self.sink.borrow().as_ref() {
222            sink.resume();
223        }
224    }
225
226    /// Enqueues one command, dropping it if the queue is full rather than
227    /// blocking the UI thread on the audio thread.
228    fn send(&self, command: Command) {
229        self.housekeeping();
230        if self.device_unavailable.get() {
231            return;
232        }
233        // With the stream stopped nothing drains the queue, so only commands
234        // the mixer must not miss are worth a slot in it. The mixer only stops
235        // with every voice silent, which makes anything acting on a voice a
236        // no-op, and the gains live in this struct and are re-sent by
237        // `wake_stream`. Queueing the rest would let a volume slider dragged on
238        // a silent screen fill the ring and push out a real clip load.
239        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    /// What every entry point does first: drop clips the mixer handed back, and
248    /// release a device the mixer has reported idle.
249    fn housekeeping(&self) {
250        self.drain_retired();
251        self.park_if_idle();
252    }
253
254    /// Drops clips the mixer handed back. Called from every engine entry point,
255    /// which is what keeps the return ring from ever filling.
256    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    /// Releases the device once the mixer has reported it idle.
264    ///
265    /// This is the UI-thread half of stopping, and it is best-effort by nature:
266    /// it can only run when the app calls the engine. On Android that is a
267    /// backstop — the AAudio callback returns `Stop` and the stream winds
268    /// itself down — but on cpal, whose callback cannot stop its own stream, it
269    /// is the only thing that does the job.
270    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            // The mixer changed its mind in the callback that raced this one:
282            // it re-checks the queue after publishing the stop and carries on
283            // if work had arrived. Undo the release rather than leave a live
284            // mixer behind a dead device.
285            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        // Only after the command is queued; `wake_stream` explains why.
322        self.wake_stream();
323        VoiceId::from_raw(voice)
324    }
325}
326
327/// Whether a command still means anything to a mixer whose stream is stopped.
328///
329/// `Play` is on the list because [`AudioEngine::start_voice`] has to queue it
330/// before it starts the stream, not after.
331fn 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    /// Takes a clip table slot and queues the clip for the mixer.
346    ///
347    /// This deliberately does not open the output device. Loading a bank of
348    /// cues is what an app does on the way into a screen, long before it plays
349    /// anything, and opening the device there was costing a silent title screen
350    /// an audio thread and an always-on DSP rail for as long as it was on
351    /// display. The command ring outlives every mixer, so the load waits in it
352    /// and is drained by the first mixer to start.
353    ///
354    /// The consequence is a narrower error contract than this used to have.
355    /// The only failure it can still report is the one it can determine here,
356    /// [`AudioError::ClipTableFull`]; a device that is missing or refuses to
357    /// open is no longer a load-time error, because finding that out means
358    /// opening it. Callers that need to know ask
359    /// [`is_available`](AudioPlayer::is_available), and the failure itself is
360    /// available from [`take_last_error`](AudioEngine::take_last_error) once a
361    /// play has tried. That also makes this agree with `NoopAudioPlayer`, which
362    /// hands out real [`SoundId`]s on a machine with no audio at all so app
363    /// logic does not have to branch.
364    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        // A stream the mixer already gave up needs no pausing, and pausing a
483        // stopped stream is an error on AAudio. Not recording a suspend here is
484        // what keeps `resume` from starting a device the app has no sound for.
485        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    /// The device rate the rig's mixer runs at, and the burst size its
515    /// callbacks arrive in. 128 frames at 48 kHz is a realistic AAudio burst.
516    const RIG_SAMPLE_RATE: f32 = 48_000.0;
517    const RIG_BURST_FRAMES: usize = 128;
518
519    /// What the engine did to the sink, so a test can tell "still running" from
520    /// "released and started again".
521    #[derive(Default)]
522    struct SinkLog {
523        suspended: Cell<bool>,
524        parks: Cell<u32>,
525        resumes: Cell<u32>,
526    }
527
528    /// A sink that keeps the mixer where the test can drive it by hand.
529    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        /// Runs the mixer for `seconds` in device-sized bursts, stopping early
589        /// the moment it asks for the stream to be released — which is what a
590        /// real device does, and what makes "did it stop?" observable.
591        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        /// Runs past the idle grace period, the way a screen nobody is touching
612        /// does.
613        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        // Loading no longer touches the device, so it no longer discovers that
844        // there isn't one; it hands back a real handle exactly as the no-op
845        // player would.
846        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        // The first play is what tries to open the device, and what reports it.
854        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        // Every later call is a no-op instead of a panic.
865        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        // The backend half. AAudio's callback returning `Stop` is only part of
919        // it; this is the call that gives the route back.
920        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        // A settings screen with the stream stopped: nothing drains the queue,
954        // so these are kept here rather than queued, and re-sent on restart.
955        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        // A volume slider dragged for far longer than the command ring is deep.
976        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        // A player tapping through a menu for three times the grace period: a
996        // cue every 300 ms, each one cut short after 50 ms the way a UI sound
997        // is when the next screen arrives.
998        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        // Two milliseconds: shorter than the burst the device asks for, so the
1021        // voice is gone again by the time the callback returns.
1022        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}