firewheel_cpal/
lib.rs

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