Skip to main content

shuyi_ui/
audio.rs

1//! Audio as data: screens ask for sounds and music through
2//! [`Ctx::audio`](crate::Ctx::audio); [`Game`](crate::Game) hands the
3//! requests, and the [`MusicState`]'s commands, to the platform in each
4//! [`FrameOutput`](crate::FrameOutput). Nothing here plays anything, so
5//! screens stay testable without a sound card.
6//!
7//! Voice clips are asked for by dialogue line id, through
8//! [`Ctx::play_voice`](crate::Ctx::play_voice), which knows which lines
9//! have a clip that may be played. `V` is the game's
10//! [`ClipVariant`]: which of a line's clips plays.
11//!
12//! The engine's widgets and screens play the same few sounds for the same
13//! things on every screen ([`MenuSound`]); which cues those are is the
14//! game's ([`AudioConfig`], from [`GameData::audio_config`]).
15//!
16//! A sound that must be stopped (rain, a hum, a cue that loops) is started
17//! with a handle: [`AudioQueue::start_sound`] gives its [`SoundId`], which
18//! [`stop_sound`](AudioQueue::stop_sound), with or without a fade, and
19//! [`set_sound_volume`](AudioQueue::set_sound_volume) take. It belongs to
20//! the screen that started it: when that screen leaves the stack, its
21//! sounds stop, so a loop can't outlive it. A loop meant to outlive a
22//! screen is started by the screen below it, or is music.
23//!
24//! A game can make a sound at run time, from samples ([`Samples`]) rather than
25//! a file: [`AudioQueue::add_sound`] makes them a cue, played from then on
26//! as a cue of the manifest is, within [`ADDED_SECONDS_MAX`] and
27//! [`ADDED_BYTES_MAX`], and [`AudioQueue::remove_sound`] frees it.
28//!
29//! ```
30//! # use shuyi_ui::audio::{AudioQueue, AudioRequest};
31//! let mut audio = AudioQueue::<()>::default();
32//! audio.play_sound("menu_beep");
33//! audio.play_music("theme");
34//! assert_eq!(audio.pending()[1], AudioRequest::play_music("theme", None));
35//! ```
36
37use crate::prelude::*;
38use shuyi_assets::audio::AudioManifest;
39pub use shuyi_assets::audio::MusicSwitch;
40use shuyi_assets::dialogue::LineId;
41#[cfg(doc)]
42use shuyi_assets::voice::ClipVariant;
43
44#[cfg(doc)]
45use crate::GameData;
46use crate::input::frame_seconds;
47use alloc::collections::{BTreeMap, BTreeSet};
48
49mod added;
50mod handles;
51mod samples;
52
53use added::Added;
54pub use added::{ADDED_BYTES_MAX, ADDED_SECONDS_MAX, AddSoundError};
55use handles::Handle;
56pub use samples::{Samples, SamplesError, SharedSamples};
57
58/// The sounds the engine's widgets and screens play: the same cue means
59/// the same thing on every screen.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum MenuSound {
62    /// The highlight moved.
63    Move,
64    /// Something was confirmed or opened.
65    Select,
66    /// Something was backed out of or closed.
67    Cancel,
68    /// Confirm on something that can't be chosen, e.g. a greyed-out item.
69    Denied,
70}
71
72/// Which of the game's sound cues the engine's widgets and screens play
73/// ([`MenuSound`], and the cursor's tick): the game's
74/// ([`GameData::audio_config`]). The default names them after what they
75/// are (`ui_move`, `ui_select`, `ui_cancel`, `ui_cursor`), with Denied
76/// playing `ui_cancel`.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct AudioConfig {
79    /// [`MenuSound::Move`]'s cue.
80    pub move_cue: &'static str,
81    /// [`MenuSound::Select`]'s cue.
82    pub select_cue: &'static str,
83    /// [`MenuSound::Cancel`]'s cue.
84    pub cancel_cue: &'static str,
85    /// [`MenuSound::Denied`]'s cue.
86    pub denied_cue: &'static str,
87    /// The cue a cursor plays once per step it moves.
88    pub cursor_cue: &'static str,
89}
90
91impl AudioConfig {
92    /// The cues named after what they are.
93    pub const DEFAULT: Self = Self {
94        move_cue: "ui_move",
95        select_cue: "ui_select",
96        cancel_cue: "ui_cancel",
97        denied_cue: "ui_cancel",
98        cursor_cue: "ui_cursor",
99    };
100
101    /// The cue of `sound`.
102    pub const fn cue(&self, sound: MenuSound) -> &'static str {
103        match sound {
104            MenuSound::Move => self.move_cue,
105            MenuSound::Select => self.select_cue,
106            MenuSound::Cancel => self.cancel_cue,
107            MenuSound::Denied => self.denied_cue,
108        }
109    }
110}
111
112impl Default for AudioConfig {
113    fn default() -> Self {
114        Self::DEFAULT
115    }
116}
117
118/// A sound started with a handle ([`AudioQueue::start_sound`]): the
119/// queue's count of them, from 1, so the same calls give the same ids on
120/// every run.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
122pub struct SoundId(u32);
123
124impl SoundId {
125    /// The count behind it.
126    pub const fn get(self) -> u32 {
127        self.0
128    }
129}
130
131/// Something a screen wants heard. Cue ids are the ones in the game's
132/// audio manifest ([`AudioManifest`]).
133#[derive(Debug, Clone, PartialEq)]
134#[non_exhaustive]
135pub enum AudioRequest<V> {
136    /// Play a sound cue once, at `volume` (0–1) times the cue's own volume.
137    PlaySound {
138        /// The sound cue.
139        cue: String,
140        /// A multiplier on the cue's volume, 0–1.
141        volume: f32,
142    },
143    /// Play a sound cue until [`StopSound`](Self::StopSound) of its `id`
144    /// (looped if the cue says so, else once), at `volume` (0–1) times the
145    /// cue's own volume.
146    StartSound {
147        /// Its handle.
148        id: SoundId,
149        /// The sound cue.
150        cue: String,
151        /// A multiplier on the cue's volume, 0–1.
152        volume: f32,
153    },
154    /// Stop the sound started as `id`, at once.
155    StopSound {
156        /// Its handle.
157        id: SoundId,
158    },
159    /// Set the volume of the sound started as `id`.
160    SoundVolume {
161        /// Its handle.
162        id: SoundId,
163        /// The new multiplier on the cue's volume, 0–1.
164        volume: f32,
165    },
166    /// Pause every sound started with a handle that is playing now
167    /// ([`AudioQueue::pause_sounds`]); one started after it plays. Sound
168    /// cues played once, the music and the voice go on.
169    PauseSounds,
170    /// Resume the sounds [`PauseSounds`](Self::PauseSounds) paused.
171    ResumeSounds,
172    /// Switch to a music cue. Asking for the track already playing does
173    /// nothing. Build it with [`AudioRequest::play_music`] (ADR-0039).
174    #[non_exhaustive]
175    PlayMusic {
176        /// The music cue.
177        cue: String,
178        /// How the track playing gives way to it; `None` for the audio
179        /// manifest's default ([`AudioManifest::music_switch`]).
180        switch: Option<MusicSwitch>,
181    },
182    /// Fade the music out over the audio manifest's default fade-out
183    /// ([`MusicSwitch`]'s `out_ms`).
184    StopMusic,
185    /// Fade the music out over `fade_ms`, for this stop only: the time
186    /// from full gain to silence, so a track at half gain takes half of
187    /// it; 0 stops at once. Build it with
188    /// [`AudioRequest::stop_music_with`] (ADR-0039).
189    #[non_exhaustive]
190    StopMusicWith {
191        /// Milliseconds a fade from full gain to silence takes.
192        fade_ms: u32,
193    },
194    /// Say a dialogue line: play its voice clip once, stopping the voice
195    /// that is playing (one voice at a time).
196    PlayVoice {
197        /// The line.
198        line: LineId,
199        /// Which of the line's clips.
200        variant: V,
201    },
202    /// Stop the voice that is playing, if any.
203    StopVoice,
204    /// The clips a scene will ask for, in script order, so the platform can load
205    /// the next few ahead of each [`PlayVoice`](Self::PlayVoice). Replaces
206    /// the list sent before.
207    PreloadVoices {
208        /// The clips, in the order they are expected.
209        lines: Vec<(LineId, V)>,
210    },
211    /// Make `samples` the sound cue `cue` from now on, as a cue of the manifest
212    /// is: [`PlaySound`](Self::PlaySound) and
213    /// [`StartSound`](Self::StartSound) play it at its own volume, full, once.
214    /// A cue added before is replaced. Only [`AudioQueue::add_sound`] makes
215    /// one, within its limits.
216    #[non_exhaustive]
217    AddSound {
218        /// The sound cue.
219        cue: String,
220        /// Its samples, shared with whatever plays them.
221        samples: SharedSamples,
222    },
223    /// Forget the sound cue `cue` that [`AddSound`](Self::AddSound) made,
224    /// stopping what plays it.
225    RemoveSound {
226        /// The sound cue.
227        cue: String,
228    },
229}
230
231impl<V> AudioRequest<V> {
232    /// A [`PlayMusic`](Self::PlayMusic) of `cue`, switching with `switch`
233    /// (`None`: the manifest's default).
234    pub fn play_music(cue: impl Into<String>, switch: Option<MusicSwitch>) -> Self {
235        Self::PlayMusic {
236            cue: cue.into(),
237            switch,
238        }
239    }
240
241    /// A [`StopMusicWith`](Self::StopMusicWith): the music fades out over
242    /// `fade_ms` (0: at once), whatever the manifest's default.
243    pub fn stop_music_with(fade_ms: u32) -> Self {
244        Self::StopMusicWith { fade_ms }
245    }
246
247    /// The cue this request plays, if any: a sound played or started, or
248    /// a track. A sound added or removed is played by none.
249    pub fn cue(&self) -> Option<&str> {
250        match self {
251            Self::PlaySound { cue, .. }
252            | Self::StartSound { cue, .. }
253            | Self::PlayMusic { cue, .. } => Some(cue),
254            Self::AddSound { .. }
255            | Self::RemoveSound { .. }
256            | Self::StopSound { .. }
257            | Self::SoundVolume { .. }
258            | Self::PauseSounds
259            | Self::ResumeSounds
260            | Self::StopMusic
261            | Self::StopMusicWith { .. }
262            | Self::PlayVoice { .. }
263            | Self::StopVoice
264            | Self::PreloadVoices { .. } => None,
265        }
266    }
267}
268
269/// The requests screens made this frame. `Game` empties it every frame.
270/// It knows which cues the [`MenuSound`]s are ([`AudioConfig`]), and the
271/// sounds started with a handle that haven't stopped.
272#[derive(Debug, Clone, PartialEq)]
273pub struct AudioQueue<V> {
274    requests: Vec<AudioRequest<V>>,
275    config: AudioConfig,
276    /// The last [`SoundId`] given; 0 before the first.
277    last_id: u32,
278    /// The sounds started with a handle and not yet stopped.
279    handles: BTreeMap<SoundId, Handle>,
280    /// The depth on the screen stack of the screen running now: the owner
281    /// of the sounds it starts.
282    owner: usize,
283    /// The owners pausing the sounds now: the depths of screens, and
284    /// the owner outside the stack that no pop releases.
285    pauses: BTreeSet<usize>,
286    /// The sounds added at run time, and what was refused.
287    added: Added,
288}
289
290impl<V> Default for AudioQueue<V> {
291    fn default() -> Self {
292        Self::new(AudioConfig::DEFAULT)
293    }
294}
295
296impl<V> AudioQueue<V> {
297    /// An empty queue that plays the [`MenuSound`]s with `config`'s cues.
298    pub fn new(config: AudioConfig) -> Self {
299        Self {
300            requests: Vec::new(),
301            config,
302            last_id: 0,
303            handles: BTreeMap::new(),
304            owner: 0,
305            pauses: BTreeSet::new(),
306            added: Added::default(),
307        }
308    }
309
310    /// Which cues the [`MenuSound`]s are.
311    pub fn config(&self) -> AudioConfig {
312        self.config
313    }
314
315    /// Plays a menu sound: its cue in the [`config`](Self::config).
316    pub fn menu(&mut self, sound: MenuSound) {
317        self.play_sound(self.config.cue(sound));
318    }
319
320    /// Plays the cursor's tick, once per step it moves.
321    pub fn cursor_tick(&mut self) {
322        self.play_sound(self.config.cursor_cue);
323    }
324
325    /// Plays sound `cue` once at its own volume.
326    pub fn play_sound(&mut self, cue: &str) {
327        self.play_sound_at(cue, 1.0);
328    }
329
330    /// Plays sound `cue` once at `volume` (0–1, clamped) times its own
331    /// volume. A cue [`remove_sound`](Self::remove_sound) removed plays
332    /// nothing.
333    pub fn play_sound_at(&mut self, cue: &str, volume: f32) {
334        if self.added.silences(cue) {
335            return;
336        }
337        self.requests.push(AudioRequest::PlaySound {
338            cue: cue.to_owned(),
339            volume: clamp_volume(volume),
340        });
341    }
342
343    /// Switches the music to `cue` (nothing happens if it's already on),
344    /// as the audio manifest's default switch says.
345    pub fn play_music(&mut self, cue: &str) {
346        self.requests.push(AudioRequest::play_music(cue, None));
347    }
348
349    /// Switches the music to `cue` as `switch` says (nothing happens if
350    /// it's already on): how long the old track fades out and the new one
351    /// fades in, and whether the two overlap.
352    pub fn play_music_with(&mut self, cue: &str, switch: MusicSwitch) {
353        self.requests
354            .push(AudioRequest::play_music(cue, Some(switch)));
355    }
356
357    /// Fades the music out over the audio manifest's default fade-out.
358    pub fn stop_music(&mut self) {
359        self.requests.push(AudioRequest::StopMusic);
360    }
361
362    /// Fades the music out over `fade_ms` instead of the default, for
363    /// this stop only: the time from full gain to silence (0 stops at
364    /// once). A quiet interval before the next track is the caller's to
365    /// time.
366    pub fn stop_music_with(&mut self, fade_ms: u32) {
367        self.requests.push(AudioRequest::stop_music_with(fade_ms));
368    }
369
370    /// Plays the `variant` clip of `line`. Screens call
371    /// [`Ctx::play_voice`](crate::Ctx::play_voice) instead, which asks only
372    /// for clips that exist and only while voices are on.
373    pub fn play_voice(&mut self, line: &LineId, variant: V) {
374        self.requests.push(AudioRequest::PlayVoice {
375            line: line.clone(),
376            variant,
377        });
378    }
379
380    /// Stops the voice that is playing, if any.
381    pub fn stop_voice(&mut self) {
382        self.requests.push(AudioRequest::StopVoice);
383    }
384
385    /// Tells the platform which clips are coming, in order
386    /// ([`Ctx::preload_voices`](crate::Ctx::preload_voices)).
387    pub fn preload_voices(&mut self, lines: Vec<(LineId, V)>) {
388        self.requests.push(AudioRequest::PreloadVoices { lines });
389    }
390
391    /// The requests made since the last [`take`](Self::take), in order.
392    pub fn pending(&self) -> &[AudioRequest<V>] {
393        &self.requests
394    }
395
396    /// Empties the queue, returning its requests in order.
397    pub fn take(&mut self) -> Vec<AudioRequest<V>> {
398        core::mem::take(&mut self.requests)
399    }
400}
401
402/// `volume` within 0–1; NaN is 0.
403fn clamp_volume(volume: f32) -> f32 {
404    if volume.is_nan() {
405        0.0
406    } else {
407        volume.clamp(0.0, 1.0)
408    }
409}
410
411/// The music cue pool `pool` yields for `seed`: the same seed always gives
412/// the same cue. `None` if the manifest has no such pool or it is empty.
413/// This is not a game's simulation RNG, so a pick never changes a
414/// battle or its replay.
415pub fn pick_from_pool<'a>(manifest: &'a AudioManifest, pool: &str, seed: u64) -> Option<&'a str> {
416    let cues = manifest.pools.get(pool)?;
417    let len = u64::try_from(cues.len()).ok().filter(|&n| n > 0)?;
418    let index = usize::try_from(splitmix64(seed) % len).ok()?;
419    cues.get(index).map(String::as_str)
420}
421
422/// One step of splitmix64: spreads nearby seeds (e.g. a counter) over the
423/// whole range.
424pub fn splitmix64(seed: u64) -> u64 {
425    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
426    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
427    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
428    z ^ (z >> 31)
429}
430
431/// Where the music is in its track, for a screen that keeps time with it,
432/// read from [`Ctx::music_clock`](crate::Ctx::music_clock). The platform
433/// reports what really sounds (a track starts only once its file has
434/// loaded); [`Game::set_music_playing`](crate::Game::set_music_playing)
435/// turns that into this.
436#[derive(Debug, Clone, PartialEq)]
437pub struct MusicClock {
438    /// The music cue sounding.
439    pub cue: String,
440    /// Seconds into the track: from 0 up to, never reaching, `length`. A
441    /// looped track is back at 0 each time it starts again.
442    pub position: f32,
443    /// The track's length in seconds (the manifest's `length_ms`).
444    pub length: f32,
445}
446
447impl MusicClock {
448    /// The clock of `cue`, which started `elapsed` seconds ago (a negative
449    /// time counts as 0). `None` if the cue is unknown, or isn't looped and
450    /// has ended (or `elapsed` isn't a number).
451    pub fn from_elapsed(manifest: &AudioManifest, cue: &str, elapsed: f64) -> Option<Self> {
452        let track = manifest.music.get(cue)?;
453        let length = f64::from(track.length_ms) / 1000.0;
454        if !elapsed.is_finite() || track.length_ms == 0 {
455            return None;
456        }
457        let elapsed = elapsed.max(0.0);
458        let position = if track.looped {
459            elapsed % length
460        } else {
461            elapsed
462        };
463        // Seconds into one track: f32 is exact to far less than a millisecond.
464        #[allow(clippy::cast_possible_truncation)]
465        let (position, length) = (position as f32, length as f32);
466        let position = if position < length {
467            position
468        } else if track.looped {
469            // Rounded up onto the length: that is the loop point.
470            0.0
471        } else {
472            return None;
473        };
474        Some(Self {
475            cue: cue.to_owned(),
476            position,
477            length,
478        })
479    }
480}
481
482/// What the platform must do to the music this frame. A track is loaded, started,
483/// has its volume changed and is stopped, which also unloads it.
484#[derive(Debug, Clone, PartialEq, serde::Serialize)]
485pub enum MusicCommand {
486    /// Start loading `cue`'s file; it will be started soon.
487    Load {
488        /// The music cue.
489        cue: String,
490    },
491    /// Play `cue` (loaded earlier) from the start at `gain`, looped if the
492    /// manifest says so. If it isn't loaded yet, play it once it is. Build
493    /// it with [`MusicCommand::start`] (ADR-0039).
494    #[non_exhaustive]
495    Start {
496        /// The music cue.
497        cue: String,
498        /// The gain it starts at (0–1, a multiplier on its manifest
499        /// volume): 1 for a track that starts at full gain, 0 for one that
500        /// fades in.
501        gain: f32,
502    },
503    /// Set `cue`'s gain (0–1, a multiplier on its manifest volume).
504    Gain {
505        /// The music cue.
506        cue: String,
507        /// The new gain.
508        gain: f32,
509    },
510    /// Stop `cue` and free it (also cancels a load).
511    Stop {
512        /// The music cue.
513        cue: String,
514    },
515}
516
517impl MusicCommand {
518    /// A [`Start`](Self::Start) of `cue` at `gain`.
519    pub fn start(cue: impl Into<String>, gain: f32) -> Self {
520        Self::Start {
521            cue: cue.into(),
522            gain,
523        }
524    }
525}
526
527/// Which tracks play, and how one gives way to the next (ADR-0054): each
528/// switch is a [`MusicSwitch`], the request's own or the default one. The
529/// old track fades out; the new one starts once it has stopped, or as it
530/// begins to fade (a crossfade), and may fade in. A cue already playing
531/// doesn't restart, and at most two tracks are loaded at once. A fade-in,
532/// and an old track's fade in a crossfade, wait until the new track sounds
533/// ([`heard`](Self::heard)), or [`LOAD_WAIT_SECS`](Self::LOAD_WAIT_SECS).
534/// Pure: feed it requests, time and what sounds, read the commands.
535#[derive(Debug, Clone, PartialEq)]
536pub struct MusicState {
537    /// The switch of a request that names none, and the fade of a stop.
538    default: MusicSwitch,
539    /// The older track: the one playing, or fading out during a switch.
540    current: Option<Track>,
541    /// The newer track: the one a switch brings in, waiting for `current`
542    /// to stop or sounding beside it; after a stop during a crossfade,
543    /// fading out beside it. Never set without `current`.
544    next: Option<Track>,
545}
546
547/// One track between its `Load` and its `Stop`.
548#[derive(Debug, Clone, PartialEq)]
549struct Track {
550    cue: String,
551    /// Whether it was sent `Start`.
552    started: bool,
553    /// Whether the music clock has named it since its `Start`.
554    heard: bool,
555    /// Seconds since its `Start`, while it isn't heard (up to the load
556    /// wait).
557    waited: f32,
558    /// Its gain, rising to 1 (playing or fading in) or falling to 0
559    /// (fading out, then stopped).
560    ramp: Ramp,
561}
562
563/// A gain moving in a straight line to 0 or 1, at one whole fade per
564/// `secs`: a fade from a gain part-way takes its share of the time.
565#[derive(Debug, Clone, PartialEq)]
566struct Ramp {
567    /// The gain when it began.
568    from: f32,
569    /// Whether it ends at 1 (else at 0).
570    rising: bool,
571    /// Seconds a fade from one end to the other takes.
572    secs: f32,
573    /// Seconds it has run.
574    elapsed: f32,
575}
576
577impl Ramp {
578    /// A gain that stays at `gain`.
579    fn at(gain: f32) -> Self {
580        Self {
581            from: gain,
582            rising: gain >= 1.0,
583            secs: 0.0,
584            elapsed: 0.0,
585        }
586    }
587
588    fn to(&self) -> f32 {
589        if self.rising { 1.0 } else { 0.0 }
590    }
591
592    /// Whether it has reached its end.
593    fn done(&self) -> bool {
594        self.elapsed >= self.secs * (self.from - self.to()).abs()
595    }
596
597    fn gain(&self) -> f32 {
598        if self.done() {
599            return self.to();
600        }
601        // Not done, so `secs` is above 0.
602        let moved = self.elapsed / self.secs;
603        let gain = if self.rising {
604            self.from + moved
605        } else {
606            self.from - moved
607        };
608        gain.clamp(0.0, 1.0)
609    }
610}
611
612impl Track {
613    /// `cue`, loaded and not started: it starts at full gain, or at 0 to
614    /// fade in over `switch`'s `in_ms`.
615    fn new(cue: &str, switch: MusicSwitch) -> Self {
616        let from = if switch.in_ms == 0 { 1.0 } else { 0.0 };
617        Self {
618            cue: cue.into(),
619            started: false,
620            heard: false,
621            waited: 0.0,
622            ramp: Ramp {
623                from,
624                rising: true,
625                secs: secs(switch.in_ms),
626                elapsed: 0.0,
627            },
628        }
629    }
630
631    fn start(&mut self, out: &mut Vec<MusicCommand>) {
632        self.started = true;
633        out.push(MusicCommand::start(self.cue.clone(), self.ramp.gain()));
634    }
635
636    /// Sends the gain toward 1 (`rising`) or 0, a whole fade taking `ms`.
637    /// A ramp already going there at that pace keeps its progress.
638    fn aim(&mut self, rising: bool, ms: u32) {
639        let secs = secs(ms);
640        if self.ramp.rising != rising || self.ramp.secs.to_bits() != secs.to_bits() {
641            self.ramp = Ramp {
642                from: self.ramp.gain(),
643                rising,
644                secs,
645                elapsed: 0.0,
646            };
647        }
648    }
649
650    /// Whether it sounds, or has been waited for long enough.
651    fn sounds(&self) -> bool {
652        self.heard || self.waited >= MusicState::LOAD_WAIT_SECS
653    }
654
655    /// Whether it is started, fading in and not yet sounding: the old
656    /// track holds its gain until it does.
657    fn coming_in_unheard(&self) -> bool {
658        self.started && self.ramp.rising && !self.sounds()
659    }
660
661    /// Counts `dt` more seconds waited for it to sound.
662    fn wait(&mut self, dt: f32) {
663        if self.started && !self.heard {
664            self.waited = (self.waited + dt).min(MusicState::LOAD_WAIT_SECS);
665        }
666    }
667
668    /// Advances its fade by `dt`, appending the gain, or the stop when a
669    /// fade-out ends (then it returns `true`). A track waiting to start,
670    /// or to sound before it fades in, doesn't move.
671    fn step(&mut self, dt: f32, out: &mut Vec<MusicCommand>) -> bool {
672        if !self.started {
673            return false;
674        }
675        let rising = self.ramp.rising;
676        if rising && (self.ramp.done() || !self.sounds()) {
677            return false;
678        }
679        self.ramp.elapsed += dt;
680        if !rising && self.ramp.done() {
681            out.push(MusicCommand::Stop {
682                cue: self.cue.clone(),
683            });
684            return true;
685        }
686        out.push(MusicCommand::Gain {
687            cue: self.cue.clone(),
688            gain: self.ramp.gain(),
689        });
690        false
691    }
692}
693
694/// `ms` in seconds.
695fn secs(ms: u32) -> f32 {
696    // A fade is far shorter than 2^24 ms.
697    #[allow(clippy::cast_precision_loss)]
698    let ms = ms as f32;
699    ms / 1000.0
700}
701
702impl MusicState {
703    /// How long a fade waits for the new track to sound, in seconds: after
704    /// that it runs anyway, so a file that never loads never holds the old
705    /// track.
706    pub const LOAD_WAIT_SECS: f32 = 3.0;
707
708    /// Silence, with switches that fade out over `fade_secs` (to the
709    /// millisecond; negative or NaN counts as 0), then start the new track
710    /// at full gain ([`MusicSwitch::after_fade`]).
711    pub fn new(fade_secs: f32) -> Self {
712        // Clamped to 0 above, and a fade is far shorter than 2^32 ms.
713        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
714        let ms = (fade_secs.max(0.0) * 1000.0).round() as u32;
715        Self::with_switch(MusicSwitch::after_fade(ms))
716    }
717
718    /// Silence, switching with `default` when a request names no switch,
719    /// and stopping with its `out_ms`.
720    pub fn with_switch(default: MusicSwitch) -> Self {
721        Self {
722            default,
723            current: None,
724            next: None,
725        }
726    }
727
728    /// The track playing, or fading out during a switch.
729    pub fn current(&self) -> Option<&str> {
730        self.current.as_ref().map(|t| t.cue.as_str())
731    }
732
733    /// The track a switch brings in, or the current one; `None` while the
734    /// music fades to silence.
735    pub fn target(&self) -> Option<&str> {
736        [&self.next, &self.current]
737            .into_iter()
738            .flatten()
739            .find(|t| t.ramp.rising)
740            .map(|t| t.cue.as_str())
741    }
742
743    /// Tells it what sounds: the music clock the platform reported for this
744    /// frame ([`Ctx::music_clock`](crate::Ctx::music_clock)). A track sounds
745    /// once the clock names it, and stays sounding after.
746    /// [`Game::set_music_playing`](crate::Game::set_music_playing) calls it
747    /// every frame.
748    pub fn heard(&mut self, clock: Option<&MusicClock>) {
749        let Some(clock) = clock else {
750            return;
751        };
752        for track in [&mut self.current, &mut self.next].into_iter().flatten() {
753            if track.started && track.cue == clock.cue {
754                track.heard = true;
755            }
756        }
757    }
758
759    /// Applies one request (sound and voice requests are ignored),
760    /// appending the commands it needs to `out`.
761    pub fn request<V>(&mut self, request: &AudioRequest<V>, out: &mut Vec<MusicCommand>) {
762        match request {
763            AudioRequest::PlaySound { .. }
764            | AudioRequest::StartSound { .. }
765            | AudioRequest::StopSound { .. }
766            | AudioRequest::SoundVolume { .. }
767            | AudioRequest::PauseSounds
768            | AudioRequest::ResumeSounds
769            | AudioRequest::PlayVoice { .. }
770            | AudioRequest::StopVoice
771            | AudioRequest::PreloadVoices { .. }
772            | AudioRequest::AddSound { .. }
773            | AudioRequest::RemoveSound { .. } => {}
774            AudioRequest::PlayMusic { cue, switch } => {
775                self.play(cue, switch.unwrap_or(self.default), out);
776            }
777            AudioRequest::StopMusic => self.stop(self.default.out_ms, out),
778            AudioRequest::StopMusicWith { fade_ms } => self.stop(*fade_ms, out),
779        }
780    }
781
782    fn play(&mut self, cue: &str, switch: MusicSwitch, out: &mut Vec<MusicCommand>) {
783        if self.current.is_none() {
784            let mut track = Track::new(cue, switch);
785            out.push(MusicCommand::Load { cue: cue.into() });
786            track.start(out);
787            self.current = Some(track);
788            return;
789        }
790        if self.target() == Some(cue) {
791            return;
792        }
793        self.drop_waiting(out);
794        if let Some(mut back) = self.take(cue) {
795            // Asked again for a track that is fading out: it comes back,
796            // and the other one goes.
797            if let Some(other) = &mut self.current {
798                other.aim(false, switch.out_ms);
799            }
800            if switch.in_ms == 0 {
801                back.ramp = Ramp::at(1.0);
802                out.push(MusicCommand::Gain {
803                    cue: cue.into(),
804                    gain: 1.0,
805                });
806            } else {
807                back.aim(true, switch.in_ms);
808            }
809            self.push(back);
810            return;
811        }
812        if self.next.is_some() {
813            // Two tracks sound: the older one stops at once.
814            if let Some(older) = self.current.take() {
815                out.push(MusicCommand::Stop { cue: older.cue });
816            }
817            self.current = self.next.take();
818        }
819        if let Some(old) = &mut self.current {
820            old.aim(false, switch.out_ms);
821        }
822        out.push(MusicCommand::Load { cue: cue.into() });
823        let mut track = Track::new(cue, switch);
824        if switch.overlap {
825            track.start(out);
826        }
827        self.next = Some(track);
828    }
829
830    /// Stops a track waiting for the old one to fade: a newer request wins.
831    fn drop_waiting(&mut self, out: &mut Vec<MusicCommand>) {
832        if let Some(next) = self.next.take_if(|t| !t.started) {
833            out.push(MusicCommand::Stop { cue: next.cue });
834        }
835    }
836
837    /// Takes `cue`'s track out, if it has one; the other stays `current`.
838    fn take(&mut self, cue: &str) -> Option<Track> {
839        if let Some(next) = self.next.take_if(|t| t.cue == cue) {
840            return Some(next);
841        }
842        let current = self.current.take_if(|t| t.cue == cue)?;
843        self.current = self.next.take();
844        Some(current)
845    }
846
847    /// Adds `track` as the newer one.
848    fn push(&mut self, track: Track) {
849        if self.current.is_none() {
850            self.current = Some(track);
851        } else {
852            self.next = Some(track);
853        }
854    }
855
856    /// Fades every track out, a whole fade taking `fade_ms`, and drops
857    /// the one waiting to start.
858    fn stop(&mut self, fade_ms: u32, out: &mut Vec<MusicCommand>) {
859        self.drop_waiting(out);
860        for track in [&mut self.current, &mut self.next].into_iter().flatten() {
861            track.aim(false, fade_ms);
862        }
863    }
864
865    /// Advances the fades by `dt` seconds, appending the gain changes, the
866    /// stop of a track faded out and the start of the one waiting for it.
867    pub fn update(&mut self, dt: f32, out: &mut Vec<MusicCommand>) {
868        let dt = frame_seconds(dt);
869        for track in [&mut self.current, &mut self.next].into_iter().flatten() {
870            track.wait(dt);
871        }
872        // In a crossfade the old track holds its gain until the new one
873        // sounds.
874        let waiting = self.next.as_ref().is_some_and(Track::coming_in_unheard);
875        if let Some(current) = &mut self.current {
876            let held = waiting && !current.ramp.rising;
877            if !held && current.step(dt, out) {
878                self.current = None;
879            }
880        }
881        if let Some(next) = &mut self.next
882            && next.step(dt, out)
883        {
884            self.next = None;
885        }
886        if self.current.is_none() {
887            self.current = self.next.take();
888            if let Some(track) = self.current.as_mut().filter(|t| !t.started) {
889                track.start(out);
890            }
891        }
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898    use crate::toy::Voice;
899    use shuyi_assets::audio::{CreditRef, MusicCue};
900
901    type AudioRequest = super::AudioRequest<Voice>;
902    type AudioQueue = super::AudioQueue<Voice>;
903
904    fn play(cue: &str) -> AudioRequest {
905        AudioRequest::PlayMusic {
906            cue: cue.into(),
907            switch: None,
908        }
909    }
910
911    fn load(cue: &str) -> MusicCommand {
912        MusicCommand::Load { cue: cue.into() }
913    }
914
915    fn start(cue: &str) -> MusicCommand {
916        MusicCommand::Start {
917            cue: cue.into(),
918            gain: 1.0,
919        }
920    }
921
922    fn stop(cue: &str) -> MusicCommand {
923        MusicCommand::Stop { cue: cue.into() }
924    }
925
926    fn gain(cue: &str, gain: f32) -> MusicCommand {
927        MusicCommand::Gain {
928            cue: cue.into(),
929            gain,
930        }
931    }
932
933    /// Applies `requests` then advances `dt`, returning the commands.
934    fn frame(m: &mut MusicState, requests: &[AudioRequest], dt: f32) -> Vec<MusicCommand> {
935        let mut out = Vec::new();
936        for r in requests {
937            m.request(r, &mut out);
938        }
939        m.update(dt, &mut out);
940        out
941    }
942
943    /// Music with a 0.5 s fade, playing `cue`.
944    fn playing(cue: &str) -> MusicState {
945        let mut m = MusicState::new(0.5);
946        frame(&mut m, &[play(cue)], 0.0);
947        m
948    }
949
950    #[test]
951    fn the_first_track_starts_at_once() {
952        let mut m = MusicState::new(0.5);
953        assert_eq!(m.current(), None);
954        assert_eq!(
955            frame(&mut m, &[play("title")], 0.1),
956            [load("title"), start("title")]
957        );
958        assert_eq!(m.current(), Some("title"));
959        assert_eq!(m.target(), Some("title"));
960        assert_eq!(frame(&mut m, &[], 0.1), []);
961    }
962
963    #[test]
964    fn the_same_cue_does_not_restart() {
965        let mut m = playing("title");
966        assert_eq!(frame(&mut m, &[play("title"), play("title")], 0.1), []);
967        assert_eq!(m.current(), Some("title"));
968    }
969
970    #[test]
971    fn a_switch_fades_out_then_starts_the_new_track() {
972        let mut m = playing("title");
973        assert_eq!(
974            frame(&mut m, &[play("battle")], 0.0),
975            [load("battle"), gain("title", 1.0)]
976        );
977        assert_eq!(m.current(), Some("title"));
978        assert_eq!(m.target(), Some("battle"));
979        assert_eq!(frame(&mut m, &[], 0.25), [gain("title", 0.5)]);
980        assert_eq!(frame(&mut m, &[], 0.125), [gain("title", 0.25)]);
981        // Asking again for the incoming track changes nothing.
982        assert_eq!(frame(&mut m, &[play("battle")], 0.0), [gain("title", 0.25)]);
983        assert_eq!(frame(&mut m, &[], 0.125), [stop("title"), start("battle")]);
984        assert_eq!(m.current(), Some("battle"));
985        assert_eq!(frame(&mut m, &[], 0.1), []);
986    }
987
988    #[test]
989    fn stop_music_fades_out_to_silence() {
990        let mut m = playing("title");
991        let stop_req = AudioRequest::StopMusic;
992        assert_eq!(
993            frame(&mut m, core::slice::from_ref(&stop_req), 0.25),
994            [gain("title", 0.5)]
995        );
996        assert_eq!(m.target(), None);
997        assert_eq!(
998            frame(&mut m, core::slice::from_ref(&stop_req), 0.0),
999            [gain("title", 0.5)]
1000        );
1001        assert_eq!(frame(&mut m, &[], 0.3), [stop("title")]);
1002        assert_eq!(m.current(), None);
1003        // Stopping silence does nothing.
1004        assert_eq!(frame(&mut m, &[stop_req], 1.0), []);
1005    }
1006
1007    #[test]
1008    fn a_new_request_during_a_fade_wins() {
1009        let mut m = playing("title");
1010        frame(&mut m, &[play("battle")], 0.25);
1011        // The waiting track is dropped; the fade keeps its progress.
1012        assert_eq!(
1013            frame(&mut m, &[play("boss")], 0.0),
1014            [stop("battle"), load("boss"), gain("title", 0.5)]
1015        );
1016        assert_eq!(frame(&mut m, &[], 0.25), [stop("title"), start("boss")]);
1017        assert_eq!(m.current(), Some("boss"));
1018        // A stop during a switch drops the waiting track too.
1019        frame(&mut m, &[play("town")], 0.25);
1020        assert_eq!(
1021            frame(&mut m, &[AudioRequest::StopMusic], 0.0),
1022            [stop("town"), gain("boss", 0.5)]
1023        );
1024        assert_eq!(frame(&mut m, &[], 0.25), [stop("boss")]);
1025        assert_eq!(m.current(), None);
1026        // And a play during a fade to silence switches to it.
1027        let mut m = playing("title");
1028        frame(&mut m, &[AudioRequest::StopMusic], 0.25);
1029        assert_eq!(
1030            frame(&mut m, &[play("battle")], 0.25),
1031            [load("battle"), stop("title"), start("battle")]
1032        );
1033    }
1034
1035    #[test]
1036    fn asking_for_the_fading_track_keeps_it() {
1037        let mut m = playing("title");
1038        frame(&mut m, &[play("battle")], 0.25);
1039        assert_eq!(
1040            frame(&mut m, &[play("title")], 1.0),
1041            [stop("battle"), gain("title", 1.0)]
1042        );
1043        assert_eq!(m.current(), Some("title"));
1044        assert_eq!(m.target(), Some("title"));
1045        let mut m = playing("title");
1046        frame(&mut m, &[AudioRequest::StopMusic], 0.25);
1047        assert_eq!(frame(&mut m, &[play("title")], 1.0), [gain("title", 1.0)]);
1048        assert_eq!(m.current(), Some("title"));
1049    }
1050
1051    #[test]
1052    fn a_zero_fade_switches_in_the_same_frame() {
1053        for secs in [0.0, -1.0, f32::NAN] {
1054            let mut m = MusicState::new(secs);
1055            frame(&mut m, &[play("title")], 0.0);
1056            assert_eq!(
1057                frame(&mut m, &[play("battle")], 0.0),
1058                [load("battle"), stop("title"), start("battle")]
1059            );
1060        }
1061    }
1062
1063    #[test]
1064    fn bad_frame_times_do_not_advance_a_fade() {
1065        let mut m = playing("title");
1066        frame(&mut m, &[play("battle")], 0.25);
1067        for dt in [-1.0, f32::NAN, f32::INFINITY] {
1068            assert_eq!(frame(&mut m, &[], dt), [gain("title", 0.5)]);
1069        }
1070    }
1071
1072    fn play_with(cue: &str, switch: MusicSwitch) -> AudioRequest {
1073        AudioRequest::play_music(cue, Some(switch))
1074    }
1075
1076    fn start_at(cue: &str, gain: f32) -> MusicCommand {
1077        MusicCommand::start(cue, gain)
1078    }
1079
1080    /// The clock of `cue`, as the platform reports a track that sounds.
1081    fn clock(cue: &str) -> MusicClock {
1082        MusicClock {
1083            cue: cue.into(),
1084            position: 0.0,
1085            length: 10.0,
1086        }
1087    }
1088
1089    /// Music with a 0.5 s fade, playing `cue`, which sounds.
1090    fn sounding(cue: &str) -> MusicState {
1091        let mut m = playing(cue);
1092        m.heard(Some(&clock(cue)));
1093        m
1094    }
1095
1096    /// `sounding("title")`, then a crossfade of 1 s to `battle`, which
1097    /// sounds and has run `secs`.
1098    fn crossfading(secs: f32) -> MusicState {
1099        let mut m = sounding("title");
1100        frame(
1101            &mut m,
1102            &[play_with("battle", MusicSwitch::crossfade(1000))],
1103            0.0,
1104        );
1105        m.heard(Some(&clock("battle")));
1106        frame(&mut m, &[], secs);
1107        m
1108    }
1109
1110    #[test]
1111    fn a_crossfade_starts_the_new_track_at_once_and_fades_both_once_it_sounds() {
1112        let mut m = sounding("title");
1113        let crossfade = play_with("battle", MusicSwitch::crossfade(1000));
1114        assert_eq!(
1115            frame(&mut m, &[crossfade], 0.0),
1116            [load("battle"), start_at("battle", 0.0)]
1117        );
1118        assert_eq!((m.current(), m.target()), (Some("title"), Some("battle")));
1119        // Until the new track sounds, the old one keeps its gain.
1120        assert_eq!(frame(&mut m, &[], 0.25), []);
1121        m.heard(Some(&clock("title")));
1122        assert_eq!(frame(&mut m, &[], 0.25), []);
1123        m.heard(Some(&clock("battle")));
1124        assert_eq!(
1125            frame(&mut m, &[], 0.5),
1126            [gain("title", 0.5), gain("battle", 0.5)]
1127        );
1128        assert_eq!(
1129            frame(&mut m, &[], 0.5),
1130            [stop("title"), gain("battle", 1.0)]
1131        );
1132        assert_eq!((m.current(), m.target()), (Some("battle"), Some("battle")));
1133        assert_eq!(frame(&mut m, &[], 0.5), []);
1134    }
1135
1136    #[test]
1137    fn a_fade_in_starts_at_silence_and_rises_once_the_track_sounds() {
1138        let mut m = MusicState::with_switch(MusicSwitch::new(500, 1000, false));
1139        assert_eq!(
1140            frame(&mut m, &[play("title")], 0.5),
1141            [load("title"), start_at("title", 0.0)]
1142        );
1143        // No clock, or another cue's, isn't this track sounding.
1144        m.heard(None);
1145        m.heard(Some(&clock("battle")));
1146        assert_eq!(frame(&mut m, &[], 0.5), []);
1147        m.heard(Some(&clock("title")));
1148        assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 0.5)]);
1149        // It stays sounding without being named again.
1150        m.heard(None);
1151        assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 1.0)]);
1152        assert_eq!(frame(&mut m, &[], 0.5), []);
1153    }
1154
1155    #[test]
1156    fn without_overlap_the_new_track_fades_in_after_the_old_one_stops() {
1157        let mut m = sounding("title");
1158        let switch = play_with("battle", MusicSwitch::new(500, 1000, false));
1159        assert_eq!(
1160            frame(&mut m, core::slice::from_ref(&switch), 0.25),
1161            [load("battle"), gain("title", 0.5)]
1162        );
1163        // The clock can't name a track before its `Start`.
1164        m.heard(Some(&clock("battle")));
1165        assert_eq!(
1166            frame(&mut m, &[], 0.25),
1167            [stop("title"), start_at("battle", 0.0)]
1168        );
1169        assert_eq!(frame(&mut m, &[], 0.5), []);
1170        m.heard(Some(&clock("battle")));
1171        assert_eq!(frame(&mut m, &[], 0.25), [gain("battle", 0.25)]);
1172        // A track waiting to start doesn't count the load wait: after a
1173        // fade-out as long as the wait, it still waits to sound.
1174        let mut m = sounding("title");
1175        let switch = play_with("battle", MusicSwitch::new(3000, 1000, false));
1176        frame(&mut m, &[switch], 0.0);
1177        assert_eq!(frame(&mut m, &[], 1.5), [gain("title", 0.5)]);
1178        assert_eq!(
1179            frame(&mut m, &[], 1.5),
1180            [stop("title"), start_at("battle", 0.0)]
1181        );
1182        assert_eq!(frame(&mut m, &[], 1.0), []);
1183    }
1184
1185    #[test]
1186    fn a_track_that_never_sounds_is_faded_in_after_the_load_wait() {
1187        let mut m = sounding("title");
1188        frame(
1189            &mut m,
1190            &[play_with("battle", MusicSwitch::crossfade(1000))],
1191            0.0,
1192        );
1193        assert_eq!(frame(&mut m, &[], 2.5), []);
1194        assert_eq!(MusicState::LOAD_WAIT_SECS, 3.0);
1195        assert_eq!(
1196            frame(&mut m, &[], 0.5),
1197            [gain("title", 0.5), gain("battle", 0.5)]
1198        );
1199        assert_eq!(
1200            frame(&mut m, &[], 0.5),
1201            [stop("title"), gain("battle", 1.0)]
1202        );
1203    }
1204
1205    #[test]
1206    fn a_switch_during_a_crossfade_stops_the_older_track_and_fades_out_the_newer() {
1207        let mut m = crossfading(0.5);
1208        assert_eq!(
1209            frame(
1210                &mut m,
1211                &[play_with("boss", MusicSwitch::crossfade(1000))],
1212                0.0
1213            ),
1214            [stop("title"), load("boss"), start_at("boss", 0.0)]
1215        );
1216        assert_eq!((m.current(), m.target()), (Some("battle"), Some("boss")));
1217        m.heard(Some(&clock("boss")));
1218        // `battle` falls from the 0.5 it had reached.
1219        assert_eq!(
1220            frame(&mut m, &[], 0.25),
1221            [gain("battle", 0.25), gain("boss", 0.25)]
1222        );
1223        assert_eq!(
1224            frame(&mut m, &[], 0.25),
1225            [stop("battle"), gain("boss", 0.5)]
1226        );
1227        // A switch without overlap during a crossfade: the new track
1228        // waits for the newer one to fade out.
1229        let mut m = crossfading(0.5);
1230        assert_eq!(
1231            frame(&mut m, &[play("boss")], 0.125),
1232            [stop("title"), load("boss"), gain("battle", 0.25)]
1233        );
1234        assert_eq!(frame(&mut m, &[], 0.125), [stop("battle"), start("boss")]);
1235    }
1236
1237    #[test]
1238    fn asking_again_for_the_track_fading_out_in_a_crossfade_brings_it_back() {
1239        let mut m = crossfading(0.25);
1240        let back = play_with("title", MusicSwitch::crossfade(1000));
1241        assert_eq!(
1242            frame(&mut m, &[back], 0.0),
1243            [gain("battle", 0.25), gain("title", 0.75)]
1244        );
1245        assert_eq!((m.current(), m.target()), (Some("battle"), Some("title")));
1246        assert_eq!(
1247            frame(&mut m, &[], 0.125),
1248            [gain("battle", 0.125), gain("title", 0.875)]
1249        );
1250        assert_eq!(
1251            frame(&mut m, &[], 0.125),
1252            [stop("battle"), gain("title", 1.0)]
1253        );
1254        assert_eq!((m.current(), m.target()), (Some("title"), Some("title")));
1255        // From 0.75 it took a quarter of the fade, and is done.
1256        assert_eq!(frame(&mut m, &[], 0.125), []);
1257        // With no fade-in it is back at full gain at once.
1258        let mut m = crossfading(0.25);
1259        let back = play_with("title", MusicSwitch::new(500, 0, true));
1260        assert_eq!(
1261            frame(&mut m, &[back], 0.0),
1262            [gain("title", 1.0), gain("battle", 0.25)]
1263        );
1264    }
1265
1266    #[test]
1267    fn stop_music_during_a_crossfade_fades_both_tracks_from_their_gains() {
1268        let mut m = crossfading(0.25);
1269        assert_eq!(
1270            frame(&mut m, &[AudioRequest::StopMusic], 0.125),
1271            [gain("title", 0.5), stop("battle")]
1272        );
1273        assert_eq!((m.current(), m.target()), (Some("title"), None));
1274        assert_eq!(frame(&mut m, &[], 0.25), [stop("title")]);
1275        assert_eq!(m.current(), None);
1276    }
1277
1278    fn stop_with(fade_ms: u32) -> AudioRequest {
1279        AudioRequest::stop_music_with(fade_ms)
1280    }
1281
1282    #[test]
1283    fn a_stop_with_its_own_fade_takes_that_long_from_full_gain() {
1284        // The default fade is 0.5 s; this stop asks for 3 s.
1285        let mut m = playing("title");
1286        assert_eq!(
1287            frame(&mut m, &[stop_with(3000)], 1.0),
1288            [gain("title", 1.0 - 1.0 / 3.0)]
1289        );
1290        assert_eq!(m.target(), None);
1291        // Asking again doesn't restart the fade.
1292        assert_eq!(
1293            frame(&mut m, &[stop_with(3000), stop_with(3000)], 1.0),
1294            [gain("title", 1.0 - 2.0 / 3.0)]
1295        );
1296        assert_eq!(frame(&mut m, &[], 1.0), [stop("title")]);
1297        assert_eq!(m.current(), None);
1298        assert_eq!(frame(&mut m, &[], 1.0), []);
1299        // The default is unchanged for the next track and the next stop.
1300        let mut q = AudioQueue::default();
1301        q.play_music("battle");
1302        q.stop_music();
1303        let requests = q.take();
1304        assert_eq!(requests, [play("battle"), AudioRequest::StopMusic]);
1305        assert_eq!(
1306            frame(&mut m, &requests, 0.25),
1307            [load("battle"), start("battle"), gain("battle", 0.5)]
1308        );
1309        assert_eq!(frame(&mut m, &[], 0.25), [stop("battle")]);
1310    }
1311
1312    #[test]
1313    fn a_stop_with_no_fade_stops_at_once_and_drops_the_waiting_track() {
1314        let mut m = playing("title");
1315        assert_eq!(frame(&mut m, &[stop_with(0)], 0.0), [stop("title")]);
1316        assert_eq!(m.current(), None);
1317        // During a switch: the track waiting to start never starts.
1318        let mut m = playing("title");
1319        frame(&mut m, &[play("battle")], 0.25);
1320        assert_eq!(
1321            frame(&mut m, &[stop_with(0)], 0.0),
1322            [stop("battle"), stop("title")]
1323        );
1324        assert_eq!((m.current(), m.target()), (None, None));
1325        assert_eq!(frame(&mut m, &[], 1.0), []);
1326    }
1327
1328    #[test]
1329    fn a_stop_with_its_own_fade_of_silence_does_nothing() {
1330        let mut m = MusicState::new(0.5);
1331        assert_eq!(frame(&mut m, &[stop_with(3000), stop_with(0)], 1.0), []);
1332        assert_eq!(frame(&mut m, &[], 5.0), []);
1333        assert_eq!(m.current(), None);
1334    }
1335
1336    #[test]
1337    fn a_stop_with_its_own_fade_during_a_switch_fades_from_where_the_gain_is() {
1338        // Half-way through the default fade-out, 2 s from full gain to
1339        // silence leaves 1 s.
1340        let mut m = playing("title");
1341        frame(&mut m, &[play("battle")], 0.25);
1342        assert_eq!(
1343            frame(&mut m, &[stop_with(2000)], 0.5),
1344            [stop("battle"), gain("title", 0.25)]
1345        );
1346        assert_eq!(frame(&mut m, &[], 0.5), [stop("title")]);
1347        assert_eq!(m.current(), None);
1348    }
1349
1350    #[test]
1351    fn a_stop_with_its_own_fade_during_a_crossfade_fades_both_tracks() {
1352        // `title` is at 0.75 and `battle` at 0.25: at 2 s a whole fade,
1353        // they take 1.5 s and 0.5 s.
1354        let mut m = crossfading(0.25);
1355        assert_eq!(
1356            frame(&mut m, &[stop_with(2000)], 0.5),
1357            [gain("title", 0.5), stop("battle")]
1358        );
1359        assert_eq!((m.current(), m.target()), (Some("title"), None));
1360        assert_eq!(frame(&mut m, &[], 0.5), [gain("title", 0.25)]);
1361        assert_eq!(frame(&mut m, &[], 0.5), [stop("title")]);
1362        assert_eq!(m.current(), None);
1363    }
1364
1365    #[test]
1366    fn the_default_switch_is_the_one_a_request_without_one_gets() {
1367        let mut m = MusicState::with_switch(MusicSwitch::crossfade(1000));
1368        frame(&mut m, &[play("title")], 0.0);
1369        m.heard(Some(&clock("title")));
1370        assert_eq!(frame(&mut m, &[], 1.0), [gain("title", 1.0)]);
1371        assert_eq!(
1372            frame(&mut m, &[play("battle")], 0.0),
1373            [load("battle"), start_at("battle", 0.0)]
1374        );
1375        // A request's own switch wins over it.
1376        let mut m = MusicState::with_switch(MusicSwitch::crossfade(1000));
1377        frame(&mut m, &[play("title")], 0.0);
1378        assert_eq!(
1379            frame(
1380                &mut m,
1381                &[play_with("battle", MusicSwitch::after_fade(0))],
1382                0.0
1383            ),
1384            [load("battle"), stop("title"), start("battle")]
1385        );
1386        assert_eq!(
1387            MusicState::new(0.5),
1388            MusicState::with_switch(MusicSwitch::after_fade(500))
1389        );
1390    }
1391
1392    #[test]
1393    fn sound_requests_leave_the_music_alone() {
1394        let mut m = playing("title");
1395        let sound = AudioRequest::PlaySound {
1396            cue: "click".into(),
1397            volume: 1.0,
1398        };
1399        assert_eq!(frame(&mut m, &[sound], 0.1), []);
1400        assert_eq!(m.current(), Some("title"));
1401    }
1402
1403    #[test]
1404    fn voice_requests_leave_the_music_alone() {
1405        let mut m = playing("title");
1406        let line = LineId::new("test_1a2b3c4d");
1407        let mut q = AudioQueue::default();
1408        q.play_voice(&line, Voice::High);
1409        q.stop_voice();
1410        q.preload_voices(vec![(line.clone(), Voice::Only)]);
1411        let expected = [
1412            AudioRequest::PlayVoice {
1413                line: line.clone(),
1414                variant: Voice::High,
1415            },
1416            AudioRequest::StopVoice,
1417            AudioRequest::PreloadVoices {
1418                lines: vec![(line, Voice::Only)],
1419            },
1420        ];
1421        assert_eq!(q.pending(), expected);
1422        assert!(expected.iter().all(|r| r.cue().is_none()));
1423        assert_eq!(frame(&mut m, &expected, 0.1), []);
1424        assert_eq!(m.current(), Some("title"));
1425    }
1426
1427    #[test]
1428    fn the_queue_records_requests_in_order() {
1429        let mut q = AudioQueue::default();
1430        q.play_sound("a");
1431        q.play_sound_at("b", 0.6);
1432        q.play_sound_at("c", 2.0);
1433        q.play_sound_at("d", -1.0);
1434        q.play_sound_at("e", f32::NAN);
1435        q.play_music("m");
1436        q.stop_music();
1437        q.stop_music_with(3000);
1438        let sound = |cue: &str, volume| AudioRequest::PlaySound {
1439            cue: cue.into(),
1440            volume,
1441        };
1442        let expected = [
1443            sound("a", 1.0),
1444            sound("b", 0.6),
1445            sound("c", 1.0),
1446            sound("d", 0.0),
1447            sound("e", 0.0),
1448            play("m"),
1449            AudioRequest::StopMusic,
1450            AudioRequest::stop_music_with(3000),
1451        ];
1452        assert_eq!(q.pending(), expected);
1453        assert_eq!(q.take(), expected);
1454        let found = q.pending();
1455        assert!(found.is_empty(), "{found:?}");
1456        assert_eq!(
1457            expected.map(|r| r.cue().map(str::to_owned))[4..],
1458            [Some("e".to_owned()), Some("m".to_owned()), None, None]
1459        );
1460    }
1461
1462    /// The queue plays each menu sound and the cursor's tick as its
1463    /// config names them; by default after what they are.
1464    #[test]
1465    fn menu_sounds_and_the_cursor_play_the_configs_cues() {
1466        let cues = |q: &mut AudioQueue| {
1467            for sound in [
1468                MenuSound::Move,
1469                MenuSound::Select,
1470                MenuSound::Cancel,
1471                MenuSound::Denied,
1472            ] {
1473                q.menu(sound);
1474            }
1475            q.cursor_tick();
1476            let taken = q.take();
1477            taken
1478                .iter()
1479                .map(|r| r.cue().unwrap_or_default().to_owned())
1480                .collect::<Vec<_>>()
1481        };
1482        let mut q = AudioQueue::default();
1483        assert_eq!(q.config(), AudioConfig::default());
1484        assert_eq!(
1485            cues(&mut q),
1486            [
1487                "ui_move",
1488                "ui_select",
1489                "ui_cancel",
1490                "ui_cancel",
1491                "ui_cursor"
1492            ]
1493        );
1494        let config = AudioConfig {
1495            move_cue: "tick",
1496            select_cue: "ding",
1497            cancel_cue: "dong",
1498            denied_cue: "buzz",
1499            cursor_cue: "step",
1500        };
1501        let mut q = AudioQueue::new(config);
1502        assert_eq!(q.config(), config);
1503        assert_eq!(cues(&mut q), ["tick", "ding", "dong", "buzz", "step"]);
1504    }
1505
1506    /// A manifest with a `fights` pool of three looped tracks and an
1507    /// empty `quiet` pool.
1508    fn manifest() -> AudioManifest {
1509        let mut m = AudioManifest::default();
1510        let tracks = ["drums", "horns", "pipes"];
1511        for cue in tracks {
1512            m.music.insert(cue.into(), track(cue, true, 10_000));
1513        }
1514        m.pools
1515            .insert("fights".into(), tracks.map(str::to_owned).to_vec());
1516        m.pools.insert("quiet".into(), Vec::new());
1517        m
1518    }
1519
1520    /// A track of `cue`, `looped` or not, `length_ms` long.
1521    fn track(cue: &str, looped: bool, length_ms: u32) -> MusicCue {
1522        MusicCue {
1523            file: format!("{cue}.ogg"),
1524            volume: 100,
1525            looped,
1526            length_ms,
1527            credit: CreditRef::Own,
1528        }
1529    }
1530
1531    #[test]
1532    fn a_pool_pick_is_a_cue_of_that_pool_and_fixed_by_the_seed() {
1533        let m = manifest();
1534        let pool = &m.pools["fights"];
1535        for seed in [0, 1, 2, u64::MAX] {
1536            let cue = pick_from_pool(&m, "fights", seed).unwrap();
1537            assert!(pool.iter().any(|c| c == cue), "{cue}");
1538            assert_eq!(pick_from_pool(&m, "fights", seed), Some(cue));
1539        }
1540        // Two seeds can pick two tracks.
1541        let picks: alloc::collections::BTreeSet<&str> = (0..8)
1542            .filter_map(|seed| pick_from_pool(&m, "fights", seed))
1543            .collect();
1544        assert!(picks.len() > 1, "{picks:?}");
1545    }
1546
1547    /// The published splitmix64 outputs for state 0 (the first two steps).
1548    #[test]
1549    fn splitmix64_matches_the_reference() {
1550        assert_eq!(splitmix64(0), 0xE220_A839_7B1D_CDAF);
1551        assert_eq!(splitmix64(0x9E37_79B9_7F4A_7C15), 0x6E78_9E6A_A1B9_65F4);
1552    }
1553
1554    #[test]
1555    fn an_unknown_or_empty_pool_picks_nothing() {
1556        let m = manifest();
1557        assert_eq!(pick_from_pool(&m, "no_such_pool", 1), None);
1558        assert_eq!(pick_from_pool(&m, "quiet", 1), None);
1559    }
1560
1561    /// A manifest with one track, `looped` or not, 2.5 s long.
1562    fn one_track(looped: bool) -> AudioManifest {
1563        let mut m = AudioManifest::default();
1564        m.music.insert("theme".into(), track("theme", looped, 2500));
1565        m
1566    }
1567
1568    /// The position of `theme`'s clock `elapsed` seconds after its start.
1569    fn position(looped: bool, elapsed: f64) -> Option<f32> {
1570        let clock = MusicClock::from_elapsed(&one_track(looped), "theme", elapsed)?;
1571        assert_eq!(clock.cue, "theme");
1572        assert!((clock.length - 2.5).abs() < f32::EPSILON, "{clock:?}");
1573        Some(clock.position)
1574    }
1575
1576    fn close(position: Option<f32>, expected: f32) -> bool {
1577        position.is_some_and(|p| (p - expected).abs() < 1e-5)
1578    }
1579
1580    #[test]
1581    fn the_clock_of_a_looped_track_wraps_at_its_length() {
1582        assert_eq!(position(true, 0.0), Some(0.0));
1583        assert!(close(position(true, 1.25), 1.25));
1584        assert!(close(position(true, 2.499), 2.499));
1585        assert_eq!(position(true, 2.5), Some(0.0));
1586        assert!(close(position(true, 3.0), 0.5));
1587        assert!(close(position(true, 2.5 * 1000.0 + 2.0), 2.0));
1588        // A hair before the loop point rounds onto the length as f32: that
1589        // is the loop point, never a position equal to the length.
1590        assert_eq!(position(true, 2.5 - 1e-12), Some(0.0));
1591    }
1592
1593    #[test]
1594    fn the_clock_of_a_track_played_once_ends_with_it() {
1595        assert_eq!(position(false, 0.0), Some(0.0));
1596        assert!(close(position(false, 2.499), 2.499));
1597        assert_eq!(position(false, 2.5 - 1e-12), None);
1598        assert_eq!(position(false, 2.5), None);
1599        assert_eq!(position(false, 3.0), None);
1600    }
1601
1602    #[test]
1603    fn the_clock_needs_a_known_cue_a_length_and_a_real_time() {
1604        let m = one_track(true);
1605        assert_eq!(MusicClock::from_elapsed(&m, "nope", 1.0), None);
1606        // A start in the future (the system clock stepped back) is 0.
1607        assert_eq!(position(true, -3.0), Some(0.0));
1608        assert_eq!(position(false, -3.0), Some(0.0));
1609        for looped in [true, false] {
1610            for elapsed in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1611                assert_eq!(position(looped, elapsed), None, "{looped} {elapsed}");
1612            }
1613            // The validator refuses a length of 0; a hand-made manifest
1614            // with one has no clock.
1615            let mut m = one_track(looped);
1616            if let Some(t) = m.music.get_mut("theme") {
1617                t.length_ms = 0;
1618            }
1619            assert_eq!(MusicClock::from_elapsed(&m, "theme", 0.0), None);
1620            assert_eq!(MusicClock::from_elapsed(&m, "theme", 1.0), None);
1621        }
1622    }
1623
1624    proptest::proptest! {
1625        /// A looped track's position is always inside the track, however
1626        /// long it has played and whatever its length.
1627        #[test]
1628        fn a_looped_position_is_always_inside_the_track(
1629            length_ms in 1u32..=600_000,
1630            elapsed in 0.0f64..1.0e7,
1631        ) {
1632            let mut m = one_track(true);
1633            if let Some(t) = m.music.get_mut("theme") {
1634                t.length_ms = length_ms;
1635            }
1636            let clock = MusicClock::from_elapsed(&m, "theme", elapsed);
1637            let clock = clock.unwrap();
1638            proptest::prop_assert!(
1639                (0.0..clock.length).contains(&clock.position),
1640                "{clock:?}"
1641            );
1642        }
1643    }
1644
1645    /// One step of [`at_most_two_tracks_load_and_every_gain_is_a_gain`].
1646    #[derive(Debug, Clone)]
1647    enum Step {
1648        Play(usize, Option<MusicSwitch>),
1649        Stop,
1650        StopWith(u32),
1651        Heard(Option<usize>),
1652        Frame(f32),
1653    }
1654
1655    fn step() -> impl proptest::strategy::Strategy<Value = Step> {
1656        use proptest::prelude::*;
1657        let switch = (0u32..1500, 0u32..1500, any::<bool>())
1658            .prop_map(|(out_ms, in_ms, overlap)| MusicSwitch::new(out_ms, in_ms, overlap));
1659        prop_oneof![
1660            (0usize..4, proptest::option::of(switch)).prop_map(|(c, s)| Step::Play(c, s)),
1661            Just(Step::Stop),
1662            (0u32..4000).prop_map(Step::StopWith),
1663            proptest::option::of(0usize..4).prop_map(Step::Heard),
1664            (0.0f32..1.0).prop_map(Step::Frame),
1665        ]
1666    }
1667
1668    proptest::proptest! {
1669        /// Whatever is asked, heard and however time passes, at most two
1670        /// tracks are between their `Load` and their `Stop`; a track is
1671        /// loaded once, started, changed and stopped only while loaded;
1672        /// and every gain is 0 to 1.
1673        #[test]
1674        fn at_most_two_tracks_load_and_every_gain_is_a_gain(
1675            default in (0u32..1500, 0u32..1500, proptest::prelude::any::<bool>()),
1676            steps in proptest::collection::vec(step(), 0..80),
1677        ) {
1678            const CUES: [&str; 4] = ["a", "b", "c", "d"];
1679            let mut m = MusicState::with_switch(MusicSwitch::new(default.0, default.1, default.2));
1680            let mut loaded: alloc::collections::BTreeSet<String> = alloc::collections::BTreeSet::new();
1681            let mut out = Vec::new();
1682            for step in steps {
1683                match step {
1684                    Step::Play(c, switch) => m.request(&AudioRequest::play_music(CUES[c], switch), &mut out),
1685                    Step::Stop => m.request(&AudioRequest::StopMusic, &mut out),
1686                    Step::StopWith(ms) => m.request(&AudioRequest::stop_music_with(ms), &mut out),
1687                    Step::Heard(c) => m.heard(c.map(|c| clock(CUES[c])).as_ref()),
1688                    Step::Frame(dt) => m.update(dt, &mut out),
1689                }
1690                for command in out.drain(..) {
1691                    match &command {
1692                        MusicCommand::Load { cue } => {
1693                            proptest::prop_assert!(loaded.insert(cue.clone()), "{command:?}");
1694                        }
1695                        MusicCommand::Start { cue, gain } | MusicCommand::Gain { cue, gain } => {
1696                            proptest::prop_assert!(loaded.contains(cue), "{command:?}");
1697                            proptest::prop_assert!((0.0..=1.0).contains(gain), "{command:?}");
1698                        }
1699                        MusicCommand::Stop { cue } => {
1700                            proptest::prop_assert!(loaded.remove(cue), "{command:?}");
1701                        }
1702                    }
1703                    proptest::prop_assert!(loaded.len() <= 2, "{loaded:?}");
1704                }
1705                let current = m.current().map(str::to_owned);
1706                proptest::prop_assert!(current.is_none_or(|c| loaded.contains(&c)));
1707            }
1708        }
1709    }
1710
1711    proptest::proptest! {
1712        /// Over enough seeds, every track in the pool comes up.
1713        #[test]
1714        fn every_track_in_a_pool_can_be_picked(start in proptest::prelude::any::<u64>()) {
1715            let m = manifest();
1716            let pool = &m.pools["fights"];
1717            let picked: alloc::collections::BTreeSet<&str> = (0..200)
1718                .filter_map(|i| pick_from_pool(&m, "fights", start.wrapping_add(i)))
1719                .collect();
1720            proptest::prop_assert_eq!(picked.len(), pool.len());
1721        }
1722    }
1723}