Skip to main content

shuyi_platform/
audio.rs

1//! [`Audio`]: the sound device a backend gives the engine, and the frame of
2//! audio [`App`](crate::App) hands it ([`AudioFrame`]).
3//!
4//! The screens' requests name the game's voice clips by its own variant
5//! type; what reaches the device names their files ([`SoundRequest`]), so
6//! a backend is the same for every game. The rules of playing (a cue's
7//! volume, picking a variant, fades, the music clock) are [`player`]'s, so a
8//! backend writes only its small [`Backend`](player::Backend) trait.
9
10pub mod player;
11
12use crate::alloc_prelude::*;
13
14use shuyi_assets::voice::{ClipVariant, clip_file};
15use shuyi_ui::audio::{AudioRequest, MusicCommand, SharedSamples, SoundId};
16
17use crate::platform::Millis;
18
19/// A sound or voice request as the device gets it: a screen's
20/// [`AudioRequest`] with each voice clip named by its file in the game's
21/// voice folder ([`clip_file`]). The music's requests are already
22/// [`MusicCommand`]s.
23#[derive(Debug, Clone, PartialEq, serde::Serialize)]
24#[non_exhaustive]
25pub enum SoundRequest {
26    /// Play a sound cue once, at `volume` (0–1) times the cue's own volume.
27    Sound {
28        /// The sound cue.
29        cue: String,
30        /// A multiplier on the cue's volume, 0–1.
31        volume: f32,
32    },
33    /// Play a sound cue until [`Stop`](Self::Stop) of its `id`: looped if
34    /// the cue says so, else once. Build it with [`SoundRequest::start`]
35    /// (ADR-0039).
36    #[non_exhaustive]
37    Start {
38        /// Its handle.
39        id: SoundId,
40        /// The sound cue.
41        cue: String,
42        /// A multiplier on the cue's volume, 0–1.
43        volume: f32,
44    },
45    /// Stop the sound started as `id`, at once: that one playback, not the
46    /// cue's others.
47    Stop {
48        /// Its handle.
49        id: SoundId,
50    },
51    /// Set the volume of the sound started as `id`.
52    Volume {
53        /// Its handle.
54        id: SoundId,
55        /// The new multiplier on the cue's volume, 0–1.
56        volume: f32,
57    },
58    /// Pause every sound started with a handle that is playing now; one
59    /// started after it plays. Sound cues played once, the music and the
60    /// voice go on. A sound paused keeps the volume it is given and sounds
61    /// at it when it resumes; one stopped while paused is gone.
62    PauseSounds,
63    /// Resume the sounds [`PauseSounds`](Self::PauseSounds) paused.
64    ResumeSounds,
65    /// Say a line: play its clip once, stopping the voice that is playing.
66    Voice {
67        /// The clip's file in the voice folder.
68        clip: String,
69    },
70    /// Stop the voice that is playing, if any.
71    StopVoice,
72    /// The clips a scene will ask for, in order, so the device can load
73    /// the next few ahead. Replaces the list sent before.
74    PreloadVoices {
75        /// Their files, in the order they are expected.
76        clips: Vec<String>,
77    },
78    /// Make `samples` the sound cue `cue`, which the manifest doesn't have:
79    /// [`Sound`](Self::Sound) and [`Start`](Self::Start) of it play it at its
80    /// own volume, full, once. A cue added before is replaced. Build it with
81    /// [`SoundRequest::add`] (ADR-0039). A record of it holds the samples'
82    /// rate, channels and frames, not the samples.
83    #[non_exhaustive]
84    Add {
85        /// The sound cue.
86        cue: String,
87        /// Its samples.
88        #[serde(serialize_with = "samples_summary")]
89        samples: SharedSamples,
90    },
91    /// Forget the sound cue `cue` that [`Add`](Self::Add) made, stopping
92    /// what plays it.
93    Remove {
94        /// The sound cue.
95        cue: String,
96    },
97}
98
99/// `samples` as a record writes it: its rate, channels and frames.
100fn samples_summary<S: serde::Serializer>(
101    samples: &SharedSamples,
102    out: S,
103) -> Result<S::Ok, S::Error> {
104    use serde::ser::SerializeStruct;
105    let mut summary = out.serialize_struct("Samples", 3)?;
106    summary.serialize_field("rate", &samples.rate())?;
107    summary.serialize_field("channels", &samples.channels())?;
108    summary.serialize_field("frames", &samples.frames())?;
109    summary.end()
110}
111
112impl SoundRequest {
113    /// A [`Start`](Self::Start) of `cue` as `id` at `volume`.
114    pub fn start(id: SoundId, cue: impl Into<String>, volume: f32) -> Self {
115        Self::Start {
116            id,
117            cue: cue.into(),
118            volume,
119        }
120    }
121
122    /// An [`Add`](Self::Add) of `samples` as `cue`.
123    pub fn add(cue: impl Into<String>, samples: SharedSamples) -> Self {
124        Self::Add {
125            cue: cue.into(),
126            samples,
127        }
128    }
129
130    /// `request` as the device gets it; `None` for a music request, which
131    /// the frame's music commands already say.
132    pub fn from_request<V: ClipVariant>(request: &AudioRequest<V>) -> Option<Self> {
133        Some(match request {
134            AudioRequest::PlaySound { cue, volume } => Self::Sound {
135                cue: cue.clone(),
136                volume: *volume,
137            },
138            AudioRequest::StartSound { id, cue, volume } => Self::start(*id, cue.clone(), *volume),
139            AudioRequest::StopSound { id } => Self::Stop { id: *id },
140            AudioRequest::SoundVolume { id, volume } => Self::Volume {
141                id: *id,
142                volume: *volume,
143            },
144            AudioRequest::PlayVoice { line, variant } => Self::Voice {
145                clip: clip_file(line, *variant),
146            },
147            AudioRequest::PauseSounds => Self::PauseSounds,
148            AudioRequest::ResumeSounds => Self::ResumeSounds,
149            AudioRequest::StopVoice => Self::StopVoice,
150            AudioRequest::PreloadVoices { lines } => Self::PreloadVoices {
151                clips: lines.iter().map(|(l, v)| clip_file(l, *v)).collect(),
152            },
153            AudioRequest::AddSound { cue, samples, .. } => Self::add(cue.clone(), samples.clone()),
154            AudioRequest::RemoveSound { cue } => Self::Remove { cue: cue.clone() },
155            // The music's, and any request this engine doesn't know yet.
156            _ => return None,
157        })
158    }
159}
160
161/// One frame's audio, as [`App`](crate::App) hands it to the device.
162#[derive(Debug, Clone, Copy)]
163pub struct AudioFrame<'a> {
164    /// The sound and voice requests the screens made, in order.
165    pub requests: &'a [SoundRequest],
166    /// What to do to the music, in order.
167    pub music: &'a [MusicCommand],
168    /// The player's music volume, 0–1: it multiplies every track's own
169    /// volume, a track already playing too.
170    pub music_volume: f32,
171    /// The player's sound volume, 0–1: it multiplies every sound's own.
172    pub sound_volume: f32,
173    /// How loud voice clips play, 0–1, the one playing too.
174    pub voice_volume: f32,
175}
176
177/// A backend's sound device.
178///
179/// **Pausing.** A backend that can pause a sound pauses it; one that
180/// can't mutes it (volume 0) and lets it run on silently. That holds for
181/// [`SoundRequest::PauseSounds`] and for [`suspend`](Self::suspend). A
182/// sound held either way keeps the volume it is given and sounds at it
183/// when it is released, and [`music_playing`](Self::music_playing) says
184/// where the track really is: a paused track's clock holds, a muted one's
185/// runs.
186pub trait Audio {
187    /// The music sounding at `now`: its cue and the seconds since it really
188    /// started (once its file had loaded), or `None` in silence: nothing
189    /// asked for, a track still loading or whose file failed. Of two
190    /// tracks sounding (a crossfade), the newest: the old one until the
191    /// new one has loaded.
192    fn music_playing(&self, now: Millis) -> Option<(&str, f64)>;
193
194    /// The voice clip sounding at `now`, by its file
195    /// ([`SoundRequest::Voice`]), or asked for and still loading: `None`
196    /// once it has ended, and for a clip stopped, replaced, whose file
197    /// failed or that loaded too late to be played. A clip held by
198    /// [`suspend`](Self::suspend) still sounds. `None` by default, for a
199    /// device that plays no voices.
200    fn voice_playing(&self, now: Millis) -> Option<&str> {
201        let _ = now;
202        None
203    }
204
205    /// Plays one frame's `frame` of audio at `now`: the volumes first, then
206    /// the requests and the music commands in order. It also checks on
207    /// what is still loading, so it is called every frame, with nothing to
208    /// play too.
209    fn play(&mut self, frame: &AudioFrame<'_>, now: Millis);
210
211    /// Stops the music, the voice and the sounds started with a handle:
212    /// the game has quit.
213    fn stop_all(&mut self);
214
215    /// Holds everything sounding (the sounds started with a handle, the
216    /// voice and the music) while the system suspends the game
217    /// ([`SystemEvent::Suspend`](crate::SystemEvent::Suspend)); sound
218    /// cues played once finish. Pauses, or mutes where the backend can't
219    /// pause. A second call does nothing.
220    ///
221    /// It has no default: every backend says what it can hold (ADR-0056).
222    fn suspend(&mut self);
223
224    /// Brings back what [`suspend`](Self::suspend) held, and leaves a
225    /// [`SoundRequest::PauseSounds`] pause in place. A call while not
226    /// suspended does nothing.
227    fn resume(&mut self);
228}
229
230/// A device that plays nothing: for a backend with no sound yet, and tests.
231#[derive(Debug, Clone, Copy, Default)]
232pub struct Silent;
233
234impl Audio for Silent {
235    fn music_playing(&self, _: Millis) -> Option<(&str, f64)> {
236        None
237    }
238
239    fn play(&mut self, _: &AudioFrame<'_>, _: Millis) {}
240
241    fn stop_all(&mut self) {}
242
243    fn suspend(&mut self) {}
244
245    fn resume(&mut self) {}
246}
247
248#[cfg(test)]
249mod tests {
250    use alloc::vec;
251
252    use shuyi_assets::dialogue::LineId;
253    use shuyi_ui::toy::Voice;
254
255    use super::*;
256
257    fn line(id: &str) -> LineId {
258        LineId::new(id)
259    }
260
261    #[test]
262    fn a_voice_is_its_clips_file_and_music_stays_with_the_commands() {
263        let play = |request: AudioRequest<Voice>| SoundRequest::from_request(&request);
264        assert_eq!(
265            play(AudioRequest::PlaySound {
266                cue: "click".into(),
267                volume: 0.5
268            }),
269            Some(SoundRequest::Sound {
270                cue: "click".into(),
271                volume: 0.5
272            })
273        );
274        let hello = line("intro.hello");
275        let only = clip_file(&hello, Voice::Only);
276        assert_eq!(
277            play(AudioRequest::PlayVoice {
278                line: hello.clone(),
279                variant: Voice::Only
280            }),
281            Some(SoundRequest::Voice { clip: only.clone() })
282        );
283        assert_eq!(
284            play(AudioRequest::PreloadVoices {
285                lines: vec![(hello.clone(), Voice::Only), (hello, Voice::Low)]
286            })
287            .map(|r| match r {
288                SoundRequest::PreloadVoices { clips } => clips.len(),
289                _ => 0,
290            }),
291            Some(2)
292        );
293        assert_eq!(play(AudioRequest::StopVoice), Some(SoundRequest::StopVoice));
294        let mut queue = shuyi_ui::audio::AudioQueue::<Voice>::default();
295        let id = queue.start_sound_at("hum", 0.5);
296        queue.set_sound_volume(id, 0.25);
297        queue.stop_sound(id, 0);
298        let got: Vec<_> = queue
299            .take()
300            .iter()
301            .filter_map(SoundRequest::from_request)
302            .collect();
303        assert_eq!(
304            got,
305            [
306                SoundRequest::start(id, "hum", 0.5),
307                SoundRequest::Volume { id, volume: 0.25 },
308                SoundRequest::Stop { id },
309            ]
310        );
311        assert_eq!(
312            play(AudioRequest::PauseSounds),
313            Some(SoundRequest::PauseSounds)
314        );
315        assert_eq!(
316            play(AudioRequest::ResumeSounds),
317            Some(SoundRequest::ResumeSounds)
318        );
319        assert_eq!(play(AudioRequest::play_music("theme", None)), None);
320        assert_eq!(play(AudioRequest::StopMusic), None);
321    }
322
323    #[test]
324    fn a_sound_added_reaches_the_device_and_its_record_holds_no_samples() {
325        let samples = shuyi_ui::audio::Samples::from_u8(11_025, 2, &[128; 8]);
326        let mut queue = shuyi_ui::audio::AudioQueue::<Voice>::default();
327        assert_eq!(
328            queue.add_sound("tone", samples.unwrap_or_else(|e| panic!("{e}"))),
329            Ok(())
330        );
331        queue.remove_sound("tone");
332        let got: Vec<_> = queue
333            .take()
334            .iter()
335            .filter_map(SoundRequest::from_request)
336            .collect();
337        let [
338            SoundRequest::Add { cue, samples, .. },
339            SoundRequest::Remove { cue: removed },
340        ] = &got[..]
341        else {
342            panic!("{got:?}");
343        };
344        assert_eq!((cue.as_str(), removed.as_str()), ("tone", "tone"));
345        assert_eq!(samples.frames(), 4);
346        let record = ron::to_string(&got[0]).unwrap_or_else(|e| panic!("{e}"));
347        assert_eq!(
348            record,
349            "Add(cue:\"tone\",samples:(rate:11025,channels:2,frames:4))"
350        );
351    }
352
353    #[test]
354    fn silence_plays_nothing() {
355        let mut silent = Silent;
356        let frame = AudioFrame {
357            requests: &[SoundRequest::StopVoice],
358            music: &[],
359            music_volume: 1.0,
360            sound_volume: 1.0,
361            voice_volume: 1.0,
362        };
363        silent.play(&frame, Millis(0));
364        silent.suspend();
365        silent.resume();
366        silent.stop_all();
367        assert_eq!(silent.music_playing(Millis(0)), None);
368        assert_eq!(silent.voice_playing(Millis(0)), None, "it plays no voices");
369    }
370}