Skip to main content

shuyi_ui/audio/
handles.rs

1//! The sounds started with a handle: [`AudioQueue`]'s ids, their fades
2//! and the screens that own them.
3
4use crate::prelude::*;
5
6use super::{AudioQueue, AudioRequest, SoundId, clamp_volume, secs};
7use crate::input::frame_seconds;
8
9/// The owner of code outside the screen stack, such as the debug view's
10/// pause (2316): no pop releases it, so its pause lasts until it resumes.
11pub(crate) const OUTSIDE_THE_STACK: usize = usize::MAX;
12
13/// A sound started with a handle and not yet stopped.
14#[derive(Debug, Clone, PartialEq)]
15pub(super) struct Handle {
16    /// The depth on the screen stack of the screen that started it.
17    owner: usize,
18    /// Its volume, 0–1.
19    volume: f32,
20    /// Its fade to silence, once it is stopped with one.
21    fade: Option<Fade>,
22}
23
24/// A volume falling in a straight line from `from` to 0 over `secs`
25/// seconds, `elapsed` of them gone.
26#[derive(Debug, Clone, PartialEq)]
27struct Fade {
28    from: f32,
29    secs: f32,
30    elapsed: f32,
31}
32
33impl<V> AudioQueue<V> {
34    /// Starts sound `cue` at its own volume, playing until it is stopped:
35    /// looped if its cue says so, else once. The handle lives until
36    /// [`stop_sound`](Self::stop_sound), or until the screen that started
37    /// it leaves the stack. A sound nothing needs to stop is
38    /// [`play_sound`](Self::play_sound)'s.
39    pub fn start_sound(&mut self, cue: &str) -> SoundId {
40        self.start_sound_at(cue, 1.0)
41    }
42
43    /// [`start_sound`](Self::start_sound) at `volume` (0–1, clamped) times
44    /// the cue's own volume. A cue [`remove_sound`](Self::remove_sound)
45    /// removed plays nothing: its id was never live.
46    pub fn start_sound_at(&mut self, cue: &str, volume: f32) -> SoundId {
47        // Skips 0 if it ever wraps: an id is never 0.
48        self.last_id = self.last_id.wrapping_add(1).max(1);
49        let id = SoundId(self.last_id);
50        if self.added.silences(cue) {
51            return id;
52        }
53        let volume = clamp_volume(volume);
54        let handle = Handle {
55            owner: self.owner,
56            volume,
57            fade: None,
58        };
59        self.handles.insert(id, handle);
60        self.requests.push(AudioRequest::StartSound {
61            id,
62            cue: cue.to_owned(),
63            volume,
64        });
65        id
66    }
67
68    /// Stops the sound started as `id`: at once with a `fade_ms` of 0,
69    /// else after its volume falls in a straight line to silence over
70    /// `fade_ms` milliseconds ([`advance`](Self::advance)). A sound already
71    /// fading keeps its fade, unless this stops it at once. An unknown or
72    /// stopped id does nothing.
73    pub fn stop_sound(&mut self, id: SoundId, fade_ms: u32) {
74        let Some(handle) = self.handles.get_mut(&id) else {
75            return;
76        };
77        if fade_ms == 0 {
78            self.stop_now(id);
79        } else if handle.fade.is_none() {
80            handle.fade = Some(Fade {
81                from: handle.volume,
82                secs: secs(fade_ms),
83                elapsed: 0.0,
84            });
85        }
86    }
87
88    /// Sets the volume of the sound started as `id` (0–1, clamped). An
89    /// unknown or stopped id, or a sound fading out, does nothing.
90    pub fn set_sound_volume(&mut self, id: SoundId, volume: f32) {
91        let Some(handle) = self.handles.get_mut(&id).filter(|h| h.fade.is_none()) else {
92            return;
93        };
94        handle.volume = clamp_volume(volume);
95        self.requests.push(AudioRequest::SoundVolume {
96            id,
97            volume: handle.volume,
98        });
99    }
100
101    /// Whether `id` was started and hasn't been stopped (a sound fading
102    /// out still counts). A cue that plays once counts until it is
103    /// stopped, as the queue knows no lengths.
104    pub fn sound_is_live(&self, id: SoundId) -> bool {
105        self.handles.contains_key(&id)
106    }
107
108    /// Advances the fades by `dt` seconds: each sends its volume, or the
109    /// stop on the frame it ends. [`Game`](crate::Game) calls it every
110    /// frame before it takes the requests. Nonsense frame times pass no
111    /// time.
112    pub fn advance(&mut self, dt: f32) {
113        let dt = frame_seconds(dt);
114        if dt <= 0.0 {
115            return;
116        }
117        let mut ended = Vec::new();
118        for (&id, handle) in &mut self.handles {
119            let Some(fade) = &mut handle.fade else {
120                continue;
121            };
122            fade.elapsed += dt;
123            if fade.elapsed >= fade.secs {
124                ended.push(id);
125            } else {
126                handle.volume = fade.from * (1.0 - fade.elapsed / fade.secs);
127                self.requests.push(AudioRequest::SoundVolume {
128                    id,
129                    volume: handle.volume,
130                });
131            }
132        }
133        for id in ended {
134            self.stop_now(id);
135        }
136    }
137
138    /// Pauses every sound started with a handle that is playing now, for
139    /// a pause menu: one started after it plays, so the menu's own sounds
140    /// are heard. Sound cues played once, the music and the voice go on.
141    /// The pause is the calling screen's and lasts until it calls
142    /// [`resume_sounds`](Self::resume_sounds) or leaves the stack. A
143    /// screen pauses at most once, so calling it every frame is fine, and
144    /// several screens pausing don't stack further: the queue sends
145    /// [`AudioRequest::PauseSounds`] when the first pause begins and
146    /// [`AudioRequest::ResumeSounds`] when the last ends.
147    pub fn pause_sounds(&mut self) {
148        if self.pauses.is_empty() {
149            self.requests.push(AudioRequest::PauseSounds);
150        }
151        self.pauses.insert(self.owner);
152    }
153
154    /// Ends the calling screen's [`pause_sounds`](Self::pause_sounds). A
155    /// screen that isn't pausing does nothing.
156    pub fn resume_sounds(&mut self) {
157        self.end_pause(self.owner);
158    }
159
160    /// Whether some screen is pausing the sounds.
161    pub fn sounds_paused(&self) -> bool {
162        !self.pauses.is_empty()
163    }
164
165    /// Makes the screen at `depth` on the stack the owner of the sounds
166    /// started from now on: the stack calls it before it runs a screen.
167    /// [`OUTSIDE_THE_STACK`] is the owner of code outside the stack.
168    pub(crate) fn set_owner(&mut self, depth: usize) {
169        self.owner = depth;
170    }
171
172    /// Stops at once the sounds of the screen at `depth`, which is leaving
173    /// the stack, and ends its pause; one fading out finishes its fade.
174    pub(crate) fn release(&mut self, depth: usize) {
175        debug_assert_ne!(depth, OUTSIDE_THE_STACK, "no pop releases it");
176        let owned: Vec<SoundId> = self
177            .handles
178            .iter()
179            .filter(|(_, h)| h.owner == depth && h.fade.is_none())
180            .map(|(&id, _)| id)
181            .collect();
182        for id in owned {
183            self.stop_now(id);
184        }
185        self.end_pause(depth);
186    }
187
188    fn end_pause(&mut self, owner: usize) {
189        if self.pauses.remove(&owner) && self.pauses.is_empty() {
190            self.requests.push(AudioRequest::ResumeSounds);
191        }
192    }
193
194    /// The live sounds and the depth of the screen that owns each, in id
195    /// order.
196    #[cfg(test)]
197    pub(crate) fn owners(&self) -> Vec<(SoundId, usize)> {
198        self.handles.iter().map(|(&id, h)| (id, h.owner)).collect()
199    }
200
201    fn stop_now(&mut self, id: SoundId) {
202        if self.handles.remove(&id).is_some() {
203            self.requests.push(AudioRequest::StopSound { id });
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::toy::Voice;
212
213    type AudioQueue = super::AudioQueue<Voice>;
214    type AudioRequest = super::AudioRequest<Voice>;
215
216    const FRAME: f32 = 1.0 / 60.0;
217
218    fn start(id: u32, volume: f32) -> AudioRequest {
219        AudioRequest::StartSound {
220            id: SoundId(id),
221            cue: "hum".into(),
222            volume,
223        }
224    }
225
226    #[test]
227    fn ids_count_from_one_and_are_the_same_on_every_run() {
228        let run = || {
229            let mut audio = AudioQueue::default();
230            let ids = [audio.start_sound("hum"), audio.start_sound("hum")];
231            audio.stop_sound(ids[0], 0);
232            (ids, audio.start_sound("hum"), audio.take())
233        };
234        let (ids, third, requests) = run();
235        assert_eq!(ids.map(SoundId::get), [1, 2]);
236        assert_eq!(third.get(), 3);
237        assert_eq!(
238            requests,
239            [
240                start(1, 1.0),
241                start(2, 1.0),
242                AudioRequest::StopSound { id: SoundId(1) },
243                start(3, 1.0),
244            ]
245        );
246        assert_eq!(run().2, requests);
247    }
248
249    #[test]
250    fn volumes_are_clamped() {
251        let mut audio = AudioQueue::default();
252        let loud = audio.start_sound_at("hum", 3.0);
253        audio.start_sound_at("hum", f32::NAN);
254        audio.set_sound_volume(loud, -1.0);
255        assert_eq!(
256            audio.take(),
257            [
258                start(1, 1.0),
259                start(2, 0.0),
260                AudioRequest::SoundVolume {
261                    id: loud,
262                    volume: 0.0
263                },
264            ]
265        );
266    }
267
268    #[test]
269    fn a_stop_without_a_fade_sends_one_stop_and_the_id_is_dead_after() {
270        let mut audio = AudioQueue::default();
271        let id = audio.start_sound("hum");
272        audio.take();
273        audio.stop_sound(id, 0);
274        audio.stop_sound(id, 0);
275        audio.stop_sound(id, 500);
276        audio.set_sound_volume(id, 0.5);
277        audio.advance(1.0);
278        assert_eq!(audio.take(), [AudioRequest::StopSound { id }]);
279        assert!(!audio.sound_is_live(id));
280    }
281
282    #[test]
283    fn an_unknown_id_does_nothing() {
284        let mut audio = AudioQueue::default();
285        audio.stop_sound(SoundId(9), 0);
286        audio.stop_sound(SoundId(9), 100);
287        audio.set_sound_volume(SoundId(9), 0.5);
288        audio.advance(1.0);
289        let got = audio.take();
290        assert!(got.is_empty(), "{got:?}");
291    }
292
293    #[test]
294    fn a_fade_sends_falling_volumes_then_one_stop_on_the_frame_it_ends() {
295        let mut audio = AudioQueue::default();
296        let id = audio.start_sound_at("hum", 0.8);
297        audio.take();
298        audio.stop_sound(id, 500);
299        // The volume can't change on its way out.
300        audio.set_sound_volume(id, 1.0);
301        let mut volumes = Vec::new();
302        let mut frames = 0;
303        let stopped = loop {
304            frames += 1;
305            audio.advance(FRAME);
306            let requests = audio.take();
307            if let [AudioRequest::SoundVolume { volume, .. }] = requests[..] {
308                volumes.push(volume);
309            } else {
310                break requests;
311            }
312            assert!(frames < 100, "the fade never ends");
313        };
314        assert_eq!(stopped, [AudioRequest::StopSound { id }]);
315        // 0.5 s of 1/60 s frames: the 30th frame, give or take the
316        // rounding of their sum.
317        assert!((30..=31).contains(&frames), "{frames}");
318        assert!(volumes.windows(2).all(|w| w[1] < w[0]), "{volumes:?}");
319        assert!(volumes[0] < 0.8 && volumes[0] > 0.75, "{volumes:?}");
320        audio.advance(FRAME);
321        let got = audio.take();
322        assert!(got.is_empty(), "{got:?}");
323    }
324
325    #[test]
326    fn bad_frame_times_pass_no_time() {
327        let mut audio = AudioQueue::default();
328        let id = audio.start_sound("hum");
329        audio.stop_sound(id, 100);
330        audio.take();
331        for dt in [f32::NAN, -1.0, f32::INFINITY, 0.0] {
332            audio.advance(dt);
333        }
334        let got = audio.take();
335        assert!(got.is_empty(), "{got:?}");
336        assert!(audio.sound_is_live(id));
337        audio.advance(0.05);
338        assert_eq!(
339            audio.take(),
340            [AudioRequest::SoundVolume { id, volume: 0.5 }]
341        );
342    }
343
344    #[test]
345    fn a_release_stops_its_owners_sounds_and_lets_a_fade_finish() {
346        let mut audio = AudioQueue::default();
347        let below = audio.start_sound("hum");
348        audio.set_owner(1);
349        let [on_top, fading] = [audio.start_sound("hum"), audio.start_sound("hum")];
350        audio.stop_sound(fading, 100);
351        audio.take();
352        audio.release(1);
353        assert_eq!(audio.take(), [AudioRequest::StopSound { id: on_top }]);
354        assert_eq!(audio.owners(), [(below, 0), (fading, 1)]);
355        audio.advance(0.1);
356        assert_eq!(audio.take(), [AudioRequest::StopSound { id: fading }]);
357        assert!(audio.sound_is_live(below));
358    }
359
360    #[test]
361    fn a_screen_pausing_twice_pauses_once_and_its_resume_ends_it() {
362        let mut audio = AudioQueue::default();
363        audio.set_owner(1);
364        audio.pause_sounds();
365        audio.pause_sounds();
366        assert!(audio.sounds_paused());
367        // Another screen's resume ends no pause of this one's.
368        audio.set_owner(0);
369        audio.resume_sounds();
370        assert_eq!(audio.take(), [AudioRequest::PauseSounds]);
371        audio.set_owner(1);
372        audio.resume_sounds();
373        audio.resume_sounds();
374        assert_eq!(audio.take(), [AudioRequest::ResumeSounds]);
375        assert!(!audio.sounds_paused());
376    }
377
378    #[test]
379    fn pauses_by_two_owners_send_one_pause_and_one_resume_when_the_last_ends() {
380        let mut audio = AudioQueue::default();
381        audio.set_owner(1);
382        audio.pause_sounds();
383        audio.set_owner(OUTSIDE_THE_STACK);
384        audio.pause_sounds();
385        audio.set_owner(1);
386        audio.resume_sounds();
387        assert_eq!(audio.take(), [AudioRequest::PauseSounds]);
388        assert!(audio.sounds_paused(), "the outside pause holds");
389        audio.set_owner(OUTSIDE_THE_STACK);
390        audio.resume_sounds();
391        assert_eq!(audio.take(), [AudioRequest::ResumeSounds]);
392    }
393
394    #[test]
395    fn a_release_ends_the_leaving_screens_pause_after_stopping_its_sounds() {
396        let mut audio = AudioQueue::default();
397        audio.set_owner(1);
398        audio.pause_sounds();
399        let menu = audio.start_sound("hum");
400        audio.take();
401        audio.release(2);
402        let got = audio.take();
403        assert!(got.is_empty(), "{got:?}");
404        audio.release(1);
405        assert_eq!(
406            audio.take(),
407            [
408                AudioRequest::StopSound { id: menu },
409                AudioRequest::ResumeSounds
410            ]
411        );
412        assert!(!audio.sounds_paused());
413    }
414}