Skip to main content

shuyi_platform/audio/
player.rs

1//! [`Player`]: the rules of playing a frame's audio, shared by every backend
2//! (game ADR-0026, ADR-0063), behind the small [`Backend`] trait a backend
3//! writes: the sound and voice requests and the music commands
4//! [`App`](crate::App) hands the sound device ([`AudioFrame`]).
5//!
6//! The player keeps the cues' volumes, picks a variant, plays at most two
7//! tracks in a crossfade, their gains moving opposite ways (ADR-0054), and
8//! plays the voice clips (game ADR-0046). [`PlayerDevice`] is the whole of a
9//! backend's [`Audio`] over it.
10//!
11//! A sound started with a handle (`SoundRequest::Start`) is one playback
12//! the player keeps by its id, so that one copy stops alone and its volume
13//! follows the requests and the player's sound volume.
14//!
15//! **Holding a sound** (ADR-0056). A sound paused under a menu
16//! (`SoundRequest::PauseSounds`), and everything sounding while the system
17//! suspends the game ([`Player::suspend`]: the sounds started with a handle,
18//! the voice and the music), is held with [`Backend::pause`]. A backend that
19//! can't pause says [`Unsupported`], and the player mutes instead: the sound
20//! runs on silently, so a track's clock runs too, and comes back at the
21//! volume it would have had. A paused track's clock holds.
22//!
23//! It also knows when each track really started ([`Player::music_playing`],
24//! game ADR-0037): a track asked for only sounds once its file has loaded, and
25//! screens that keep time with the music need the real moment.
26
27pub(crate) mod voice;
28
29use alloc::collections::{BTreeMap, BTreeSet};
30use alloc::format;
31use core::fmt;
32use core::future::Future;
33
34use shuyi_assets::audio::{AudioManifest, Files};
35use shuyi_ui::audio::{MusicCommand, SharedSamples, SoundId};
36
37use self::voice::Voices;
38use super::{Audio, AudioFrame, SoundRequest};
39use crate::alloc_prelude::*;
40use crate::platform::Millis;
41
42/// A playback rate of 1.0, as [`Backend::set_rate`] takes it: 16.16 fixed
43/// point, so a sound plays as recorded.
44pub const RATE_ONE: u32 = 1 << 16;
45
46/// What a [`Backend`]'s provided method says on a backend without the
47/// feature: the player then carries on with the feature's fallback.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Unsupported;
50
51impl fmt::Display for Unsupported {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str("this backend doesn't have this feature")
54    }
55}
56
57impl core::error::Error for Unsupported {}
58
59/// What playing audio needs from the platform: all a backend writes for the
60/// sound of a game (ADR-0063).
61///
62/// A feature some backends lack is a provided method, whose default does
63/// nothing and says [`Unsupported`], so that adding one breaks no backend.
64pub trait Backend {
65    /// A decoded sound or track.
66    type Sound;
67    /// A music track or voice clip being loaded.
68    type Loading;
69    /// One playback of a sound, which stops alone.
70    type Playing;
71
72    /// Decodes a sound file's bytes.
73    fn load_sound(&mut self, bytes: &[u8]) -> impl Future<Output = Result<Self::Sound, String>>;
74    /// Starts loading and decoding the music or voice file at `path`.
75    fn load_music(&mut self, path: &str) -> Self::Loading;
76    /// The track, once `loading` has finished (called once per frame).
77    fn poll_music(&mut self, loading: &mut Self::Loading) -> Option<Result<Self::Sound, String>>;
78    /// How long `sound` plays, in seconds, if that is known: it is for an
79    /// OGG file loaded with [`load_music`](Self::load_music).
80    fn length(&self, sound: &Self::Sound) -> Option<f64>;
81    /// Plays `sound` from the start at `volume` (0–1): this playback.
82    fn play(&mut self, sound: &Self::Sound, volume: f32, looped: bool) -> Self::Playing;
83    /// Changes the volume of `sound` wherever it plays.
84    fn set_volume(&mut self, sound: &Self::Sound, volume: f32);
85    /// Stops `sound`.
86    fn stop(&mut self, sound: &Self::Sound);
87    /// Changes the volume of `playing` alone.
88    fn set_playing_volume(&mut self, playing: &Self::Playing, volume: f32);
89    /// Stops `playing` alone: other copies of its sound play on.
90    fn stop_playing(&mut self, playing: Self::Playing);
91
92    /// Readies the sound file `file` of the manifest. By default it reads
93    /// the file with `files` and decodes it with
94    /// [`load_sound`](Self::load_sound); a device that loads a sound from
95    /// its own path says so here.
96    fn load_sound_file(
97        &mut self,
98        file: &str,
99        files: &Files<'_>,
100    ) -> impl Future<Output = Result<Self::Sound, String>> {
101        let bytes = (files.sound)(file);
102        async move {
103            match bytes {
104                Some(bytes) => self.load_sound(&bytes).await,
105                None => Err("not in the asset bundle".to_owned()),
106            }
107        }
108    }
109
110    /// Stops `playing` so that [`resume`](Self::resume) goes on from where
111    /// it was. Without it the player mutes the sound, which runs on silently.
112    fn pause(&mut self, playing: &Self::Playing) -> Result<(), Unsupported> {
113        let _ = playing;
114        Err(Unsupported)
115    }
116
117    /// Goes on with `playing` from where [`pause`](Self::pause) stopped it.
118    fn resume(&mut self, playing: &Self::Playing) -> Result<(), Unsupported> {
119        let _ = playing;
120        Err(Unsupported)
121    }
122
123    /// Sets how fast `sound` plays from its next [`play`](Self::play), in
124    /// 16.16 fixed point ([`RATE_ONE`] plays it as recorded). Without it the
125    /// sound plays at 1.0.
126    fn set_rate(&mut self, sound: &Self::Sound, rate: u32) -> Result<(), Unsupported> {
127        let _ = (sound, rate);
128        Err(Unsupported)
129    }
130
131    /// Readies `samples`, samples a game made at run time, as a sound to play
132    /// like one loaded from a file. By default it can't, and the cue plays
133    /// nothing, with a warning in the log.
134    fn add_sound(&mut self, samples: &SharedSamples) -> Result<Self::Sound, String> {
135        let _ = samples;
136        Err(Unsupported.to_string())
137    }
138
139    /// Lets go of `sound`, which [`add_sound`](Self::add_sound) made: it
140    /// stops wherever it plays and its memory is freed. By default it is
141    /// stopped and dropped.
142    fn remove_sound(&mut self, sound: Self::Sound) {
143        self.stop(&sound);
144    }
145
146    /// The system suspends the game (`on`) or has resumed it: a backend whose output
147    /// can stop stops it here, after the player has held what sounds.
148    fn hold_output(&mut self, on: bool) {
149        let _ = on;
150    }
151
152    /// Says a problem of the player's in the backend's log, by default
153    /// nowhere.
154    fn warn(&mut self, _line: &str) {}
155}
156
157/// A [`Backend`]'s provided feature, each warned of once.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
159enum Feature {
160    Pause,
161    Rate,
162}
163
164impl Feature {
165    /// What the log says of a backend without it.
166    const fn warning(self) -> &'static str {
167        match self {
168            Self::Pause => "this backend can't pause sounds: they are muted instead",
169            Self::Rate => "this backend can't change a sound's rate: it plays as recorded",
170        }
171    }
172}
173
174/// What the player knows of the backend's provided methods.
175#[derive(Debug, Default)]
176pub(crate) struct Provided {
177    /// The features already warned of.
178    warned: BTreeSet<Feature>,
179    /// Whether the backend can't pause: a held sound is muted.
180    mutes: bool,
181}
182
183impl Provided {
184    /// Whether `result`, of a call to a provided method for `feature`, is
185    /// done: the first [`Unsupported`] of each feature is a warning.
186    fn call(
187        &mut self,
188        feature: Feature,
189        result: Result<(), Unsupported>,
190        warnings: &mut Vec<String>,
191    ) -> bool {
192        if result.is_ok() {
193            return true;
194        }
195        if self.warned.insert(feature) {
196            warnings.push(feature.warning().to_owned());
197        }
198        self.mutes |= feature == Feature::Pause;
199        false
200    }
201
202    /// Pauses `playing`, or resumes it if not `on`: whether the backend did.
203    pub(crate) fn hold<B: Backend>(
204        &mut self,
205        backend: &mut B,
206        playing: &B::Playing,
207        on: bool,
208        warnings: &mut Vec<String>,
209    ) -> bool {
210        if self.mutes {
211            return false;
212        }
213        let result = if on {
214            backend.pause(playing)
215        } else {
216            backend.resume(playing)
217        };
218        self.call(Feature::Pause, result, warnings)
219    }
220}
221
222/// The audio player: owns the decoded sounds and the music tracks.
223pub struct Player<B: Backend> {
224    manifest: AudioManifest,
225    /// Every sound cue's decoded variants.
226    sounds: BTreeMap<String, Vec<B::Sound>>,
227    /// The sounds the game added at run time, by cue.
228    added: BTreeMap<String, B::Sound>,
229    /// Music tracks loading or loaded, by cue.
230    tracks: BTreeMap<String, Track<B>>,
231    /// Loads stopped before they finished: polled until they do, then
232    /// dropped, so a finished decode doesn't stay in memory.
233    abandoned: Vec<B::Loading>,
234    /// Where music files are, e.g. `music` or `C:/Games/some-game/music`.
235    music_dir: String,
236    /// The voice clips (game ADR-0046).
237    voices: Voices<B>,
238    rng: VariantRng,
239    /// Problems to log (missing files, decode errors).
240    warnings: Vec<String>,
241    /// The player's music and sound volumes (the Options screen's), 0–1:
242    /// multipliers on every cue's own volume.
243    music_volume: f32,
244    sound_volume: f32,
245    /// The sounds started with a handle and not stopped.
246    handled: BTreeMap<SoundId, Handled<B>>,
247    /// The voice volume asked for, 0–1, kept while suspended.
248    voice_volume: f32,
249    /// Whether the system suspended the game: everything is held.
250    suspended: bool,
251    /// What the backend's provided methods have said.
252    provided: Provided,
253    /// The clock at the last [`play`](Self::play).
254    now: Option<f64>,
255}
256
257/// A sound started with a handle: its playback and what its volume is
258/// made of.
259struct Handled<B: Backend> {
260    playing: B::Playing,
261    /// The cue's own volume, 0–1.
262    cue_volume: f32,
263    /// The volume its requests gave, 0–1.
264    volume: f32,
265    /// Whether `PauseSounds` paused it, until `ResumeSounds`.
266    paused: bool,
267}
268
269impl<B: Backend> Handled<B> {
270    /// What it plays at, with the player's sound volume `sound`: nothing
271    /// while the game is `suspended`, or while it is paused on a backend
272    /// that `mutes`.
273    fn level(&self, sound: f32, suspended: bool, mutes: bool) -> f32 {
274        if suspended || (self.paused && mutes) {
275            0.0
276        } else {
277            self.cue_volume * self.volume * sound
278        }
279    }
280}
281
282struct Track<B: Backend> {
283    state: TrackState<B>,
284    /// Asked to play (it starts as soon as it's loaded).
285    playing: bool,
286    /// Its playback, once the backend was told to play it.
287    playback: Option<B::Playing>,
288    /// When the backend was told to play it, in the seconds of the clock
289    /// given to [`Player::play`]; `None` until then.
290    started_at: Option<f64>,
291    /// When the backend paused it, until the frame after its resume: its
292    /// clock holds.
293    held_at: Option<f64>,
294    /// Fade gain, 0–1.
295    gain: f32,
296    /// The cue's own volume, 0–1.
297    volume: f32,
298    looped: bool,
299}
300
301enum TrackState<B: Backend> {
302    Loading(B::Loading),
303    Ready(B::Sound),
304    Failed,
305}
306
307impl<B: Backend> Player<B> {
308    /// Readies every sound in `manifest` (reading its files through `files`,
309    /// the game's own sounds). A sound that fails is left out with a warning.
310    /// Music tracks are looked for in `music_dir` and voice clips in
311    /// `voice_dir`. `seed` seeds the variant picker.
312    pub async fn load(
313        backend: &mut B,
314        manifest: AudioManifest,
315        files: &Files<'_>,
316        music_dir: String,
317        voice_dir: String,
318        seed: u64,
319    ) -> Self {
320        let mut warnings = Vec::new();
321        let mut sounds = BTreeMap::new();
322        for (cue, sound) in &manifest.sounds {
323            let mut variants = Vec::new();
324            for file in &sound.files {
325                match backend.load_sound_file(file, files).await {
326                    Ok(s) => variants.push(s),
327                    Err(e) => {
328                        let shown = (files.sound_shown)(file);
329                        warnings.push(format!("sound \"{cue}\": {shown}: {e}"));
330                    }
331                }
332            }
333            sounds.insert(cue.clone(), variants);
334        }
335        Self {
336            manifest,
337            sounds,
338            added: BTreeMap::new(),
339            tracks: BTreeMap::new(),
340            abandoned: Vec::new(),
341            music_dir,
342            voices: Voices::new(voice_dir),
343            rng: VariantRng::new(seed),
344            warnings,
345            music_volume: 1.0,
346            sound_volume: 1.0,
347            handled: BTreeMap::new(),
348            voice_volume: 1.0,
349            suspended: false,
350            provided: Provided::default(),
351            now: None,
352        }
353    }
354
355    /// Sets the player's volumes (the Options screen's, 0–1): every sound
356    /// and track plays at its own volume times these. A change reaches the
357    /// tracks and the sounds started with a handle already playing.
358    pub fn set_volumes(&mut self, backend: &mut B, music: f32, sound: f32) {
359        if (sound - self.sound_volume).abs() > f32::EPSILON {
360            self.sound_volume = sound;
361            self.set_handled_volumes(backend);
362        }
363        if (music - self.music_volume).abs() <= f32::EPSILON {
364            return;
365        }
366        self.music_volume = music;
367        self.set_track_volumes(backend);
368    }
369
370    /// The player's music volume as the tracks hear it: nothing while
371    /// suspended.
372    fn music_level(&self) -> f32 {
373        if self.suspended {
374            0.0
375        } else {
376            self.music_volume
377        }
378    }
379
380    /// Sets every track playing to its volume times its gain and the
381    /// music level.
382    fn set_track_volumes(&self, backend: &mut B) {
383        let music = self.music_level();
384        for track in self.tracks.values() {
385            if let (true, TrackState::Ready(sound)) = (track.playing, &track.state) {
386                backend.set_volume(sound, track.volume * track.gain * music);
387            }
388        }
389    }
390
391    /// The level of `handled`, as things are.
392    fn level_of(&self, handled: &Handled<B>) -> f32 {
393        handled.level(self.sound_volume, self.suspended, self.provided.mutes)
394    }
395
396    /// Sets every sound started with a handle to its level.
397    fn set_handled_volumes(&self, backend: &mut B) {
398        for handled in self.handled.values() {
399            backend.set_playing_volume(&handled.playing, self.level_of(handled));
400        }
401    }
402
403    /// Pauses everything sounding, or resumes it if not `on`, while the
404    /// system holds the game: the sounds started with a handle (those a
405    /// `PauseSounds` holds stay as they are), the tracks and the voice.
406    fn hold_all(&mut self, backend: &mut B, on: bool) {
407        let (provided, warnings) = (&mut self.provided, &mut self.warnings);
408        for handled in self.handled.values().filter(|handled| !handled.paused) {
409            provided.hold(backend, &handled.playing, on, warnings);
410        }
411        for track in self.tracks.values_mut() {
412            if let (true, Some(playback)) = (track.playing, &track.playback)
413                && provided.hold(backend, playback, on, warnings)
414                && on
415            {
416                track.held_at = self.now;
417            }
418        }
419        self.voices.hold(backend, on, self.now, provided, warnings);
420    }
421
422    /// Holds everything sounding while the system suspends the game: the
423    /// sounds started with a handle, the voice and the music. It pauses them,
424    /// or mutes them on a backend that can't. Sounds played once finish. A
425    /// second call does nothing.
426    pub fn suspend(&mut self, backend: &mut B) {
427        if self.suspended {
428            return;
429        }
430        self.suspended = true;
431        self.hold_all(backend, true);
432        if self.provided.mutes {
433            self.set_handled_volumes(backend);
434            self.set_track_volumes(backend);
435            self.voices.set_volume(backend, 0.0);
436        }
437    }
438
439    /// Brings back what [`suspend`](Self::suspend) held, at the volumes
440    /// asked for meanwhile; a sound paused under a menu stays held. A call
441    /// while not suspended does nothing.
442    pub fn resume(&mut self, backend: &mut B) {
443        if !self.suspended {
444            return;
445        }
446        self.suspended = false;
447        self.hold_all(backend, false);
448        self.set_handled_volumes(backend);
449        self.set_track_volumes(backend);
450        self.voices.set_volume(backend, self.voice_volume);
451    }
452
453    /// Plays one frame's sound and voice `requests`, applies the `music`
454    /// commands and checks on tracks and clips still loading. `now` is the
455    /// time in seconds on a clock that keeps running while the game isn't
456    /// drawn (game ADR-0037): a track that starts in this call started at `now`.
457    pub fn play(
458        &mut self,
459        backend: &mut B,
460        requests: &[SoundRequest],
461        music: &[MusicCommand],
462        now: f64,
463    ) {
464        self.now = Some(now);
465        if !self.suspended {
466            self.release_clocks(now);
467        }
468        for request in requests {
469            self.sound(backend, request);
470            self.voices
471                .request(backend, request, now, &mut self.abandoned);
472        }
473        for command in music {
474            self.music(backend, command, now);
475        }
476        self.poll(backend, now);
477    }
478
479    /// After a resume, moves the clocks that held on by the time away.
480    fn release_clocks(&mut self, now: f64) {
481        for track in self.tracks.values_mut() {
482            if let (Some(at), Some(started_at)) = (track.held_at.take(), &mut track.started_at) {
483                *started_at += (now - at).max(0.0);
484            }
485        }
486        self.voices.release_clock(now);
487    }
488
489    /// The music sounding at `now` (on [`play`](Self::play)'s clock): its
490    /// cue and the seconds since it started. `None` in silence, which
491    /// includes a track still loading or whose file failed. Of two tracks
492    /// (a crossfade) it is the newest that sounds: the old one until the
493    /// new one has loaded. A track played once is still reported after it has ended
494    /// (not every device says when it does; `ui` knows its length). A paused
495    /// track's seconds hold; a muted one's run on.
496    pub fn music_playing(&self, now: f64) -> Option<(&str, f64)> {
497        self.tracks
498            .iter()
499            .filter_map(|(cue, track)| Some((cue.as_str(), track.started_at?, track.held_at)))
500            .max_by(|a, b| a.1.total_cmp(&b.1))
501            .map(|(cue, started_at, held_at)| (cue, (held_at.unwrap_or(now) - started_at).max(0.0)))
502    }
503
504    /// The file of the voice clip sounding at `now` (on
505    /// [`play`](Self::play)'s clock), or asked for and still loading and
506    /// not yet late (0.3 s after it was asked for). `None` once it has ended, or was
507    /// stopped, failed or given up on. A paused clip still sounds; a clip
508    /// whose length the backend doesn't know sounds until it is stopped.
509    pub fn voice_playing(&self, now: f64) -> Option<&str> {
510        self.voices.sounding(now)
511    }
512
513    /// Applies one sound request (the voice's are [`Voices`]').
514    fn sound(&mut self, backend: &mut B, request: &SoundRequest) {
515        match request {
516            SoundRequest::Sound { cue, volume } => {
517                // A looped cue played this way (a bug the debug build
518                // catches) plays once: nothing could stop it.
519                self.play_sound(backend, cue, *volume, false);
520            }
521            SoundRequest::Start {
522                id, cue, volume, ..
523            } => {
524                self.start_sound(backend, *id, cue, *volume);
525            }
526            SoundRequest::Stop { id } => {
527                if let Some(handled) = self.handled.remove(id) {
528                    backend.stop_playing(handled.playing);
529                }
530            }
531            SoundRequest::Volume { id, volume } => {
532                if let Some(handled) = self.handled.get_mut(id) {
533                    handled.volume = *volume;
534                    let (sound, suspended) = (self.sound_volume, self.suspended);
535                    let level = handled.level(sound, suspended, self.provided.mutes);
536                    backend.set_playing_volume(&handled.playing, level);
537                }
538            }
539            SoundRequest::PauseSounds => self.pause_handled(backend, true),
540            SoundRequest::ResumeSounds => self.pause_handled(backend, false),
541            SoundRequest::Add { cue, samples, .. } => self.add_sound(backend, cue, samples),
542            SoundRequest::Remove { cue } => {
543                if let Some(sound) = self.added.remove(cue) {
544                    backend.remove_sound(sound);
545                }
546            }
547            _ => {}
548        }
549    }
550
551    /// Makes `samples` the sound cue `cue`, in place of one added before.
552    fn add_sound(&mut self, backend: &mut B, cue: &str, samples: &SharedSamples) {
553        if let Some(old) = self.added.remove(cue) {
554            backend.remove_sound(old);
555        }
556        match backend.add_sound(samples) {
557            Ok(sound) => {
558                self.added.insert(cue.to_owned(), sound);
559            }
560            Err(e) => self
561                .warnings
562                .push(format!("sound \"{cue}\": can't be added: {e}")),
563        }
564    }
565
566    /// Plays a variant of sound `cue` at `volume` times its own and the
567    /// player's sound volume, looped or not: the playback, unless the cue
568    /// is unknown or none of its files loaded.
569    fn play_sound(
570        &mut self,
571        backend: &mut B,
572        cue: &str,
573        volume: f32,
574        looped: bool,
575    ) -> Option<B::Playing> {
576        let manifest = self.manifest.sounds.get(cue);
577        let (variants, own) = match (manifest, self.sounds.get(cue), self.added.get(cue)) {
578            (Some(def), Some(variants), _) => (variants.as_slice(), percent(def.volume)),
579            // A sound added plays at its own volume, full.
580            (None, _, Some(added)) => (core::slice::from_ref(added), 1.0),
581            _ => {
582                self.warnings.push(format!("unknown sound cue \"{cue}\""));
583                return None;
584            }
585        };
586        if variants.is_empty() {
587            return None; // Its files failed to load (warned then).
588        }
589        let sound = &variants[self.rng.below(variants.len())];
590        let volume = own * volume * self.sound_volume;
591        Some(backend.play(sound, volume, looped))
592    }
593
594    /// Pauses every sound started with a handle, or resumes the ones paused.
595    /// While suspended they are held already, and stay so until the resume.
596    fn pause_handled(&mut self, backend: &mut B, paused: bool) {
597        let (sound, suspended) = (self.sound_volume, self.suspended);
598        let (provided, warnings) = (&mut self.provided, &mut self.warnings);
599        for handled in self.handled.values_mut() {
600            if handled.paused != paused {
601                handled.paused = paused;
602                if !suspended {
603                    provided.hold(backend, &handled.playing, paused, warnings);
604                }
605                if provided.mutes {
606                    let level = handled.level(sound, suspended, true);
607                    backend.set_playing_volume(&handled.playing, level);
608                }
609            }
610        }
611    }
612
613    /// Starts sound `cue` as `id`, looped if its cue says so.
614    fn start_sound(&mut self, backend: &mut B, id: SoundId, cue: &str, volume: f32) {
615        let def = self.manifest.sounds.get(cue);
616        let (looped, cue_volume) = def.map_or((false, 1.0), |d| (d.looped, percent(d.volume)));
617        if let Some(playing) = self.play_sound(backend, cue, volume, looped) {
618            let handled = Handled {
619                playing,
620                cue_volume,
621                volume,
622                paused: false,
623            };
624            if self.suspended {
625                backend.set_playing_volume(&handled.playing, 0.0);
626            }
627            if let Some(old) = self.handled.insert(id, handled) {
628                backend.stop_playing(old.playing);
629            }
630        }
631    }
632
633    fn music(&mut self, backend: &mut B, command: &MusicCommand, now: f64) {
634        match command {
635            MusicCommand::Load { cue } => {
636                self.stop_track(backend, cue);
637                let Some(def) = self.manifest.music.get(cue) else {
638                    self.warnings.push(format!("unknown music cue \"{cue}\""));
639                    return;
640                };
641                let path = format!("{}/{}", self.music_dir, def.file);
642                let track = Track {
643                    state: TrackState::Loading(backend.load_music(&path)),
644                    playing: false,
645                    playback: None,
646                    started_at: None,
647                    held_at: None,
648                    gain: 1.0,
649                    volume: percent(def.volume),
650                    looped: def.looped,
651                };
652                self.tracks.insert(cue.clone(), track);
653            }
654            MusicCommand::Start { cue, gain, .. } => {
655                let music = self.music_level();
656                if let Some(track) = self.tracks.get_mut(cue) {
657                    track.playing = true;
658                    track.gain = *gain;
659                    if let TrackState::Ready(sound) = &track.state {
660                        let volume = track.volume * track.gain * music;
661                        track.playback = Some(backend.play(sound, volume, track.looped));
662                        track.started_at = Some(now);
663                        track.held_at = None;
664                    }
665                }
666            }
667            MusicCommand::Gain { cue, gain } => {
668                let music = self.music_level();
669                if let Some(track) = self.tracks.get_mut(cue) {
670                    track.gain = *gain;
671                    if let (true, TrackState::Ready(sound)) = (track.playing, &track.state) {
672                        let volume = track.volume * track.gain * music;
673                        backend.set_volume(sound, volume);
674                    }
675                }
676            }
677            MusicCommand::Stop { cue } => self.stop_track(backend, cue),
678        }
679    }
680
681    /// Stops and frees `cue`'s track, if it has one.
682    fn stop_track(&mut self, backend: &mut B, cue: &str) {
683        let Some(track) = self.tracks.remove(cue) else {
684            return;
685        };
686        match track.state {
687            TrackState::Ready(sound) if track.playing => backend.stop(&sound),
688            TrackState::Loading(loading) => self.abandoned.push(loading),
689            TrackState::Ready(_) | TrackState::Failed => {}
690        }
691    }
692
693    /// Stops all music, the voice and the sounds started with a handle
694    /// (the game quit, e.g. on the web page that stays open).
695    pub fn stop_all(&mut self, backend: &mut B) {
696        let cues: Vec<String> = self.tracks.keys().cloned().collect();
697        for cue in cues {
698            self.stop_track(backend, &cue);
699        }
700        for handled in core::mem::take(&mut self.handled).into_values() {
701            backend.stop_playing(handled.playing);
702        }
703        self.voices.stop_all(backend, &mut self.abandoned);
704    }
705
706    /// Sets how loud voice clips play (0–1; `FrameOutput::voice_volume`),
707    /// the one playing included.
708    pub fn set_voice_volume(&mut self, backend: &mut B, volume: f32) {
709        self.voice_volume = volume;
710        if !self.suspended {
711            self.voices.set_volume(backend, volume);
712        }
713    }
714
715    /// Sets how fast sound `cue` plays from its next play, in 16.16 fixed
716    /// point ([`RATE_ONE`] as recorded). On a backend without a rate it plays
717    /// as recorded, and the log says so once.
718    pub fn set_rate(&mut self, backend: &mut B, cue: &str, rate: u32) {
719        for sound in self.sounds.get(cue).into_iter().flatten() {
720            let result = backend.set_rate(sound, rate);
721            self.provided
722                .call(Feature::Rate, result, &mut self.warnings);
723        }
724    }
725
726    /// Finishes loads: a track asked to play starts now.
727    fn poll(&mut self, backend: &mut B, now: f64) {
728        let music = self.music_level();
729        for (cue, track) in &mut self.tracks {
730            let TrackState::Loading(loading) = &mut track.state else {
731                continue;
732            };
733            match backend.poll_music(loading) {
734                None => {}
735                Some(Ok(sound)) => {
736                    if track.playing {
737                        let volume = track.volume * track.gain * music;
738                        track.playback = Some(backend.play(&sound, volume, track.looped));
739                        track.started_at = Some(now);
740                    }
741                    track.state = TrackState::Ready(sound);
742                }
743                Some(Err(e)) => {
744                    let file = self
745                        .manifest
746                        .music
747                        .get(cue)
748                        .map_or("?", |m| m.file.as_str());
749                    let dir = &self.music_dir;
750                    self.warnings
751                        .push(format!("music \"{cue}\": {dir}/{file}: {e}"));
752                    track.state = TrackState::Failed;
753                }
754            }
755        }
756        self.voices
757            .poll(backend, now, &mut self.warnings, &mut self.abandoned);
758        self.abandoned
759            .retain_mut(|loading| backend.poll_music(loading).is_none());
760    }
761
762    /// Problems found since the last call, for the log.
763    pub fn take_warnings(&mut self) -> Vec<String> {
764        core::mem::take(&mut self.warnings)
765    }
766}
767
768/// The [`Audio`] of a backend that writes only [`Backend`]: its backend and,
769/// once the game has loaded, the [`Player`] over it. Before that there is
770/// nothing to play.
771pub struct PlayerDevice<B: Backend> {
772    backend: B,
773    player: Option<Player<B>>,
774}
775
776impl<B: Backend> PlayerDevice<B> {
777    /// The device on `backend`, with no player yet.
778    pub const fn new(backend: B) -> Self {
779        Self {
780            backend,
781            player: None,
782        }
783    }
784
785    /// The backend, for [`Player::load`].
786    pub const fn backend_mut(&mut self) -> &mut B {
787        &mut self.backend
788    }
789
790    /// The game has loaded: `player` plays from now on.
791    pub fn set_player(&mut self, player: Player<B>) {
792        self.player = Some(player);
793    }
794
795    /// Says the player's problems in the backend's log.
796    fn log_warnings(&mut self) {
797        if let Some(player) = &mut self.player {
798            for warning in player.take_warnings() {
799                self.backend.warn(&warning);
800            }
801        }
802    }
803}
804
805impl<B: Backend> fmt::Debug for PlayerDevice<B> {
806    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
807        f.debug_struct("PlayerDevice")
808            .field("loaded", &self.player.is_some())
809            .finish_non_exhaustive()
810    }
811}
812
813impl<B: Backend> Audio for PlayerDevice<B> {
814    fn music_playing(&self, now: Millis) -> Option<(&str, f64)> {
815        self.player.as_ref()?.music_playing(now.as_secs_f64())
816    }
817
818    fn voice_playing(&self, now: Millis) -> Option<&str> {
819        self.player.as_ref()?.voice_playing(now.as_secs_f64())
820    }
821
822    fn play(&mut self, frame: &AudioFrame<'_>, now: Millis) {
823        let Some(player) = &mut self.player else {
824            return;
825        };
826        let backend = &mut self.backend;
827        player.set_volumes(backend, frame.music_volume, frame.sound_volume);
828        player.set_voice_volume(backend, frame.voice_volume);
829        player.play(backend, frame.requests, frame.music, now.as_secs_f64());
830        self.log_warnings();
831    }
832
833    fn stop_all(&mut self) {
834        if let Some(player) = &mut self.player {
835            player.stop_all(&mut self.backend);
836        }
837    }
838
839    fn suspend(&mut self) {
840        if let Some(player) = &mut self.player {
841            player.suspend(&mut self.backend);
842        }
843        self.backend.hold_output(true);
844        self.log_warnings();
845    }
846
847    fn resume(&mut self) {
848        self.backend.hold_output(false);
849        if let Some(player) = &mut self.player {
850            player.resume(&mut self.backend);
851        }
852        self.log_warnings();
853    }
854}
855
856/// A manifest volume (percent) as a 0–1 factor.
857fn percent(volume: u8) -> f32 {
858    f32::from(volume) / 100.0
859}
860
861/// Picks sound variants (`audio.md`: footsteps vary each step). A small
862/// `SplitMix64`, separate from core's simulation RNG (game ADR-0019): what
863/// plays never affects the game.
864pub(crate) struct VariantRng(u64);
865
866impl VariantRng {
867    pub(crate) fn new(seed: u64) -> Self {
868        Self(seed)
869    }
870
871    fn next_u64(&mut self) -> u64 {
872        self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
873        let mut z = self.0;
874        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
875        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
876        z ^ (z >> 31)
877    }
878
879    /// A number below `n` (`n` ≥ 1).
880    pub(crate) fn below(&mut self, n: usize) -> usize {
881        let n = u64::try_from(n.max(1)).unwrap_or(u64::MAX);
882        usize::try_from(self.next_u64() % n).unwrap_or(0)
883    }
884}
885
886#[cfg(test)]
887mod tests {
888    use core::cell::RefCell;
889
890    use alloc::rc::Rc;
891    use core::pin::pin;
892    use core::task::{Context, Poll, Waker};
893
894    use alloc::borrow::Cow;
895
896    use proptest::prelude::*;
897    use serde::Deserialize;
898    use shuyi_assets::audio::{Bytes, CreditRef, MusicCue, SoundCue};
899    use shuyi_assets::voice::ClipVariant;
900    use shuyi_ui::audio::AudioRequest;
901
902    use super::Player as Audio;
903    use super::*;
904
905    /// Which recording of a line a test clip is: its only one, or a
906    /// second take.
907    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize)]
908    pub(crate) enum Take {
909        /// The line's only clip.
910        #[default]
911        Only,
912        /// A second take.
913        Second,
914    }
915
916    impl ClipVariant for Take {
917        fn suffix(self) -> &'static str {
918            match self {
919                Self::Only => "",
920                Self::Second => ".2",
921            }
922        }
923
924        fn split() -> &'static [Self] {
925            &[Self::Second]
926        }
927    }
928
929    /// The audio requests of the tests.
930    pub(crate) type Request = AudioRequest<Take>;
931
932    impl<B: Backend> Audio<B> {
933        /// [`play`](Audio::play) with the screens' own requests, as `App`
934        /// hands them to the device.
935        pub(crate) fn hear(
936            &mut self,
937            backend: &mut B,
938            requests: &[Request],
939            music: &[MusicCommand],
940            now: f64,
941        ) {
942            let requests: Vec<SoundRequest> = requests
943                .iter()
944                .filter_map(SoundRequest::from_request)
945                .collect();
946            self.play(backend, &requests, music, now);
947        }
948    }
949
950    /// Runs a future that never waits on anything external.
951    pub(crate) fn block_on<F: Future>(future: F) -> F::Output {
952        let mut future = pin!(future);
953        let mut cx = Context::from_waker(Waker::noop());
954        loop {
955            if let Poll::Ready(v) = future.as_mut().poll(&mut cx) {
956                return v;
957            }
958        }
959    }
960
961    /// A mono 16-bit WAV of `samples` of a square wave, made here so no
962    /// test file is shipped.
963    fn wav(samples: u16) -> Vec<u8> {
964        let data_len = u32::from(samples) * 2;
965        let mut w = Vec::new();
966        w.extend_from_slice(b"RIFF");
967        w.extend_from_slice(&(36 + data_len).to_le_bytes());
968        w.extend_from_slice(b"WAVEfmt ");
969        w.extend_from_slice(&16u32.to_le_bytes());
970        w.extend_from_slice(&1u16.to_le_bytes()); // PCM
971        w.extend_from_slice(&1u16.to_le_bytes()); // mono
972        w.extend_from_slice(&22_050u32.to_le_bytes());
973        w.extend_from_slice(&44_100u32.to_le_bytes());
974        w.extend_from_slice(&2u16.to_le_bytes());
975        w.extend_from_slice(&16u16.to_le_bytes());
976        w.extend_from_slice(b"data");
977        w.extend_from_slice(&data_len.to_le_bytes());
978        for i in 0..samples {
979            let s: i16 = if i % 50 < 25 { 8_000 } else { -8_000 };
980            w.extend_from_slice(&s.to_le_bytes());
981        }
982        w
983    }
984
985    /// Reads a PCM WAV's sample count, the way a decoder would refuse
986    /// anything else.
987    fn decode_wav(bytes: &[u8]) -> Result<usize, String> {
988        let field = |at: usize| {
989            bytes
990                .get(at..at + 4)
991                .and_then(|b| <[u8; 4]>::try_from(b).ok())
992        };
993        let ok =
994            field(0) == Some(*b"RIFF") && field(8) == Some(*b"WAVE") && field(36) == Some(*b"data");
995        let len = field(40).map(u32::from_le_bytes).unwrap_or_default() as usize;
996        if ok && bytes.len() == 44 + len {
997            Ok(len / 2)
998        } else {
999            Err("not a PCM WAV".to_owned())
1000        }
1001    }
1002
1003    pub(crate) type Log = Rc<RefCell<Vec<String>>>;
1004
1005    /// Records every call. Sounds are names; music and voice loads finish
1006    /// when their path is put in `ready` (or fail if in `broken`). A file
1007    /// plays for `length` seconds, if set. Each play is a playback,
1008    /// numbered from 1 (`#1`).
1009    #[derive(Default)]
1010    pub(crate) struct Fake {
1011        pub(crate) log: Log,
1012        pub(crate) ready: Vec<String>,
1013        pub(crate) broken: Vec<String>,
1014        pub(crate) length: Option<f64>,
1015        pub(crate) plays: u32,
1016    }
1017
1018    impl Backend for Fake {
1019        type Sound = String;
1020        type Loading = String;
1021        type Playing = u32;
1022
1023        fn load_sound(&mut self, bytes: &[u8]) -> impl Future<Output = Result<String, String>> {
1024            core::future::ready(decode_wav(bytes).map(|samples| format!("wav{samples}")))
1025        }
1026
1027        fn load_music(&mut self, path: &str) -> String {
1028            self.log.borrow_mut().push(format!("load {path}"));
1029            path.to_owned()
1030        }
1031
1032        fn poll_music(&mut self, loading: &mut String) -> Option<Result<String, String>> {
1033            if self.broken.contains(loading) {
1034                Some(Err("no such file".into()))
1035            } else if self.ready.contains(loading) {
1036                Some(Ok(loading.clone()))
1037            } else {
1038                None
1039            }
1040        }
1041
1042        fn length(&self, _sound: &String) -> Option<f64> {
1043            self.length
1044        }
1045
1046        fn play(&mut self, sound: &String, volume: f32, looped: bool) -> u32 {
1047            let looped = if looped { " looped" } else { "" };
1048            self.log
1049                .borrow_mut()
1050                .push(format!("play {sound} {volume:.2}{looped}"));
1051            self.plays += 1;
1052            self.plays
1053        }
1054
1055        fn set_volume(&mut self, sound: &String, volume: f32) {
1056            self.log
1057                .borrow_mut()
1058                .push(format!("volume {sound} {volume:.2}"));
1059        }
1060
1061        fn stop(&mut self, sound: &String) {
1062            self.log.borrow_mut().push(format!("stop {sound}"));
1063        }
1064
1065        fn set_playing_volume(&mut self, playing: &u32, volume: f32) {
1066            self.log
1067                .borrow_mut()
1068                .push(format!("volume #{playing} {volume:.2}"));
1069        }
1070
1071        fn stop_playing(&mut self, playing: u32) {
1072            self.log.borrow_mut().push(format!("stop #{playing}"));
1073        }
1074
1075        fn add_sound(&mut self, samples: &SharedSamples) -> Result<String, String> {
1076            let sound = format!("pcm{}", samples.frames());
1077            self.log.borrow_mut().push(format!("add {sound}"));
1078            Ok(sound)
1079        }
1080
1081        fn warn(&mut self, line: &str) {
1082            self.log.borrow_mut().push(format!("warn {line}"));
1083        }
1084    }
1085
1086    impl Fake {
1087        pub(crate) fn calls(&self) -> Vec<String> {
1088            core::mem::take(&mut self.log.borrow_mut())
1089        }
1090    }
1091
1092    fn manifest() -> AudioManifest {
1093        let mut m = AudioManifest::default();
1094        let sound = |files: &[&str], volume| {
1095            SoundCue::new(
1096                files.iter().map(|f| (*f).to_owned()).collect(),
1097                volume,
1098                CreditRef::Own,
1099            )
1100        };
1101        m.sounds.insert("beep".into(), sound(&["sfx/beep.wav"], 50));
1102        let rain = sound(&["sfx/beep.wav"], 50).with_looped(true);
1103        m.sounds.insert("rain".into(), rain);
1104        let steps = ["sfx/step_1.wav", "sfx/step_2.wav", "sfx/step_3.wav"];
1105        m.sounds.insert("step".into(), sound(&steps, 100));
1106        m.sounds
1107            .insert("broken".into(), sound(&["sfx/broken.wav"], 100));
1108        let track = |file: &str, volume, looped| MusicCue {
1109            file: file.into(),
1110            volume,
1111            looped,
1112            length_ms: 60_000,
1113            credit: CreditRef::Own,
1114        };
1115        m.music.insert("title".into(), track("title.ogg", 80, true));
1116        m.music
1117            .insert("sting".into(), track("sting.ogg", 100, false));
1118        m.music.insert("lost".into(), track("lost.ogg", 100, true));
1119        m
1120    }
1121
1122    /// The generated WAV for every sound but `broken`, which isn't a WAV.
1123    fn bytes(file: &str) -> Bytes {
1124        thread_local! {
1125            static WAVS: (&'static [u8], &'static [u8], &'static [u8], &'static [u8]) = (
1126                Vec::leak(wav(100)),
1127                Vec::leak(wav(200)),
1128                Vec::leak(wav(300)),
1129                Vec::leak(wav(400)),
1130            );
1131        }
1132        let wav = WAVS.with(|w| match file {
1133            "sfx/beep.wav" => Some(w.0),
1134            "sfx/step_1.wav" => Some(w.1),
1135            "sfx/step_2.wav" => Some(w.2),
1136            "sfx/step_3.wav" => Some(w.3),
1137            "sfx/broken.wav" => Some(&w.0[..20]),
1138            _ => None,
1139        });
1140        wav.map(Cow::Borrowed)
1141    }
1142
1143    /// How the warnings name a sound file.
1144    fn shown(file: &str) -> String {
1145        format!("bundle/{file}")
1146    }
1147
1148    /// The sounds of [`bytes`], named by [`shown`].
1149    pub(crate) fn files() -> Files<'static> {
1150        Files::new(&bytes, &shown, &|_: &str| false, "music")
1151    }
1152
1153    pub(crate) fn audio(backend: &mut Fake) -> Audio<Fake> {
1154        let (music, voice) = ("music".into(), "voice/en".into());
1155        block_on(Audio::load(backend, manifest(), &files(), music, voice, 7))
1156    }
1157
1158    fn sound(cue: &str, volume: f32) -> Request {
1159        Request::PlaySound {
1160            cue: cue.into(),
1161            volume,
1162        }
1163    }
1164
1165    fn cmd(f: fn(String) -> MusicCommand, cue: &str) -> MusicCommand {
1166        f(cue.into())
1167    }
1168
1169    fn load(cue: String) -> MusicCommand {
1170        MusicCommand::Load { cue }
1171    }
1172
1173    fn start(cue: String) -> MusicCommand {
1174        MusicCommand::start(cue, 1.0)
1175    }
1176
1177    fn stop(cue: String) -> MusicCommand {
1178        MusicCommand::Stop { cue }
1179    }
1180
1181    fn gain(cue: &str, gain: f32) -> MusicCommand {
1182        MusicCommand::Gain {
1183            cue: cue.into(),
1184            gain,
1185        }
1186    }
1187
1188    #[test]
1189    fn a_started_sound_is_one_playback_its_requests_and_the_sound_volume_reach() {
1190        let mut fake = Fake::default();
1191        let mut audio = audio(&mut fake);
1192        audio.take_warnings();
1193        let mut queue = shuyi_ui::audio::AudioQueue::<Take>::default();
1194        let [first, second] = [queue.start_sound("rain"), queue.start_sound("rain")];
1195        let once = queue.start_sound("beep");
1196        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1197        assert_eq!(
1198            fake.calls(),
1199            [
1200                "play wav100 0.50 looped",
1201                "play wav100 0.50 looped",
1202                "play wav100 0.50"
1203            ]
1204        );
1205        queue.set_sound_volume(first, 0.5);
1206        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1207        assert_eq!(fake.calls(), ["volume #1 0.25"]);
1208        // The player's sound volume reaches every playback, at its own.
1209        audio.set_volumes(&mut fake, 1.0, 0.4);
1210        assert_eq!(
1211            fake.calls(),
1212            ["volume #1 0.10", "volume #2 0.20", "volume #3 0.20"]
1213        );
1214        // A stop stops that copy alone; the other plays on.
1215        queue.stop_sound(first, 0);
1216        queue.stop_sound(once, 0);
1217        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1218        assert_eq!(fake.calls(), ["stop #1", "stop #3"]);
1219        audio.hear(&mut fake, &[], &[], 0.0);
1220        let got = fake.calls();
1221        assert!(got.is_empty(), "{got:?}");
1222        audio.stop_all(&mut fake);
1223        assert_eq!(fake.calls(), ["stop #2"]);
1224        queue.set_sound_volume(second, 1.0);
1225        queue.stop_sound(second, 0);
1226        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1227        assert!(fake.calls().is_empty(), "a stopped id is gone");
1228    }
1229
1230    #[test]
1231    fn a_pause_mutes_the_sounds_playing_and_its_resume_restores_them() {
1232        let mut fake = Fake::default();
1233        let mut audio = audio(&mut fake);
1234        let mut queue = shuyi_ui::audio::AudioQueue::<Take>::default();
1235        let below = queue.start_sound("rain");
1236        queue.pause_sounds();
1237        let menu = queue.start_sound_at("rain", 0.5);
1238        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1239        assert_eq!(
1240            fake.calls(),
1241            [
1242                "play wav100 0.50 looped",
1243                "volume #1 0.00",
1244                "play wav100 0.25 looped"
1245            ]
1246        );
1247        // A paused sound keeps the volumes it is given, silently.
1248        queue.set_sound_volume(below, 0.5);
1249        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1250        audio.set_volumes(&mut fake, 1.0, 0.5);
1251        assert_eq!(
1252            fake.calls(),
1253            ["volume #1 0.00", "volume #1 0.00", "volume #2 0.12"]
1254        );
1255        queue.resume_sounds();
1256        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1257        assert_eq!(fake.calls(), ["volume #1 0.12"]);
1258        assert!(queue.sound_is_live(menu));
1259    }
1260
1261    #[test]
1262    fn a_suspend_mutes_the_sounds_and_the_music_and_the_resume_restores_them() {
1263        let mut fake = Fake {
1264            ready: vec!["music/title.ogg".into()],
1265            ..Fake::default()
1266        };
1267        let mut audio = audio(&mut fake);
1268        let mut queue = shuyi_ui::audio::AudioQueue::<Take>::default();
1269        let [below, other] = [queue.start_sound("rain"), queue.start_sound("rain")];
1270        queue.pause_sounds();
1271        let music = [cmd(load, "title"), cmd(start, "title")];
1272        audio.hear(&mut fake, &queue.take(), &music, 0.0);
1273        fake.calls();
1274        audio.suspend(&mut fake);
1275        assert_eq!(
1276            fake.calls(),
1277            [
1278                "volume #1 0.00",
1279                "volume #2 0.00",
1280                "volume music/title.ogg 0.00"
1281            ]
1282        );
1283        audio.suspend(&mut fake);
1284        assert!(fake.calls().is_empty(), "a second suspend does nothing");
1285        // It mutes: the track's clock runs on.
1286        assert_eq!(audio.music_playing(2.0), Some(("title", 2.0)));
1287        audio.resume(&mut fake);
1288        // The pause under a menu holds; the music comes back.
1289        assert_eq!(
1290            fake.calls(),
1291            [
1292                "volume #1 0.00",
1293                "volume #2 0.00",
1294                "volume music/title.ogg 0.80"
1295            ]
1296        );
1297        audio.resume(&mut fake);
1298        assert!(fake.calls().is_empty(), "a second resume does nothing");
1299        queue.resume_sounds();
1300        audio.hear(&mut fake, &queue.take(), &[], 3.0);
1301        assert_eq!(fake.calls(), ["volume #1 0.50", "volume #2 0.50"]);
1302        assert!(queue.sound_is_live(below) && queue.sound_is_live(other));
1303    }
1304
1305    /// `frames` frames of 8-bit silence, mono, at 11,025 a second.
1306    fn added(frames: usize) -> shuyi_ui::audio::Samples {
1307        let samples = shuyi_ui::audio::Samples::from_u8(11_025, 1, &vec![128; frames]);
1308        samples.unwrap_or_else(|e| panic!("{e}"))
1309    }
1310
1311    #[test]
1312    fn an_added_sound_plays_as_a_cue_at_its_own_volume_until_it_is_removed() {
1313        let mut fake = Fake::default();
1314        let mut audio = audio(&mut fake);
1315        audio.take_warnings();
1316        fake.calls();
1317        let mut queue = shuyi_ui::audio::AudioQueue::<Take>::default();
1318        assert_eq!(queue.add_sound("tone", added(1102)), Ok(()));
1319        queue.play_sound_at("tone", 0.5);
1320        let id = queue.start_sound("tone");
1321        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1322        assert_eq!(
1323            fake.calls(),
1324            ["add pcm1102", "play pcm1102 0.50", "play pcm1102 1.00"]
1325        );
1326        // The player's sound volume scales it as any cue's.
1327        audio.set_volumes(&mut fake, 1.0, 0.5);
1328        fake.calls();
1329        queue.play_sound("tone");
1330        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1331        assert_eq!(fake.calls(), ["play pcm1102 0.50"]);
1332        // Added again, it replaces the one before; removed, it stops.
1333        assert_eq!(queue.add_sound("tone", added(551)), Ok(()));
1334        queue.play_sound("tone");
1335        queue.remove_sound("tone");
1336        queue.stop_sound(id, 0);
1337        audio.hear(&mut fake, &queue.take(), &[], 0.0);
1338        assert_eq!(
1339            fake.calls(),
1340            [
1341                "stop pcm1102",
1342                "add pcm551",
1343                "play pcm551 0.50",
1344                "stop pcm551",
1345                "stop #2"
1346            ]
1347        );
1348        let found = audio.take_warnings();
1349        assert!(found.is_empty(), "{found:?}");
1350        // A request for it after its removal is a cue the player doesn't know.
1351        audio.hear(&mut fake, &[sound("tone", 1.0)], &[], 0.0);
1352        let found = fake.calls();
1353        assert!(found.is_empty(), "{found:?}");
1354        assert_eq!(audio.take_warnings(), ["unknown sound cue \"tone\""]);
1355    }
1356
1357    #[test]
1358    fn a_looped_cue_played_without_a_handle_plays_once() {
1359        let mut fake = Fake::default();
1360        let mut audio = audio(&mut fake);
1361        audio.hear(&mut fake, &[sound("rain", 1.0)], &[], 0.0);
1362        assert_eq!(fake.calls(), ["play wav100 0.50"]);
1363    }
1364
1365    #[test]
1366    fn a_generated_wav_plays_through_the_app_path() {
1367        let mut fake = Fake::default();
1368        let mut audio = audio(&mut fake);
1369        assert_eq!(
1370            audio.take_warnings(),
1371            ["sound \"broken\": bundle/sfx/broken.wav: not a PCM WAV"]
1372        );
1373        let music_request = Request::play_music("title", None);
1374        audio.hear(
1375            &mut fake,
1376            &[sound("beep", 1.0), music_request, sound("beep", 0.5)],
1377            &[],
1378            0.0,
1379        );
1380        assert_eq!(fake.calls(), ["play wav100 0.50", "play wav100 0.25"]);
1381        // A sound whose file failed to decode is silent, not a crash.
1382        audio.hear(&mut fake, &[sound("broken", 1.0)], &[], 0.0);
1383        let got = fake.calls();
1384        assert!(got.is_empty(), "{got:?}");
1385        let got = audio.take_warnings();
1386        assert!(got.is_empty(), "{got:?}");
1387    }
1388
1389    #[test]
1390    fn unknown_or_missing_files_are_warnings() {
1391        let mut fake = Fake::default();
1392        let mut m = manifest();
1393        if let Some(beep) = m.sounds.get_mut("beep") {
1394            beep.files.push("sfx/gone.wav".into());
1395        }
1396        let dirs = ("music".into(), "voice/en".into());
1397        let mut audio = block_on(Audio::load(&mut fake, m, &files(), dirs.0, dirs.1, 1));
1398        assert_eq!(
1399            audio.take_warnings()[0],
1400            "sound \"beep\": bundle/sfx/gone.wav: not in the asset bundle"
1401        );
1402        audio.hear(&mut fake, &[sound("nope", 1.0)], &[cmd(load, "nope")], 0.0);
1403        assert_eq!(
1404            audio.take_warnings(),
1405            ["unknown sound cue \"nope\"", "unknown music cue \"nope\""]
1406        );
1407        let got = fake.calls();
1408        assert!(got.is_empty(), "{got:?}");
1409    }
1410
1411    #[test]
1412    fn variants_are_picked_at_random() {
1413        let mut fake = Fake::default();
1414        let mut audio = audio(&mut fake);
1415        let steps: Vec<_> = (0..30).map(|_| sound("step", 1.0)).collect();
1416        audio.hear(&mut fake, &steps, &[], 0.0);
1417        let calls = fake.calls();
1418        assert_eq!(calls.len(), 30);
1419        for variant in ["wav200", "wav300", "wav400"] {
1420            assert!(
1421                calls.iter().any(|c| c.contains(variant)),
1422                "{variant} never played: {calls:?}"
1423            );
1424        }
1425    }
1426
1427    #[test]
1428    fn music_loads_then_starts_when_ready() {
1429        let mut fake = Fake::default();
1430        let mut audio = audio(&mut fake);
1431        audio.play(
1432            &mut fake,
1433            &[],
1434            &[cmd(load, "title"), cmd(start, "title")],
1435            0.0,
1436        );
1437        assert_eq!(fake.calls(), ["load music/title.ogg"]);
1438        audio.play(&mut fake, &[], &[], 0.0);
1439        assert!(fake.calls().is_empty(), "still loading");
1440        fake.ready.push("music/title.ogg".into());
1441        audio.play(&mut fake, &[], &[], 0.0);
1442        assert_eq!(fake.calls(), ["play music/title.ogg 0.80 looped"]);
1443        audio.play(&mut fake, &[], &[], 0.0);
1444        assert!(fake.calls().is_empty(), "started once");
1445        // A fade scales the cue's own volume; then stop frees it.
1446        audio.play(&mut fake, &[], &[gain("title", 0.5)], 0.0);
1447        assert_eq!(fake.calls(), ["volume music/title.ogg 0.40"]);
1448        audio.play(&mut fake, &[], &[cmd(stop, "title")], 0.0);
1449        assert_eq!(fake.calls(), ["stop music/title.ogg"]);
1450        assert!(audio.tracks.is_empty());
1451    }
1452
1453    /// Ticket 0805: the player's volumes multiply every cue's own.
1454    #[test]
1455    fn the_players_volumes_scale_sounds_and_music() {
1456        let mut fake = Fake::default();
1457        fake.ready.push("music/title.ogg".into());
1458        let mut audio = audio(&mut fake);
1459        audio.set_volumes(&mut fake, 0.5, 0.2);
1460        assert!(fake.calls().is_empty(), "nothing is playing yet");
1461        // Sounds: the cue's 50 %, the request's volume, the player's 20 %.
1462        audio.hear(
1463            &mut fake,
1464            &[sound("beep", 1.0), sound("beep", 0.5)],
1465            &[],
1466            0.0,
1467        );
1468        assert_eq!(fake.calls(), ["play wav100 0.10", "play wav100 0.05"]);
1469        // Music starts at the cue's 80 % times the player's 50 %.
1470        let begin = [cmd(load, "title"), cmd(start, "title")];
1471        audio.play(&mut fake, &[], &begin, 0.0);
1472        assert_eq!(
1473            fake.calls(),
1474            ["load music/title.ogg", "play music/title.ogg 0.40 looped"]
1475        );
1476        // The same volumes again change nothing; a new music volume
1477        // reaches the track playing, and a fade multiplies with it.
1478        audio.set_volumes(&mut fake, 0.5, 0.2);
1479        let got = fake.calls();
1480        assert!(got.is_empty(), "{got:?}");
1481        audio.set_volumes(&mut fake, 1.0, 0.2);
1482        assert_eq!(fake.calls(), ["volume music/title.ogg 0.80"]);
1483        audio.play(&mut fake, &[], &[gain("title", 0.5)], 0.0);
1484        assert_eq!(fake.calls(), ["volume music/title.ogg 0.40"]);
1485        audio.set_volumes(&mut fake, 0.5, 0.2);
1486        assert_eq!(fake.calls(), ["volume music/title.ogg 0.20"]);
1487        // Silent: sounds and music at 0.
1488        audio.set_volumes(&mut fake, 0.0, 0.0);
1489        assert_eq!(fake.calls(), ["volume music/title.ogg 0.00"]);
1490        audio.hear(&mut fake, &[sound("beep", 1.0)], &[], 0.0);
1491        assert_eq!(fake.calls(), ["play wav100 0.00"]);
1492    }
1493
1494    /// A track already loaded and started later, or still loading when the
1495    /// volume is set, starts at the player's volume.
1496    #[test]
1497    fn a_track_that_starts_later_uses_the_players_volume() {
1498        let mut fake = Fake::default();
1499        let mut audio = audio(&mut fake);
1500        let begin = [cmd(load, "title"), cmd(start, "title")];
1501        audio.play(&mut fake, &[], &begin, 0.0);
1502        fake.calls();
1503        // Still loading: nothing to change yet.
1504        audio.set_volumes(&mut fake, 0.25, 1.0);
1505        let got = fake.calls();
1506        assert!(got.is_empty(), "{got:?}");
1507        fake.ready.push("music/title.ogg".into());
1508        audio.play(&mut fake, &[], &[], 0.0);
1509        assert_eq!(fake.calls(), ["play music/title.ogg 0.20 looped"]);
1510        // Started again once loaded (game ADR-0026: `Start` on a ready track).
1511        audio.play(&mut fake, &[], &[cmd(start, "title")], 0.0);
1512        assert_eq!(fake.calls(), ["play music/title.ogg 0.20 looped"]);
1513    }
1514
1515    #[test]
1516    fn a_loaded_track_starts_at_once_and_plays_once_if_not_looped() {
1517        let mut fake = Fake::default();
1518        fake.ready.push("music/sting.ogg".into());
1519        let mut audio = audio(&mut fake);
1520        audio.play(&mut fake, &[], &[cmd(load, "sting")], 0.0);
1521        assert_eq!(fake.calls(), ["load music/sting.ogg"]);
1522        audio.play(
1523            &mut fake,
1524            &[],
1525            &[gain("sting", 0.5), cmd(start, "sting")],
1526            0.0,
1527        );
1528        assert_eq!(fake.calls(), ["play music/sting.ogg 1.00"]);
1529    }
1530
1531    #[test]
1532    fn a_gain_before_the_track_is_ready_applies_when_it_starts() {
1533        let mut fake = Fake::default();
1534        let mut audio = audio(&mut fake);
1535        audio.play(
1536            &mut fake,
1537            &[],
1538            &[cmd(load, "title"), cmd(start, "title")],
1539            0.0,
1540        );
1541        audio.play(&mut fake, &[], &[gain("title", 0.5)], 0.0);
1542        fake.ready.push("music/title.ogg".into());
1543        audio.play(&mut fake, &[], &[], 0.0);
1544        assert_eq!(
1545            fake.calls(),
1546            ["load music/title.ogg", "play music/title.ogg 0.40 looped"]
1547        );
1548    }
1549
1550    #[test]
1551    fn a_stopped_load_is_polled_until_done_then_dropped() {
1552        let mut fake = Fake::default();
1553        let mut audio = audio(&mut fake);
1554        audio.play(
1555            &mut fake,
1556            &[],
1557            &[cmd(load, "title"), cmd(stop, "title")],
1558            0.0,
1559        );
1560        assert_eq!(audio.abandoned.len(), 1);
1561        audio.play(&mut fake, &[], &[], 0.0);
1562        assert_eq!(audio.abandoned.len(), 1);
1563        fake.ready.push("music/title.ogg".into());
1564        audio.play(&mut fake, &[], &[], 0.0);
1565        assert!(audio.abandoned.is_empty(), "{:?}", audio.abandoned);
1566        assert_eq!(fake.calls(), ["load music/title.ogg"], "never played");
1567    }
1568
1569    #[test]
1570    fn a_missing_track_is_a_warning_and_silence() {
1571        let mut fake = Fake::default();
1572        fake.broken.push("music/lost.ogg".into());
1573        let mut audio = audio(&mut fake);
1574        audio.take_warnings();
1575        audio.play(
1576            &mut fake,
1577            &[],
1578            &[cmd(load, "lost"), cmd(start, "lost")],
1579            0.0,
1580        );
1581        assert_eq!(
1582            audio.take_warnings(),
1583            ["music \"lost\": music/lost.ogg: no such file"]
1584        );
1585        audio.play(&mut fake, &[], &[gain("lost", 0.5), cmd(stop, "lost")], 0.0);
1586        assert_eq!(fake.calls(), ["load music/lost.ogg"]);
1587    }
1588
1589    #[test]
1590    fn loading_again_replaces_the_track() {
1591        let mut fake = Fake::default();
1592        fake.ready.push("music/title.ogg".into());
1593        let mut audio = audio(&mut fake);
1594        audio.play(
1595            &mut fake,
1596            &[],
1597            &[cmd(load, "title"), cmd(start, "title")],
1598            0.0,
1599        );
1600        audio.play(&mut fake, &[], &[], 0.0);
1601        audio.play(&mut fake, &[], &[cmd(load, "title")], 0.0);
1602        assert_eq!(
1603            fake.calls(),
1604            [
1605                "load music/title.ogg",
1606                "play music/title.ogg 0.80 looped",
1607                "stop music/title.ogg",
1608                "load music/title.ogg",
1609            ]
1610        );
1611    }
1612
1613    #[test]
1614    fn a_ready_track_not_started_is_freed_silently() {
1615        let mut fake = Fake::default();
1616        fake.ready.push("music/title.ogg".into());
1617        let mut audio = audio(&mut fake);
1618        audio.play(&mut fake, &[], &[cmd(load, "title")], 0.0);
1619        audio.play(
1620            &mut fake,
1621            &[],
1622            &[gain("title", 0.5), cmd(stop, "title")],
1623            0.0,
1624        );
1625        assert_eq!(fake.calls(), ["load music/title.ogg"]);
1626        assert!(audio.tracks.is_empty());
1627        // Commands for tracks that aren't there do nothing.
1628        audio.play(
1629            &mut fake,
1630            &[],
1631            &[cmd(start, "title"), gain("title", 1.0)],
1632            0.0,
1633        );
1634        let got = fake.calls();
1635        assert!(got.is_empty(), "{got:?}");
1636    }
1637
1638    #[test]
1639    fn stop_all_silences_every_track() {
1640        let mut fake = Fake::default();
1641        fake.ready.push("music/title.ogg".into());
1642        let mut audio = audio(&mut fake);
1643        let cmds = [cmd(load, "title"), cmd(start, "title"), cmd(load, "sting")];
1644        audio.play(&mut fake, &[], &cmds, 0.0);
1645        audio.stop_all(&mut fake);
1646        assert!(audio.tracks.is_empty());
1647        assert_eq!(
1648            fake.calls(),
1649            [
1650                "load music/title.ogg",
1651                "load music/sting.ogg",
1652                "play music/title.ogg 0.80 looped",
1653                "stop music/title.ogg",
1654            ]
1655        );
1656        assert_eq!(audio.abandoned.len(), 1);
1657    }
1658
1659    /// Ticket 0227: nothing is reported while the track loads; from the
1660    /// moment it starts, the seconds since; nothing after its stop.
1661    #[test]
1662    fn the_music_playing_is_reported_from_its_real_start() {
1663        let mut fake = Fake::default();
1664        let mut audio = audio(&mut fake);
1665        assert_eq!(audio.music_playing(10.0), None);
1666        let cmds = [cmd(load, "title"), cmd(start, "title")];
1667        audio.play(&mut fake, &[], &cmds, 10.0);
1668        audio.play(&mut fake, &[], &[], 11.0);
1669        assert_eq!(audio.music_playing(11.5), None, "still loading");
1670        fake.ready.push("music/title.ogg".into());
1671        assert_eq!(audio.music_playing(12.0), None, "loaded, not yet started");
1672        audio.play(&mut fake, &[], &[], 12.5);
1673        assert_eq!(audio.music_playing(12.5), Some(("title", 0.0)));
1674        assert_eq!(audio.music_playing(14.0), Some(("title", 1.5)));
1675        // A fade doesn't move it, and it keeps counting past the loop
1676        // (`ui` wraps it at the track's length).
1677        audio.play(&mut fake, &[], &[gain("title", 0.5)], 15.0);
1678        assert_eq!(audio.music_playing(100.0), Some(("title", 87.5)));
1679        // A clock that stepped back gives 0, not a negative time.
1680        assert_eq!(audio.music_playing(12.0), Some(("title", 0.0)));
1681        audio.play(&mut fake, &[], &[cmd(stop, "title")], 101.0);
1682        assert_eq!(audio.music_playing(101.0), None);
1683    }
1684
1685    #[test]
1686    fn a_loaded_track_is_reported_from_its_start_command() {
1687        let mut fake = Fake::default();
1688        fake.ready.push("music/sting.ogg".into());
1689        let mut audio = audio(&mut fake);
1690        audio.play(&mut fake, &[], &[cmd(load, "sting")], 1.0);
1691        assert_eq!(audio.music_playing(2.0), None, "ready, not asked to play");
1692        audio.play(&mut fake, &[], &[cmd(start, "sting")], 3.0);
1693        assert_eq!(audio.music_playing(3.25), Some(("sting", 0.25)));
1694        // Loading it again replaces the track: silence until it restarts.
1695        audio.play(&mut fake, &[], &[cmd(load, "sting")], 4.0);
1696        assert_eq!(audio.music_playing(4.5), None);
1697    }
1698
1699    #[test]
1700    fn a_track_that_fails_to_load_is_never_reported() {
1701        let mut fake = Fake::default();
1702        fake.broken.push("music/lost.ogg".into());
1703        let mut audio = audio(&mut fake);
1704        let cmds = [cmd(load, "lost"), cmd(start, "lost")];
1705        audio.play(&mut fake, &[], &cmds, 1.0);
1706        audio.play(&mut fake, &[], &[], 2.0);
1707        assert_eq!(audio.music_playing(3.0), None);
1708    }
1709
1710    /// As `MusicState` switches tracks: the new one loads during the fade,
1711    /// then the old one stops and the new one starts.
1712    #[test]
1713    fn after_a_switch_the_report_follows_the_new_track_once_it_starts() {
1714        let mut fake = Fake::default();
1715        fake.ready.push("music/title.ogg".into());
1716        let mut audio = audio(&mut fake);
1717        let cmds = [cmd(load, "title"), cmd(start, "title")];
1718        audio.play(&mut fake, &[], &cmds, 1.0);
1719        let fading = [cmd(load, "sting"), gain("title", 0.5)];
1720        audio.play(&mut fake, &[], &fading, 5.0);
1721        assert_eq!(audio.music_playing(5.25), Some(("title", 4.25)));
1722        // The fade ends before the new track has loaded: silence.
1723        let switch = [cmd(stop, "title"), cmd(start, "sting")];
1724        audio.play(&mut fake, &[], &switch, 5.5);
1725        assert_eq!(audio.music_playing(6.0), None);
1726        fake.ready.push("music/sting.ogg".into());
1727        audio.play(&mut fake, &[], &[], 7.0);
1728        assert_eq!(audio.music_playing(7.5), Some(("sting", 0.5)));
1729    }
1730
1731    /// Should two tracks ever sound at once, the one that started last is
1732    /// reported, whichever its cue.
1733    #[test]
1734    fn of_two_tracks_the_one_started_last_is_reported() {
1735        for (first, second) in [("sting", "title"), ("title", "sting")] {
1736            let mut fake = Fake::default();
1737            fake.ready.push("music/title.ogg".into());
1738            fake.ready.push("music/sting.ogg".into());
1739            let mut audio = audio(&mut fake);
1740            let cmds = [cmd(load, first), cmd(load, second)];
1741            audio.play(&mut fake, &[], &cmds, 0.0);
1742            audio.play(&mut fake, &[], &[cmd(start, first)], 1.0);
1743            audio.play(&mut fake, &[], &[cmd(start, second)], 2.0);
1744            assert_eq!(audio.music_playing(2.5), Some((second, 0.5)));
1745        }
1746    }
1747
1748    /// A track that fades in starts at its `Start`'s gain, loaded or not.
1749    #[test]
1750    fn a_start_at_gain_zero_plays_at_volume_zero() {
1751        let mut fake = Fake::default();
1752        fake.ready.push("music/sting.ogg".into());
1753        let mut audio = audio(&mut fake);
1754        let silent = MusicCommand::start("sting", 0.0);
1755        audio.play(&mut fake, &[], &[cmd(load, "sting"), silent], 0.0);
1756        audio.play(&mut fake, &[], &[gain("sting", 0.5)], 0.0);
1757        assert_eq!(
1758            fake.calls(),
1759            [
1760                "load music/sting.ogg",
1761                "play music/sting.ogg 0.00",
1762                "volume music/sting.ogg 0.50"
1763            ]
1764        );
1765        // Still loading: it starts at that gain once loaded.
1766        let silent = MusicCommand::start("title", 0.0);
1767        audio.play(&mut fake, &[], &[cmd(load, "title"), silent], 0.0);
1768        fake.ready.push("music/title.ogg".into());
1769        audio.play(&mut fake, &[], &[], 0.0);
1770        assert_eq!(
1771            fake.calls(),
1772            ["load music/title.ogg", "play music/title.ogg 0.00 looped"]
1773        );
1774    }
1775
1776    /// A crossfade, as `MusicState` sends it: the two tracks play at once,
1777    /// their gains moving opposite ways, and the report follows the new
1778    /// one once it sounds.
1779    #[test]
1780    fn two_tracks_play_at_once_in_a_crossfade() {
1781        let mut fake = Fake::default();
1782        fake.ready.push("music/title.ogg".into());
1783        let mut audio = audio(&mut fake);
1784        let cmds = [cmd(load, "title"), cmd(start, "title")];
1785        audio.play(&mut fake, &[], &cmds, 1.0);
1786        let crossfade = [cmd(load, "sting"), MusicCommand::start("sting", 0.0)];
1787        audio.play(&mut fake, &[], &crossfade, 2.0);
1788        assert_eq!(audio.music_playing(2.5), Some(("title", 1.5)));
1789        fake.ready.push("music/sting.ogg".into());
1790        audio.play(&mut fake, &[], &[], 3.0);
1791        assert_eq!(audio.music_playing(3.5), Some(("sting", 0.5)));
1792        let halfway = [gain("title", 0.5), gain("sting", 0.5)];
1793        audio.play(&mut fake, &[], &halfway, 3.5);
1794        audio.play(
1795            &mut fake,
1796            &[],
1797            &[cmd(stop, "title"), gain("sting", 1.0)],
1798            4.0,
1799        );
1800        assert_eq!(
1801            fake.calls(),
1802            [
1803                "load music/title.ogg",
1804                "play music/title.ogg 0.80 looped",
1805                "load music/sting.ogg",
1806                "play music/sting.ogg 0.00",
1807                "volume music/title.ogg 0.40",
1808                "volume music/sting.ogg 0.50",
1809                "stop music/title.ogg",
1810                "volume music/sting.ogg 1.00",
1811            ]
1812        );
1813        assert_eq!(audio.music_playing(4.5), Some(("sting", 1.5)));
1814    }
1815
1816    #[test]
1817    fn percent_is_a_factor() {
1818        assert!((percent(100) - 1.0).abs() < f32::EPSILON);
1819        assert!((percent(60) - 0.6).abs() < f32::EPSILON);
1820        assert!(percent(0).abs() < f32::EPSILON);
1821    }
1822
1823    #[test]
1824    fn the_rng_is_deterministic_per_seed() {
1825        let draws = |seed| {
1826            let mut rng = VariantRng::new(seed);
1827            (0..8).map(|_| rng.next_u64()).collect::<Vec<_>>()
1828        };
1829        assert_eq!(draws(1), draws(1));
1830        assert_ne!(draws(1), draws(2));
1831        // SplitMix64's published first output for seed 0.
1832        assert_eq!(VariantRng::new(0).next_u64(), 0xE220_A839_7B1D_CDAF);
1833        assert_eq!(VariantRng::new(5).below(0), 0);
1834        assert_eq!(VariantRng::new(5).below(1), 0);
1835    }
1836
1837    /// A backend of the required methods alone, as a new platform's first day
1838    /// is: every provided method is at its default.
1839    #[derive(Default)]
1840    struct Required {
1841        volumes: Vec<(u32, f32)>,
1842        plays: u32,
1843    }
1844
1845    impl Backend for Required {
1846        type Sound = u8;
1847        type Loading = u8;
1848        type Playing = u32;
1849
1850        fn load_sound(&mut self, _: &[u8]) -> impl Future<Output = Result<u8, String>> {
1851            core::future::ready(Ok(0))
1852        }
1853
1854        fn load_music(&mut self, _: &str) -> u8 {
1855            0
1856        }
1857
1858        fn poll_music(&mut self, _: &mut u8) -> Option<Result<u8, String>> {
1859            Some(Ok(0))
1860        }
1861
1862        fn length(&self, _: &u8) -> Option<f64> {
1863            None
1864        }
1865
1866        fn play(&mut self, _: &u8, _: f32, _: bool) -> u32 {
1867            self.plays += 1;
1868            self.plays
1869        }
1870
1871        fn set_volume(&mut self, _: &u8, _: f32) {}
1872
1873        fn stop(&mut self, _: &u8) {}
1874
1875        fn set_playing_volume(&mut self, playing: &u32, volume: f32) {
1876            self.volumes.push((*playing, volume));
1877        }
1878
1879        fn stop_playing(&mut self, _: u32) {}
1880    }
1881
1882    /// The player of the tests' manifest on `backend`.
1883    fn loaded<B: Backend>(backend: &mut B) -> Player<B> {
1884        let (music, voice) = ("music".to_owned(), "voice".to_owned());
1885        let files = files();
1886        let mut player = block_on(Player::load(backend, manifest(), &files, music, voice, 1));
1887        player.take_warnings();
1888        player
1889    }
1890
1891    /// A handle, as a queue gives one.
1892    fn handle() -> SoundId {
1893        shuyi_ui::audio::AudioQueue::<Take>::default().start_sound("rain")
1894    }
1895
1896    fn required() -> (Required, Player<Required>) {
1897        let mut backend = Required::default();
1898        let player = loaded(&mut backend);
1899        (backend, player)
1900    }
1901
1902    #[test]
1903    fn the_provided_methods_default_to_unsupported() {
1904        let mut backend = Required::default();
1905        assert_eq!(RATE_ONE, 65_536);
1906        assert_eq!(backend.pause(&1), Err(Unsupported));
1907        assert_eq!(backend.resume(&1), Err(Unsupported));
1908        assert_eq!(backend.set_rate(&0, RATE_ONE), Err(Unsupported));
1909        backend.warn("said nowhere");
1910        assert_eq!(
1911            Unsupported.to_string(),
1912            "this backend doesn't have this feature"
1913        );
1914        assert_eq!(
1915            block_on(backend.load_sound_file("nope", &files())),
1916            Err("not in the asset bundle".to_owned())
1917        );
1918        let samples = SharedSamples::new(added(4));
1919        assert_eq!(backend.add_sound(&samples), Err(Unsupported.to_string()));
1920        backend.remove_sound(0);
1921    }
1922
1923    #[test]
1924    fn a_backend_that_cant_add_a_sound_warns_and_plays_nothing() {
1925        let (mut backend, mut player) = required();
1926        let mut queue = shuyi_ui::audio::AudioQueue::<Take>::default();
1927        assert_eq!(queue.add_sound("tone", added(4)), Ok(()));
1928        queue.play_sound("tone");
1929        player.hear(&mut backend, &queue.take(), &[], 0.0);
1930        assert_eq!(backend.plays, 0);
1931        assert_eq!(
1932            player.take_warnings(),
1933            [
1934                "sound \"tone\": can't be added: this backend doesn't have this feature",
1935                "unknown sound cue \"tone\""
1936            ]
1937        );
1938    }
1939
1940    /// The acceptance's backend: it pauses twice, warns once, naming the
1941    /// feature, and the sound is muted until it resumes.
1942    #[test]
1943    fn pausing_without_a_pause_warns_once_and_mutes_until_the_resume() {
1944        let (mut backend, mut player) = required();
1945        let start = SoundRequest::start(handle(), "rain", 1.0);
1946        player.play(&mut backend, &[start], &[], 0.0);
1947        for _ in 0..2 {
1948            player.play(&mut backend, &[SoundRequest::PauseSounds], &[], 0.0);
1949            player.play(&mut backend, &[SoundRequest::ResumeSounds], &[], 0.0);
1950        }
1951        assert_eq!(
1952            player.take_warnings(),
1953            ["this backend can't pause sounds: they are muted instead"]
1954        );
1955        assert_eq!(
1956            backend.volumes,
1957            [(1, 0.0), (1, 0.5), (1, 0.0), (1, 0.5)],
1958            "muted until each resume"
1959        );
1960        player.suspend(&mut backend);
1961        player.resume(&mut backend);
1962        assert_eq!(player.take_warnings(), [] as [String; 0]);
1963    }
1964
1965    #[test]
1966    fn a_rate_without_a_rate_warns_once_and_plays_as_recorded() {
1967        let (mut backend, mut player) = required();
1968        player.set_rate(&mut backend, "beep", 2 * RATE_ONE);
1969        player.set_rate(&mut backend, "beep", RATE_ONE);
1970        player.set_rate(&mut backend, "nope", RATE_ONE);
1971        assert_eq!(
1972            player.take_warnings(),
1973            ["this backend can't change a sound's rate: it plays as recorded"]
1974        );
1975    }
1976
1977    /// A device before the game has loaded plays nothing, and after it is
1978    /// the player, its warnings in the backend's log.
1979    #[test]
1980    fn the_device_is_the_player_once_the_game_has_loaded() {
1981        use crate::audio::Audio as _;
1982        let mut device = PlayerDevice::new(Fake::default());
1983        let requests = [SoundRequest::Sound {
1984            cue: "nope".to_owned(),
1985            volume: 1.0,
1986        }];
1987        let frame = AudioFrame {
1988            requests: &requests,
1989            music: &[],
1990            music_volume: 0.5,
1991            sound_volume: 0.5,
1992            voice_volume: 0.5,
1993        };
1994        device.play(&frame, Millis(0));
1995        device.suspend();
1996        device.resume();
1997        device.stop_all();
1998        assert_eq!(device.music_playing(Millis(0)), None);
1999        assert!(format!("{device:?}").contains("loaded: false"));
2000        let player = audio(device.backend_mut());
2001        device.set_player(player);
2002        device.play(&frame, Millis(0));
2003        assert!(format!("{device:?}").contains("loaded: true"));
2004        let said = device.backend_mut().calls();
2005        let nope = "warn unknown sound cue \"nope\"".to_owned();
2006        assert!(said.contains(&nope), "{said:?}");
2007        let music = [cmd(load, "title"), cmd(start, "title")];
2008        device.backend_mut().ready.push("music/title.ogg".into());
2009        let frame = AudioFrame {
2010            requests: &[],
2011            music: &music,
2012            ..frame
2013        };
2014        device.play(&frame, Millis(1000));
2015        assert_eq!(device.music_playing(Millis(2000)), Some(("title", 1.0)));
2016        assert_eq!(device.voice_playing(Millis(2000)), None);
2017        let say = [SoundRequest::Voice {
2018            clip: "a.ogg".into(),
2019        }];
2020        device.play(
2021            &AudioFrame {
2022                requests: &say,
2023                ..frame
2024            },
2025            Millis(2000),
2026        );
2027        assert_eq!(device.voice_playing(Millis(2000)), Some("a.ogg"));
2028        device.backend_mut().calls();
2029        device.suspend();
2030        let said = device.backend_mut().calls();
2031        let muted = "volume music/title.ogg 0.00".to_owned();
2032        assert!(said.contains(&muted), "{said:?}");
2033        let warned = "warn this backend can't pause sounds: they are muted instead".to_owned();
2034        assert!(said.contains(&warned), "{said:?}");
2035        device.resume();
2036        let said = device.backend_mut().calls();
2037        let back = "volume music/title.ogg 0.40".to_owned();
2038        assert!(said.contains(&back), "{said:?}");
2039        device.stop_all();
2040        assert_eq!(device.music_playing(Millis(2000)), None);
2041    }
2042
2043    /// A frame sends the volumes every time: the same ones, to within a
2044    /// rounding, touch nothing playing.
2045    #[test]
2046    fn the_same_sound_volume_again_leaves_a_started_sound_alone() {
2047        let mut fake = Fake::default();
2048        let mut audio = audio(&mut fake);
2049        let start_rain = SoundRequest::start(handle(), "rain", 1.0);
2050        audio.play(&mut fake, &[start_rain], &[], 0.0);
2051        fake.calls();
2052        audio.set_volumes(&mut fake, 1.0, 1.0);
2053        assert_eq!(fake.calls(), [] as [String; 0]);
2054        audio.set_volumes(&mut fake, 1.0, 1.0 - f32::EPSILON);
2055        assert_eq!(fake.calls(), [] as [String; 0]);
2056    }
2057
2058    /// A loaded track starts at its cue's volume times its gain and the music
2059    /// volume, and a fade multiplies with all of them.
2060    #[test]
2061    fn a_ready_track_starts_at_its_gain_and_a_fade_keeps_the_music_volume() {
2062        let mut fake = Fake::default();
2063        fake.ready.push("music/title.ogg".into());
2064        let mut audio = audio(&mut fake);
2065        audio.set_volumes(&mut fake, 0.5, 1.0);
2066        audio.play(&mut fake, &[], &[cmd(load, "title")], 0.0);
2067        fake.calls();
2068        let start_half = MusicCommand::start("title", 0.5);
2069        audio.play(&mut fake, &[], &[start_half], 0.0);
2070        assert_eq!(fake.calls(), ["play music/title.ogg 0.20 looped"]);
2071        audio.play(&mut fake, &[], &[gain("title", 0.25)], 0.0);
2072        assert_eq!(fake.calls(), ["volume music/title.ogg 0.10"]);
2073    }
2074
2075    /// A backend that pauses: what a held playback does, by the numbers.
2076    #[derive(Default)]
2077    struct Pausing {
2078        inner: Fake,
2079        paused: Vec<u32>,
2080    }
2081
2082    impl Backend for Pausing {
2083        type Sound = String;
2084        type Loading = String;
2085        type Playing = u32;
2086
2087        fn load_sound(&mut self, bytes: &[u8]) -> impl Future<Output = Result<String, String>> {
2088            self.inner.load_sound(bytes)
2089        }
2090
2091        fn load_music(&mut self, path: &str) -> String {
2092            self.inner.load_music(path)
2093        }
2094
2095        fn poll_music(&mut self, loading: &mut String) -> Option<Result<String, String>> {
2096            self.inner.poll_music(loading)
2097        }
2098
2099        fn length(&self, sound: &String) -> Option<f64> {
2100            self.inner.length(sound)
2101        }
2102
2103        fn play(&mut self, sound: &String, volume: f32, looped: bool) -> u32 {
2104            self.inner.play(sound, volume, looped)
2105        }
2106
2107        fn set_volume(&mut self, sound: &String, volume: f32) {
2108            self.inner.set_volume(sound, volume);
2109        }
2110
2111        fn stop(&mut self, sound: &String) {
2112            self.inner.stop(sound);
2113        }
2114
2115        fn set_playing_volume(&mut self, playing: &u32, volume: f32) {
2116            self.inner.set_playing_volume(playing, volume);
2117        }
2118
2119        fn stop_playing(&mut self, playing: u32) {
2120            self.inner.stop_playing(playing);
2121        }
2122
2123        fn pause(&mut self, playing: &u32) -> Result<(), Unsupported> {
2124            self.paused.push(*playing);
2125            Ok(())
2126        }
2127
2128        fn resume(&mut self, playing: &u32) -> Result<(), Unsupported> {
2129            self.paused.retain(|paused| paused != playing);
2130            Ok(())
2131        }
2132    }
2133
2134    fn pausing() -> (Pausing, Player<Pausing>) {
2135        let mut backend = Pausing::default();
2136        backend.inner.ready.push("music/title.ogg".into());
2137        let player = loaded(&mut backend);
2138        (backend, player)
2139    }
2140
2141    #[test]
2142    fn a_backend_that_pauses_holds_its_playbacks_and_their_clock() {
2143        let (mut backend, mut player) = pausing();
2144        let start_rain = SoundRequest::start(handle(), "rain", 1.0);
2145        let music = [cmd(load, "title"), cmd(start, "title")];
2146        player.play(&mut backend, &[start_rain], &music, 1.0);
2147        backend.inner.calls();
2148        player.play(&mut backend, &[], &[], 2.0);
2149        player.suspend(&mut backend);
2150        assert_eq!(backend.paused, [1, 2], "the sound and the track");
2151        // No fallback: nothing went silent by its volume.
2152        assert_eq!(backend.inner.calls(), [] as [String; 0]);
2153        // The clock holds at the frame before the suspend.
2154        assert_eq!(player.music_playing(50.0), Some(("title", 1.0)));
2155        player.resume(&mut backend);
2156        assert_eq!(backend.paused, [] as [u32; 0]);
2157        assert_eq!(player.music_playing(50.0), Some(("title", 1.0)));
2158        player.play(&mut backend, &[], &[], 50.0);
2159        assert_eq!(player.music_playing(51.5), Some(("title", 2.5)));
2160        assert_eq!(player.take_warnings(), [] as [String; 0]);
2161    }
2162
2163    /// Paused by a menu and held by a suspend, a playback is paused once, and
2164    /// goes on only when neither holds it.
2165    #[test]
2166    fn a_menus_pause_and_a_suspend_hold_one_playback_once() {
2167        let (mut backend, mut player) = pausing();
2168        let start_rain = SoundRequest::start(handle(), "rain", 1.0);
2169        player.play(&mut backend, &[start_rain], &[], 0.0);
2170        player.play(&mut backend, &[SoundRequest::PauseSounds], &[], 0.0);
2171        assert_eq!(backend.paused, [1]);
2172        player.suspend(&mut backend);
2173        assert_eq!(backend.paused, [1], "not paused twice");
2174        player.play(&mut backend, &[SoundRequest::ResumeSounds], &[], 0.0);
2175        assert_eq!(backend.paused, [1], "still suspended");
2176        player.resume(&mut backend);
2177        assert_eq!(backend.paused, [] as [u32; 0]);
2178        // The other way round: paused by a menu during the suspend.
2179        player.suspend(&mut backend);
2180        assert_eq!(backend.paused, [1]);
2181        player.play(&mut backend, &[SoundRequest::PauseSounds], &[], 0.0);
2182        player.resume(&mut backend);
2183        assert_eq!(backend.paused, [1], "the menu holds it still");
2184        player.play(&mut backend, &[SoundRequest::ResumeSounds], &[], 0.0);
2185        assert_eq!(backend.paused, [] as [u32; 0]);
2186    }
2187
2188    proptest! {
2189        /// Every variant gets its chance, whatever the seed.
2190        #[test]
2191        fn every_variant_gets_picked(seed: u64, n in 1usize..=6) {
2192            let mut rng = VariantRng::new(seed);
2193            let mut seen = vec![false; n];
2194            for _ in 0..200 {
2195                let i = rng.below(n);
2196                prop_assert!(i < n);
2197                seen[i] = true;
2198            }
2199            prop_assert!(seen.iter().all(|&s| s), "{seen:?}");
2200        }
2201    }
2202}