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