Skip to main content

game_toolkit_audio/
handle.rs

1//! [`SoundHandle`]: a toolkit-owned handle to one playing (or queued) sound instance.
2
3use std::sync::{Arc, Mutex, MutexGuard};
4use std::time::Duration;
5
6use kira::sound::PlaybackState;
7use kira::sound::static_sound::StaticSoundHandle;
8use kira::{Decibels, Tween};
9
10use crate::amplitude_to_db;
11
12/// Controls one playing sound instance, as returned by [`crate::Audio::play`] and friends.
13///
14/// Dropping the handle does not stop the sound; it keeps playing until it ends (or forever,
15/// if it loops). Clones refer to the same instance.
16///
17/// A handle can exist before the sound actually plays: a sound started with
18/// [`crate::PlayOptions::deferred`] before audio is available (on the web, before the first
19/// user gesture) is *pending*. Volume, pause and stop calls on a pending handle are remembered
20/// and applied when the sound starts.
21#[derive(Clone, Debug)]
22pub struct SoundHandle {
23    slot: Arc<Mutex<Slot>>,
24}
25
26#[derive(Debug)]
27pub(crate) enum Slot {
28    /// Queued until audio starts, with the state to apply once it does: the last volume
29    /// change (target and fade duration) and whether it was paused.
30    Pending {
31        volume: Option<(Decibels, Duration)>,
32        paused: bool,
33    },
34    Playing(StaticSoundHandle),
35    /// Stopped while still pending; it never plays.
36    Cancelled,
37}
38
39impl SoundHandle {
40    pub(crate) fn playing(handle: StaticSoundHandle) -> Self {
41        Self::from_slot(Slot::Playing(handle))
42    }
43
44    pub(crate) fn pending() -> Self {
45        Self::from_slot(Slot::Pending {
46            volume: None,
47            paused: false,
48        })
49    }
50
51    fn from_slot(slot: Slot) -> Self {
52        Self {
53            slot: Arc::new(Mutex::new(slot)),
54        }
55    }
56
57    pub(crate) fn lock(&self) -> MutexGuard<'_, Slot> {
58        // A panic while holding the lock can't leave the slot half-updated, so ignore poison.
59        self.slot
60            .lock()
61            .unwrap_or_else(std::sync::PoisonError::into_inner)
62    }
63
64    /// Set the volume as a linear amplitude (`1.0` is the sound's own level, `0.0` silence).
65    pub fn set_volume(&self, volume: f32) {
66        self.set_volume_db(amplitude_to_db(volume).0);
67    }
68
69    /// Set the volume in decibels (`0.0` is the sound's own level, `-6.0` roughly half).
70    pub fn set_volume_db(&self, db: f32) {
71        self.fade_volume_db(db, Duration::ZERO);
72    }
73
74    /// Fade the volume to a linear amplitude over `duration`.
75    pub fn fade_volume(&self, volume: f32, duration: Duration) {
76        self.fade_volume_db(amplitude_to_db(volume).0, duration);
77    }
78
79    /// Fade the volume to `db` decibels over `duration`.
80    pub fn fade_volume_db(&self, db: f32, duration: Duration) {
81        match &mut *self.lock() {
82            // Replayed when the sound starts, so a fade-in requested early still fades.
83            Slot::Pending { volume, .. } => *volume = Some((Decibels(db), duration)),
84            Slot::Playing(h) => h.set_volume(Decibels(db), tween(duration)),
85            Slot::Cancelled => {}
86        }
87    }
88
89    /// Pause playback. [`SoundHandle::resume`] continues where it left off.
90    pub fn pause(&self) {
91        match &mut *self.lock() {
92            Slot::Pending { paused, .. } => *paused = true,
93            Slot::Playing(h) => h.pause(Tween::default()),
94            Slot::Cancelled => {}
95        }
96    }
97
98    /// Resume a paused sound.
99    pub fn resume(&self) {
100        match &mut *self.lock() {
101            Slot::Pending { paused, .. } => *paused = false,
102            Slot::Playing(h) => h.resume(Tween::default()),
103            Slot::Cancelled => {}
104        }
105    }
106
107    /// Stop the sound for good. A stopped sound can't be resumed; play it again instead.
108    pub fn stop(&self) {
109        let mut slot = self.lock();
110        match &mut *slot {
111            Slot::Pending { .. } => *slot = Slot::Cancelled,
112            Slot::Playing(h) => h.stop(Tween::default()),
113            Slot::Cancelled => {}
114        }
115    }
116
117    /// Whether the sound is currently audible: playing, or fading in or out. `false` while
118    /// paused, pending or finished.
119    pub fn is_playing(&self) -> bool {
120        match &*self.lock() {
121            Slot::Playing(h) => h.state().is_advancing(),
122            Slot::Pending { .. } | Slot::Cancelled => false,
123        }
124    }
125
126    /// Whether the sound is paused (or will start paused, if pending).
127    pub fn is_paused(&self) -> bool {
128        match &*self.lock() {
129            Slot::Playing(h) => h.state() == PlaybackState::Paused,
130            Slot::Pending { paused, .. } => *paused,
131            Slot::Cancelled => false,
132        }
133    }
134
135    /// Whether the sound is still waiting for audio to start (see [`crate::PlayOptions::deferred`]).
136    pub fn is_pending(&self) -> bool {
137        matches!(&*self.lock(), Slot::Pending { .. })
138    }
139
140    /// Whether the sound has ended or was stopped. A looping sound only finishes when stopped.
141    pub fn is_finished(&self) -> bool {
142        match &*self.lock() {
143            Slot::Playing(h) => h.state() == PlaybackState::Stopped,
144            Slot::Pending { .. } => false,
145            Slot::Cancelled => true,
146        }
147    }
148
149    /// Run `f` on the underlying [`kira`] handle, for anything this type doesn't cover
150    /// (playback rate, panning, seeking). `None` while the sound is pending or was cancelled.
151    pub fn with_kira<R>(&self, f: impl FnOnce(&mut StaticSoundHandle) -> R) -> Option<R> {
152        match &mut *self.lock() {
153            Slot::Playing(h) => Some(f(h)),
154            Slot::Pending { .. } | Slot::Cancelled => None,
155        }
156    }
157}
158
159pub(crate) fn tween(duration: Duration) -> Tween {
160    if duration.is_zero() {
161        Tween::default()
162    } else {
163        Tween {
164            duration,
165            ..Tween::default()
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn pending_handle_remembers_state() {
176        let h = SoundHandle::pending();
177        assert!(h.is_pending() && !h.is_playing() && !h.is_finished());
178        h.set_volume_db(-12.0);
179        h.pause();
180        assert!(h.is_paused());
181        match &*h.lock() {
182            Slot::Pending { volume, paused } => {
183                assert_eq!(*volume, Some((Decibels(-12.0), Duration::ZERO)));
184                assert!(*paused);
185            }
186            other => panic!("expected pending, got {other:?}"),
187        }
188        h.resume();
189        assert!(!h.is_paused());
190        h.fade_volume_db(-3.0, Duration::from_secs(2));
191        assert!(matches!(
192            &*h.lock(),
193            Slot::Pending { volume: Some((Decibels(db), d)), .. }
194                if *db == -3.0 && *d == Duration::from_secs(2)
195        ));
196    }
197
198    #[test]
199    fn stopping_a_pending_handle_cancels_it() {
200        let h = SoundHandle::pending();
201        let clone = h.clone();
202        h.stop();
203        assert!(clone.is_finished() && !clone.is_pending());
204        assert!(clone.with_kira(|_| ()).is_none());
205    }
206}