audio_engine/
engine.rs

1use std::{
2    hash::Hash,
3    sync::{Arc, Mutex},
4};
5
6use cpal::{
7    traits::{DeviceTrait, HostTrait, StreamTrait},
8    SampleRate, StreamError,
9};
10
11use super::{Mixer, Sound, SoundSource};
12use crate::converter::{ChannelConverter, SampleRateConverter};
13
14use backend::Backend;
15
16#[cfg(not(target_arch = "wasm32"))]
17mod backend {
18    use super::create_device;
19    use crate::Mixer;
20    use std::{
21        hash::Hash,
22        sync::{Arc, Mutex},
23    };
24
25    struct StreamEventLoop<G: Eq + Hash + Send + 'static> {
26        mixer: Arc<Mutex<Mixer<G>>>,
27        stream: Option<cpal::platform::Stream>,
28    }
29
30    impl<G: Eq + Hash + Send + 'static> StreamEventLoop<G> {
31        fn run(
32            &mut self,
33            event_channel: std::sync::mpsc::Sender<StreamEvent>,
34            stream_event_receiver: std::sync::mpsc::Receiver<StreamEvent>,
35        ) {
36            // Trigger first device creation
37            event_channel.send(StreamEvent::RecreateStream).unwrap();
38
39            let mut handled = false;
40            let error_callback = move |err| {
41                log::error!("stream error: {}", err);
42                if !handled {
43                    // The Stream could have send multiple errors. I confirmed this happening on
44                    // android (a error before the stream close, and a error after closing it).
45                    handled = true;
46                    event_channel.send(StreamEvent::RecreateStream).unwrap()
47                }
48            };
49
50            while let Ok(event) = stream_event_receiver.recv() {
51                match event {
52                    StreamEvent::RecreateStream => {
53                        log::debug!("recreating audio device");
54
55                        // Droping the stream is unsound in android, see:
56                        // https://github.com/katyo/oboe-rs/issues/41
57                        #[cfg(target_os = "android")]
58                        std::mem::forget(self.stream.take());
59
60                        #[cfg(not(target_os = "android"))]
61                        drop(self.stream.take());
62
63                        let stream = create_device(&self.mixer, error_callback.clone());
64                        let stream = match stream {
65                            Ok(x) => x,
66                            Err(x) => {
67                                log::error!("creating audio device failed: {}", x);
68                                return;
69                            }
70                        };
71                        self.stream = Some(stream);
72                    }
73                    StreamEvent::Drop => {
74                        // Droping the stream is unsound in android, see:
75                        // https://github.com/katyo/oboe-rs/issues/41
76                        #[cfg(target_os = "android")]
77                        std::mem::forget(self.stream.take());
78
79                        return;
80                    }
81                }
82            }
83        }
84    }
85
86    enum StreamEvent {
87        RecreateStream,
88        Drop,
89    }
90
91    pub struct Backend {
92        join: Option<std::thread::JoinHandle<()>>,
93        sender: std::sync::mpsc::Sender<StreamEvent>,
94    }
95    impl Backend {
96        pub(super) fn start<G: Eq + Hash + Send + 'static>(
97            mixer: Arc<Mutex<Mixer<G>>>,
98        ) -> Result<Self, &'static str> {
99            let (sender, receiver) = std::sync::mpsc::channel::<StreamEvent>();
100            let join = {
101                let sender = sender.clone();
102                std::thread::spawn(move || {
103                    log::trace!("starting thread");
104                    StreamEventLoop {
105                        mixer,
106                        stream: None,
107                    }
108                    .run(sender, receiver)
109                })
110            };
111            Ok(Self {
112                join: Some(join),
113                sender,
114            })
115        }
116    }
117
118    impl Drop for Backend {
119        fn drop(&mut self) {
120            self.sender.send(StreamEvent::Drop).unwrap();
121            self.join.take().unwrap().join().unwrap();
122        }
123    }
124}
125#[cfg(target_arch = "wasm32")]
126mod backend {
127    use super::create_device;
128    use crate::Mixer;
129    use std::{
130        hash::Hash,
131        sync::{Arc, Mutex},
132    };
133
134    pub struct Backend {
135        _stream: cpal::Stream,
136    }
137    impl Backend {
138        pub(super) fn start<G: Eq + Hash + Send + 'static>(
139            mixer: Arc<Mutex<Mixer<G>>>,
140        ) -> Result<Self, &'static str> {
141            // On Wasm backend, I cannot created a second thread to handle stream errors, but
142            // errors in the wasm backend (AudioContext) is unexpected. In fact, cpal doesn't create
143            // any StreamError in its wasm backend.
144            let stream = create_device(&mixer, |err| log::error!("stream error: {err}"));
145            let stream = match stream {
146                Ok(x) => x,
147                Err(x) => {
148                    log::error!("creating audio device failed: {}", x);
149                    return Err(x);
150                }
151            };
152            Ok(Self { _stream: stream })
153        }
154
155        pub(super) fn resume(&self) {
156            match self._stream.as_inner() {
157                cpal::platform::StreamInner::WebAudio(x) => {
158                    let _ = x.audio_context().resume();
159                }
160                #[allow(unreachable_patterns)]
161                _ => {}
162            }
163        }
164    }
165}
166
167/// The main struct of the crate.
168///
169/// This hold all existing `SoundSource`s and `cpal::platform::Stream`.
170///
171/// Each sound is associated with a group, which is purely used by
172/// [`set_group_volume`](AudioEngine::set_group_volume), to allow mixing multiple sounds together.
173pub struct AudioEngine<G: Eq + Hash + Send + 'static = ()> {
174    mixer: Arc<Mutex<Mixer<G>>>,
175    _backend: Backend,
176}
177impl<G: Default + Eq + Hash + Send> AudioEngine<G> {
178    /// Add a new Sound in the default Group.
179    ///
180    /// Same as calling [`new_sound_with_group(G::default(), source)`](Self::new_sound_with_group).
181    ///
182    /// See [Self::new_sound_with_group], for more information.
183    pub fn new_sound<T: SoundSource + Send + 'static>(
184        &self,
185        source: T,
186    ) -> Result<Sound<G>, &'static str> {
187        self.new_sound_with_group(G::default(), source)
188    }
189}
190impl AudioEngine {
191    /// Tries to create a new AudioEngine.
192    ///
193    /// `cpal` will spawn a new thread where the sound samples will be sampled, mixed, and outputed
194    /// to the output stream.
195    pub fn new() -> Result<Self, &'static str> {
196        AudioEngine::with_groups::<()>()
197    }
198
199    /// Tries to create a new AudioEngine, with the given type to represent sound groups.
200    ///
201    /// `cpal` will spawn a new thread where the sound samples will be sampled, mixed, and outputed
202    /// to the output stream.
203    ///
204    /// # Example
205    ///
206    /// ```no_run
207    /// # fn main() -> Result<(), &'static str> {
208    /// # let my_fx = audio_engine::SineWave::new(44100, 500.0);
209    /// # let my_music = audio_engine::SineWave::new(44100, 440.0);
210    /// use audio_engine::{AudioEngine, WavDecoder};
211    ///
212    /// #[derive(Eq, Hash, PartialEq)]
213    /// enum Group {
214    ///     Effect,
215    ///     Music,
216    /// }
217    ///
218    /// let audio_engine = AudioEngine::with_groups::<Group>()?;
219    /// let mut fx = audio_engine.new_sound_with_group(Group::Effect, my_fx)?;
220    /// let mut music = audio_engine.new_sound_with_group(Group::Music, my_music)?;
221    ///
222    /// fx.play();
223    /// music.play();
224    ///
225    /// // decrease music volume, for example
226    /// audio_engine.set_group_volume(Group::Music, 0.1);
227    /// # Ok(())
228    /// # }
229    /// ```
230    pub fn with_groups<G: Eq + Hash + Send>() -> Result<AudioEngine<G>, &'static str> {
231        let mixer = Arc::new(Mutex::new(Mixer::<G>::new(2, super::SampleRate(48000))));
232        let backend = Backend::start(mixer.clone())?;
233
234        Ok(AudioEngine::<G> {
235            mixer,
236            _backend: backend,
237        })
238    }
239}
240impl<G: Eq + Hash + Send> AudioEngine<G> {
241    //// Call `resume()` on the underlying
242    ///[`AudioContext`](https://developer.mozilla.org/pt-BR/docs/Web/API/AudioContext).
243    ///
244    /// On Chrome, if a `AudioContext` is created before a user interaction, the `AudioContext` will
245    /// start in the "supended" state. To resume the `AudioContext`, `AudioContext.resume()` must be
246    /// called.
247    #[cfg(target_arch = "wasm32")]
248    pub fn resume(&self) {
249        self._backend.resume()
250    }
251
252    /// The sample rate that is currently being outputed to the device.
253    pub fn sample_rate(&self) -> u32 {
254        self.mixer.lock().unwrap().sample_rate()
255    }
256
257    /// The sample rate of the current output device.
258    ///
259    /// May change when the device changes.
260    pub fn channels(&self) -> u16 {
261        self.mixer.lock().unwrap().channels()
262    }
263
264    /// Add a new Sound with the given Group.
265    ///
266    /// The added sound starts in the stopped state, and [`play`](Sound::play) must be called to
267    /// start playing it.
268    ///
269    /// If the [number of channels](SoundSource::channels) of `source` mismatch the [output number of
270    /// channel](Self::channels), `source` will be wrapped in a [`ChannelConverter`].
271    ///
272    /// If the [sample rate](SoundSource::sample_rate) of `source` mismatch the [output
273    /// sample rate](Self::sample_rate), `source` will be wrapped in a [`SampleRateConverter`].
274    pub fn new_sound_with_group<T: SoundSource + Send + 'static>(
275        &self,
276        group: G,
277        source: T,
278    ) -> Result<Sound<G>, &'static str> {
279        let mut mixer = self.mixer.lock().unwrap();
280
281        log::debug!(
282            "adding sound: channels {}, sample_rate {}",
283            source.channels(),
284            mixer.channels()
285        );
286
287        let sound: Box<dyn SoundSource + Send> = if source.sample_rate() != mixer.sample_rate() {
288            if source.channels() == mixer.channels() {
289                Box::new(SampleRateConverter::new(source, mixer.sample_rate()))
290            } else {
291                Box::new(ChannelConverter::new(
292                    SampleRateConverter::new(source, mixer.sample_rate()),
293                    mixer.channels(),
294                ))
295            }
296        } else if source.channels() == mixer.channels() {
297            Box::new(source)
298        } else {
299            Box::new(ChannelConverter::new(source, mixer.channels()))
300        };
301
302        let id = mixer.add_sound(group, sound);
303        mixer.mark_to_remove(id, false);
304        drop(mixer);
305
306        Ok(Sound {
307            mixer: self.mixer.clone(),
308            id,
309        })
310    }
311
312    /// Set the volume of the given group.
313    ///
314    /// The volume of all sounds associated with this group is multiplied by this volume.
315    pub fn set_group_volume(&self, group: G, volume: f32) {
316        self.mixer.lock().unwrap().set_group_volume(group, volume)
317    }
318}
319
320fn create_device<G: Eq + Hash + Send + 'static>(
321    mixer: &Arc<Mutex<Mixer<G>>>,
322    error_callback: impl FnMut(StreamError) + Send + Clone + 'static,
323) -> Result<cpal::Stream, &'static str> {
324    let host = cpal::default_host();
325    let device = host
326        .default_output_device()
327        .ok_or("no output device available")?;
328    let mut supported_configs_range = device
329        .supported_output_configs()
330        .map_err(|_| "error while querying formats")?
331        .map(|x| {
332            let sample_rate = SampleRate(48000);
333            if x.min_sample_rate() <= sample_rate && sample_rate <= x.max_sample_rate() {
334                return x.with_sample_rate(sample_rate);
335            }
336
337            let sample_rate = SampleRate(44100);
338            if x.min_sample_rate() <= sample_rate && sample_rate <= x.max_sample_rate() {
339                return x.with_sample_rate(sample_rate);
340            }
341
342            x.with_max_sample_rate()
343        })
344        .collect::<Vec<_>>();
345    supported_configs_range.sort_unstable_by(|a, b| {
346        let key = |x: &cpal::SupportedStreamConfig| {
347            (
348                x.sample_rate().0 == 48000,
349                x.sample_rate().0 == 441000,
350                x.channels() == 2,
351                x.channels() == 1,
352                x.sample_format() == cpal::SampleFormat::I16,
353                x.sample_rate().0,
354            )
355        };
356        key(a).cmp(&key(b))
357    });
358    if log::max_level() >= log::LevelFilter::Trace {
359        for config in &supported_configs_range {
360            log::trace!("config {:?}", config);
361        }
362    }
363    let stream = loop {
364        let config = if let Some(config) = supported_configs_range.pop() {
365            config
366        } else {
367            return Err("no supported config");
368        };
369        let sample_format = config.sample_format();
370        let config = config.config();
371        mixer
372            .lock()
373            .unwrap()
374            .set_config(config.channels, super::SampleRate(config.sample_rate.0));
375
376        let stream = {
377            use cpal::SampleFormat::*;
378            match sample_format {
379                I16 => stream::<i16, G, _>(mixer, error_callback.clone(), &device, &config),
380                U16 => stream::<u16, G, _>(mixer, error_callback.clone(), &device, &config),
381                F32 => stream::<f32, G, _>(mixer, error_callback.clone(), &device, &config),
382            }
383        };
384        let stream = match stream {
385            Ok(x) => {
386                log::info!(
387                    "created {:?} stream with config {:?}",
388                    sample_format,
389                    config
390                );
391                x
392            }
393            Err(e) => {
394                log::error!("failed to create stream with config {:?}: {:?}", config, e);
395                continue;
396            }
397        };
398        stream.play().unwrap();
399        break stream;
400    };
401    Ok(stream)
402}
403
404fn stream<T, G, E>(
405    mixer: &Arc<Mutex<Mixer<G>>>,
406    error_callback: E,
407    device: &cpal::Device,
408    config: &cpal::StreamConfig,
409) -> Result<cpal::Stream, cpal::BuildStreamError>
410where
411    T: cpal::Sample,
412    G: Eq + Hash + Send + 'static,
413    E: FnMut(StreamError) + Send + 'static,
414{
415    let mixer = mixer.clone();
416    let mut input_buffer = Vec::new();
417    device.build_output_stream(
418        config,
419        move |output_buffer: &mut [T], _| {
420            input_buffer.clear();
421            input_buffer.resize(output_buffer.len(), 0);
422            mixer.lock().unwrap().write_samples(&mut input_buffer);
423            // convert the samples from i16 to T, and write them in the output buffer.
424            output_buffer
425                .iter_mut()
426                .zip(input_buffer.iter())
427                .for_each(|(a, b)| *a = T::from(b));
428        },
429        error_callback,
430    )
431}