Skip to main content

game_toolkit_audio/
stream.rs

1//! Audio generated while it plays: [`AudioStream`] sources and their [`StreamHandle`].
2//!
3//! A stream is pulled from the audio callback itself, so it needs no decoder thread and works
4//! on the web as well as natively. Keep [`AudioStream::fill`] cheap and non-blocking: it runs
5//! on the audio thread.
6
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
9
10use kira::sound::{Sound, SoundData};
11use kira::{Frame, info::Info};
12
13/// A source of mono samples, produced on demand at its own sample rate.
14///
15/// Implemented for any `FnMut(&mut [f32]) + Send + 'static`, so a closure that owns a
16/// synthesizer or an emulated sound chip can be played directly.
17pub trait AudioStream: Send + 'static {
18    /// Overwrite `out` with the next samples in `-1.0..=1.0`.
19    fn fill(&mut self, out: &mut [f32]);
20}
21
22impl<F: FnMut(&mut [f32]) + Send + 'static> AudioStream for F {
23    fn fill(&mut self, out: &mut [f32]) {
24        self(out);
25    }
26}
27
28/// Samples requested from a stream at once.
29const CHUNK: usize = 512;
30
31/// State shared between a [`StreamHandle`] and the playing sound.
32#[derive(Debug)]
33struct Shared {
34    /// Linear amplitude as `f32` bits.
35    volume: AtomicU32,
36    paused: AtomicBool,
37    stopped: AtomicBool,
38    /// Still queued until audio starts (see [`crate::PlayOptions::deferred`]).
39    pending: AtomicBool,
40}
41
42/// Controls one playing [`AudioStream`], as returned by [`crate::Audio::play_stream`].
43///
44/// Dropping the handle does not stop the stream; it plays until [`StreamHandle::stop`].
45/// Clones refer to the same stream. Changes apply from the next audio block, and volume
46/// changes, pausing and stopping ramp over that block so they do not click.
47#[derive(Clone, Debug)]
48pub struct StreamHandle {
49    shared: Arc<Shared>,
50}
51
52impl StreamHandle {
53    fn new(volume: f32, pending: bool) -> Self {
54        Self {
55            shared: Arc::new(Shared {
56                // Clamped like set_volume: a negative start volume is silence, not an
57                // inverted signal.
58                volume: AtomicU32::new(volume.max(0.0).to_bits()),
59                paused: AtomicBool::new(false),
60                stopped: AtomicBool::new(false),
61                pending: AtomicBool::new(pending),
62            }),
63        }
64    }
65
66    /// Set the volume as a linear amplitude (`1.0` is the stream's own level, `0.0` silence).
67    pub fn set_volume(&self, volume: f32) {
68        self.shared
69            .volume
70            .store(volume.max(0.0).to_bits(), Ordering::Relaxed);
71    }
72
73    /// The current linear volume.
74    pub fn volume(&self) -> f32 {
75        f32::from_bits(self.shared.volume.load(Ordering::Relaxed))
76    }
77
78    /// Pause: the stream fades out over one audio block, then outputs silence and is not
79    /// asked for samples until resumed.
80    pub fn pause(&self) {
81        self.shared.paused.store(true, Ordering::Relaxed);
82    }
83
84    /// Resume a paused stream where it left off, fading back in over one audio block.
85    pub fn resume(&self) {
86        self.shared.paused.store(false, Ordering::Relaxed);
87    }
88
89    /// Stop the stream for good; it fades out over one audio block and is then dropped from
90    /// the mixer.
91    pub fn stop(&self) {
92        self.shared.stopped.store(true, Ordering::Relaxed);
93    }
94
95    /// Whether the stream is audible: started, not paused and not stopped.
96    pub fn is_playing(&self) -> bool {
97        !self.is_pending() && !self.is_paused() && !self.is_finished()
98    }
99
100    /// Whether the stream is paused.
101    pub fn is_paused(&self) -> bool {
102        self.shared.paused.load(Ordering::Relaxed)
103    }
104
105    /// Whether the stream is still waiting for audio to start.
106    pub fn is_pending(&self) -> bool {
107        self.shared.pending.load(Ordering::Relaxed)
108    }
109
110    /// Whether the stream was stopped.
111    pub fn is_finished(&self) -> bool {
112        self.shared.stopped.load(Ordering::Relaxed)
113    }
114
115    pub(crate) fn mark_started(&self) {
116        self.shared.pending.store(false, Ordering::Relaxed);
117    }
118}
119
120/// A stream ready to hand to kira.
121pub(crate) struct StreamData {
122    stream: Box<dyn AudioStream>,
123    sample_rate: u32,
124    handle: StreamHandle,
125}
126
127impl StreamData {
128    pub(crate) fn new(
129        stream: Box<dyn AudioStream>,
130        sample_rate: u32,
131        volume: f32,
132        pending: bool,
133    ) -> (Self, StreamHandle) {
134        let handle = StreamHandle::new(volume, pending);
135        let data = Self {
136            stream,
137            sample_rate: sample_rate.max(1),
138            handle: handle.clone(),
139        };
140        (data, handle)
141    }
142
143    pub(crate) fn handle(&self) -> &StreamHandle {
144        &self.handle
145    }
146}
147
148impl SoundData for StreamData {
149    type Error = std::convert::Infallible;
150    type Handle = ();
151
152    fn into_sound(self) -> Result<(Box<dyn Sound>, ()), Self::Error> {
153        let sound = StreamSound::new(self.stream, self.sample_rate, self.handle.shared);
154        Ok((Box::new(sound), ()))
155    }
156}
157
158/// The kira side: pulls chunks from the stream and resamples them linearly to the output
159/// rate.
160struct StreamSound {
161    stream: Box<dyn AudioStream>,
162    sample_rate: f64,
163    shared: Arc<Shared>,
164    chunk: Vec<f32>,
165    next_in_chunk: usize,
166    /// The two stream samples the output position lies between, and how far along it is.
167    previous: f32,
168    current: f32,
169    phase: f64,
170    /// Whether `previous` and `current` hold the first two samples yet.
171    primed: bool,
172    /// The gain the last block ended on; each block ramps from it to the target gain.
173    applied_volume: f32,
174}
175
176impl StreamSound {
177    fn new(stream: Box<dyn AudioStream>, sample_rate: u32, shared: Arc<Shared>) -> Self {
178        // Start at the target volume (or silence if paused) so playback does not fade in.
179        let applied_volume = if shared.paused.load(Ordering::Relaxed) {
180            0.0
181        } else {
182            f32::from_bits(shared.volume.load(Ordering::Relaxed))
183        };
184        Self {
185            stream,
186            sample_rate: f64::from(sample_rate),
187            shared,
188            chunk: vec![0.0; CHUNK],
189            next_in_chunk: CHUNK,
190            previous: 0.0,
191            current: 0.0,
192            phase: 0.0,
193            primed: false,
194            applied_volume,
195        }
196    }
197
198    fn next_sample(&mut self) -> f32 {
199        if self.next_in_chunk == self.chunk.len() {
200            self.stream.fill(&mut self.chunk);
201            self.next_in_chunk = 0;
202        }
203        let s = self.chunk[self.next_in_chunk];
204        self.next_in_chunk += 1;
205        s
206    }
207
208    /// Produces `out` at an output frame duration of `dt` seconds.
209    fn render(&mut self, out: &mut [Frame], dt: f64) {
210        let silenced = self.shared.paused.load(Ordering::Relaxed)
211            || self.shared.stopped.load(Ordering::Relaxed);
212        let target = if silenced {
213            0.0
214        } else {
215            f32::from_bits(self.shared.volume.load(Ordering::Relaxed))
216        };
217        if silenced && self.applied_volume == 0.0 {
218            out.fill(Frame::ZERO);
219            return;
220        }
221        if !self.primed {
222            self.previous = self.next_sample();
223            self.current = self.next_sample();
224            self.primed = true;
225        }
226        let from = self.applied_volume;
227        let len = out.len() as f32;
228        let step = self.sample_rate * dt;
229        for (i, frame) in out.iter_mut().enumerate() {
230            // Linear ramp that lands exactly on `target` at the block's last frame.
231            let volume = from + (target - from) * ((i + 1) as f32 / len);
232            let s = self.previous + (self.current - self.previous) * self.phase as f32;
233            *frame = Frame::from_mono(s * volume);
234            self.phase += step;
235            while self.phase >= 1.0 {
236                self.phase -= 1.0;
237                self.previous = self.current;
238                self.current = self.next_sample();
239            }
240        }
241        if !out.is_empty() {
242            self.applied_volume = target;
243        }
244    }
245}
246
247impl Sound for StreamSound {
248    fn process(&mut self, out: &mut [Frame], dt: f64, _info: &Info<'_>) {
249        self.render(out, dt);
250    }
251
252    fn finished(&self) -> bool {
253        // Only once the fade-out block has played, so stopping does not click.
254        self.shared.stopped.load(Ordering::Relaxed) && self.applied_volume == 0.0
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn sound(stream: impl AudioStream, rate: u32) -> (StreamSound, StreamHandle) {
263        sound_at(stream, rate, 1.0)
264    }
265
266    fn sound_at(stream: impl AudioStream, rate: u32, volume: f32) -> (StreamSound, StreamHandle) {
267        let (data, handle) = StreamData::new(Box::new(stream), rate, volume, false);
268        let sound = StreamSound::new(data.stream, data.sample_rate, handle.shared.clone());
269        (sound, handle)
270    }
271
272    fn ramp() -> impl AudioStream {
273        let mut n = 0u32;
274        move |out: &mut [f32]| {
275            for s in out {
276                *s = n as f32;
277                n += 1;
278            }
279        }
280    }
281
282    #[test]
283    fn same_rate_passes_samples_through() {
284        let (mut s, _) = sound(ramp(), 100);
285        let mut out = [Frame::ZERO; 4];
286        s.render(&mut out, 1.0 / 100.0);
287        let left: Vec<f32> = out.iter().map(|f| f.left).collect();
288        assert_eq!(left, [0.0, 1.0, 2.0, 3.0]);
289    }
290
291    #[test]
292    fn half_rate_interpolates() {
293        let (mut s, _) = sound(ramp(), 50);
294        let mut out = [Frame::ZERO; 6];
295        s.render(&mut out, 1.0 / 100.0);
296        let left: Vec<f32> = out.iter().map(|f| f.left).collect();
297        assert_eq!(left, [0.0, 0.5, 1.0, 1.5, 2.0, 2.5]);
298    }
299
300    #[test]
301    fn paused_and_stopped_streams_fade_out_then_go_silent() {
302        let (mut s, handle) = sound(|out: &mut [f32]| out.fill(1.0), 100);
303        let mut out = [Frame::ZERO; 4];
304        handle.pause();
305        s.render(&mut out, 0.01);
306        let left: Vec<f32> = out.iter().map(|f| f.left).collect();
307        assert_eq!(left, [0.75, 0.5, 0.25, 0.0]);
308        s.render(&mut out, 0.01);
309        assert!(out.iter().all(|f| *f == Frame::ZERO));
310
311        handle.resume();
312        s.render(&mut out, 0.01);
313        let left: Vec<f32> = out.iter().map(|f| f.left).collect();
314        assert_eq!(left, [0.25, 0.5, 0.75, 1.0]);
315
316        handle.stop();
317        assert!(handle.is_finished() && !handle.is_playing());
318        assert!(!s.finished(), "dropped before the fade-out block");
319        s.render(&mut out, 0.01);
320        assert_eq!(out[3].left, 0.0);
321        assert!(s.finished());
322    }
323
324    #[test]
325    fn a_paused_stream_is_not_asked_for_samples() {
326        use std::sync::atomic::AtomicUsize;
327
328        let pulled = Arc::new(AtomicUsize::new(0));
329        let counter = Arc::clone(&pulled);
330        let (mut s, handle) = sound(
331            move |out: &mut [f32]| {
332                counter.fetch_add(1, Ordering::Relaxed);
333                out.fill(1.0);
334            },
335            100,
336        );
337        let mut out = [Frame::ZERO; 4];
338        handle.pause();
339        s.render(&mut out, 0.01); // fade-out block
340        let after_fade = pulled.load(Ordering::Relaxed);
341        s.render(&mut out, 0.01);
342        s.render(&mut out, 0.01);
343        assert_eq!(pulled.load(Ordering::Relaxed), after_fade);
344    }
345
346    #[test]
347    fn the_mixer_pulls_samples_from_a_playing_stream() {
348        use std::sync::atomic::AtomicUsize;
349
350        use kira::backend::mock::{MockBackend, MockBackendSettings};
351        use kira::{AudioManager, AudioManagerSettings};
352
353        let pulled = Arc::new(AtomicUsize::new(0));
354        let counter = Arc::clone(&pulled);
355        let stream = move |out: &mut [f32]| {
356            counter.fetch_add(out.len(), Ordering::Relaxed);
357            out.fill(0.5);
358        };
359        let mut manager = AudioManager::<MockBackend>::new(AudioManagerSettings {
360            backend_settings: MockBackendSettings { sample_rate: 100 },
361            ..AudioManagerSettings::default()
362        })
363        .unwrap();
364        let (data, handle) = StreamData::new(Box::new(stream), 100, 1.0, false);
365        manager.play(data).unwrap();
366        handle.mark_started();
367        manager.backend_mut().on_start_processing();
368        manager.backend_mut().process();
369        assert!(
370            pulled.load(Ordering::Relaxed) > 0,
371            "stream was never asked for samples"
372        );
373        assert!(handle.is_playing());
374
375        handle.stop();
376        manager.backend_mut().on_start_processing();
377        manager.backend_mut().process();
378        assert!(handle.is_finished());
379    }
380
381    #[test]
382    fn negative_start_volume_is_silence() {
383        let (mut s, handle) = sound_at(|out: &mut [f32]| out.fill(1.0), 100, -0.5);
384        assert_eq!(handle.volume(), 0.0);
385        let mut out = [Frame::ZERO; 2];
386        s.render(&mut out, 0.01);
387        assert!(out.iter().all(|f| f.left == 0.0 && f.right == 0.0));
388    }
389
390    #[test]
391    fn volume_changes_ramp_over_one_block() {
392        let (mut s, handle) = sound(|out: &mut [f32]| out.fill(1.0), 100);
393        handle.set_volume(0.25);
394        let mut out = [Frame::ZERO; 3];
395        s.render(&mut out, 0.01);
396        let left: Vec<f32> = out.iter().map(|f| f.left).collect();
397        assert_eq!(left, [0.75, 0.5, 0.25]);
398        s.render(&mut out, 0.01);
399        assert!(out.iter().all(|f| f.left == 0.25));
400    }
401
402    #[test]
403    fn a_stream_starts_at_its_volume_without_fading_in() {
404        let (mut s, _) = sound_at(|out: &mut [f32]| out.fill(1.0), 100, 0.5);
405        let mut out = [Frame::ZERO; 3];
406        s.render(&mut out, 0.01);
407        assert!(out.iter().all(|f| f.left == 0.5));
408    }
409}