Skip to main content

firewheel_cpal/
lib.rs

1use core::{fmt::Debug, num::NonZeroU32, time::Duration};
2use std::sync::{
3    Arc,
4    atomic::{AtomicBool, Ordering},
5    mpsc,
6};
7
8use audioadapter_buffers::direct::InterleavedSlice;
9pub use cpal;
10
11use bevy_platform::time::Instant;
12pub use cpal::{DeviceId, HostId};
13use cpal::{
14    SampleFormat,
15    traits::{DeviceTrait, HostTrait, StreamTrait},
16};
17use firewheel_core::node::StreamStatus;
18use firewheel_graph::{
19    ActivateInfo, FirewheelContext,
20    backend::BackendProcessInfo,
21    error::{ActivateError, CompileGraphError},
22    processor::FirewheelProcessor,
23};
24use fixed_resample::{ReadStatus, ResamplingChannelConfig, ResamplingProd};
25
26#[cfg(all(feature = "log", not(feature = "tracing")))]
27use log::{error, info, warn};
28#[cfg(feature = "tracing")]
29use tracing::{error, info, warn};
30
31/// 1024 samples is a latency of about 23 milliseconds, which should
32/// be good enough for most games.
33const DEFAULT_MAX_BLOCK_FRAMES: u32 = 1024;
34const BUILD_STREAM_TIMEOUT: Duration = Duration::from_secs(5);
35const UNDERRUN_LOG_COOLDOWN: Duration = Duration::from_secs(3);
36
37/// The configuration of an output audio stream in the CPAL backend.
38#[derive(Debug, Clone, PartialEq)]
39pub struct CpalOutputConfig {
40    /// The host to use. Set to `None` to use the
41    /// system's default audio host.
42    pub host: Option<cpal::HostId>,
43
44    /// The id of the output device to use. Set to `None` to use the
45    /// system's default output device.
46    ///
47    /// By default this is set to `None`.
48    pub device_id: Option<DeviceId>,
49
50    /// The desired sample rate to use. Set to `None` to use the device's
51    /// default sample rate.
52    ///
53    /// By default this is set to `None`.
54    pub desired_sample_rate: Option<u32>,
55
56    /// The latency/block size of the audio stream to use. Set to
57    /// `None` to use the device's default value.
58    ///
59    /// Smaller values may give better latency, but is not supported on
60    /// all platforms and may lead to performance issues.
61    ///
62    /// This currently has no effect on iOS platforms.
63    ///
64    /// By default this is set to `Some(1024)`.
65    pub desired_block_frames: Option<u32>,
66
67    /// Whether or not to fall back to the default device  if a device
68    /// with the given configuration could not be found.
69    ///
70    /// By default this is set to `true`.
71    pub fallback: bool,
72}
73
74impl Default for CpalOutputConfig {
75    fn default() -> Self {
76        Self {
77            host: None,
78            device_id: None,
79            desired_sample_rate: None,
80            desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
81            fallback: true,
82        }
83    }
84}
85
86/// The configuration of an input audio stream in the CPAL backend.
87#[derive(Debug, Clone, PartialEq)]
88pub struct CpalInputConfig {
89    /// The host to use. Set to `None` to use the
90    /// system's default audio host.
91    pub host: Option<cpal::HostId>,
92
93    /// The id of the input device to use. Set to `None` to use the
94    /// system's default input device.
95    ///
96    /// By default this is set to `None`.
97    pub device_id: Option<DeviceId>,
98
99    /// The latency/block size of the audio stream to use. Set to
100    /// `None` to use the device's default value.
101    ///
102    /// Smaller values may give better latency, but is not supported on
103    /// all platforms and may lead to performance issues.
104    ///
105    /// This currently has no effect on iOS platforms.
106    ///
107    /// By default this is set to `Some(1024)`.
108    pub desired_block_frames: Option<u32>,
109
110    /// The configuration of the input to output stream channel.
111    pub channel_config: ResamplingChannelConfig,
112
113    /// Whether or not to fall back to the default device  if a device
114    /// with the given configuration could not be found.
115    ///
116    /// By default this is set to `true`.
117    pub fallback: bool,
118
119    /// If `true`, then an error will be returned if an input stream could
120    /// not be started. If `false`, then the output stream will still
121    /// attempt to start with no input stream.
122    ///
123    /// By default this is set to `false`.
124    pub fail_on_no_input: bool,
125}
126
127impl Default for CpalInputConfig {
128    fn default() -> Self {
129        Self {
130            host: None,
131            device_id: None,
132            desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
133            channel_config: ResamplingChannelConfig::default(),
134            fallback: true,
135            fail_on_no_input: false,
136        }
137    }
138}
139
140/// The configuration of a CPAL stream.
141#[derive(Debug, Default, Clone, PartialEq)]
142pub struct CpalConfig {
143    /// The configuration of the output stream.
144    pub output: CpalOutputConfig,
145
146    /// The configuration of the input stream.
147    ///
148    /// Set to `None` for no input stream.
149    ///
150    /// By default this is set to `None`.
151    pub input: Option<CpalInputConfig>,
152}
153
154/// A struct used to retrieve the list of available audio devices
155/// for a given system audio host (API).
156pub struct HostEnumerator {
157    pub host: cpal::Host,
158}
159
160impl HostEnumerator {
161    /// The system backend host id (API) this enumerator is using.
162    pub fn host_id(&self) -> cpal::HostId {
163        self.host.id()
164    }
165
166    /// Get the list of available input audio devices.
167    pub fn input_devices(&self) -> Vec<DeviceInfo> {
168        let mut devices = Vec::with_capacity(8);
169
170        let default_device = self.host.default_input_device();
171        let default_device_id = default_device.and_then(|d| match d.id() {
172            Ok(id) => Some(id),
173            Err(e) => {
174                #[cfg(any(feature = "log", feature = "tracing"))]
175                warn!("Failed to get ID of default audio input device: {}", e);
176                #[cfg(not(any(feature = "log", feature = "tracing")))]
177                let _ = e;
178                None
179            }
180        });
181
182        match self.host.input_devices() {
183            Ok(input_devices) => {
184                for device in input_devices {
185                    let Ok(id) = device.id() else {
186                        continue;
187                    };
188
189                    let is_default = if let Some(default_device_id) = &default_device_id {
190                        &id == default_device_id
191                    } else {
192                        false
193                    };
194
195                    let name = device.description().map(|d| d.name().to_string()).ok();
196
197                    devices.push(DeviceInfo {
198                        id,
199                        name,
200                        is_default,
201                    })
202                }
203            }
204            Err(e) => {
205                #[cfg(any(feature = "log", feature = "tracing"))]
206                error!("Failed to get input audio devices: {}", e);
207                #[cfg(not(any(feature = "log", feature = "tracing")))]
208                let _ = e;
209            }
210        }
211
212        devices
213    }
214
215    /// Get the list of available output audio devices.
216    pub fn output_devices(&self) -> Vec<DeviceInfo> {
217        let mut devices = Vec::with_capacity(8);
218
219        let default_device = self.host.default_output_device();
220        let default_device_id = default_device.and_then(|d| match d.id() {
221            Ok(id) => Some(id),
222            Err(e) => {
223                #[cfg(any(feature = "log", feature = "tracing"))]
224                warn!("Failed to get ID of default audio output device: {}", e);
225                #[cfg(not(any(feature = "log", feature = "tracing")))]
226                let _ = e;
227                None
228            }
229        });
230
231        match self.host.output_devices() {
232            Ok(output_devices) => {
233                for device in output_devices {
234                    let Ok(id) = device.id() else {
235                        continue;
236                    };
237
238                    let is_default = if let Some(default_device_id) = &default_device_id {
239                        &id == default_device_id
240                    } else {
241                        false
242                    };
243
244                    let name = device.description().map(|d| d.name().to_string()).ok();
245
246                    devices.push(DeviceInfo {
247                        id,
248                        name,
249                        is_default,
250                    })
251                }
252            }
253            Err(e) => {
254                #[cfg(any(feature = "log", feature = "tracing"))]
255                error!("Failed to get output audio devices: {}", e);
256                #[cfg(not(any(feature = "log", feature = "tracing")))]
257                let _ = e;
258            }
259        }
260
261        devices
262    }
263
264    /// Get a struct used to retrieve extra information for the given audio
265    /// device.
266    ///
267    /// Returns `None` if the device could not be found.
268    pub fn get_device(&self, device_id: &cpal::DeviceId) -> Option<cpal::Device> {
269        self.host.device_by_id(device_id)
270    }
271}
272
273/// Information about an audio device.
274#[derive(Debug, Clone, PartialEq)]
275pub struct DeviceInfo {
276    /// A stable identifier for an audio device across all supported platforms.
277    ///
278    /// Device IDs should remain stable across application restarts and can be
279    /// serialized using `Display`/`FromStr`.
280    ///
281    /// A device ID consists of a [`HostId`] identifying the audio backend and
282    /// a device-specific identifier string.
283    pub id: cpal::DeviceId,
284    /// The display name of the device.
285    pub name: Option<String>,
286    /// Whether or not this device is the default input/output device.
287    pub is_default: bool,
288}
289
290/// Information about a running CPAL audio stream.
291#[derive(Debug, Clone, PartialEq)]
292pub struct CpalStreamInfo {
293    /// The sample rate of the audio stream.
294    pub sample_rate: NonZeroU32,
295    /// The maximum number of frames that can appear in a single process cyle.
296    pub max_block_frames: NonZeroU32,
297    /// The number of input audio channels in the stream.
298    pub num_stream_in_channels: u32,
299    /// The number of output audio channels in the stream.
300    pub num_stream_out_channels: u32,
301    /// The latency of the input to output stream in seconds.
302    pub input_to_output_latency_seconds: f64,
303    /// The ID of the output audio device.
304    pub out_device_id: Option<DeviceId>,
305    /// The ID of the input audio device.
306    pub in_device_id: Option<DeviceId>,
307}
308
309/// The system audio hosts (APIs) that are available on this system.
310///
311/// The first host in the list is the default one for the system.
312pub fn available_hosts() -> Vec<cpal::HostId> {
313    cpal::available_hosts()
314}
315
316/// Get a struct used to retrieve the list of available audio devices
317/// for the default system audio host (API).
318pub fn default_host_enumerator() -> HostEnumerator {
319    HostEnumerator {
320        host: cpal::default_host(),
321    }
322}
323
324/// Get a struct used to retrieve the list of available audio devices
325/// for the given system audio host (API).
326pub fn host_enumerator(api: HostId) -> Result<HostEnumerator, cpal::Error> {
327    cpal::host_from_id(api).map(|host| HostEnumerator { host })
328}
329
330/// A CPAL stream running a [`FirewheelProcessor`].
331///
332/// The audio stream is automatically stopped when this struct is dropped.
333pub struct CpalStream {
334    _out_stream_handle: cpal::Stream,
335    _in_stream_handle: Option<cpal::Stream>,
336    from_err_rx: mpsc::Receiver<IoStreamError>,
337    stream_info: CpalStreamInfo,
338    input_stream_running: Option<Arc<AtomicBool>>,
339    output_stream_running: Arc<AtomicBool>,
340}
341
342impl CpalStream {
343    /// Create a new audio stream with the given [`FirewheelContext`].
344    pub fn new(cx: &mut FirewheelContext, config: CpalConfig) -> Result<Self, StartStreamError> {
345        #[cfg(any(feature = "log", feature = "tracing"))]
346        info!("Attempting to start CPAL audio stream...");
347
348        if cx.is_active() {
349            return Err(StartStreamError::AlreadyActive);
350        }
351
352        let host = if let Some(host_id) = config.output.host {
353            match cpal::host_from_id(host_id) {
354                Ok(host) => host,
355                Err(e) => {
356                    #[cfg(any(feature = "log", feature = "tracing"))]
357                    warn!(
358                        "Requested audio host {:?} is not available: {}. Falling back to default host...",
359                        &host_id, e
360                    );
361                    #[cfg(not(any(feature = "log", feature = "tracing")))]
362                    let _ = e;
363                    cpal::default_host()
364                }
365            }
366        } else {
367            cpal::default_host()
368        };
369
370        let mut out_device = None;
371        if let Some(device_id) = &config.output.device_id {
372            if let Some(device) = host.device_by_id(device_id)
373                && device.supports_output()
374            {
375                out_device = Some(device);
376            }
377
378            if out_device.is_none() {
379                #[cfg(any(feature = "log", feature = "tracing"))]
380                warn!(
381                    "Could not find requested audio output device: {}. Falling back to default device...",
382                    &device_id
383                );
384            }
385        }
386
387        if out_device.is_none() {
388            let Some(default_device) = host.default_output_device() else {
389                return Err(StartStreamError::DefaultOutputDeviceNotFound);
390            };
391            out_device = Some(default_device);
392        }
393        let out_device = out_device.unwrap();
394
395        let out_device_id = match out_device.id() {
396            Ok(id) => Some(id),
397            Err(e) => {
398                #[cfg(any(feature = "log", feature = "tracing"))]
399                warn!("Failed to get id of output audio device: {}", e);
400                #[cfg(not(any(feature = "log", feature = "tracing")))]
401                let _ = e;
402                None
403            }
404        };
405
406        let default_config = out_device
407            .default_output_config()
408            .map_err(StartStreamError::FailedToGetConfig)?;
409
410        let default_sample_rate = default_config.sample_rate();
411        // Try to use the common sample rates by default.
412        let try_common_sample_rates = default_sample_rate != 44100 && default_sample_rate != 48000;
413
414        #[cfg(not(target_os = "ios"))]
415        let desired_block_frames =
416            if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
417                config
418                    .output
419                    .desired_block_frames
420                    .map(|f| f.clamp(min, max))
421            } else {
422                None
423            };
424
425        // For some reason fixed buffer sizes on iOS doesn't work in CPAL.
426        // I'm not sure if this is a problem on CPAL's end, but I have disabled
427        // it for the time being.
428        #[cfg(target_os = "ios")]
429        let desired_block_frames: Option<u32> = None;
430
431        let mut supports_desired_sample_rate = false;
432        let mut supports_44100 = false;
433        let mut supports_48000 = false;
434
435        if config.output.desired_sample_rate.is_some() || try_common_sample_rates {
436            for cpal_config in out_device
437                .supported_output_configs()
438                .map_err(StartStreamError::FailedToGetConfig)?
439            {
440                if let Some(sr) = config.output.desired_sample_rate
441                    && !supports_desired_sample_rate
442                    && cpal_config.try_with_sample_rate(sr).is_some()
443                {
444                    supports_desired_sample_rate = true;
445                    break;
446                }
447
448                if try_common_sample_rates {
449                    if !supports_44100 {
450                        supports_44100 = cpal_config.try_with_sample_rate(44100).is_some();
451                    }
452                    if !supports_48000 {
453                        supports_48000 = cpal_config.try_with_sample_rate(48000).is_some();
454                    }
455                }
456            }
457        }
458
459        let sample_rate = if supports_desired_sample_rate {
460            config.output.desired_sample_rate.unwrap()
461        } else if try_common_sample_rates {
462            if supports_44100 {
463                44100
464            } else if supports_48000 {
465                48000
466            } else {
467                default_sample_rate
468            }
469        } else {
470            default_sample_rate
471        };
472
473        let num_out_channels = default_config.channels() as usize;
474        assert_ne!(num_out_channels, 0);
475
476        let desired_buffer_size = if let Some(samples) = desired_block_frames {
477            cpal::BufferSize::Fixed(samples)
478        } else {
479            cpal::BufferSize::Default
480        };
481
482        let out_stream_config = cpal::StreamConfig {
483            channels: num_out_channels as u16,
484            sample_rate,
485            buffer_size: desired_buffer_size,
486        };
487
488        let max_block_frames = match out_stream_config.buffer_size {
489            cpal::BufferSize::Default => DEFAULT_MAX_BLOCK_FRAMES as usize,
490            cpal::BufferSize::Fixed(f) => f as usize,
491        };
492
493        let (err_to_cx_tx, from_err_rx) = mpsc::channel();
494
495        let mut input_stream = StartInputStreamResult::NotStarted;
496        if let Some(input_config) = &config.input {
497            input_stream = start_input_stream(
498                input_config,
499                out_stream_config.sample_rate,
500                err_to_cx_tx.clone(),
501            )?;
502        }
503
504        let (
505            in_stream_handle,
506            input_stream_cons,
507            num_stream_in_channels,
508            in_device_id,
509            input_to_output_latency_seconds,
510            input_stream_running,
511        ) = if let StartInputStreamResult::Started {
512            stream_handle,
513            cons,
514            num_stream_in_channels,
515            in_device_id,
516            input_stream_running,
517        } = input_stream
518        {
519            let input_to_output_latency_seconds = cons.latency_seconds();
520
521            (
522                Some(stream_handle),
523                Some(cons),
524                num_stream_in_channels,
525                in_device_id,
526                input_to_output_latency_seconds,
527                Some(input_stream_running),
528            )
529        } else {
530            (None, None, 0, None, 0.0, None)
531        };
532
533        let activate_info = ActivateInfo {
534            sample_rate: NonZeroU32::new(out_stream_config.sample_rate).unwrap(),
535            max_block_frames: NonZeroU32::new(max_block_frames as u32).unwrap(),
536            num_stream_in_channels,
537            num_stream_out_channels: num_out_channels as u32,
538            input_to_output_latency_seconds,
539        };
540
541        let processor = cx.activate(activate_info)?;
542
543        let output_stream_running = Arc::new(AtomicBool::new(true));
544
545        let mut callback = OutputCallback::new(
546            num_out_channels,
547            max_block_frames,
548            out_stream_config.sample_rate,
549            processor,
550            input_stream_cons,
551            err_to_cx_tx.clone(),
552            input_stream_running.as_ref().map(Arc::clone),
553            Arc::clone(&output_stream_running),
554        );
555
556        let out_sample_format = default_config.sample_format();
557
558        #[cfg(any(feature = "log", feature = "tracing"))]
559        info!(
560            "Starting output audio stream with device \"{:?}\" with configuration {:?} (native sample format: {:?})",
561            &out_device_id, &out_stream_config, out_sample_format,
562        );
563
564        let scratch_capacity = max_block_frames * num_out_channels;
565
566        #[cfg(any(
567            target_os = "linux",
568            target_os = "freebsd",
569            target_os = "dragonfly",
570            target_os = "netbsd"
571        ))]
572        let is_alsa = host.id() == HostId::Alsa;
573
574        macro_rules! build_output_streams {
575            ($sample_format:expr, $(($format:path, $primitive_type:ty)),*) => {
576                match $sample_format {
577                    $($format => {
578                        let mut scratch = scratch_vec(scratch_capacity);
579
580                        out_device.build_output_stream(
581                            out_stream_config,
582                            move |output: &mut [$primitive_type], info: &cpal::OutputCallbackInfo| {
583                                for out_chunk in output.chunks_mut(scratch_capacity) {
584                                    let buf = &mut scratch[..out_chunk.len()];
585
586                                    callback.callback(buf, info);
587
588                                    for (o, &f) in out_chunk.iter_mut().zip(buf.iter()) {
589                                        // TODO: Add dithering option for better quality?
590                                        *o = <$primitive_type as cpal::FromSample<f32>>::from_sample_(f);
591                                    }
592                                }
593                            },
594                            err_callback(
595                                false,
596                                output_stream_running.clone(),
597                                err_to_cx_tx.clone(),
598                                #[cfg(any(
599                                    target_os = "linux",
600                                    target_os = "freebsd",
601                                    target_os = "dragonfly",
602                                    target_os = "netbsd"
603                                ))]
604                                is_alsa
605                            ),
606                            Some(BUILD_STREAM_TIMEOUT),
607                        )
608                    },)*
609                    _ => unreachable!(),
610                }
611            }
612        }
613
614        // The cpal ASIO backend requires the callback buffer type to match the
615        // driver's native format (unlike WASAPI, which converts internally).
616        // For non-f32 formats, render into an f32 scratch buffer and convert
617        // on the way out. The f32 path stays a direct passthrough.
618        let out_stream_handle = if let SampleFormat::F32 = out_sample_format {
619            out_device.build_output_stream(
620                out_stream_config,
621                move |output: &mut [f32], info: &cpal::OutputCallbackInfo| {
622                    callback.callback(output, info);
623                },
624                err_callback(
625                    false,
626                    output_stream_running.clone(),
627                    err_to_cx_tx.clone(),
628                    #[cfg(any(
629                        target_os = "linux",
630                        target_os = "freebsd",
631                        target_os = "dragonfly",
632                        target_os = "netbsd"
633                    ))]
634                    is_alsa,
635                ),
636                Some(BUILD_STREAM_TIMEOUT),
637            )
638        } else {
639            build_output_streams!(
640                out_sample_format,
641                (SampleFormat::I8, i8),
642                (SampleFormat::I16, i16),
643                (SampleFormat::I32, i32),
644                (SampleFormat::I64, i64),
645                (SampleFormat::U8, u8),
646                (SampleFormat::U16, u16),
647                (SampleFormat::U32, u32),
648                (SampleFormat::U64, u64),
649                (SampleFormat::F64, f64)
650            )
651        }
652        .map_err(StartStreamError::BuildStreamError)?;
653
654        if let Err(e) = out_stream_handle.play() {
655            if let cpal::ErrorKind::RealtimeDenied = e.kind() {
656                #[cfg(any(feature = "log", feature = "tracing"))]
657                warn!(
658                    "Failed to get realtime priority for output audio thread. This may increase latency and/or audio glitches"
659                );
660            } else {
661                return Err(StartStreamError::PlayStreamError(e));
662            }
663        }
664
665        let stream_info = CpalStreamInfo {
666            sample_rate: activate_info.sample_rate,
667            max_block_frames: activate_info.max_block_frames,
668            num_stream_in_channels: activate_info.num_stream_in_channels,
669            num_stream_out_channels: activate_info.num_stream_out_channels,
670            input_to_output_latency_seconds: activate_info.input_to_output_latency_seconds,
671            out_device_id,
672            in_device_id,
673        };
674
675        Ok(Self {
676            _out_stream_handle: out_stream_handle,
677            _in_stream_handle: in_stream_handle,
678            from_err_rx,
679            stream_info,
680            input_stream_running,
681            output_stream_running,
682        })
683    }
684
685    /// Information about the running audio stream
686    pub fn info(&self) -> &CpalStreamInfo {
687        &self.stream_info
688    }
689
690    /// Poll the status of the audio stream and log any errors/warnings that have occurred.
691    ///
692    /// Note, if an error is returned, it doesn't always mean that the stream has stopped.
693    /// Instead, use [`CpalStream::all_streams_ok()`] to check if the stream is still running
694    /// or if the stream needs to be recreated.
695    pub fn poll_status(&mut self) -> mpsc::TryIter<'_, IoStreamError> {
696        if self._in_stream_handle.is_some() && !self.input_stream_ok() {
697            self._in_stream_handle = None;
698        }
699
700        self.from_err_rx.try_iter()
701    }
702
703    /// Log any stream errors/warnings that have occurred.
704    ///
705    /// Same as [`CpalStream::poll_status`], but automatically logs all of the errors/
706    /// warnings to the log output.
707    #[cfg(any(feature = "log", feature = "tracing"))]
708    pub fn log_status(&mut self) {
709        for e in self.from_err_rx.try_iter() {
710            error!("Audio stream error occurred: {}", e);
711        }
712    }
713
714    /// Returns `true` if the output audio stream is still running.
715    ///
716    /// Returns `false` if the stream has stopped unexpectedly (i.e. an audio device
717    /// was disconnected). When this happens, this `CpalStream` instance should be dropped,
718    /// and a new one created.
719    pub fn output_stream_ok(&self) -> bool {
720        self.output_stream_running.load(Ordering::Relaxed)
721    }
722
723    /// Returns `true` if the input audio stream is still running or if an input audio
724    /// stream was never created.
725    ///
726    /// Returns `false` if there is no input stream, or if the input stream has stopped
727    /// unexpectedly (i.e. an audio device was disconnected). When this happens, this
728    /// `CpalStream` instance should be dropped, and a new one created.
729    pub fn input_stream_ok(&self) -> bool {
730        self.input_stream_running
731            .as_ref()
732            .map(|r| r.load(Ordering::Relaxed))
733            .unwrap_or(true)
734    }
735
736    /// Returns `true` if the all audio streams (input and/or output) are still running.
737    ///
738    /// Returns `false` if any audio stream has stopped unexpectedly (i.e. an audio device
739    /// was disconnected). When this happens, this `CpalStream` instance should be dropped,
740    /// and a new one created.
741    pub fn all_streams_ok(&self) -> bool {
742        self.output_stream_ok() && self.input_stream_ok()
743    }
744}
745
746impl Drop for CpalStream {
747    fn drop(&mut self) {
748        // Make sure any remaining errors/warnings get logged.
749        #[cfg(any(feature = "log", feature = "tracing"))]
750        self.log_status();
751    }
752}
753
754fn start_input_stream(
755    config: &CpalInputConfig,
756    output_sample_rate: cpal::SampleRate,
757    err_to_cx_tx: mpsc::Sender<IoStreamError>,
758) -> Result<StartInputStreamResult, StartStreamError> {
759    let host = if let Some(host_id) = config.host {
760        match cpal::host_from_id(host_id) {
761            Ok(host) => host,
762            Err(e) => {
763                #[cfg(any(feature = "log", feature = "tracing"))]
764                warn!(
765                    "Requested audio host {:?} is not available: {}. Falling back to default host...",
766                    &host_id, e
767                );
768                #[cfg(not(any(feature = "log", feature = "tracing")))]
769                let _ = e;
770                cpal::default_host()
771            }
772        }
773    } else {
774        cpal::default_host()
775    };
776
777    let mut in_device = None;
778    if let Some(device_id) = &config.device_id {
779        if let Some(device) = host.device_by_id(device_id)
780            && device.supports_input()
781        {
782            in_device = Some(device);
783        }
784
785        if in_device.is_none() {
786            if config.fallback {
787                #[cfg(any(feature = "log", feature = "tracing"))]
788                warn!(
789                    "Could not find requested audio input device: {}. Falling back to default device...",
790                    &device_id
791                );
792            } else {
793                #[cfg(any(feature = "log", feature = "tracing"))]
794                warn!(
795                    "Could not find requested audio input device: {}. No input stream will be started.",
796                    &device_id
797                );
798                return Ok(StartInputStreamResult::NotStarted);
799            }
800        }
801    }
802
803    if in_device.is_none() {
804        if let Some(default_device) = host.default_input_device() {
805            in_device = Some(default_device);
806        } else if config.fail_on_no_input {
807            return Err(StartStreamError::DefaultInputDeviceNotFound);
808        } else {
809            #[cfg(any(feature = "log", feature = "tracing"))]
810            warn!("No default audio input device found. Input stream will not be started.");
811            return Ok(StartInputStreamResult::NotStarted);
812        }
813    }
814    let in_device = in_device.unwrap();
815
816    let in_device_id = match in_device.id() {
817        Ok(id) => Some(id),
818        Err(e) => {
819            #[cfg(any(feature = "log", feature = "tracing"))]
820            warn!("Failed to get id of input audio device: {}", e);
821            #[cfg(not(any(feature = "log", feature = "tracing")))]
822            let _ = e;
823            None
824        }
825    };
826
827    let default_config = in_device
828        .default_input_config()
829        .map_err(StartStreamError::FailedToGetConfig)?;
830
831    #[cfg(not(target_os = "ios"))]
832    let desired_block_frames =
833        if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
834            config.desired_block_frames.map(|f| f.clamp(min, max))
835        } else {
836            None
837        };
838
839    // For some reason fixed buffer sizes on iOS doesn't work in CPAL.
840    // I'm not sure if this is a problem on CPAL's end, but I have disabled
841    // it for the time being.
842    #[cfg(target_os = "ios")]
843    let desired_block_frames: Option<u32> = None;
844
845    let supported_configs = in_device
846        .supported_input_configs()
847        .map_err(StartStreamError::FailedToGetConfig)?;
848
849    let mut min_sample_rate = u32::MAX;
850    let mut max_sample_rate = 0;
851    for config in supported_configs.into_iter() {
852        min_sample_rate = min_sample_rate.min(config.min_sample_rate());
853        max_sample_rate = max_sample_rate.max(config.max_sample_rate());
854    }
855    let sample_rate = output_sample_rate.clamp(min_sample_rate, max_sample_rate);
856
857    #[cfg(not(feature = "resample_inputs"))]
858    if sample_rate != output_sample_rate {
859        if config.fail_on_no_input {
860            return Err(StartStreamError::CouldNotMatchSampleRate(
861                output_sample_rate,
862            ));
863        } else {
864            #[cfg(any(feature = "log", feature = "tracing"))]
865            warn!(
866                "Could not use output sample rate {} for the input sample rate. Input stream will not be started",
867                output_sample_rate
868            );
869            return Ok(StartInputStreamResult::NotStarted);
870        }
871    }
872
873    let num_in_channels = default_config.channels() as usize;
874    assert_ne!(num_in_channels, 0);
875
876    let desired_buffer_size = if let Some(samples) = desired_block_frames {
877        cpal::BufferSize::Fixed(samples)
878    } else {
879        cpal::BufferSize::Default
880    };
881
882    let max_block_frames = match desired_buffer_size {
883        cpal::BufferSize::Default => DEFAULT_MAX_BLOCK_FRAMES as usize,
884        cpal::BufferSize::Fixed(f) => f as usize,
885    };
886
887    let stream_config = cpal::StreamConfig {
888        channels: num_in_channels as u16,
889        sample_rate,
890        buffer_size: desired_buffer_size,
891    };
892
893    let (prod, cons) = fixed_resample::resampling_channel::<f32>(
894        num_in_channels,
895        sample_rate,
896        output_sample_rate,
897        true,
898        config.channel_config,
899    );
900
901    let input_stream_running = Arc::new(AtomicBool::new(true));
902
903    #[cfg(any(feature = "log", feature = "tracing"))]
904    info!(
905        "Starting input audio stream with device \"{:?}\" with configuration {:?}",
906        &in_device_id, &stream_config
907    );
908
909    let mut callback = InputCallback {
910        prod,
911        err_to_cx_tx: err_to_cx_tx.clone(),
912        input_stream_running: Arc::clone(&input_stream_running),
913    };
914
915    let in_sample_format = default_config.sample_format();
916
917    let scratch_capacity = max_block_frames * num_in_channels;
918
919    #[cfg(any(
920        target_os = "linux",
921        target_os = "freebsd",
922        target_os = "dragonfly",
923        target_os = "netbsd"
924    ))]
925    let is_alsa = host.id() == HostId::Alsa;
926
927    macro_rules! build_input_stream {
928        ($sample_format:expr, $(($format:path, $primitive_type:ty)),*) => {
929            match $sample_format {
930                $($format => {
931                    let mut scratch = scratch_vec(scratch_capacity);
932
933                    in_device.build_input_stream(
934                        stream_config,
935                        move |input: &[$primitive_type], _info: &cpal::InputCallbackInfo| {
936                            for in_chunk in input.chunks(scratch_capacity) {
937                                for (o, &i) in scratch.iter_mut().zip(in_chunk.iter()) {
938                                    *o = <f32 as cpal::FromSample<$primitive_type>>::from_sample_(i);
939                                }
940
941                                callback.callback(&scratch[..in_chunk.len()]);
942                            }
943                        },
944                        err_callback(
945                            true,
946                            input_stream_running.clone(),
947                            err_to_cx_tx.clone(),
948                            #[cfg(any(
949                                target_os = "linux",
950                                target_os = "freebsd",
951                                target_os = "dragonfly",
952                                target_os = "netbsd"
953                            ))]
954                            is_alsa
955                        ),
956                        Some(BUILD_STREAM_TIMEOUT),
957                    )
958                },)*
959                _ => unreachable!(),
960            }
961        }
962    }
963
964    // The cpal ASIO backend requires the callback buffer type to match the
965    // driver's native format (unlike WASAPI, which converts internally).
966    // For non-f32 formats, render into an f32 scratch buffer and convert
967    // on the way out. The f32 path stays a direct passthrough.
968    let stream_handle = if let SampleFormat::F32 = in_sample_format {
969        in_device.build_input_stream(
970            stream_config,
971            move |input: &[f32], _info: &cpal::InputCallbackInfo| {
972                callback.callback(input);
973            },
974            err_callback(
975                true,
976                input_stream_running.clone(),
977                err_to_cx_tx.clone(),
978                #[cfg(any(
979                    target_os = "linux",
980                    target_os = "freebsd",
981                    target_os = "dragonfly",
982                    target_os = "netbsd"
983                ))]
984                is_alsa,
985            ),
986            Some(BUILD_STREAM_TIMEOUT),
987        )
988    } else {
989        build_input_stream!(
990            in_sample_format,
991            (SampleFormat::I8, i8),
992            (SampleFormat::I16, i16),
993            (SampleFormat::I32, i32),
994            (SampleFormat::I64, i64),
995            (SampleFormat::U8, u8),
996            (SampleFormat::U16, u16),
997            (SampleFormat::U32, u32),
998            (SampleFormat::U64, u64),
999            (SampleFormat::F64, f64)
1000        )
1001    };
1002
1003    let stream_handle = match stream_handle {
1004        Ok(s) => s,
1005        Err(e) => {
1006            if config.fail_on_no_input {
1007                return Err(StartStreamError::BuildStreamError(e));
1008            } else {
1009                #[cfg(any(feature = "log", feature = "tracing"))]
1010                error!(
1011                    "Failed to build input audio stream, input stream will not be started. {}",
1012                    e
1013                );
1014                return Ok(StartInputStreamResult::NotStarted);
1015            }
1016        }
1017    };
1018
1019    if let Err(e) = stream_handle.play() {
1020        if let cpal::ErrorKind::RealtimeDenied = e.kind() {
1021            #[cfg(any(feature = "log", feature = "tracing"))]
1022            warn!(
1023                "Failed to get realtime priority for input audio thread. This may increase latency and/or audio glitches"
1024            );
1025        } else if config.fail_on_no_input {
1026            return Err(StartStreamError::PlayStreamError(e));
1027        } else {
1028            #[cfg(any(feature = "log", feature = "tracing"))]
1029            error!(
1030                "Failed to start input audio stream, input stream will not be started. {}",
1031                e
1032            );
1033            return Ok(StartInputStreamResult::NotStarted);
1034        }
1035    }
1036
1037    Ok(StartInputStreamResult::Started {
1038        stream_handle,
1039        cons,
1040        num_stream_in_channels: num_in_channels as u32,
1041        in_device_id,
1042        input_stream_running,
1043    })
1044}
1045
1046#[allow(clippy::large_enum_variant)]
1047enum StartInputStreamResult {
1048    NotStarted,
1049    Started {
1050        stream_handle: cpal::Stream,
1051        cons: fixed_resample::ResamplingCons<f32>,
1052        num_stream_in_channels: u32,
1053        in_device_id: Option<DeviceId>,
1054        input_stream_running: Arc<AtomicBool>,
1055    },
1056}
1057
1058struct InputCallback {
1059    prod: ResamplingProd<f32>,
1060    err_to_cx_tx: mpsc::Sender<IoStreamError>,
1061    input_stream_running: Arc<AtomicBool>,
1062}
1063
1064impl InputCallback {
1065    fn callback(&mut self, input: &[f32]) {
1066        let _ = self.prod.push_interleaved(input);
1067    }
1068}
1069
1070impl Drop for InputCallback {
1071    fn drop(&mut self) {
1072        self.input_stream_running.store(false, Ordering::Relaxed);
1073        let _ = self
1074            .err_to_cx_tx
1075            .send(IoStreamError::Input(cpal::Error::new(
1076                cpal::ErrorKind::StreamInvalidated,
1077            )));
1078    }
1079}
1080
1081struct OutputCallback {
1082    num_out_channels: usize,
1083    processor: FirewheelProcessor,
1084    sample_rate: u32,
1085    sample_rate_recip: f64,
1086    predicted_delta_time: Duration,
1087    prev_instant: Option<Instant>,
1088    stream_start_instant: Instant,
1089    input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
1090    input_buffer: Vec<f32>,
1091    err_to_cx_tx: mpsc::Sender<IoStreamError>,
1092    input_stream_running: Option<Arc<AtomicBool>>,
1093    output_stream_running: Arc<AtomicBool>,
1094}
1095
1096impl OutputCallback {
1097    #[allow(clippy::too_many_arguments)]
1098    fn new(
1099        num_out_channels: usize,
1100        max_block_frames: usize,
1101        sample_rate: u32,
1102        processor: FirewheelProcessor,
1103        input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
1104        err_to_cx_tx: mpsc::Sender<IoStreamError>,
1105        input_stream_running: Option<Arc<AtomicBool>>,
1106        output_stream_running: Arc<AtomicBool>,
1107    ) -> Self {
1108        let stream_start_instant = Instant::now();
1109
1110        let input_buffer = if let Some(cons) = &input_stream_cons {
1111            scratch_vec(max_block_frames * cons.num_channels())
1112        } else {
1113            Vec::new()
1114        };
1115
1116        Self {
1117            num_out_channels,
1118            processor,
1119            sample_rate,
1120            sample_rate_recip: f64::from(sample_rate).recip(),
1121            predicted_delta_time: Duration::default(),
1122            prev_instant: None,
1123            stream_start_instant,
1124            input_stream_cons,
1125            input_buffer,
1126            err_to_cx_tx,
1127            input_stream_running,
1128            output_stream_running,
1129        }
1130    }
1131
1132    fn callback(&mut self, output: &mut [f32], info: &cpal::OutputCallbackInfo) {
1133        let process_timestamp = bevy_platform::time::Instant::now();
1134
1135        let frames = output.len() / self.num_out_channels;
1136
1137        let (underflow, dropped_frames) = if let Some(prev_instant) = self.prev_instant {
1138            let delta_time = process_timestamp - prev_instant;
1139
1140            let underflow = delta_time > self.predicted_delta_time;
1141
1142            let dropped_frames = if underflow {
1143                (delta_time.as_secs_f64() * self.sample_rate as f64).round() as u32
1144            } else {
1145                0
1146            };
1147
1148            (underflow, dropped_frames)
1149        } else {
1150            self.prev_instant = Some(process_timestamp);
1151            (false, 0)
1152        };
1153
1154        // Calculate the next predicted stream time to detect underflows.
1155        //
1156        // Add a little bit of wiggle room to account for tiny clock
1157        // inaccuracies and rounding errors.
1158        self.predicted_delta_time =
1159            Duration::from_secs_f64(frames as f64 * self.sample_rate_recip * 1.5);
1160
1161        let duration_since_stream_start =
1162            process_timestamp.duration_since(self.stream_start_instant);
1163
1164        // TODO: PLEASE FIX ME:
1165        //
1166        // It appears that for some reason, both Windows and Linux will sometimes return a timestamp which
1167        // has a value less than the previous timestamp. I am unsure if this is a bug with the APIs, a bug
1168        // with CPAL, or I'm just misunderstaning how the timestamps are supposed to be used. Either way,
1169        // it is disabled for now and `bevy_platform::time::Instance::now()` is being used as a workaround above.
1170        //
1171        // let (internal_clock_secs, underflow) = if let Some(instant) =
1172        //     &self.first_internal_clock_instant
1173        // {
1174        //     if let Some(prev_stream_instant) = &self.prev_stream_instant {
1175        //         if info
1176        //             .timestamp()
1177        //             .playback
1178        //             .duration_since(prev_stream_instant)
1179        //             .is_none()
1180        //         {
1181        //             // If this occurs in other APIs as well, then either CPAL is doing
1182        //             // something wrong, or I'm doing something wrong.
1183        //             error!("CPAL and/or the system audio API returned invalid timestamp. Please notify the Firewheel developers of this bug.");
1184        //         }
1185        //     }
1186        //
1187        //     let internal_clock_secs = info
1188        //         .timestamp()
1189        //         .playback
1190        //         .duration_since(instant)
1191        //         .map(|s| s.as_secs_f64())
1192        //         .unwrap_or_else(|| self.predicted_stream_secs.unwrap_or(0.0));
1193        //
1194        //     let underflow = if let Some(predicted_stream_secs) = self.predicted_stream_secs {
1195        //         // If the stream time is significantly greater than the predicted stream
1196        //         // time, it means an output underflow has occurred.
1197        //         internal_clock_secs > predicted_stream_secs
1198        //     } else {
1199        //         false
1200        //     };
1201        //
1202        //     // Calculate the next predicted stream time to detect underflows.
1203        //     //
1204        //     // Add a little bit of wiggle room to account for tiny clock
1205        //     // inaccuracies and rounding errors.
1206        //     self.predicted_stream_secs =
1207        //         Some(internal_clock_secs + (frames as f64 * self.sample_rate_recip * 1.2));
1208        //
1209        //     self.prev_stream_instant = Some(info.timestamp().playback);
1210        //
1211        //     (ClockSeconds(internal_clock_secs), underflow)
1212        // } else {
1213        //     self.first_internal_clock_instant = Some(info.timestamp().playback);
1214        //     (ClockSeconds(0.0), false)
1215        // };
1216
1217        let (num_in_channels, input_stream_status) = if let Some(cons) = &mut self.input_stream_cons
1218        {
1219            let num_in_channels = cons.num_channels();
1220            let num_input_samples = frames * num_in_channels;
1221
1222            if self
1223                .input_stream_running
1224                .as_ref()
1225                .unwrap()
1226                .load(Ordering::Relaxed)
1227            {
1228                let status =
1229                    cons.read_interleaved(&mut self.input_buffer[..num_input_samples], false);
1230
1231                let status = match status {
1232                    ReadStatus::UnderflowOccurred { num_frames_read: _ } => {
1233                        StreamStatus::OUTPUT_UNDERFLOW
1234                    }
1235                    ReadStatus::OverflowCorrected {
1236                        num_frames_discarded: _,
1237                    } => StreamStatus::INPUT_OVERFLOW,
1238                    _ => StreamStatus::empty(),
1239                };
1240
1241                (num_in_channels, status)
1242            } else {
1243                self.input_buffer[..num_input_samples].fill(0.0);
1244
1245                (num_in_channels, StreamStatus::CLOSED)
1246            }
1247        } else {
1248            (0, StreamStatus::empty())
1249        };
1250
1251        let mut output_stream_status = StreamStatus::empty();
1252        if underflow {
1253            output_stream_status.insert(StreamStatus::OUTPUT_UNDERFLOW);
1254        }
1255
1256        let timestamp = info.timestamp();
1257        let process_to_playback_delay = timestamp.playback.duration_since(timestamp.callback);
1258
1259        self.processor.process(
1260            &InterleavedSlice::new(
1261                &self.input_buffer[..frames * num_in_channels],
1262                num_in_channels,
1263                frames,
1264            )
1265            .unwrap(),
1266            &mut InterleavedSlice::new_mut(output, self.num_out_channels, frames).unwrap(),
1267            BackendProcessInfo {
1268                frames,
1269                process_timestamp: Some(process_timestamp),
1270                duration_since_stream_start,
1271                input_stream_status,
1272                output_stream_status,
1273                dropped_frames,
1274                process_to_playback_delay: Some(process_to_playback_delay),
1275            },
1276        );
1277    }
1278}
1279
1280impl Drop for OutputCallback {
1281    fn drop(&mut self) {
1282        self.output_stream_running.store(false, Ordering::Relaxed);
1283        let _ = self
1284            .err_to_cx_tx
1285            .send(IoStreamError::Input(cpal::Error::new(
1286                cpal::ErrorKind::StreamInvalidated,
1287            )));
1288    }
1289}
1290
1291fn err_callback(
1292    is_input: bool,
1293    is_running: Arc<AtomicBool>,
1294    err_to_cx_tx: mpsc::Sender<IoStreamError>,
1295    #[cfg(any(
1296        target_os = "linux",
1297        target_os = "freebsd",
1298        target_os = "dragonfly",
1299        target_os = "netbsd"
1300    ))]
1301    is_alsa: bool,
1302) -> impl FnMut(cpal::Error) + Send + 'static {
1303    let mut last_underrun_msg_instant: Option<Instant> = None;
1304
1305    move |err| {
1306        let do_send = match err.kind() {
1307            cpal::ErrorKind::Xrun => {
1308                let mut do_send = true;
1309                if let Some(instant) = last_underrun_msg_instant
1310                    && instant.elapsed() < UNDERRUN_LOG_COOLDOWN
1311                {
1312                    do_send = false;
1313                }
1314
1315                if do_send {
1316                    last_underrun_msg_instant = Some(Instant::now());
1317                }
1318
1319                do_send
1320            }
1321            cpal::ErrorKind::DeviceNotAvailable
1322            | cpal::ErrorKind::HostUnavailable
1323            | cpal::ErrorKind::BackendError
1324            | cpal::ErrorKind::Other
1325            | cpal::ErrorKind::PermissionDenied
1326            | cpal::ErrorKind::StreamInvalidated => {
1327                is_running.store(false, Ordering::Relaxed);
1328                true
1329            }
1330            _ => false,
1331        };
1332
1333        // Alsa can be very spammy with its warning messages.
1334        #[cfg(any(
1335            target_os = "linux",
1336            target_os = "freebsd",
1337            target_os = "dragonfly",
1338            target_os = "netbsd"
1339        ))]
1340        let do_send = if do_send
1341            && is_alsa
1342            && err.kind() != cpal::ErrorKind::StreamInvalidated
1343            && let Some(msg) = err.message()
1344            // Add more filters here if needed.
1345            && msg.contains("snd_pcm_avail_delay")
1346        {
1347            false
1348        } else {
1349            do_send
1350        };
1351
1352        if do_send
1353            && let Err(e) = err_to_cx_tx.send(if is_input {
1354                IoStreamError::Input(err)
1355            } else {
1356                IoStreamError::Output(err)
1357            })
1358        {
1359            // Make sure the error gets logged even if the handle has been dropped.
1360            #[cfg(any(feature = "log", feature = "tracing"))]
1361            error!("Audio stream error occurred: {}", e.0);
1362            #[cfg(not(any(feature = "log", feature = "tracing")))]
1363            let _ = e;
1364        }
1365    }
1366}
1367
1368/// An error occurred while trying to start a CPAL audio stream.
1369#[derive(Debug, thiserror::Error)]
1370pub enum StartStreamError {
1371    /// The Firewheel context is already active. Either it has never been activated
1372    /// or the [`FirewheelProcessor`] counterpart has not been dropped yet.
1373    ///
1374    /// Note, in rare cases where the audio thread crashes without cleanly
1375    /// dropping its contents, this may never succeed. Consider adding a
1376    /// timeout to avoid deadlocking.
1377    #[error("Failed to activate Firewheel context: The Firewheel context is already active")]
1378    AlreadyActive,
1379    /// The audio graph failed to compile.
1380    #[error("Failed to activate Firewheel context: Audio graph failed to compile: {0}")]
1381    GraphCompileError(#[from] CompileGraphError),
1382
1383    #[error("The requested audio input device was not found: {0}")]
1384    InputDeviceNotFound(String),
1385    #[error("The requested audio output device was not found: {0}")]
1386    OutputDeviceNotFound(String),
1387    #[error("Failed to get default input output device")]
1388    DefaultInputDeviceNotFound,
1389    #[error("Failed to get default audio output device")]
1390    DefaultOutputDeviceNotFound,
1391    #[error("Failed to get audio device config: {0}")]
1392    FailedToGetConfig(cpal::Error),
1393    #[error("Failed to build audio stream: {0}")]
1394    BuildStreamError(cpal::Error),
1395    #[error("Failed to play audio stream: {0}")]
1396    PlayStreamError(cpal::Error),
1397
1398    #[cfg(not(feature = "resample_inputs"))]
1399    #[error("Not able to use a sample rate of {0} for the input audio device")]
1400    CouldNotMatchSampleRate(u32),
1401}
1402
1403impl From<ActivateError> for StartStreamError {
1404    fn from(e: ActivateError) -> Self {
1405        match e {
1406            ActivateError::AlreadyActive => Self::AlreadyActive,
1407            ActivateError::GraphCompileError(e) => Self::GraphCompileError(e),
1408        }
1409    }
1410}
1411
1412#[derive(Debug, thiserror::Error)]
1413pub enum IoStreamError {
1414    /// An error on the input stream occurred.
1415    #[error("Input audio stream error: {0}")]
1416    Input(cpal::Error),
1417    /// An error on the output stream occurred.
1418    #[error("Output audio stream error: {0}")]
1419    Output(cpal::Error),
1420}
1421
1422fn scratch_vec(len: usize) -> Vec<f32> {
1423    let mut v = Vec::new();
1424    v.reserve_exact(len);
1425    v.resize(len, 0.0f32);
1426    v
1427}