Skip to main content

game_toolkit_audio/
lib.rs

1//! Audio subsystem built on [`kira`].
2//!
3//! Minimal first-cut API: load static sound buffers (from disk or bytes) and play them on the
4//! main track. Mixer tracks, tweens, spatial audio and streaming are intentionally deferred.
5//!
6//! On the web, browsers keep an `AudioContext` suspended until the first user gesture, and kira
7//! offers no way to resume it. [`Audio`] therefore opens the backend lazily there: sounds can be
8//! loaded right away, but playback only starts once [`Audio::start`] has run (the toolkit's core
9//! calls it on the first key press, click or touch).
10
11#![forbid(unsafe_code)]
12
13use std::collections::HashMap;
14#[cfg(not(target_arch = "wasm32"))]
15use std::path::Path;
16use std::sync::Arc;
17
18use anyhow::{Result, anyhow};
19use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle, StaticSoundSettings};
20use kira::{AudioManager, AudioManagerSettings, Decibels, DefaultBackend, Frame, Tween};
21
22#[cfg(feature = "synth")]
23mod synth;
24#[cfg(feature = "synth")]
25pub use synth::Synth;
26#[cfg(feature = "synth")]
27pub use synthie;
28
29#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
30pub struct SoundId(pub u32);
31
32pub struct Audio {
33    manager: Option<AudioManager<DefaultBackend>>,
34    sounds: HashMap<SoundId, StaticSoundData>,
35    next_id: u32,
36    /// Main-track gain, remembered so a volume set before [`Audio::start`] still applies.
37    master_volume: Decibels,
38}
39
40impl Audio {
41    /// Create the audio subsystem. On native targets this opens the output device immediately;
42    /// on the web it defers that to [`Audio::start`].
43    pub fn new() -> Result<Self> {
44        let mut audio = Self {
45            manager: None,
46            sounds: HashMap::new(),
47            next_id: 1,
48            master_volume: Decibels::IDENTITY,
49        };
50        if !cfg!(target_arch = "wasm32") {
51            audio.start()?;
52        }
53        Ok(audio)
54    }
55
56    /// Open the output device if it isn't open yet. Idempotent. On the web this must run
57    /// inside a user-gesture handler, otherwise the browser keeps the `AudioContext` muted.
58    pub fn start(&mut self) -> Result<()> {
59        if self.manager.is_some() {
60            return Ok(());
61        }
62        let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
63            .map_err(|e| anyhow!("kira AudioManager init: {e}"))?;
64        manager
65            .main_track()
66            .set_volume(self.master_volume, Tween::default());
67        self.manager = Some(manager);
68        Ok(())
69    }
70
71    /// Whether the output device is open, i.e. [`Audio::play`] can produce sound.
72    pub fn is_started(&self) -> bool {
73        self.manager.is_some()
74    }
75
76    /// Load a sound file from disk. Not available on the web; use [`Audio::load_sound_bytes`].
77    #[cfg(not(target_arch = "wasm32"))]
78    pub fn load_sound(&mut self, path: impl AsRef<Path>) -> Result<SoundId> {
79        let data = StaticSoundData::from_file(path.as_ref())
80            .map_err(|e| anyhow!("kira load {}: {e}", path.as_ref().display()))?;
81        Ok(self.insert(data))
82    }
83
84    /// Decode an encoded sound file (wav, ogg, flac, mp3) held in memory.
85    pub fn load_sound_bytes(&mut self, bytes: impl Into<Vec<u8>>) -> Result<SoundId> {
86        let data = StaticSoundData::from_cursor(std::io::Cursor::new(bytes.into()))
87            .map_err(|e| anyhow!("kira decode: {e}"))?;
88        Ok(self.insert(data))
89    }
90
91    fn insert(&mut self, data: StaticSoundData) -> SoundId {
92        let id = SoundId(self.next_id);
93        self.next_id += 1;
94        self.sounds.insert(id, data);
95        id
96    }
97
98    fn manager(&mut self) -> Result<&mut AudioManager<DefaultBackend>> {
99        self.manager
100            .as_mut()
101            .ok_or_else(|| anyhow!("audio not started yet (waiting for a user gesture)"))
102    }
103
104    pub fn play(&mut self, id: SoundId) -> Result<StaticSoundHandle> {
105        let data = self
106            .sounds
107            .get(&id)
108            .ok_or_else(|| anyhow!("unknown SoundId {id:?}"))?
109            .clone();
110        self.manager()?
111            .play(data)
112            .map_err(|e| anyhow!("kira play: {e}"))
113    }
114
115    /// Play mono PCM samples (`-1.0..=1.0`) directly, e.g. a buffer rendered by
116    /// [`Synth`]. The samples are duplicated to both channels.
117    pub fn play_samples(&mut self, samples: &[f32], sample_rate: u32) -> Result<StaticSoundHandle> {
118        let frames: Arc<[Frame]> = samples
119            .iter()
120            .map(|&s| Frame { left: s, right: s })
121            .collect();
122        let data = StaticSoundData {
123            sample_rate,
124            frames,
125            settings: StaticSoundSettings::default(),
126            slice: None,
127        };
128        self.manager()?
129            .play(data)
130            .map_err(|e| anyhow!("kira play: {e}"))
131    }
132
133    /// Master volume on the default main track, as a linear amplitude
134    /// (`1.0` is unity gain, `0.0` is silence). Converted to decibels for kira.
135    pub fn set_master_volume(&mut self, volume: f32) {
136        self.master_volume = if volume <= 0.0 {
137            Decibels::SILENCE
138        } else {
139            Decibels::from(20.0 * volume.log10())
140        };
141        if let Some(manager) = self.manager.as_mut() {
142            manager
143                .main_track()
144                .set_volume(self.master_volume, Tween::default());
145        }
146    }
147}