Skip to main content

game_toolkit_audio/
lib.rs

1//! Audio subsystem built on [`kira`].
2//!
3//! Load static sound buffers (from disk or bytes) and play them on the main track. Every play
4//! call returns a [`SoundHandle`] that controls that instance: volume (instant or faded), pause,
5//! resume, stop, and whether it's still playing. [`PlayOptions`] covers looping, a start volume
6//! and deferred start in one call:
7//!
8//! ```no_run
9//! # use game_toolkit_audio::{Audio, PlayOptions};
10//! # fn demo(audio: &mut Audio, music: game_toolkit_audio::SoundId) -> anyhow::Result<()> {
11//! // Background music: loop at 30% volume, starting as soon as audio is available.
12//! let handle = audio.play_with(music, PlayOptions::looped().volume(0.3).deferred())?;
13//! handle.pause();
14//! handle.resume();
15//! handle.fade_volume(0.0, std::time::Duration::from_secs(2));
16//! # Ok(()) }
17//! ```
18//!
19//! Audio generated while it plays (a synthesizer, an emulated sound chip) goes through
20//! [`Audio::play_stream`], or [`Audio::play_stereo_stream`] for separate left and right channels. With the `synth` feature, [`Audio::play_synth`] plays chiptune
21//! music written in tracker notation live on the built-in synth, and [`Synth`] renders effects
22//! and jingles to PCM (see [`synth`]). Mixer tracks, spatial audio and file streaming are not wrapped;
23//! [`kira`] is re-exported for anything beyond this API, and [`SoundHandle::with_kira`]
24//! reaches the underlying handle.
25//!
26//! On the web, browsers keep an `AudioContext` suspended until the first user gesture, and kira
27//! offers no way to resume it. [`Audio`] therefore opens the backend lazily there: sounds can be
28//! loaded right away, but playback only starts once [`Audio::start`] has run (the toolkit's core
29//! calls it on the first key press, click or touch, or sooner if the page already had a gesture
30//! and the browser lets audio start). Until then plays fail with
31//! [`AudioError::NotStarted`], unless they were [`PlayOptions::deferred`], which queues them.
32//!
33//! [`Audio::suspend`] pauses everything at once. On the web, the toolkit's core also
34//! suspends the `AudioContext` while the game's canvas is detached from the page (see
35//! `DetachHandle`), since the game gets no frames then to do it itself.
36
37#![forbid(unsafe_code)]
38
39use std::collections::HashMap;
40#[cfg(not(target_arch = "wasm32"))]
41use std::path::Path;
42use std::sync::Arc;
43
44use anyhow::{Result, anyhow};
45use kira::sound::static_sound::{StaticSoundData, StaticSoundSettings};
46use kira::track::{TrackBuilder, TrackHandle};
47use kira::{AudioManager, AudioManagerSettings, Decibels, Frame, Tween};
48
49mod handle;
50use handle::Slot;
51pub use handle::SoundHandle;
52mod stream;
53pub use kira;
54pub use stream::{AudioStream, StereoStream, StreamHandle};
55use stream::{Source, StreamData};
56
57#[cfg(target_arch = "wasm32")]
58mod web;
59#[cfg(target_arch = "wasm32")]
60pub use web::DetachHandle;
61
62#[cfg(target_arch = "wasm32")]
63type Backend = web::WebBackend;
64#[cfg(not(target_arch = "wasm32"))]
65type Backend = kira::DefaultBackend;
66
67#[cfg(feature = "synth")]
68mod music;
69#[cfg(feature = "synth")]
70use music::PreparedSynth;
71#[cfg(feature = "synth")]
72pub use music::SynthHandle;
73#[cfg(feature = "synth")]
74pub mod synth;
75#[cfg(feature = "synth")]
76pub use synth::Synth;
77#[cfg(feature = "synth")]
78pub use synthie;
79
80#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
81pub struct SoundId(pub u32);
82
83/// Why a sound didn't play.
84#[derive(Debug)]
85#[non_exhaustive]
86pub enum AudioError {
87    /// Audio hasn't started yet: on the web, the player hasn't pressed a key, clicked or
88    /// touched the page. Not a real failure; try again later, check [`Audio::is_started`]
89    /// first, or play with [`PlayOptions::deferred`] to queue the sound.
90    NotStarted,
91    /// The [`SoundId`] wasn't returned by this [`Audio`].
92    UnknownSound(SoundId),
93    /// kira refused to play the sound (e.g. its sound limit was reached).
94    Backend(String),
95}
96
97impl std::fmt::Display for AudioError {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        match self {
100            Self::NotStarted => f.write_str("audio not started yet (waiting for a user gesture)"),
101            Self::UnknownSound(id) => write!(f, "unknown SoundId {id:?}"),
102            Self::Backend(e) => write!(f, "kira play: {e}"),
103        }
104    }
105}
106
107impl std::error::Error for AudioError {}
108
109/// How to play a sound: see [`Audio::play_with`]. The default plays once at the sound's own
110/// volume, and fails with [`AudioError::NotStarted`] before audio is available.
111#[derive(Copy, Clone, Debug, PartialEq)]
112pub struct PlayOptions {
113    /// Loop the whole sound until stopped.
114    pub looped: bool,
115    /// Linear amplitude (`1.0` is the sound's own level, `0.0` silence).
116    pub volume: f32,
117    /// If audio hasn't started yet (web, before the first user gesture), queue the sound and
118    /// start it once audio starts, instead of failing with [`AudioError::NotStarted`]. Meant
119    /// for music and ambience; a one-shot effect would play late, out of context.
120    pub deferred: bool,
121}
122
123impl Default for PlayOptions {
124    fn default() -> Self {
125        Self {
126            looped: false,
127            volume: 1.0,
128            deferred: false,
129        }
130    }
131}
132
133impl PlayOptions {
134    /// Options for a sound that loops until stopped.
135    pub fn looped() -> Self {
136        Self {
137            looped: true,
138            ..Self::default()
139        }
140    }
141
142    /// Set [`PlayOptions::volume`].
143    pub fn volume(self, volume: f32) -> Self {
144        Self { volume, ..self }
145    }
146
147    /// Set [`PlayOptions::deferred`].
148    pub fn deferred(self) -> Self {
149        Self {
150            deferred: true,
151            ..self
152        }
153    }
154}
155
156/// The open output: kira's manager and the bus every sound plays on.
157struct Manager {
158    kira: AudioManager<Backend>,
159    /// A sub track between the sounds and the main track, so [`Audio::suspend`] can pause
160    /// them all at once (kira's main track can't pause).
161    bus: TrackHandle,
162}
163
164pub struct Audio {
165    manager: Option<Manager>,
166    /// Set by [`Audio::suspend`]; applied to the bus once audio starts.
167    suspended: bool,
168    #[cfg(target_arch = "wasm32")]
169    detach: DetachHandle,
170    sounds: HashMap<SoundId, StaticSoundData>,
171    next_id: u32,
172    /// Main-track gain, remembered so a volume set before [`Audio::start`] still applies.
173    master_volume: Decibels,
174    /// [`PlayOptions::deferred`] sounds waiting for [`Audio::start`].
175    queued: Vec<(StaticSoundData, SoundHandle)>,
176    /// [`PlayOptions::deferred`] streams waiting for [`Audio::start`].
177    queued_streams: Vec<StreamData>,
178    /// [`PlayOptions::deferred`] synths waiting for [`Audio::start`].
179    #[cfg(feature = "synth")]
180    queued_synths: Vec<PreparedSynth>,
181}
182
183impl Audio {
184    /// Create the audio subsystem. On native targets this opens the output device immediately;
185    /// on the web it defers that to [`Audio::start`].
186    pub fn new() -> Result<Self> {
187        let mut audio = Self {
188            manager: None,
189            suspended: false,
190            #[cfg(target_arch = "wasm32")]
191            detach: DetachHandle::default(),
192            sounds: HashMap::new(),
193            next_id: 1,
194            master_volume: Decibels::IDENTITY,
195            queued: Vec::new(),
196            queued_streams: Vec::new(),
197            #[cfg(feature = "synth")]
198            queued_synths: Vec::new(),
199        };
200        if !cfg!(target_arch = "wasm32") {
201            audio.start()?;
202        }
203        Ok(audio)
204    }
205
206    /// Open the output device if it isn't open yet, and start any [`PlayOptions::deferred`]
207    /// sounds queued until then. Idempotent. On the web this must run inside a user-gesture
208    /// handler, otherwise the browser keeps the `AudioContext` muted.
209    pub fn start(&mut self) -> Result<()> {
210        if self.manager.is_some() {
211            return Ok(());
212        }
213        #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
214        let mut settings = AudioManagerSettings::<Backend>::default();
215        #[cfg(target_arch = "wasm32")]
216        {
217            settings.backend_settings = self.detach.clone();
218        }
219        let mut kira = AudioManager::<Backend>::new(settings)
220            .map_err(|e| anyhow!("kira AudioManager init: {e}"))?;
221        kira.main_track()
222            .set_volume(self.master_volume, Tween::default());
223        let mut bus = kira
224            .add_sub_track(TrackBuilder::new())
225            .map_err(|e| anyhow!("kira bus track: {e}"))?;
226        if self.suspended {
227            bus.pause(Tween::default());
228        }
229        self.manager = Some(Manager { kira, bus });
230        for (data, handle) in std::mem::take(&mut self.queued) {
231            self.start_queued(data, &handle);
232        }
233        for data in std::mem::take(&mut self.queued_streams) {
234            if !data.handle().is_finished() {
235                self.start_stream(data);
236            }
237        }
238        #[cfg(feature = "synth")]
239        for prepared in std::mem::take(&mut self.queued_synths) {
240            if !prepared.handle().is_finished() {
241                self.start_synth(prepared);
242            }
243        }
244        Ok(())
245    }
246
247    fn start_queued(&mut self, data: StaticSoundData, handle: &SoundHandle) {
248        let mut slot = handle.lock();
249        let Slot::Pending { volume, paused } = &*slot else {
250            return; // stopped while queued
251        };
252        let (volume, paused) = (*volume, *paused);
253        // An instant volume change replaces the start volume; a fade starts from it once
254        // the sound plays, over the duration that was asked for.
255        let (data, fade) = match volume {
256            Some((db, duration)) if duration.is_zero() => (data.volume(db), None),
257            Some((db, duration)) => (data, Some((db, duration))),
258            None => (data, None),
259        };
260        match self.play_now(data) {
261            Ok(mut kira) => {
262                if let Some((db, duration)) = fade {
263                    kira.set_volume(db, handle::tween(duration));
264                }
265                if paused {
266                    kira.pause(Tween::default());
267                }
268                *slot = Slot::Playing(kira);
269            }
270            Err(e) => {
271                log::warn!("queued sound failed to start: {e}");
272                *slot = Slot::Cancelled;
273            }
274        }
275    }
276
277    /// Whether the output device is open, i.e. [`Audio::play`] can produce sound. Always
278    /// `true` natively; on the web, `false` until the first user gesture.
279    pub fn is_started(&self) -> bool {
280        self.manager.is_some()
281    }
282
283    /// Load a sound file from disk. Not available on the web; use [`Audio::load_sound_bytes`].
284    #[cfg(not(target_arch = "wasm32"))]
285    pub fn load_sound(&mut self, path: impl AsRef<Path>) -> Result<SoundId> {
286        let data = StaticSoundData::from_file(path.as_ref())
287            .map_err(|e| anyhow!("kira load {}: {e}", path.as_ref().display()))?;
288        Ok(self.insert(data))
289    }
290
291    /// Decode an encoded sound file (wav, ogg, flac, mp3) held in memory.
292    pub fn load_sound_bytes(&mut self, bytes: impl Into<Vec<u8>>) -> Result<SoundId> {
293        let data = StaticSoundData::from_cursor(std::io::Cursor::new(bytes.into()))
294            .map_err(|e| anyhow!("kira decode: {e}"))?;
295        Ok(self.insert(data))
296    }
297
298    fn insert(&mut self, data: StaticSoundData) -> SoundId {
299        let id = SoundId(self.next_id);
300        self.next_id += 1;
301        self.sounds.insert(id, data);
302        id
303    }
304
305    /// Play a loaded sound once, at its own volume. The returned [`SoundHandle`] controls this
306    /// instance (volume, pause, stop, `is_playing`); dropping it lets the sound play out.
307    ///
308    /// Fails with [`AudioError::NotStarted`] on the web until the player's first key press,
309    /// click or touch (check [`Audio::is_started`]), so don't `?` it in `update`. For music
310    /// that should start as soon as it can, use [`Audio::play_with`] and
311    /// [`PlayOptions::deferred`].
312    pub fn play(&mut self, id: SoundId) -> Result<SoundHandle, AudioError> {
313        self.play_with(id, PlayOptions::default())
314    }
315
316    /// Play a loaded sound with [`PlayOptions`]: looping, a start volume, and whether to
317    /// queue it until audio starts.
318    pub fn play_with(
319        &mut self,
320        id: SoundId,
321        options: PlayOptions,
322    ) -> Result<SoundHandle, AudioError> {
323        let data = self
324            .sounds
325            .get(&id)
326            .ok_or(AudioError::UnknownSound(id))?
327            .clone();
328        self.play_data(data, options)
329    }
330
331    /// Play mono PCM samples (`-1.0..=1.0`) directly, e.g. a buffer rendered by
332    /// [`Synth`]. The samples are duplicated to both channels. Fails like [`Audio::play`]
333    /// before audio has started.
334    pub fn play_samples(
335        &mut self,
336        samples: &[f32],
337        sample_rate: u32,
338    ) -> Result<SoundHandle, AudioError> {
339        self.play_samples_with(samples, sample_rate, PlayOptions::default())
340    }
341
342    /// [`Audio::play_samples`] with [`PlayOptions`].
343    pub fn play_samples_with(
344        &mut self,
345        samples: &[f32],
346        sample_rate: u32,
347        options: PlayOptions,
348    ) -> Result<SoundHandle, AudioError> {
349        let frames: Arc<[Frame]> = samples
350            .iter()
351            .map(|&s| Frame { left: s, right: s })
352            .collect();
353        let data = StaticSoundData {
354            sample_rate,
355            frames,
356            settings: StaticSoundSettings::default(),
357            slice: None,
358        };
359        self.play_data(data, options)
360    }
361
362    fn play_data(
363        &mut self,
364        data: StaticSoundData,
365        options: PlayOptions,
366    ) -> Result<SoundHandle, AudioError> {
367        let mut data = data.volume(amplitude_to_db(options.volume));
368        if options.looped {
369            data = data.loop_region(..);
370        }
371        if self.manager.is_none() && options.deferred {
372            let handle = SoundHandle::pending();
373            self.queued.push((data, handle.clone()));
374            return Ok(handle);
375        }
376        self.play_now(data).map(SoundHandle::playing)
377    }
378
379    fn play_now(
380        &mut self,
381        data: StaticSoundData,
382    ) -> Result<kira::sound::static_sound::StaticSoundHandle, AudioError> {
383        self.manager
384            .as_mut()
385            .ok_or(AudioError::NotStarted)?
386            .bus
387            .play(data)
388            .map_err(|e| AudioError::Backend(e.to_string()))
389    }
390
391    /// Play audio generated while it plays, such as a synthesizer or an emulated sound
392    /// chip: `stream` is asked for mono samples at `sample_rate` as the mixer needs them,
393    /// on the audio thread, and resampled to the output rate. It plays until stopped
394    /// through the returned handle. Fails like [`Audio::play`] before audio has started.
395    pub fn play_stream(
396        &mut self,
397        stream: impl AudioStream,
398        sample_rate: u32,
399    ) -> Result<StreamHandle, AudioError> {
400        self.play_stream_with(stream, sample_rate, PlayOptions::default())
401    }
402
403    /// [`Audio::play_stream`] with [`PlayOptions`]. `volume` and `deferred` apply;
404    /// `looped` has no meaning for a stream, which runs until stopped.
405    pub fn play_stream_with(
406        &mut self,
407        stream: impl AudioStream,
408        sample_rate: u32,
409        options: PlayOptions,
410    ) -> Result<StreamHandle, AudioError> {
411        self.start_or_queue(Source::mono(Box::new(stream)), sample_rate, options)
412    }
413
414    /// Like [`Audio::play_stream`], for a stereo source: `stream` fills `[left, right]` frames.
415    pub fn play_stereo_stream(
416        &mut self,
417        stream: impl StereoStream,
418        sample_rate: u32,
419    ) -> Result<StreamHandle, AudioError> {
420        self.play_stereo_stream_with(stream, sample_rate, PlayOptions::default())
421    }
422
423    /// [`Audio::play_stereo_stream`] with [`PlayOptions`], as for [`Audio::play_stream_with`].
424    pub fn play_stereo_stream_with(
425        &mut self,
426        stream: impl StereoStream,
427        sample_rate: u32,
428        options: PlayOptions,
429    ) -> Result<StreamHandle, AudioError> {
430        self.start_or_queue(Source::Stereo(Box::new(stream)), sample_rate, options)
431    }
432
433    fn start_or_queue(
434        &mut self,
435        source: Source,
436        sample_rate: u32,
437        options: PlayOptions,
438    ) -> Result<StreamHandle, AudioError> {
439        let pending = self.manager.is_none();
440        if pending && !options.deferred {
441            return Err(AudioError::NotStarted);
442        }
443        let (data, handle) = StreamData::new(source, sample_rate, options.volume, pending);
444        if pending {
445            self.queued_streams.push(data);
446        } else {
447            self.start_stream(data);
448        }
449        Ok(handle)
450    }
451
452    fn start_stream(&mut self, data: StreamData) {
453        let handle = data.handle().clone();
454        let Some(manager) = self.manager.as_mut() else {
455            return;
456        };
457        match manager.bus.play(data) {
458            Ok(()) => handle.mark_started(),
459            Err(e) => {
460                log::warn!("stream failed to start: {e}");
461                handle.stop();
462            }
463        }
464    }
465
466    /// Play a [`synth::Song`] live on the synth, for music: the song is sequenced
467    /// sample-accurately on the audio thread, so tempo and drums stay tight whatever the
468    /// frame rate. The returned [`SynthHandle`] controls volume (ducking), pause and stop, and
469    /// sends live [`synth::AudioEvent`]s.
470    ///
471    /// [`PlayOptions`] apply as for other sounds: [`PlayOptions::looped`] loops the song,
472    /// otherwise it plays once (whatever the song's own [`synth::Song::looped`] says), and
473    /// `volume` and `deferred` work as usual. A song that plays once ends by itself
474    /// after its release and effect tails ([`SynthHandle::is_finished`] turns `true`); a
475    /// looping one plays until stopped. Dropping the last handle stops the synth either way.
476    /// Fails like [`Audio::play`] before audio has started.
477    ///
478    /// ```no_run
479    /// # use game_toolkit_audio::{Audio, PlayOptions};
480    /// use game_toolkit_audio::synth::{ChannelNo, DrumHit, Preset, Song, Tempo};
481    /// # fn demo(audio: &mut Audio) -> anyhow::Result<()> {
482    /// let song = Song::new(Tempo::bpm(140.0, 4))
483    ///     .patch(Preset::C64Bass.params())
484    ///     .lane(ChannelNo(0), "E2@0 . E3 . E2 . E3 . G2 . G3 . A2 . A3 -")?
485    ///     .lane(ChannelNo(3), "k h s h k h s h k h s h k k s o")?;
486    /// let music = audio.play_synth(song, PlayOptions::looped().volume(0.4).deferred())?;
487    /// let _ = music.drum(DrumHit::Crash); // a live hit on a game event
488    /// music.fade_volume(0.1, std::time::Duration::from_millis(300)); // duck
489    /// # Ok(()) }
490    /// ```
491    #[cfg(feature = "synth")]
492    pub fn play_synth(
493        &mut self,
494        mut song: synth::Song,
495        options: PlayOptions,
496    ) -> Result<SynthHandle, AudioError> {
497        song.looped = options.looped;
498        let data =
499            synthie::kira::SynthSoundData::new().with_sequencer(synth::SongPlayer::new(song));
500        self.play_synth_sound(data, options)
501    }
502
503    /// [`Audio::play_synth`] for a synth set up by hand: start patches per channel, a custom
504    /// [`synthie::prelude::Sequencer`] instead of a song, another channel count, or none at
505    /// all to play it only from [`SynthHandle`] events. `options.looped` has no meaning here;
506    /// the sequencer decides.
507    #[cfg(feature = "synth")]
508    pub fn play_synth_sound<S, const N: usize>(
509        &mut self,
510        data: synthie::kira::SynthSoundData<S, N>,
511        options: PlayOptions,
512    ) -> Result<SynthHandle, AudioError>
513    where
514        S: synthie::prelude::Sequencer + Send + 'static,
515    {
516        let pending = self.manager.is_none();
517        if pending && !options.deferred {
518            return Err(AudioError::NotStarted);
519        }
520        let data = data.with_volume(amplitude_to_db(options.volume));
521        let prepared = PreparedSynth::new(data, pending);
522        let handle = prepared.handle().clone();
523        if pending {
524            self.queued_synths.push(prepared);
525        } else {
526            self.start_synth(prepared);
527        }
528        Ok(handle)
529    }
530
531    #[cfg(feature = "synth")]
532    fn start_synth(&mut self, prepared: PreparedSynth) {
533        let handle = prepared.handle().clone();
534        let Some(manager) = self.manager.as_mut() else {
535            return;
536        };
537        match manager.bus.play(prepared) {
538            Ok(()) => handle.mark_started(),
539            Err(e) => {
540                log::warn!("synth failed to start: {e}");
541                handle.stop();
542            }
543        }
544    }
545
546    /// Master volume on the default main track, as a linear amplitude
547    /// (`1.0` is unity gain, `0.0` is silence). Converted to decibels for kira.
548    pub fn set_master_volume(&mut self, volume: f32) {
549        self.master_volume = amplitude_to_db(volume);
550        if let Some(manager) = self.manager.as_mut() {
551            manager
552                .kira
553                .main_track()
554                .set_volume(self.master_volume, Tween::default());
555        }
556    }
557
558    /// Pause everything that's playing (sounds, streams, synths) until [`Audio::resume`],
559    /// e.g. for a pause menu or when the host page hides the game. Sounds started while
560    /// suspended wait too. Works before audio has started: it then starts suspended.
561    ///
562    /// Independent of the per-sound [`SoundHandle::pause`], and of the toolkit suspending the
563    /// web output while the canvas is detached from the page (`DetachHandle`, web only):
564    /// reattaching the canvas doesn't undo a suspend.
565    pub fn suspend(&mut self) {
566        self.set_suspended(true);
567    }
568
569    /// Undo [`Audio::suspend`]: everything it paused continues where it left off.
570    pub fn resume(&mut self) {
571        self.set_suspended(false);
572    }
573
574    /// Whether [`Audio::suspend`] is in effect.
575    pub fn is_suspended(&self) -> bool {
576        self.suspended
577    }
578
579    fn set_suspended(&mut self, suspended: bool) {
580        if self.suspended == suspended {
581            return;
582        }
583        self.suspended = suspended;
584        if let Some(manager) = self.manager.as_mut() {
585            if suspended {
586                manager.bus.pause(Tween::default());
587            } else {
588                manager.bus.resume(Tween::default());
589            }
590        }
591    }
592
593    /// Web only: the handle that suspends the output while the game's canvas is detached
594    /// from the page. The toolkit's core drives it; see [`DetachHandle`].
595    #[cfg(target_arch = "wasm32")]
596    pub fn detach_handle(&self) -> DetachHandle {
597        self.detach.clone()
598    }
599}
600
601/// Linear amplitude (`1.0` unity, `0.0` silence) to kira's decibels.
602pub(crate) fn amplitude_to_db(volume: f32) -> Decibels {
603    if volume <= 0.0 {
604        Decibels::SILENCE
605    } else {
606        Decibels::from(20.0 * volume.log10())
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn amplitude_to_db_maps_unity_and_silence() {
616        assert_eq!(amplitude_to_db(1.0), Decibels::IDENTITY);
617        assert_eq!(amplitude_to_db(0.0), Decibels::SILENCE);
618        assert!((amplitude_to_db(0.5).0 + 6.02).abs() < 0.01);
619    }
620
621    #[test]
622    fn play_options_builders() {
623        let o = PlayOptions::looped().volume(0.25).deferred();
624        assert!(o.looped && o.deferred);
625        assert_eq!(o.volume, 0.25);
626        assert_eq!(PlayOptions::default().volume, 1.0);
627    }
628
629    #[test]
630    fn suspend_before_start_is_remembered() {
631        let mut audio = Audio {
632            manager: None,
633            suspended: false,
634            #[cfg(target_arch = "wasm32")]
635            detach: DetachHandle::default(),
636            sounds: HashMap::new(),
637            next_id: 1,
638            master_volume: Decibels::IDENTITY,
639            queued: Vec::new(),
640            queued_streams: Vec::new(),
641            #[cfg(feature = "synth")]
642            queued_synths: Vec::new(),
643        };
644        assert!(!audio.is_suspended());
645        audio.suspend();
646        audio.suspend();
647        assert!(audio.is_suspended());
648        audio.resume();
649        assert!(!audio.is_suspended());
650    }
651}