web_audio_api/io/
cpal.rs

1//! Audio IO management API
2use std::sync::atomic::Ordering;
3use std::sync::Arc;
4use std::sync::Mutex;
5
6use cpal::{
7    traits::{DeviceTrait, HostTrait, StreamTrait},
8    BuildStreamError, Device, OutputCallbackInfo, SampleFormat, Stream, StreamConfig,
9    SupportedBufferSize,
10};
11use crossbeam_channel::Receiver;
12
13use super::{AudioBackendManager, RenderThreadInit};
14
15use crate::buffer::AudioBuffer;
16use crate::context::AudioContextLatencyCategory;
17use crate::context::AudioContextOptions;
18use crate::io::microphone::MicrophoneRender;
19use crate::media_devices::{MediaDeviceInfo, MediaDeviceInfoKind};
20use crate::render::RenderThread;
21use crate::{AtomicF64, MAX_CHANNELS};
22
23// I doubt this construct is entirely safe. Stream is not Send/Sync (probably for a good reason) so
24// it should be managed from a single thread instead.
25// <https://github.com/orottier/web-audio-api-rs/issues/357>
26mod private {
27    use super::*;
28
29    #[derive(Clone)]
30    pub struct ThreadSafeClosableStream(Arc<Mutex<Option<Stream>>>);
31
32    impl ThreadSafeClosableStream {
33        pub fn new(stream: Stream) -> Self {
34            #[allow(clippy::arc_with_non_send_sync)]
35            Self(Arc::new(Mutex::new(Some(stream))))
36        }
37
38        pub fn close(&self) {
39            self.0.lock().unwrap().take(); // will Drop
40        }
41
42        pub fn resume(&self) -> bool {
43            if let Some(s) = self.0.lock().unwrap().as_ref() {
44                if let Err(e) = s.play() {
45                    panic!("Error resuming cpal stream: {:?}", e);
46                }
47                return true;
48            }
49
50            false
51        }
52
53        pub fn suspend(&self) -> bool {
54            if let Some(s) = self.0.lock().unwrap().as_ref() {
55                if let Err(e) = s.pause() {
56                    panic!("Error suspending cpal stream: {:?}", e);
57                }
58                return true;
59            }
60
61            false
62        }
63    }
64
65    // SAFETY:
66    // The cpal `Stream` is marked !Sync and !Send because some platforms are not thread-safe
67    // https://github.com/RustAudio/cpal/commit/33ddf749548d87bf54ce18eb342f954cec1465b2
68    // Since we wrap the Stream in a Mutex, we should be fine
69    unsafe impl Sync for ThreadSafeClosableStream {}
70    unsafe impl Send for ThreadSafeClosableStream {}
71}
72use private::ThreadSafeClosableStream;
73
74fn get_host() -> cpal::Host {
75    #[cfg(feature = "cpal-jack")]
76    {
77        // seems to be always Some when jack is installed,
78        // even if it's not running
79        if let Some(jack_id) = cpal::available_hosts()
80            .into_iter()
81            .find(|id| *id == cpal::HostId::Jack)
82        {
83            let jack_host = cpal::host_from_id(jack_id).unwrap();
84
85            // if jack is not running, the host can't access devices
86            // fallback to default host
87            return match jack_host.devices() {
88                Ok(devices) => {
89                    // no jack devices found, jack is not running
90                    if devices.count() == 0 {
91                        log::warn!("No jack devices found, fallback to default host");
92                        cpal::default_host()
93                    } else {
94                        jack_host
95                    }
96                }
97                // cpal does not seems to return Err at this point
98                // but just in case, fallback to default host
99                Err(_) => cpal::default_host(),
100            };
101        }
102    }
103
104    cpal::default_host()
105}
106
107/// Audio backend using the `cpal` library
108#[derive(Clone)]
109#[allow(unused)]
110pub(crate) struct CpalBackend {
111    stream: ThreadSafeClosableStream,
112    output_latency: Arc<AtomicF64>,
113    sample_rate: f32,
114    number_of_channels: usize,
115    sink_id: String,
116}
117
118impl AudioBackendManager for CpalBackend {
119    fn build_output(options: AudioContextOptions, render_thread_init: RenderThreadInit) -> Self
120    where
121        Self: Sized,
122    {
123        let host = get_host();
124
125        log::info!("Audio Output Host: cpal {:?}", host.id());
126
127        let RenderThreadInit {
128            state,
129            frames_played,
130            ctrl_msg_recv,
131            load_value_send,
132            event_send,
133        } = render_thread_init;
134
135        let device = if options.sink_id.is_empty() {
136            host.default_output_device()
137                .expect("InvalidStateError - no output device available")
138        } else {
139            Self::enumerate_devices_sync()
140                .into_iter()
141                .find(|e| e.device_id() == options.sink_id)
142                .map(|e| *e.device().downcast::<cpal::Device>().unwrap())
143                .unwrap_or_else(|| {
144                    host.default_output_device()
145                        .expect("InvalidStateError - no output device available")
146                })
147        };
148
149        log::info!("Output device: {:?}", device.name());
150
151        let default_device_config = device
152            .default_output_config()
153            .expect("InvalidStateError - error while querying device output config");
154
155        // we grab the largest number of channels provided by the soundcard
156        // clamped to MAX_CHANNELS, this value cannot be changed by the user
157        let number_of_channels = usize::from(default_device_config.channels()).min(MAX_CHANNELS);
158
159        // override default device configuration with the options provided by
160        // the user when creating the `AudioContext`
161        let mut preferred_config: StreamConfig = default_device_config.clone().into();
162        // make sure the number of channels is clamped to MAX_CHANNELS
163        preferred_config.channels = number_of_channels as u16;
164
165        // set specific sample rate if requested
166        if let Some(sample_rate) = options.sample_rate {
167            crate::assert_valid_sample_rate(sample_rate);
168            preferred_config.sample_rate.0 = sample_rate as u32;
169        }
170
171        // always try to set a decent buffer size
172        let buffer_size = super::buffer_size_for_latency_category(
173            options.latency_hint,
174            preferred_config.sample_rate.0 as f32,
175        ) as u32;
176
177        let clamped_buffer_size: u32 = match default_device_config.buffer_size() {
178            SupportedBufferSize::Unknown => buffer_size,
179            SupportedBufferSize::Range { min, max } => buffer_size.clamp(*min, *max),
180        };
181
182        preferred_config.buffer_size = cpal::BufferSize::Fixed(clamped_buffer_size);
183
184        // On android detected range for the buffer size seems to be too big, use default buffer size instead
185        // See https://github.com/orottier/web-audio-api-rs/issues/515
186        if cfg!(target_os = "android") {
187            if let AudioContextLatencyCategory::Balanced
188            | AudioContextLatencyCategory::Interactive = options.latency_hint
189            {
190                preferred_config.buffer_size = cpal::BufferSize::Default;
191            }
192        }
193
194        // report the picked sample rate to the render thread, i.e. if the requested
195        // sample rate is not supported by the hardware, it will fallback to the
196        // default device sample rate
197        let mut sample_rate = preferred_config.sample_rate.0 as f32;
198
199        // shared atomic to report output latency to the control thread
200        let output_latency = Arc::new(AtomicF64::new(0.));
201
202        let mut renderer = RenderThread::new(
203            sample_rate,
204            preferred_config.channels as usize,
205            ctrl_msg_recv.clone(),
206            Arc::clone(&state),
207            Arc::clone(&frames_played),
208            event_send.clone(),
209        );
210        renderer.set_load_value_sender(load_value_send.clone());
211        renderer.spawn_garbage_collector_thread();
212
213        log::debug!(
214            "Attempt output stream with preferred config: {:?}",
215            &preferred_config
216        );
217
218        let spawned = spawn_output_stream(
219            &device,
220            default_device_config.sample_format(),
221            &preferred_config,
222            renderer,
223            Arc::clone(&output_latency),
224        );
225
226        let stream = match spawned {
227            Ok(stream) => {
228                log::debug!("Output stream set up successfully");
229                stream
230            }
231            Err(e) => {
232                log::warn!("Output stream build failed with preferred config: {}", e);
233
234                let mut supported_config: StreamConfig = default_device_config.clone().into();
235                // make sure number of channels is clamped to MAX_CHANNELS
236                supported_config.channels = number_of_channels as u16;
237                // fallback to device default sample rate
238                sample_rate = supported_config.sample_rate.0 as f32;
239
240                log::debug!(
241                    "Attempt output stream with fallback config: {:?}",
242                    &supported_config
243                );
244
245                let mut renderer = RenderThread::new(
246                    sample_rate,
247                    supported_config.channels as usize,
248                    ctrl_msg_recv,
249                    state,
250                    frames_played,
251                    event_send,
252                );
253                renderer.set_load_value_sender(load_value_send);
254                renderer.spawn_garbage_collector_thread();
255
256                let spawned = spawn_output_stream(
257                    &device,
258                    default_device_config.sample_format(),
259                    &supported_config,
260                    renderer,
261                    Arc::clone(&output_latency),
262                );
263
264                spawned
265                    .expect("InvalidStateError - Unable to spawn output stream with default config")
266            }
267        };
268
269        // Required because some hosts don't play the stream automatically
270        stream
271            .play()
272            .expect("InvalidStateError - Output stream refused to play");
273
274        CpalBackend {
275            stream: ThreadSafeClosableStream::new(stream),
276            output_latency,
277            sample_rate,
278            number_of_channels,
279            sink_id: options.sink_id,
280        }
281    }
282
283    fn build_input(
284        options: AudioContextOptions,
285        number_of_channels: Option<u32>,
286    ) -> (Self, Receiver<AudioBuffer>)
287    where
288        Self: Sized,
289    {
290        let host = get_host();
291
292        log::info!("Audio Input Host: cpal {:?}", host.id());
293
294        let device = if options.sink_id.is_empty() {
295            host.default_input_device()
296                .expect("InvalidStateError - no input device available")
297        } else {
298            Self::enumerate_devices_sync()
299                .into_iter()
300                .find(|e| e.device_id() == options.sink_id)
301                .map(|e| *e.device().downcast::<cpal::Device>().unwrap())
302                .unwrap_or_else(|| {
303                    host.default_input_device()
304                        .expect("InvalidStateError - no input device available")
305                })
306        };
307
308        log::info!("Input device: {:?}", device.name());
309
310        let supported = device
311            .default_input_config()
312            .expect("InvalidStateError - error while querying device input config");
313
314        // clone the config, we may need to fall back on it later
315        let mut preferred: StreamConfig = supported.clone().into();
316
317        if let Some(number_of_channels) = number_of_channels {
318            preferred.channels = number_of_channels as u16;
319        }
320
321        // set specific sample rate if requested
322        if let Some(sample_rate) = options.sample_rate {
323            crate::assert_valid_sample_rate(sample_rate);
324            preferred.sample_rate.0 = sample_rate as u32;
325        }
326
327        // always try to set a decent buffer size
328        let buffer_size = super::buffer_size_for_latency_category(
329            options.latency_hint,
330            preferred.sample_rate.0 as f32,
331        ) as u32;
332
333        let clamped_buffer_size: u32 = match supported.buffer_size() {
334            SupportedBufferSize::Unknown => buffer_size,
335            SupportedBufferSize::Range { min, max } => buffer_size.clamp(*min, *max),
336        };
337
338        preferred.buffer_size = cpal::BufferSize::Fixed(clamped_buffer_size);
339        let mut sample_rate = preferred.sample_rate.0 as f32;
340        let mut number_of_channels = preferred.channels as usize;
341
342        let smoothing = 3; // todo, use buffering to smooth frame drops
343        let (sender, mut receiver) = crossbeam_channel::bounded(smoothing);
344        let renderer = MicrophoneRender::new(number_of_channels, sample_rate, sender);
345
346        log::debug!(
347            "Attempt input stream with preferred config: {:?}",
348            &preferred
349        );
350
351        let spawned = spawn_input_stream(&device, supported.sample_format(), &preferred, renderer);
352
353        // the required block size preferred config may not be supported, in that
354        // case, fallback the supported config
355        let stream = match spawned {
356            Ok(stream) => {
357                log::debug!("Input stream set up successfully");
358                stream
359            }
360            Err(e) => {
361                log::warn!("Output stream build failed with preferred config: {}", e);
362
363                let supported_config: StreamConfig = supported.clone().into();
364                // fallback to device default sample rate and channel count
365                number_of_channels = usize::from(supported_config.channels);
366                sample_rate = supported_config.sample_rate.0 as f32;
367
368                log::debug!(
369                    "Attempt output stream with fallback config: {:?}",
370                    &supported_config
371                );
372
373                // setup a new comms channel
374                let (sender, receiver2) = crossbeam_channel::bounded(smoothing);
375                receiver = receiver2; // overwrite earlier
376
377                let renderer = MicrophoneRender::new(number_of_channels, sample_rate, sender);
378
379                let spawned = spawn_input_stream(
380                    &device,
381                    supported.sample_format(),
382                    &supported_config,
383                    renderer,
384                );
385                spawned
386                    .expect("InvalidStateError - Unable to spawn input stream with default config")
387            }
388        };
389
390        // Required because some hosts don't play the stream automatically
391        stream
392            .play()
393            .expect("InvalidStateError - Input stream refused to play");
394
395        let backend = CpalBackend {
396            stream: ThreadSafeClosableStream::new(stream),
397            output_latency: Arc::new(AtomicF64::new(0.)),
398            sample_rate,
399            number_of_channels,
400            sink_id: options.sink_id,
401        };
402
403        (backend, receiver)
404    }
405
406    fn resume(&self) -> bool {
407        self.stream.resume()
408    }
409
410    fn suspend(&self) -> bool {
411        self.stream.suspend()
412    }
413
414    fn close(&self) {
415        self.stream.close()
416    }
417
418    fn sample_rate(&self) -> f32 {
419        self.sample_rate
420    }
421
422    fn number_of_channels(&self) -> usize {
423        self.number_of_channels
424    }
425
426    fn output_latency(&self) -> f64 {
427        self.output_latency.load(Ordering::Relaxed)
428    }
429
430    fn sink_id(&self) -> &str {
431        self.sink_id.as_str()
432    }
433
434    fn enumerate_devices_sync() -> Vec<MediaDeviceInfo>
435    where
436        Self: Sized,
437    {
438        let host = get_host();
439
440        let input_devices = host.input_devices().unwrap().map(|d| {
441            let num_channels = d.default_input_config().unwrap().channels();
442            (d, MediaDeviceInfoKind::AudioInput, num_channels)
443        });
444
445        let output_devices = host.output_devices().unwrap().map(|d| {
446            let num_channels = d.default_output_config().unwrap().channels();
447            (d, MediaDeviceInfoKind::AudioOutput, num_channels)
448        });
449
450        // cf. https://github.com/orottier/web-audio-api-rs/issues/356
451        let mut list = Vec::<MediaDeviceInfo>::new();
452
453        for (device, kind, num_channels) in input_devices.chain(output_devices) {
454            let mut index = 0;
455
456            loop {
457                let device_id = crate::media_devices::DeviceId::as_string(
458                    kind,
459                    "cpal".to_string(),
460                    device.name().unwrap(),
461                    num_channels,
462                    index,
463                );
464
465                if !list.iter().any(|d| d.device_id() == device_id) {
466                    let device = MediaDeviceInfo::new(
467                        device_id,
468                        None,
469                        kind,
470                        device.name().unwrap(),
471                        Box::new(device),
472                    );
473
474                    list.push(device);
475                    break;
476                } else {
477                    index += 1;
478                }
479            }
480        }
481
482        list
483    }
484}
485
486fn latency_in_seconds(infos: &OutputCallbackInfo) -> f64 {
487    let timestamp = infos.timestamp();
488    timestamp
489        .playback
490        .duration_since(&timestamp.callback)
491        .map(|delta| delta.as_secs() as f64 + delta.subsec_nanos() as f64 * 1e-9)
492        .unwrap_or(0.0)
493}
494
495/// Creates an output stream
496///
497/// # Arguments:
498///
499/// * `device` - the output audio device on which the stream is created
500/// * `sample_format` - audio sample format of the stream
501/// * `config` - stream configuration
502/// * `render` - the render thread which process the audio data
503fn spawn_output_stream(
504    device: &Device,
505    sample_format: SampleFormat,
506    config: &StreamConfig,
507    mut render: RenderThread,
508    output_latency: Arc<AtomicF64>,
509) -> Result<Stream, BuildStreamError> {
510    let err_fn = |err| log::error!("an error occurred on the output audio stream: {}", err);
511
512    match sample_format {
513        SampleFormat::F32 => device.build_output_stream(
514            config,
515            move |d: &mut [f32], i: &OutputCallbackInfo| {
516                render.render(d);
517                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
518            },
519            err_fn,
520            None,
521        ),
522        SampleFormat::F64 => device.build_output_stream(
523            config,
524            move |d: &mut [f64], i: &OutputCallbackInfo| {
525                render.render(d);
526                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
527            },
528            err_fn,
529            None,
530        ),
531        SampleFormat::U8 => device.build_output_stream(
532            config,
533            move |d: &mut [u8], i: &OutputCallbackInfo| {
534                render.render(d);
535                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
536            },
537            err_fn,
538            None,
539        ),
540        SampleFormat::U16 => device.build_output_stream(
541            config,
542            move |d: &mut [u16], i: &OutputCallbackInfo| {
543                render.render(d);
544                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
545            },
546            err_fn,
547            None,
548        ),
549        SampleFormat::U32 => device.build_output_stream(
550            config,
551            move |d: &mut [u32], i: &OutputCallbackInfo| {
552                render.render(d);
553                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
554            },
555            err_fn,
556            None,
557        ),
558        SampleFormat::U64 => device.build_output_stream(
559            config,
560            move |d: &mut [u64], i: &OutputCallbackInfo| {
561                render.render(d);
562                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
563            },
564            err_fn,
565            None,
566        ),
567        SampleFormat::I8 => device.build_output_stream(
568            config,
569            move |d: &mut [i8], i: &OutputCallbackInfo| {
570                render.render(d);
571                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
572            },
573            err_fn,
574            None,
575        ),
576        SampleFormat::I16 => device.build_output_stream(
577            config,
578            move |d: &mut [i16], i: &OutputCallbackInfo| {
579                render.render(d);
580                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
581            },
582            err_fn,
583            None,
584        ),
585        SampleFormat::I32 => device.build_output_stream(
586            config,
587            move |d: &mut [i32], i: &OutputCallbackInfo| {
588                render.render(d);
589                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
590            },
591            err_fn,
592            None,
593        ),
594        SampleFormat::I64 => device.build_output_stream(
595            config,
596            move |d: &mut [i64], i: &OutputCallbackInfo| {
597                render.render(d);
598                output_latency.store(latency_in_seconds(i), Ordering::Relaxed);
599            },
600            err_fn,
601            None,
602        ),
603        _ => panic!("Unknown cpal output sample format"),
604    }
605}
606
607/// Creates an input stream
608///
609/// # Arguments:
610///
611/// * `device` - the input audio device on which the stream is created
612/// * `sample_format` - audio sample format of the stream
613/// * `config` - stream configuration
614/// * `render` - the render thread which process the audio data
615fn spawn_input_stream(
616    device: &Device,
617    sample_format: SampleFormat,
618    config: &StreamConfig,
619    render: MicrophoneRender,
620) -> Result<Stream, BuildStreamError> {
621    let err_fn = |err| log::error!("an error occurred on the input audio stream: {}", err);
622
623    match sample_format {
624        SampleFormat::F32 => {
625            device.build_input_stream(config, move |d: &[f32], _c| render.render(d), err_fn, None)
626        }
627        SampleFormat::F64 => {
628            device.build_input_stream(config, move |d: &[f64], _c| render.render(d), err_fn, None)
629        }
630        SampleFormat::U8 => {
631            device.build_input_stream(config, move |d: &[u8], _c| render.render(d), err_fn, None)
632        }
633        SampleFormat::U16 => {
634            device.build_input_stream(config, move |d: &[u16], _c| render.render(d), err_fn, None)
635        }
636        SampleFormat::U32 => {
637            device.build_input_stream(config, move |d: &[u32], _c| render.render(d), err_fn, None)
638        }
639        SampleFormat::U64 => {
640            device.build_input_stream(config, move |d: &[u64], _c| render.render(d), err_fn, None)
641        }
642        SampleFormat::I8 => {
643            device.build_input_stream(config, move |d: &[i8], _c| render.render(d), err_fn, None)
644        }
645        SampleFormat::I16 => {
646            device.build_input_stream(config, move |d: &[i16], _c| render.render(d), err_fn, None)
647        }
648        SampleFormat::I32 => {
649            device.build_input_stream(config, move |d: &[i32], _c| render.render(d), err_fn, None)
650        }
651        SampleFormat::I64 => {
652            device.build_input_stream(config, move |d: &[i64], _c| render.render(d), err_fn, None)
653        }
654        _ => panic!("Unknown cpal input sample format"),
655    }
656}