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`]. 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). Until then plays fail with
30//! [`AudioError::NotStarted`], unless they were [`PlayOptions::deferred`], which queues them.
31
32#![forbid(unsafe_code)]
33
34use std::collections::HashMap;
35#[cfg(not(target_arch = "wasm32"))]
36use std::path::Path;
37use std::sync::Arc;
38
39use anyhow::{Result, anyhow};
40use kira::sound::static_sound::{StaticSoundData, StaticSoundSettings};
41use kira::{AudioManager, AudioManagerSettings, Decibels, DefaultBackend, Frame, Tween};
42
43mod handle;
44use handle::Slot;
45pub use handle::SoundHandle;
46mod stream;
47pub use kira;
48use stream::StreamData;
49pub use stream::{AudioStream, StreamHandle};
50
51#[cfg(feature = "synth")]
52mod music;
53#[cfg(feature = "synth")]
54use music::PreparedSynth;
55#[cfg(feature = "synth")]
56pub use music::SynthHandle;
57#[cfg(feature = "synth")]
58pub mod synth;
59#[cfg(feature = "synth")]
60pub use synth::Synth;
61#[cfg(feature = "synth")]
62pub use synthie;
63
64#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
65pub struct SoundId(pub u32);
66
67/// Why a sound didn't play.
68#[derive(Debug)]
69#[non_exhaustive]
70pub enum AudioError {
71    /// Audio hasn't started yet: on the web, the player hasn't pressed a key, clicked or
72    /// touched the page. Not a real failure; try again later, check [`Audio::is_started`]
73    /// first, or play with [`PlayOptions::deferred`] to queue the sound.
74    NotStarted,
75    /// The [`SoundId`] wasn't returned by this [`Audio`].
76    UnknownSound(SoundId),
77    /// kira refused to play the sound (e.g. its sound limit was reached).
78    Backend(String),
79}
80
81impl std::fmt::Display for AudioError {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Self::NotStarted => f.write_str("audio not started yet (waiting for a user gesture)"),
85            Self::UnknownSound(id) => write!(f, "unknown SoundId {id:?}"),
86            Self::Backend(e) => write!(f, "kira play: {e}"),
87        }
88    }
89}
90
91impl std::error::Error for AudioError {}
92
93/// How to play a sound: see [`Audio::play_with`]. The default plays once at the sound's own
94/// volume, and fails with [`AudioError::NotStarted`] before audio is available.
95#[derive(Copy, Clone, Debug, PartialEq)]
96pub struct PlayOptions {
97    /// Loop the whole sound until stopped.
98    pub looped: bool,
99    /// Linear amplitude (`1.0` is the sound's own level, `0.0` silence).
100    pub volume: f32,
101    /// If audio hasn't started yet (web, before the first user gesture), queue the sound and
102    /// start it once audio starts, instead of failing with [`AudioError::NotStarted`]. Meant
103    /// for music and ambience; a one-shot effect would play late, out of context.
104    pub deferred: bool,
105}
106
107impl Default for PlayOptions {
108    fn default() -> Self {
109        Self {
110            looped: false,
111            volume: 1.0,
112            deferred: false,
113        }
114    }
115}
116
117impl PlayOptions {
118    /// Options for a sound that loops until stopped.
119    pub fn looped() -> Self {
120        Self {
121            looped: true,
122            ..Self::default()
123        }
124    }
125
126    /// Set [`PlayOptions::volume`].
127    pub fn volume(self, volume: f32) -> Self {
128        Self { volume, ..self }
129    }
130
131    /// Set [`PlayOptions::deferred`].
132    pub fn deferred(self) -> Self {
133        Self {
134            deferred: true,
135            ..self
136        }
137    }
138}
139
140pub struct Audio {
141    manager: Option<AudioManager<DefaultBackend>>,
142    sounds: HashMap<SoundId, StaticSoundData>,
143    next_id: u32,
144    /// Main-track gain, remembered so a volume set before [`Audio::start`] still applies.
145    master_volume: Decibels,
146    /// [`PlayOptions::deferred`] sounds waiting for [`Audio::start`].
147    queued: Vec<(StaticSoundData, SoundHandle)>,
148    /// [`PlayOptions::deferred`] streams waiting for [`Audio::start`].
149    queued_streams: Vec<StreamData>,
150    /// [`PlayOptions::deferred`] synths waiting for [`Audio::start`].
151    #[cfg(feature = "synth")]
152    queued_synths: Vec<PreparedSynth>,
153}
154
155impl Audio {
156    /// Create the audio subsystem. On native targets this opens the output device immediately;
157    /// on the web it defers that to [`Audio::start`].
158    pub fn new() -> Result<Self> {
159        let mut audio = Self {
160            manager: None,
161            sounds: HashMap::new(),
162            next_id: 1,
163            master_volume: Decibels::IDENTITY,
164            queued: Vec::new(),
165            queued_streams: Vec::new(),
166            #[cfg(feature = "synth")]
167            queued_synths: Vec::new(),
168        };
169        if !cfg!(target_arch = "wasm32") {
170            audio.start()?;
171        }
172        Ok(audio)
173    }
174
175    /// Open the output device if it isn't open yet, and start any [`PlayOptions::deferred`]
176    /// sounds queued until then. Idempotent. On the web this must run inside a user-gesture
177    /// handler, otherwise the browser keeps the `AudioContext` muted.
178    pub fn start(&mut self) -> Result<()> {
179        if self.manager.is_some() {
180            return Ok(());
181        }
182        let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
183            .map_err(|e| anyhow!("kira AudioManager init: {e}"))?;
184        manager
185            .main_track()
186            .set_volume(self.master_volume, Tween::default());
187        self.manager = Some(manager);
188        for (data, handle) in std::mem::take(&mut self.queued) {
189            self.start_queued(data, &handle);
190        }
191        for data in std::mem::take(&mut self.queued_streams) {
192            if !data.handle().is_finished() {
193                self.start_stream(data);
194            }
195        }
196        #[cfg(feature = "synth")]
197        for prepared in std::mem::take(&mut self.queued_synths) {
198            if !prepared.handle().is_finished() {
199                self.start_synth(prepared);
200            }
201        }
202        Ok(())
203    }
204
205    fn start_queued(&mut self, data: StaticSoundData, handle: &SoundHandle) {
206        let mut slot = handle.lock();
207        let Slot::Pending { volume, paused } = &*slot else {
208            return; // stopped while queued
209        };
210        let (volume, paused) = (*volume, *paused);
211        // An instant volume change replaces the start volume; a fade starts from it once
212        // the sound plays, over the duration that was asked for.
213        let (data, fade) = match volume {
214            Some((db, duration)) if duration.is_zero() => (data.volume(db), None),
215            Some((db, duration)) => (data, Some((db, duration))),
216            None => (data, None),
217        };
218        match self.play_now(data) {
219            Ok(mut kira) => {
220                if let Some((db, duration)) = fade {
221                    kira.set_volume(db, handle::tween(duration));
222                }
223                if paused {
224                    kira.pause(Tween::default());
225                }
226                *slot = Slot::Playing(kira);
227            }
228            Err(e) => {
229                log::warn!("queued sound failed to start: {e}");
230                *slot = Slot::Cancelled;
231            }
232        }
233    }
234
235    /// Whether the output device is open, i.e. [`Audio::play`] can produce sound. Always
236    /// `true` natively; on the web, `false` until the first user gesture.
237    pub fn is_started(&self) -> bool {
238        self.manager.is_some()
239    }
240
241    /// Load a sound file from disk. Not available on the web; use [`Audio::load_sound_bytes`].
242    #[cfg(not(target_arch = "wasm32"))]
243    pub fn load_sound(&mut self, path: impl AsRef<Path>) -> Result<SoundId> {
244        let data = StaticSoundData::from_file(path.as_ref())
245            .map_err(|e| anyhow!("kira load {}: {e}", path.as_ref().display()))?;
246        Ok(self.insert(data))
247    }
248
249    /// Decode an encoded sound file (wav, ogg, flac, mp3) held in memory.
250    pub fn load_sound_bytes(&mut self, bytes: impl Into<Vec<u8>>) -> Result<SoundId> {
251        let data = StaticSoundData::from_cursor(std::io::Cursor::new(bytes.into()))
252            .map_err(|e| anyhow!("kira decode: {e}"))?;
253        Ok(self.insert(data))
254    }
255
256    fn insert(&mut self, data: StaticSoundData) -> SoundId {
257        let id = SoundId(self.next_id);
258        self.next_id += 1;
259        self.sounds.insert(id, data);
260        id
261    }
262
263    /// Play a loaded sound once, at its own volume. The returned [`SoundHandle`] controls this
264    /// instance (volume, pause, stop, `is_playing`); dropping it lets the sound play out.
265    ///
266    /// Fails with [`AudioError::NotStarted`] on the web until the player's first key press,
267    /// click or touch (check [`Audio::is_started`]), so don't `?` it in `update`. For music
268    /// that should start as soon as it can, use [`Audio::play_with`] and
269    /// [`PlayOptions::deferred`].
270    pub fn play(&mut self, id: SoundId) -> Result<SoundHandle, AudioError> {
271        self.play_with(id, PlayOptions::default())
272    }
273
274    /// Play a loaded sound with [`PlayOptions`]: looping, a start volume, and whether to
275    /// queue it until audio starts.
276    pub fn play_with(
277        &mut self,
278        id: SoundId,
279        options: PlayOptions,
280    ) -> Result<SoundHandle, AudioError> {
281        let data = self
282            .sounds
283            .get(&id)
284            .ok_or(AudioError::UnknownSound(id))?
285            .clone();
286        self.play_data(data, options)
287    }
288
289    /// Play mono PCM samples (`-1.0..=1.0`) directly, e.g. a buffer rendered by
290    /// [`Synth`]. The samples are duplicated to both channels. Fails like [`Audio::play`]
291    /// before audio has started.
292    pub fn play_samples(
293        &mut self,
294        samples: &[f32],
295        sample_rate: u32,
296    ) -> Result<SoundHandle, AudioError> {
297        self.play_samples_with(samples, sample_rate, PlayOptions::default())
298    }
299
300    /// [`Audio::play_samples`] with [`PlayOptions`].
301    pub fn play_samples_with(
302        &mut self,
303        samples: &[f32],
304        sample_rate: u32,
305        options: PlayOptions,
306    ) -> Result<SoundHandle, AudioError> {
307        let frames: Arc<[Frame]> = samples
308            .iter()
309            .map(|&s| Frame { left: s, right: s })
310            .collect();
311        let data = StaticSoundData {
312            sample_rate,
313            frames,
314            settings: StaticSoundSettings::default(),
315            slice: None,
316        };
317        self.play_data(data, options)
318    }
319
320    fn play_data(
321        &mut self,
322        data: StaticSoundData,
323        options: PlayOptions,
324    ) -> Result<SoundHandle, AudioError> {
325        let mut data = data.volume(amplitude_to_db(options.volume));
326        if options.looped {
327            data = data.loop_region(..);
328        }
329        if self.manager.is_none() && options.deferred {
330            let handle = SoundHandle::pending();
331            self.queued.push((data, handle.clone()));
332            return Ok(handle);
333        }
334        self.play_now(data).map(SoundHandle::playing)
335    }
336
337    fn play_now(
338        &mut self,
339        data: StaticSoundData,
340    ) -> Result<kira::sound::static_sound::StaticSoundHandle, AudioError> {
341        self.manager
342            .as_mut()
343            .ok_or(AudioError::NotStarted)?
344            .play(data)
345            .map_err(|e| AudioError::Backend(e.to_string()))
346    }
347
348    /// Play audio generated while it plays, such as a synthesizer or an emulated sound
349    /// chip: `stream` is asked for mono samples at `sample_rate` as the mixer needs them,
350    /// on the audio thread, and resampled to the output rate. It plays until stopped
351    /// through the returned handle. Fails like [`Audio::play`] before audio has started.
352    pub fn play_stream(
353        &mut self,
354        stream: impl AudioStream,
355        sample_rate: u32,
356    ) -> Result<StreamHandle, AudioError> {
357        self.play_stream_with(stream, sample_rate, PlayOptions::default())
358    }
359
360    /// [`Audio::play_stream`] with [`PlayOptions`]. `volume` and `deferred` apply;
361    /// `looped` has no meaning for a stream, which runs until stopped.
362    pub fn play_stream_with(
363        &mut self,
364        stream: impl AudioStream,
365        sample_rate: u32,
366        options: PlayOptions,
367    ) -> Result<StreamHandle, AudioError> {
368        let pending = self.manager.is_none();
369        if pending && !options.deferred {
370            return Err(AudioError::NotStarted);
371        }
372        let (data, handle) =
373            StreamData::new(Box::new(stream), sample_rate, options.volume, pending);
374        if pending {
375            self.queued_streams.push(data);
376        } else {
377            self.start_stream(data);
378        }
379        Ok(handle)
380    }
381
382    fn start_stream(&mut self, data: StreamData) {
383        let handle = data.handle().clone();
384        let Some(manager) = self.manager.as_mut() else {
385            return;
386        };
387        match manager.play(data) {
388            Ok(()) => handle.mark_started(),
389            Err(e) => {
390                log::warn!("stream failed to start: {e}");
391                handle.stop();
392            }
393        }
394    }
395
396    /// Play a [`synth::Song`] live on the synth, for music: the song is sequenced
397    /// sample-accurately on the audio thread, so tempo and drums stay tight whatever the
398    /// frame rate. The returned [`SynthHandle`] controls volume (ducking), pause and stop, and
399    /// sends live [`synth::AudioEvent`]s.
400    ///
401    /// [`PlayOptions`] apply as for other sounds: [`PlayOptions::looped`] loops the song,
402    /// otherwise it plays once (whatever the song's own [`synth::Song::looped`] says), and
403    /// `volume` and `deferred` work as usual. A song that plays once ends by itself
404    /// after its release and effect tails ([`SynthHandle::is_finished`] turns `true`); a
405    /// looping one plays until stopped. Dropping the last handle stops the synth either way.
406    /// Fails like [`Audio::play`] before audio has started.
407    ///
408    /// ```no_run
409    /// # use game_toolkit_audio::{Audio, PlayOptions};
410    /// use game_toolkit_audio::synth::{ChannelNo, DrumHit, Song, Tempo, preset};
411    /// # fn demo(audio: &mut Audio) -> anyhow::Result<()> {
412    /// let song = Song::new(Tempo::bpm(140.0, 4))
413    ///     .patch(preset("C64 Bass").unwrap())
414    ///     .lane(ChannelNo(0), "E2@0 . E3 . E2 . E3 . G2 . G3 . A2 . A3 -")?
415    ///     .lane(ChannelNo(3), "k h s h k h s h k h s h k k s o")?;
416    /// let music = audio.play_synth(song, PlayOptions::looped().volume(0.4).deferred())?;
417    /// let _ = music.drum(DrumHit::Crash); // a live hit on a game event
418    /// music.fade_volume(0.1, std::time::Duration::from_millis(300)); // duck
419    /// # Ok(()) }
420    /// ```
421    #[cfg(feature = "synth")]
422    pub fn play_synth(
423        &mut self,
424        mut song: synth::Song,
425        options: PlayOptions,
426    ) -> Result<SynthHandle, AudioError> {
427        song.looped = options.looped;
428        let data =
429            synthie::kira::SynthSoundData::new().with_sequencer(synth::SongPlayer::new(song));
430        self.play_synth_sound(data, options)
431    }
432
433    /// [`Audio::play_synth`] for a synth set up by hand: start patches per channel, a custom
434    /// [`synthie::prelude::Sequencer`] instead of a song, another channel count, or none at
435    /// all to play it only from [`SynthHandle`] events. `options.looped` has no meaning here;
436    /// the sequencer decides.
437    #[cfg(feature = "synth")]
438    pub fn play_synth_sound<S, const N: usize>(
439        &mut self,
440        data: synthie::kira::SynthSoundData<S, N>,
441        options: PlayOptions,
442    ) -> Result<SynthHandle, AudioError>
443    where
444        S: synthie::prelude::Sequencer + Send + 'static,
445    {
446        let pending = self.manager.is_none();
447        if pending && !options.deferred {
448            return Err(AudioError::NotStarted);
449        }
450        let data = data.with_volume(amplitude_to_db(options.volume));
451        let prepared = PreparedSynth::new(data, pending);
452        let handle = prepared.handle().clone();
453        if pending {
454            self.queued_synths.push(prepared);
455        } else {
456            self.start_synth(prepared);
457        }
458        Ok(handle)
459    }
460
461    #[cfg(feature = "synth")]
462    fn start_synth(&mut self, prepared: PreparedSynth) {
463        let handle = prepared.handle().clone();
464        let Some(manager) = self.manager.as_mut() else {
465            return;
466        };
467        match manager.play(prepared) {
468            Ok(()) => handle.mark_started(),
469            Err(e) => {
470                log::warn!("synth failed to start: {e}");
471                handle.stop();
472            }
473        }
474    }
475
476    /// Master volume on the default main track, as a linear amplitude
477    /// (`1.0` is unity gain, `0.0` is silence). Converted to decibels for kira.
478    pub fn set_master_volume(&mut self, volume: f32) {
479        self.master_volume = amplitude_to_db(volume);
480        if let Some(manager) = self.manager.as_mut() {
481            manager
482                .main_track()
483                .set_volume(self.master_volume, Tween::default());
484        }
485    }
486}
487
488/// Linear amplitude (`1.0` unity, `0.0` silence) to kira's decibels.
489pub(crate) fn amplitude_to_db(volume: f32) -> Decibels {
490    if volume <= 0.0 {
491        Decibels::SILENCE
492    } else {
493        Decibels::from(20.0 * volume.log10())
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn amplitude_to_db_maps_unity_and_silence() {
503        assert_eq!(amplitude_to_db(1.0), Decibels::IDENTITY);
504        assert_eq!(amplitude_to_db(0.0), Decibels::SILENCE);
505        assert!((amplitude_to_db(0.5).0 + 6.02).abs() < 0.01);
506    }
507
508    #[test]
509    fn play_options_builders() {
510        let o = PlayOptions::looped().volume(0.25).deferred();
511        assert!(o.looped && o.deferred);
512        assert_eq!(o.volume, 0.25);
513        assert_eq!(PlayOptions::default().volume, 1.0);
514    }
515}