Skip to main content

firewheel_cpal/
lib.rs

1use core::{
2    fmt::Debug,
3    num::{NonZeroU32, NonZeroUsize},
4    str::FromStr,
5    time::Duration,
6    u32,
7};
8use std::sync::mpsc;
9
10pub use cpal;
11
12use bevy_platform::time::Instant;
13use cpal::{
14    traits::{DeviceTrait, HostTrait, StreamTrait},
15    DeviceId, HostId, HostUnavailable,
16};
17use firewheel_core::{node::StreamStatus, StreamInfo};
18use firewheel_graph::{
19    backend::{AudioBackend, BackendProcessInfo, DeviceInfoSimple, SimpleStreamConfig},
20    processor::FirewheelProcessor,
21};
22use fixed_resample::{ReadStatus, ResamplingChannelConfig};
23use ringbuf::traits::{Consumer, Producer, Split};
24
25#[cfg(all(feature = "log", not(feature = "tracing")))]
26use log::{error, info, warn};
27#[cfg(feature = "tracing")]
28use tracing::{error, info, warn};
29
30/// 1024 samples is a latency of about 23 milliseconds, which should
31/// be good enough for most games.
32const DEFAULT_MAX_BLOCK_FRAMES: u32 = 1024;
33const INPUT_ALLOC_BLOCK_FRAMES: usize = 4096;
34const BUILD_STREAM_TIMEOUT: Duration = Duration::from_secs(5);
35const MSG_CHANNEL_CAPACITY: usize = 4;
36const MAX_INPUT_CHANNELS: usize = 16;
37
38/// The configuration of an output audio stream in the CPAL backend.
39#[derive(Debug, Clone, PartialEq)]
40pub struct CpalOutputConfig {
41    /// The host to use. Set to `None` to use the
42    /// system's default audio host.
43    pub host: Option<cpal::HostId>,
44
45    /// The id of the output device to use. Set to `None` to use the
46    /// system's default output device.
47    ///
48    /// By default this is set to `None`.
49    pub device_id: Option<DeviceId>,
50
51    /// The desired sample rate to use. Set to `None` to use the device's
52    /// default sample rate.
53    ///
54    /// By default this is set to `None`.
55    pub desired_sample_rate: Option<u32>,
56
57    /// The latency/block size of the audio stream to use. Set to
58    /// `None` to use the device's default value.
59    ///
60    /// Smaller values may give better latency, but is not supported on
61    /// all platforms and may lead to performance issues.
62    ///
63    /// This currently has no effect on iOS platforms.
64    ///
65    /// By default this is set to `Some(1024)`.
66    pub desired_block_frames: Option<u32>,
67
68    /// Whether or not to fall back to the default device  if a device
69    /// with the given configuration could not be found.
70    ///
71    /// By default this is set to `true`.
72    pub fallback: bool,
73}
74
75impl Default for CpalOutputConfig {
76    fn default() -> Self {
77        Self {
78            host: None,
79            device_id: None,
80            desired_sample_rate: None,
81            desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
82            fallback: true,
83        }
84    }
85}
86
87/// The configuration of an input audio stream in the CPAL backend.
88#[derive(Debug, Clone, PartialEq)]
89pub struct CpalInputConfig {
90    /// The host to use. Set to `None` to use the
91    /// system's default audio host.
92    pub host: Option<cpal::HostId>,
93
94    /// The id of the input device to use. Set to `None` to use the
95    /// system's default input device.
96    ///
97    /// By default this is set to `None`.
98    pub device_id: Option<DeviceId>,
99
100    /// The latency/block size of the audio stream to use. Set to
101    /// `None` to use the device's default value.
102    ///
103    /// Smaller values may give better latency, but is not supported on
104    /// all platforms and may lead to performance issues.
105    ///
106    /// This currently has no effect on iOS platforms.
107    ///
108    /// By default this is set to `Some(1024)`.
109    pub desired_block_frames: Option<u32>,
110
111    /// The configuration of the input to output stream channel.
112    pub channel_config: ResamplingChannelConfig,
113
114    /// Whether or not to fall back to the default device  if a device
115    /// with the given configuration could not be found.
116    ///
117    /// By default this is set to `true`.
118    pub fallback: bool,
119
120    /// If `true`, then an error will be returned if an input stream could
121    /// not be started. If `false`, then the output stream will still
122    /// attempt to start with no input stream.
123    ///
124    /// By default this is set to `false`.
125    pub fail_on_no_input: bool,
126}
127
128impl Default for CpalInputConfig {
129    fn default() -> Self {
130        Self {
131            host: None,
132            device_id: None,
133            desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
134            channel_config: ResamplingChannelConfig::default(),
135            fallback: true,
136            fail_on_no_input: false,
137        }
138    }
139}
140
141/// The configuration of a CPAL stream.
142#[derive(Debug, Clone, PartialEq)]
143pub struct CpalConfig {
144    /// The configuration of the output stream.
145    pub output: CpalOutputConfig,
146
147    /// The configuration of the input stream.
148    ///
149    /// Set to `None` for no input stream.
150    ///
151    /// By default this is set to `None`.
152    pub input: Option<CpalInputConfig>,
153}
154
155impl Default for CpalConfig {
156    fn default() -> Self {
157        Self {
158            output: CpalOutputConfig::default(),
159            input: None,
160        }
161    }
162}
163
164/// A struct used to retrieve the list of available audio devices
165/// on the system and their available ocnfigurations.
166pub struct CpalEnumerator;
167
168impl CpalEnumerator {
169    /// The system audio hosts (APIs) that are available on this system.
170    ///
171    /// The first host in the list is the default one for the system.
172    pub fn available_hosts(&self) -> Vec<cpal::HostId> {
173        cpal::available_hosts()
174    }
175
176    /// Get a struct used to retrieve the list of available audio devices
177    /// for the default system audio host (API).
178    pub fn default_host(&self) -> HostEnumerator {
179        HostEnumerator {
180            host: cpal::default_host(),
181        }
182    }
183
184    /// Get a struct used to retrieve the list of available audio devices
185    /// for the given system audio host (API).
186    pub fn get_host(&self, api: HostId) -> Result<HostEnumerator, HostUnavailable> {
187        cpal::host_from_id(api).map(|host| HostEnumerator { host })
188    }
189}
190
191/// A struct used to retrieve the list of available audio devices
192/// for a given system audio host (API).
193pub struct HostEnumerator {
194    pub host: cpal::Host,
195}
196
197impl HostEnumerator {
198    /// The system backend host id (API) this enumerator is using.
199    pub fn host_id(&self) -> cpal::HostId {
200        self.host.id()
201    }
202
203    /// Get the list of available input audio devices.
204    pub fn input_devices(&self) -> Vec<DeviceInfo> {
205        let mut devices = Vec::with_capacity(8);
206
207        let default_device = self.host.default_input_device();
208        let default_device_id = default_device.and_then(|d| match d.id() {
209            Ok(id) => Some(id),
210            Err(e) => {
211                warn!("Failed to get ID of default audio input device: {}", e);
212                None
213            }
214        });
215
216        match self.host.input_devices() {
217            Ok(input_devices) => {
218                for device in input_devices {
219                    let Ok(id) = device.id() else {
220                        continue;
221                    };
222
223                    let is_default = if let Some(default_device_id) = &default_device_id {
224                        &id == default_device_id
225                    } else {
226                        false
227                    };
228
229                    let name = device.description().map(|d| d.name().to_string()).ok();
230
231                    devices.push(DeviceInfo {
232                        id,
233                        name,
234                        is_default,
235                    })
236                }
237            }
238            Err(e) => {
239                error!("Failed to get input audio devices: {}", e);
240            }
241        }
242
243        devices
244    }
245
246    /// Get the list of available output audio devices.
247    pub fn output_devices(&self) -> Vec<DeviceInfo> {
248        let mut devices = Vec::with_capacity(8);
249
250        let default_device = self.host.default_output_device();
251        let default_device_id = default_device.and_then(|d| match d.id() {
252            Ok(id) => Some(id),
253            Err(e) => {
254                warn!("Failed to get ID of default audio output device: {}", e);
255                None
256            }
257        });
258
259        match self.host.output_devices() {
260            Ok(output_devices) => {
261                for device in output_devices {
262                    let Ok(id) = device.id() else {
263                        continue;
264                    };
265
266                    let is_default = if let Some(default_device_id) = &default_device_id {
267                        &id == default_device_id
268                    } else {
269                        false
270                    };
271
272                    let name = device.description().map(|d| d.name().to_string()).ok();
273
274                    devices.push(DeviceInfo {
275                        id,
276                        name,
277                        is_default,
278                    })
279                }
280            }
281            Err(e) => {
282                error!("Failed to get output audio devices: {}", e);
283            }
284        }
285
286        devices
287    }
288
289    /// Get a struct used to retrieve extra information for the given audio
290    /// device.
291    ///
292    /// Returns `None` if the device could not be found.
293    pub fn get_device(&self, device_id: &cpal::DeviceId) -> Option<cpal::Device> {
294        self.host.device_by_id(device_id)
295    }
296}
297
298/// Information about an audio device.
299#[derive(Debug, Clone, PartialEq)]
300pub struct DeviceInfo {
301    /// A stable identifier for an audio device across all supported platforms.
302    ///
303    /// Device IDs should remain stable across application restarts and can be
304    /// serialized using `Display`/`FromStr`.
305    ///
306    /// A device ID consists of a [`HostId`] identifying the audio backend and
307    /// a device-specific identifier string.
308    pub id: cpal::DeviceId,
309    /// The display name of the device.
310    pub name: Option<String>,
311    /// Whether or not this device is the default input/output device.
312    pub is_default: bool,
313}
314
315/// A CPAL backend for Firewheel
316pub struct CpalBackend {
317    from_err_rx: mpsc::Receiver<cpal::StreamError>,
318    to_stream_tx: ringbuf::HeapProd<CtxToStreamMsg>,
319    _out_stream_handle: cpal::Stream,
320    _in_stream_handle: Option<cpal::Stream>,
321}
322
323impl AudioBackend for CpalBackend {
324    type Enumerator = CpalEnumerator;
325    type Config = CpalConfig;
326    type StartStreamError = StreamStartError;
327    type StreamError = cpal::StreamError;
328    type Instant = bevy_platform::time::Instant;
329
330    fn enumerator() -> Self::Enumerator {
331        CpalEnumerator {}
332    }
333
334    fn input_devices_simple(&mut self) -> Vec<DeviceInfoSimple> {
335        let enumerator = CpalEnumerator {};
336        let host_enumerator = enumerator.default_host();
337
338        let mut default_device_index = None;
339
340        let mut devices: Vec<DeviceInfoSimple> = host_enumerator
341            .input_devices()
342            .iter()
343            .enumerate()
344            .map(|(i, info)| {
345                if info.is_default {
346                    default_device_index = Some(i);
347                }
348
349                DeviceInfoSimple {
350                    name: info.name.clone().unwrap_or_else(|| String::from("unkown")),
351                    id: format!("{}", info.id),
352                }
353            })
354            .collect();
355
356        // Make sure the default device is the first in the list.
357        if let Some(i) = default_device_index {
358            devices.swap(0, i);
359        }
360
361        devices
362    }
363
364    fn output_devices_simple(&mut self) -> Vec<DeviceInfoSimple> {
365        let enumerator = CpalEnumerator {};
366        let host_enumerator = enumerator.default_host();
367
368        let mut default_device_index = None;
369
370        let mut devices: Vec<DeviceInfoSimple> = host_enumerator
371            .output_devices()
372            .iter()
373            .enumerate()
374            .map(|(i, info)| {
375                if info.is_default {
376                    default_device_index = Some(i);
377                }
378
379                DeviceInfoSimple {
380                    name: info.name.clone().unwrap_or_else(|| String::from("unkown")),
381                    id: format!("{}", info.id),
382                }
383            })
384            .collect();
385
386        // Make sure the default device is the first in the list.
387        if let Some(i) = default_device_index {
388            devices.swap(0, i);
389        }
390
391        devices
392    }
393
394    fn convert_simple_config(&mut self, config: &SimpleStreamConfig) -> Self::Config {
395        let string_to_id = |s: Option<&String>| -> Option<DeviceId> {
396            s.and_then(|s| match DeviceId::from_str(s) {
397                Ok(id) => Some(id),
398                Err(e) => {
399                    warn!(
400                        "Failed to convert string to DeviceID, falling back to default device: {}",
401                        e
402                    );
403                    None
404                }
405            })
406        };
407
408        CpalConfig {
409            output: CpalOutputConfig {
410                device_id: string_to_id(config.output.device.as_ref()),
411                desired_sample_rate: config.desired_sample_rate,
412                desired_block_frames: config.desired_block_frames,
413                ..Default::default()
414            },
415            input: config.input.as_ref().map(|input_config| CpalInputConfig {
416                device_id: string_to_id(input_config.device.as_ref()),
417                desired_block_frames: config.desired_block_frames,
418                ..Default::default()
419            }),
420        }
421    }
422
423    fn start_stream(config: Self::Config) -> Result<(Self, StreamInfo), Self::StartStreamError> {
424        info!("Attempting to start CPAL audio stream...");
425
426        let host = if let Some(host_id) = config.output.host {
427            match cpal::host_from_id(host_id) {
428                Ok(host) => host,
429                Err(e) => {
430                    warn!("Requested audio host {:?} is not available: {}. Falling back to default host...", &host_id, e);
431                    cpal::default_host()
432                }
433            }
434        } else {
435            cpal::default_host()
436        };
437
438        let mut out_device = None;
439        if let Some(device_id) = &config.output.device_id {
440            if let Some(device) = host.device_by_id(device_id) {
441                if device.supports_output() {
442                    out_device = Some(device);
443                }
444            }
445
446            if out_device.is_none() {
447                warn!("Could not find requested audio output device: {}. Falling back to default device...", &device_id);
448            }
449        }
450
451        if out_device.is_none() {
452            let Some(default_device) = host.default_output_device() else {
453                return Err(StreamStartError::DefaultOutputDeviceNotFound);
454            };
455            out_device = Some(default_device);
456        }
457        let out_device = out_device.unwrap();
458
459        let output_device_id = out_device.id().map(|d| d.to_string()).unwrap_or_else(|e| {
460            warn!("Failed to get id of output audio device: {}", e);
461            String::from("unknown")
462        });
463
464        let default_config = out_device.default_output_config()?;
465
466        let default_sample_rate = default_config.sample_rate();
467        // Try to use the common sample rates by default.
468        let try_common_sample_rates = default_sample_rate != 44100 && default_sample_rate != 48000;
469
470        #[cfg(not(target_os = "ios"))]
471        let desired_block_frames =
472            if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
473                config
474                    .output
475                    .desired_block_frames
476                    .map(|f| f.clamp(min, max))
477            } else {
478                None
479            };
480
481        // For some reason fixed buffer sizes on iOS doesn't work in CPAL.
482        // I'm not sure if this is a problem on CPAL's end, but I have disabled
483        // it for the time being.
484        #[cfg(target_os = "ios")]
485        let desired_block_frames: Option<u32> = None;
486
487        let mut supports_desired_sample_rate = false;
488        let mut supports_44100 = false;
489        let mut supports_48000 = false;
490
491        if config.output.desired_sample_rate.is_some() || try_common_sample_rates {
492            for cpal_config in out_device.supported_output_configs()? {
493                if let Some(sr) = config.output.desired_sample_rate {
494                    if !supports_desired_sample_rate {
495                        if cpal_config.try_with_sample_rate(sr).is_some() {
496                            supports_desired_sample_rate = true;
497                            break;
498                        }
499                    }
500                }
501
502                if try_common_sample_rates {
503                    if !supports_44100 {
504                        if cpal_config.try_with_sample_rate(44100).is_some() {
505                            supports_44100 = true;
506                        }
507                    }
508                    if !supports_48000 {
509                        if cpal_config.try_with_sample_rate(48000).is_some() {
510                            supports_48000 = true;
511                        }
512                    }
513                }
514            }
515        }
516
517        let sample_rate = if supports_desired_sample_rate {
518            config.output.desired_sample_rate.unwrap()
519        } else if try_common_sample_rates {
520            if supports_44100 {
521                44100
522            } else if supports_48000 {
523                48000
524            } else {
525                default_sample_rate
526            }
527        } else {
528            default_sample_rate
529        };
530
531        let num_out_channels = default_config.channels() as usize;
532        assert_ne!(num_out_channels, 0);
533
534        let desired_buffer_size = if let Some(samples) = desired_block_frames {
535            cpal::BufferSize::Fixed(samples)
536        } else {
537            cpal::BufferSize::Default
538        };
539
540        let out_stream_config = cpal::StreamConfig {
541            channels: num_out_channels as u16,
542            sample_rate,
543            buffer_size: desired_buffer_size,
544        };
545
546        let max_block_frames = match out_stream_config.buffer_size {
547            cpal::BufferSize::Default => DEFAULT_MAX_BLOCK_FRAMES as usize,
548            cpal::BufferSize::Fixed(f) => f as usize,
549        };
550
551        let (err_to_cx_tx, from_err_rx) = mpsc::channel();
552
553        let mut input_stream = StartInputStreamResult::NotStarted;
554        if let Some(input_config) = &config.input {
555            input_stream = start_input_stream(
556                input_config,
557                out_stream_config.sample_rate,
558                err_to_cx_tx.clone(),
559            )?;
560        }
561
562        let (
563            input_stream_handle,
564            input_stream_cons,
565            num_stream_in_channels,
566            input_device_id,
567            input_to_output_latency_seconds,
568        ) = if let StartInputStreamResult::Started {
569            stream_handle,
570            cons,
571            num_stream_in_channels,
572            input_device_id,
573        } = input_stream
574        {
575            let input_to_output_latency_seconds = cons.latency_seconds();
576
577            (
578                Some(stream_handle),
579                Some(cons),
580                num_stream_in_channels,
581                Some(input_device_id),
582                input_to_output_latency_seconds,
583            )
584        } else {
585            (None, None, 0, None, 0.0)
586        };
587
588        let (to_stream_tx, from_cx_rx) =
589            ringbuf::HeapRb::<CtxToStreamMsg>::new(MSG_CHANNEL_CAPACITY).split();
590
591        let mut data_callback = DataCallback::new(
592            num_out_channels,
593            from_cx_rx,
594            out_stream_config.sample_rate,
595            input_stream_cons,
596        );
597
598        info!(
599            "Starting output audio stream with device \"{}\" with configuration {:?}",
600            &output_device_id, &out_stream_config
601        );
602
603        let out_stream_handle = out_device.build_output_stream(
604            &out_stream_config,
605            move |output: &mut [f32], info: &cpal::OutputCallbackInfo| {
606                data_callback.callback(output, info);
607            },
608            move |err| {
609                let _ = err_to_cx_tx.send(err);
610            },
611            Some(BUILD_STREAM_TIMEOUT),
612        )?;
613
614        out_stream_handle.play()?;
615
616        let stream_info = StreamInfo {
617            sample_rate: NonZeroU32::new(out_stream_config.sample_rate).unwrap(),
618            max_block_frames: NonZeroU32::new(max_block_frames as u32).unwrap(),
619            num_stream_in_channels,
620            num_stream_out_channels: num_out_channels as u32,
621            input_to_output_latency_seconds,
622            output_device_id,
623            input_device_id,
624            // The engine will overwrite the other values.
625            ..Default::default()
626        };
627
628        Ok((
629            Self {
630                from_err_rx,
631                to_stream_tx,
632                _out_stream_handle: out_stream_handle,
633                _in_stream_handle: input_stream_handle,
634            },
635            stream_info,
636        ))
637    }
638
639    fn set_processor(&mut self, processor: FirewheelProcessor<Self>) {
640        if let Err(_) = self
641            .to_stream_tx
642            .try_push(CtxToStreamMsg::NewProcessor(processor))
643        {
644            panic!("Failed to send new processor to cpal stream");
645        }
646    }
647
648    fn poll_status(&mut self) -> Result<(), Self::StreamError> {
649        if let Ok(e) = self.from_err_rx.try_recv() {
650            Err(e)
651        } else {
652            Ok(())
653        }
654    }
655
656    fn delay_from_last_process(&self, process_timestamp: Self::Instant) -> Option<Duration> {
657        Some(process_timestamp.elapsed())
658    }
659}
660
661fn start_input_stream(
662    config: &CpalInputConfig,
663    output_sample_rate: cpal::SampleRate,
664    err_to_cx_tx: mpsc::Sender<cpal::StreamError>,
665) -> Result<StartInputStreamResult, StreamStartError> {
666    let host = if let Some(host_id) = config.host {
667        match cpal::host_from_id(host_id) {
668            Ok(host) => host,
669            Err(e) => {
670                warn!("Requested audio host {:?} is not available: {}. Falling back to default host...", &host_id, e);
671                cpal::default_host()
672            }
673        }
674    } else {
675        cpal::default_host()
676    };
677
678    let mut in_device = None;
679    if let Some(device_id) = &config.device_id {
680        if let Some(device) = host.device_by_id(device_id) {
681            if device.supports_input() {
682                in_device = Some(device);
683            }
684        }
685
686        if in_device.is_none() {
687            if config.fallback {
688                warn!("Could not find requested audio input device: {}. Falling back to default device...", &device_id);
689            } else {
690                warn!("Could not find requested audio input device: {}. No input stream will be started.", &device_id);
691                return Ok(StartInputStreamResult::NotStarted);
692            }
693        }
694    }
695
696    if in_device.is_none() {
697        if let Some(default_device) = host.default_input_device() {
698            in_device = Some(default_device);
699        } else if config.fail_on_no_input {
700            return Err(StreamStartError::DefaultInputDeviceNotFound);
701        } else {
702            warn!("No default audio input device found. Input stream will not be started.");
703            return Ok(StartInputStreamResult::NotStarted);
704        }
705    }
706    let in_device = in_device.unwrap();
707
708    let in_device_id = in_device.id().map(|id| id.to_string()).unwrap_or_else(|e| {
709        warn!("Failed to get ID of input audio device: {}", e);
710        String::from("unknown")
711    });
712
713    let default_config = in_device.default_input_config()?;
714
715    #[cfg(not(target_os = "ios"))]
716    let desired_block_frames =
717        if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
718            config.desired_block_frames.map(|f| f.clamp(min, max))
719        } else {
720            None
721        };
722
723    // For some reason fixed buffer sizes on iOS doesn't work in CPAL.
724    // I'm not sure if this is a problem on CPAL's end, but I have disabled
725    // it for the time being.
726    #[cfg(target_os = "ios")]
727    let desired_block_frames: Option<u32> = None;
728
729    let supported_configs = in_device.supported_input_configs()?;
730
731    let mut min_sample_rate = u32::MAX;
732    let mut max_sample_rate = 0;
733    for config in supported_configs.into_iter() {
734        min_sample_rate = min_sample_rate.min(config.min_sample_rate());
735        max_sample_rate = max_sample_rate.max(config.max_sample_rate());
736    }
737    let sample_rate = output_sample_rate.clamp(min_sample_rate, max_sample_rate);
738
739    #[cfg(not(feature = "resample_inputs"))]
740    if sample_rate != output_sample_rate {
741        if config.fail_on_no_input {
742            return Err(StreamStartError::CouldNotMatchSampleRate(
743                output_sample_rate,
744            ));
745        } else {
746            warn!("Could not use output sample rate {} for the input sample rate. Input stream will not be started", output_sample_rate);
747            return Ok(StartInputStreamResult::NotStarted);
748        }
749    }
750
751    let num_in_channels = default_config.channels() as usize;
752    assert_ne!(num_in_channels, 0);
753
754    let desired_buffer_size = if let Some(samples) = desired_block_frames {
755        cpal::BufferSize::Fixed(samples)
756    } else {
757        cpal::BufferSize::Default
758    };
759
760    let stream_config = cpal::StreamConfig {
761        channels: num_in_channels as u16,
762        sample_rate,
763        buffer_size: desired_buffer_size,
764    };
765
766    let (mut prod, cons) = fixed_resample::resampling_channel::<f32, MAX_INPUT_CHANNELS>(
767        NonZeroUsize::new(num_in_channels).unwrap(),
768        sample_rate,
769        output_sample_rate,
770        config.channel_config,
771    );
772
773    info!(
774        "Starting input audio stream with device \"{}\" with configuration {:?}",
775        &in_device_id, &stream_config
776    );
777
778    let stream_handle = match in_device.build_input_stream(
779        &stream_config,
780        move |input: &[f32], _info: &cpal::InputCallbackInfo| {
781            let _ = prod.push_interleaved(input);
782        },
783        move |err| {
784            let _ = err_to_cx_tx.send(err);
785        },
786        Some(BUILD_STREAM_TIMEOUT),
787    ) {
788        Ok(s) => s,
789        Err(e) => {
790            if config.fail_on_no_input {
791                return Err(StreamStartError::BuildStreamError(e));
792            } else {
793                error!(
794                    "Failed to build input audio stream, input stream will not be started. {}",
795                    e
796                );
797                return Ok(StartInputStreamResult::NotStarted);
798            }
799        }
800    };
801
802    if let Err(e) = stream_handle.play() {
803        if config.fail_on_no_input {
804            return Err(StreamStartError::PlayStreamError(e));
805        } else {
806            error!(
807                "Failed to start input audio stream, input stream will not be started. {}",
808                e
809            );
810            return Ok(StartInputStreamResult::NotStarted);
811        }
812    }
813
814    Ok(StartInputStreamResult::Started {
815        stream_handle,
816        cons,
817        num_stream_in_channels: num_in_channels as u32,
818        input_device_id: in_device_id,
819    })
820}
821
822enum StartInputStreamResult {
823    NotStarted,
824    Started {
825        stream_handle: cpal::Stream,
826        cons: fixed_resample::ResamplingCons<f32>,
827        num_stream_in_channels: u32,
828        input_device_id: String,
829    },
830}
831
832struct DataCallback {
833    num_out_channels: usize,
834    from_cx_rx: ringbuf::HeapCons<CtxToStreamMsg>,
835    processor: Option<FirewheelProcessor<CpalBackend>>,
836    sample_rate: u32,
837    sample_rate_recip: f64,
838    //_first_internal_clock_instant: Option<cpal::StreamInstant>,
839    //_prev_stream_instant: Option<cpal::StreamInstant>,
840    predicted_delta_time: Duration,
841    prev_instant: Option<Instant>,
842    stream_start_instant: Instant,
843    input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
844    input_buffer: Vec<f32>,
845}
846
847impl DataCallback {
848    fn new(
849        num_out_channels: usize,
850        from_cx_rx: ringbuf::HeapCons<CtxToStreamMsg>,
851        sample_rate: u32,
852        input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
853    ) -> Self {
854        let stream_start_instant = Instant::now();
855
856        let input_buffer = if let Some(cons) = &input_stream_cons {
857            let mut v = Vec::new();
858            v.reserve_exact(INPUT_ALLOC_BLOCK_FRAMES * cons.num_channels().get());
859            v.resize(INPUT_ALLOC_BLOCK_FRAMES * cons.num_channels().get(), 0.0);
860            v
861        } else {
862            Vec::new()
863        };
864
865        Self {
866            num_out_channels,
867            from_cx_rx,
868            processor: None,
869            sample_rate,
870            sample_rate_recip: f64::from(sample_rate).recip(),
871            //_first_internal_clock_instant: None,
872            //_prev_stream_instant: None,
873            predicted_delta_time: Duration::default(),
874            prev_instant: None,
875            stream_start_instant,
876            input_stream_cons,
877            input_buffer,
878        }
879    }
880
881    fn callback(&mut self, output: &mut [f32], _info: &cpal::OutputCallbackInfo) {
882        let process_timestamp = bevy_platform::time::Instant::now();
883
884        for msg in self.from_cx_rx.pop_iter() {
885            let CtxToStreamMsg::NewProcessor(p) = msg;
886            self.processor = Some(p);
887        }
888
889        let frames = output.len() / self.num_out_channels;
890
891        let (underflow, dropped_frames) = if let Some(prev_instant) = self.prev_instant {
892            let delta_time = process_timestamp - prev_instant;
893
894            let underflow = delta_time > self.predicted_delta_time;
895
896            let dropped_frames = if underflow {
897                (delta_time.as_secs_f64() * self.sample_rate as f64).round() as u32
898            } else {
899                0
900            };
901
902            (underflow, dropped_frames)
903        } else {
904            self.prev_instant = Some(process_timestamp);
905            (false, 0)
906        };
907
908        // Calculate the next predicted stream time to detect underflows.
909        //
910        // Add a little bit of wiggle room to account for tiny clock
911        // innacuracies and rounding errors.
912        self.predicted_delta_time =
913            Duration::from_secs_f64(frames as f64 * self.sample_rate_recip * 1.5);
914
915        let duration_since_stream_start =
916            process_timestamp.duration_since(self.stream_start_instant);
917
918        // TODO: PLEASE FIX ME:
919        //
920        // It appears that for some reason, both Windows and Linux will sometimes return a timestamp which
921        // has a value less than the previous timestamp. I am unsure if this is a bug with the APIs, a bug
922        // with CPAL, or I'm just misunderstaning how the timestamps are supposed to be used. Either way,
923        // it is disabled for now and `bevy_platform::time::Instance::now()` is being used as a workaround above.
924        //
925        // let (internal_clock_secs, underflow) = if let Some(instant) =
926        //     &self.first_internal_clock_instant
927        // {
928        //     if let Some(prev_stream_instant) = &self.prev_stream_instant {
929        //         if info
930        //             .timestamp()
931        //             .playback
932        //             .duration_since(prev_stream_instant)
933        //             .is_none()
934        //         {
935        //             // If this occurs in other APIs as well, then either CPAL is doing
936        //             // something wrong, or I'm doing something wrong.
937        //             error!("CPAL and/or the system audio API returned invalid timestamp. Please notify the Firewheel developers of this bug.");
938        //         }
939        //     }
940        //
941        //     let internal_clock_secs = info
942        //         .timestamp()
943        //         .playback
944        //         .duration_since(instant)
945        //         .map(|s| s.as_secs_f64())
946        //         .unwrap_or_else(|| self.predicted_stream_secs.unwrap_or(0.0));
947        //
948        //     let underflow = if let Some(predicted_stream_secs) = self.predicted_stream_secs {
949        //         // If the stream time is significantly greater than the predicted stream
950        //         // time, it means an output underflow has occurred.
951        //         internal_clock_secs > predicted_stream_secs
952        //     } else {
953        //         false
954        //     };
955        //
956        //     // Calculate the next predicted stream time to detect underflows.
957        //     //
958        //     // Add a little bit of wiggle room to account for tiny clock
959        //     // innacuracies and rounding errors.
960        //     self.predicted_stream_secs =
961        //         Some(internal_clock_secs + (frames as f64 * self.sample_rate_recip * 1.2));
962        //
963        //     self.prev_stream_instant = Some(info.timestamp().playback);
964        //
965        //     (ClockSeconds(internal_clock_secs), underflow)
966        // } else {
967        //     self.first_internal_clock_instant = Some(info.timestamp().playback);
968        //     (ClockSeconds(0.0), false)
969        // };
970
971        let (num_in_channels, input_stream_status) = if let Some(cons) = &mut self.input_stream_cons
972        {
973            let num_in_channels = cons.num_channels().get();
974
975            let num_input_samples = frames * num_in_channels;
976            // Some platforms like wasapi might occasionally send a really large number of frames
977            // to process. Since CPAL doesn't tell us the actual maximum block size of the stream,
978            // there is not much we can do about it except to allocate when that happens.
979            if num_input_samples > self.input_buffer.len() {
980                self.input_buffer.resize(num_input_samples, 0.0);
981            }
982
983            let status = cons.read_interleaved(&mut self.input_buffer[..num_input_samples]);
984
985            let status = match status {
986                ReadStatus::UnderflowOccurred { num_frames_read: _ } => {
987                    StreamStatus::OUTPUT_UNDERFLOW
988                }
989                ReadStatus::OverflowCorrected {
990                    num_frames_discarded: _,
991                } => StreamStatus::INPUT_OVERFLOW,
992                _ => StreamStatus::empty(),
993            };
994
995            (num_in_channels, status)
996        } else {
997            (0, StreamStatus::empty())
998        };
999
1000        if let Some(processor) = &mut self.processor {
1001            let mut output_stream_status = StreamStatus::empty();
1002
1003            if underflow {
1004                output_stream_status.insert(StreamStatus::OUTPUT_UNDERFLOW);
1005            }
1006
1007            processor.process_interleaved(
1008                &self.input_buffer[..frames * num_in_channels],
1009                output,
1010                BackendProcessInfo {
1011                    num_in_channels,
1012                    num_out_channels: self.num_out_channels,
1013                    frames,
1014                    process_timestamp,
1015                    duration_since_stream_start,
1016                    input_stream_status,
1017                    output_stream_status,
1018                    dropped_frames,
1019                },
1020            );
1021        } else {
1022            output.fill(0.0);
1023            return;
1024        }
1025    }
1026}
1027
1028enum CtxToStreamMsg {
1029    NewProcessor(FirewheelProcessor<CpalBackend>),
1030}
1031
1032/// An error occured while trying to start a CPAL audio stream.
1033#[derive(Debug, thiserror::Error)]
1034pub enum StreamStartError {
1035    #[error("The requested audio input device was not found: {0}")]
1036    InputDeviceNotFound(String),
1037    #[error("The requested audio output device was not found: {0}")]
1038    OutputDeviceNotFound(String),
1039    #[error("Could not get audio devices: {0}")]
1040    FailedToGetDevices(#[from] cpal::DevicesError),
1041    #[error("Failed to get default input output device")]
1042    DefaultInputDeviceNotFound,
1043    #[error("Failed to get default audio output device")]
1044    DefaultOutputDeviceNotFound,
1045    #[error("Failed to get audio device configs: {0}")]
1046    FailedToGetConfigs(#[from] cpal::SupportedStreamConfigsError),
1047    #[error("Failed to get audio device config: {0}")]
1048    FailedToGetConfig(#[from] cpal::DefaultStreamConfigError),
1049    #[error("Failed to build audio stream: {0}")]
1050    BuildStreamError(#[from] cpal::BuildStreamError),
1051    #[error("Failed to play audio stream: {0}")]
1052    PlayStreamError(#[from] cpal::PlayStreamError),
1053
1054    #[cfg(not(feature = "resample_inputs"))]
1055    #[error("Not able to use a samplerate of {0} for the input audio device")]
1056    CouldNotMatchSampleRate(u32),
1057}