1use core::{fmt::Debug, num::NonZeroU32, time::Duration};
2use std::sync::{
3 Arc,
4 atomic::{AtomicBool, Ordering},
5 mpsc,
6};
7
8use audioadapter_buffers::direct::InterleavedSlice;
9pub use cpal;
10
11use bevy_platform::time::Instant;
12pub use cpal::{DeviceId, HostId};
13use cpal::{
14 SampleFormat,
15 traits::{DeviceTrait, HostTrait, StreamTrait},
16};
17use firewheel_core::node::StreamStatus;
18use firewheel_graph::{
19 ActivateInfo, FirewheelContext,
20 backend::BackendProcessInfo,
21 error::{ActivateError, CompileGraphError},
22 processor::FirewheelProcessor,
23};
24use fixed_resample::{ReadStatus, ResamplingChannelConfig, ResamplingProd};
25
26#[cfg(all(feature = "log", not(feature = "tracing")))]
27use log::{error, info, warn};
28#[cfg(feature = "tracing")]
29use tracing::{error, info, warn};
30
31const DEFAULT_MAX_BLOCK_FRAMES: u32 = 1024;
34const BUILD_STREAM_TIMEOUT: Duration = Duration::from_secs(5);
35const UNDERRUN_LOG_COOLDOWN: Duration = Duration::from_secs(3);
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct CpalOutputConfig {
40 pub host: Option<cpal::HostId>,
43
44 pub device_id: Option<DeviceId>,
49
50 pub desired_sample_rate: Option<u32>,
55
56 pub desired_block_frames: Option<u32>,
66
67 pub fallback: bool,
72}
73
74impl Default for CpalOutputConfig {
75 fn default() -> Self {
76 Self {
77 host: None,
78 device_id: None,
79 desired_sample_rate: None,
80 desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
81 fallback: true,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct CpalInputConfig {
89 pub host: Option<cpal::HostId>,
92
93 pub device_id: Option<DeviceId>,
98
99 pub desired_block_frames: Option<u32>,
109
110 pub channel_config: ResamplingChannelConfig,
112
113 pub fallback: bool,
118
119 pub fail_on_no_input: bool,
125}
126
127impl Default for CpalInputConfig {
128 fn default() -> Self {
129 Self {
130 host: None,
131 device_id: None,
132 desired_block_frames: Some(DEFAULT_MAX_BLOCK_FRAMES),
133 channel_config: ResamplingChannelConfig::default(),
134 fallback: true,
135 fail_on_no_input: false,
136 }
137 }
138}
139
140#[derive(Debug, Default, Clone, PartialEq)]
142pub struct CpalConfig {
143 pub output: CpalOutputConfig,
145
146 pub input: Option<CpalInputConfig>,
152}
153
154pub struct HostEnumerator {
157 pub host: cpal::Host,
158}
159
160impl HostEnumerator {
161 pub fn host_id(&self) -> cpal::HostId {
163 self.host.id()
164 }
165
166 pub fn input_devices(&self) -> Vec<DeviceInfo> {
168 let mut devices = Vec::with_capacity(8);
169
170 let default_device = self.host.default_input_device();
171 let default_device_id = default_device.and_then(|d| match d.id() {
172 Ok(id) => Some(id),
173 Err(e) => {
174 #[cfg(any(feature = "log", feature = "tracing"))]
175 warn!("Failed to get ID of default audio input device: {}", e);
176 #[cfg(not(any(feature = "log", feature = "tracing")))]
177 let _ = e;
178 None
179 }
180 });
181
182 match self.host.input_devices() {
183 Ok(input_devices) => {
184 for device in input_devices {
185 let Ok(id) = device.id() else {
186 continue;
187 };
188
189 let is_default = if let Some(default_device_id) = &default_device_id {
190 &id == default_device_id
191 } else {
192 false
193 };
194
195 let name = device.description().map(|d| d.name().to_string()).ok();
196
197 devices.push(DeviceInfo {
198 id,
199 name,
200 is_default,
201 })
202 }
203 }
204 Err(e) => {
205 #[cfg(any(feature = "log", feature = "tracing"))]
206 error!("Failed to get input audio devices: {}", e);
207 #[cfg(not(any(feature = "log", feature = "tracing")))]
208 let _ = e;
209 }
210 }
211
212 devices
213 }
214
215 pub fn output_devices(&self) -> Vec<DeviceInfo> {
217 let mut devices = Vec::with_capacity(8);
218
219 let default_device = self.host.default_output_device();
220 let default_device_id = default_device.and_then(|d| match d.id() {
221 Ok(id) => Some(id),
222 Err(e) => {
223 #[cfg(any(feature = "log", feature = "tracing"))]
224 warn!("Failed to get ID of default audio output device: {}", e);
225 #[cfg(not(any(feature = "log", feature = "tracing")))]
226 let _ = e;
227 None
228 }
229 });
230
231 match self.host.output_devices() {
232 Ok(output_devices) => {
233 for device in output_devices {
234 let Ok(id) = device.id() else {
235 continue;
236 };
237
238 let is_default = if let Some(default_device_id) = &default_device_id {
239 &id == default_device_id
240 } else {
241 false
242 };
243
244 let name = device.description().map(|d| d.name().to_string()).ok();
245
246 devices.push(DeviceInfo {
247 id,
248 name,
249 is_default,
250 })
251 }
252 }
253 Err(e) => {
254 #[cfg(any(feature = "log", feature = "tracing"))]
255 error!("Failed to get output audio devices: {}", e);
256 #[cfg(not(any(feature = "log", feature = "tracing")))]
257 let _ = e;
258 }
259 }
260
261 devices
262 }
263
264 pub fn get_device(&self, device_id: &cpal::DeviceId) -> Option<cpal::Device> {
269 self.host.device_by_id(device_id)
270 }
271}
272
273#[derive(Debug, Clone, PartialEq)]
275pub struct DeviceInfo {
276 pub id: cpal::DeviceId,
284 pub name: Option<String>,
286 pub is_default: bool,
288}
289
290#[derive(Debug, Clone, PartialEq)]
292pub struct CpalStreamInfo {
293 pub sample_rate: NonZeroU32,
295 pub max_block_frames: NonZeroU32,
297 pub num_stream_in_channels: u32,
299 pub num_stream_out_channels: u32,
301 pub input_to_output_latency_seconds: f64,
303 pub out_device_id: Option<DeviceId>,
305 pub in_device_id: Option<DeviceId>,
307}
308
309pub fn available_hosts() -> Vec<cpal::HostId> {
313 cpal::available_hosts()
314}
315
316pub fn default_host_enumerator() -> HostEnumerator {
319 HostEnumerator {
320 host: cpal::default_host(),
321 }
322}
323
324pub fn host_enumerator(api: HostId) -> Result<HostEnumerator, cpal::Error> {
327 cpal::host_from_id(api).map(|host| HostEnumerator { host })
328}
329
330pub struct CpalStream {
334 _out_stream_handle: cpal::Stream,
335 _in_stream_handle: Option<cpal::Stream>,
336 from_err_rx: mpsc::Receiver<IoStreamError>,
337 stream_info: CpalStreamInfo,
338 input_stream_running: Option<Arc<AtomicBool>>,
339 output_stream_running: Arc<AtomicBool>,
340}
341
342impl CpalStream {
343 pub fn new(cx: &mut FirewheelContext, config: CpalConfig) -> Result<Self, StartStreamError> {
345 #[cfg(any(feature = "log", feature = "tracing"))]
346 info!("Attempting to start CPAL audio stream...");
347
348 if cx.is_active() {
349 return Err(StartStreamError::AlreadyActive);
350 }
351
352 let host = if let Some(host_id) = config.output.host {
353 match cpal::host_from_id(host_id) {
354 Ok(host) => host,
355 Err(e) => {
356 #[cfg(any(feature = "log", feature = "tracing"))]
357 warn!(
358 "Requested audio host {:?} is not available: {}. Falling back to default host...",
359 &host_id, e
360 );
361 #[cfg(not(any(feature = "log", feature = "tracing")))]
362 let _ = e;
363 cpal::default_host()
364 }
365 }
366 } else {
367 cpal::default_host()
368 };
369
370 let mut out_device = None;
371 if let Some(device_id) = &config.output.device_id {
372 if let Some(device) = host.device_by_id(device_id)
373 && device.supports_output()
374 {
375 out_device = Some(device);
376 }
377
378 if out_device.is_none() {
379 #[cfg(any(feature = "log", feature = "tracing"))]
380 warn!(
381 "Could not find requested audio output device: {}. Falling back to default device...",
382 &device_id
383 );
384 }
385 }
386
387 if out_device.is_none() {
388 let Some(default_device) = host.default_output_device() else {
389 return Err(StartStreamError::DefaultOutputDeviceNotFound);
390 };
391 out_device = Some(default_device);
392 }
393 let out_device = out_device.unwrap();
394
395 let out_device_id = match out_device.id() {
396 Ok(id) => Some(id),
397 Err(e) => {
398 #[cfg(any(feature = "log", feature = "tracing"))]
399 warn!("Failed to get id of output audio device: {}", e);
400 #[cfg(not(any(feature = "log", feature = "tracing")))]
401 let _ = e;
402 None
403 }
404 };
405
406 let default_config = out_device
407 .default_output_config()
408 .map_err(StartStreamError::FailedToGetConfig)?;
409
410 let default_sample_rate = default_config.sample_rate();
411 let try_common_sample_rates = default_sample_rate != 44100 && default_sample_rate != 48000;
413
414 #[cfg(not(target_os = "ios"))]
415 let desired_block_frames =
416 if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
417 config
418 .output
419 .desired_block_frames
420 .map(|f| f.clamp(min, max))
421 } else {
422 None
423 };
424
425 #[cfg(target_os = "ios")]
429 let desired_block_frames: Option<u32> = None;
430
431 let mut supports_desired_sample_rate = false;
432 let mut supports_44100 = false;
433 let mut supports_48000 = false;
434
435 if config.output.desired_sample_rate.is_some() || try_common_sample_rates {
436 for cpal_config in out_device
437 .supported_output_configs()
438 .map_err(StartStreamError::FailedToGetConfig)?
439 {
440 if let Some(sr) = config.output.desired_sample_rate
441 && !supports_desired_sample_rate
442 && cpal_config.try_with_sample_rate(sr).is_some()
443 {
444 supports_desired_sample_rate = true;
445 break;
446 }
447
448 if try_common_sample_rates {
449 if !supports_44100 {
450 supports_44100 = cpal_config.try_with_sample_rate(44100).is_some();
451 }
452 if !supports_48000 {
453 supports_48000 = cpal_config.try_with_sample_rate(48000).is_some();
454 }
455 }
456 }
457 }
458
459 let sample_rate = if supports_desired_sample_rate {
460 config.output.desired_sample_rate.unwrap()
461 } else if try_common_sample_rates {
462 if supports_44100 {
463 44100
464 } else if supports_48000 {
465 48000
466 } else {
467 default_sample_rate
468 }
469 } else {
470 default_sample_rate
471 };
472
473 let num_out_channels = default_config.channels() as usize;
474 assert_ne!(num_out_channels, 0);
475
476 let desired_buffer_size = if let Some(samples) = desired_block_frames {
477 cpal::BufferSize::Fixed(samples)
478 } else {
479 cpal::BufferSize::Default
480 };
481
482 let out_stream_config = cpal::StreamConfig {
483 channels: num_out_channels as u16,
484 sample_rate,
485 buffer_size: desired_buffer_size,
486 };
487
488 let max_block_frames = match out_stream_config.buffer_size {
489 cpal::BufferSize::Default => DEFAULT_MAX_BLOCK_FRAMES as usize,
490 cpal::BufferSize::Fixed(f) => f as usize,
491 };
492
493 let (err_to_cx_tx, from_err_rx) = mpsc::channel();
494
495 let mut input_stream = StartInputStreamResult::NotStarted;
496 if let Some(input_config) = &config.input {
497 input_stream = start_input_stream(
498 input_config,
499 out_stream_config.sample_rate,
500 err_to_cx_tx.clone(),
501 )?;
502 }
503
504 let (
505 in_stream_handle,
506 input_stream_cons,
507 num_stream_in_channels,
508 in_device_id,
509 input_to_output_latency_seconds,
510 input_stream_running,
511 ) = if let StartInputStreamResult::Started {
512 stream_handle,
513 cons,
514 num_stream_in_channels,
515 in_device_id,
516 input_stream_running,
517 } = input_stream
518 {
519 let input_to_output_latency_seconds = cons.latency_seconds();
520
521 (
522 Some(stream_handle),
523 Some(cons),
524 num_stream_in_channels,
525 in_device_id,
526 input_to_output_latency_seconds,
527 Some(input_stream_running),
528 )
529 } else {
530 (None, None, 0, None, 0.0, None)
531 };
532
533 let activate_info = ActivateInfo {
534 sample_rate: NonZeroU32::new(out_stream_config.sample_rate).unwrap(),
535 max_block_frames: NonZeroU32::new(max_block_frames as u32).unwrap(),
536 num_stream_in_channels,
537 num_stream_out_channels: num_out_channels as u32,
538 input_to_output_latency_seconds,
539 };
540
541 let processor = cx.activate(activate_info)?;
542
543 let output_stream_running = Arc::new(AtomicBool::new(true));
544
545 let mut callback = OutputCallback::new(
546 num_out_channels,
547 max_block_frames,
548 out_stream_config.sample_rate,
549 processor,
550 input_stream_cons,
551 err_to_cx_tx.clone(),
552 input_stream_running.as_ref().map(Arc::clone),
553 Arc::clone(&output_stream_running),
554 );
555
556 let out_sample_format = default_config.sample_format();
557
558 #[cfg(any(feature = "log", feature = "tracing"))]
559 info!(
560 "Starting output audio stream with device \"{:?}\" with configuration {:?} (native sample format: {:?})",
561 &out_device_id, &out_stream_config, out_sample_format,
562 );
563
564 let scratch_capacity = max_block_frames * num_out_channels;
565
566 #[cfg(any(
567 target_os = "linux",
568 target_os = "freebsd",
569 target_os = "dragonfly",
570 target_os = "netbsd"
571 ))]
572 let is_alsa = host.id() == HostId::Alsa;
573
574 macro_rules! build_output_streams {
575 ($sample_format:expr, $(($format:path, $primitive_type:ty)),*) => {
576 match $sample_format {
577 $($format => {
578 let mut scratch = scratch_vec(scratch_capacity);
579
580 out_device.build_output_stream(
581 out_stream_config,
582 move |output: &mut [$primitive_type], info: &cpal::OutputCallbackInfo| {
583 for out_chunk in output.chunks_mut(scratch_capacity) {
584 let buf = &mut scratch[..out_chunk.len()];
585
586 callback.callback(buf, info);
587
588 for (o, &f) in out_chunk.iter_mut().zip(buf.iter()) {
589 *o = <$primitive_type as cpal::FromSample<f32>>::from_sample_(f);
591 }
592 }
593 },
594 err_callback(
595 false,
596 output_stream_running.clone(),
597 err_to_cx_tx.clone(),
598 #[cfg(any(
599 target_os = "linux",
600 target_os = "freebsd",
601 target_os = "dragonfly",
602 target_os = "netbsd"
603 ))]
604 is_alsa
605 ),
606 Some(BUILD_STREAM_TIMEOUT),
607 )
608 },)*
609 _ => unreachable!(),
610 }
611 }
612 }
613
614 let out_stream_handle = if let SampleFormat::F32 = out_sample_format {
619 out_device.build_output_stream(
620 out_stream_config,
621 move |output: &mut [f32], info: &cpal::OutputCallbackInfo| {
622 callback.callback(output, info);
623 },
624 err_callback(
625 false,
626 output_stream_running.clone(),
627 err_to_cx_tx.clone(),
628 #[cfg(any(
629 target_os = "linux",
630 target_os = "freebsd",
631 target_os = "dragonfly",
632 target_os = "netbsd"
633 ))]
634 is_alsa,
635 ),
636 Some(BUILD_STREAM_TIMEOUT),
637 )
638 } else {
639 build_output_streams!(
640 out_sample_format,
641 (SampleFormat::I8, i8),
642 (SampleFormat::I16, i16),
643 (SampleFormat::I32, i32),
644 (SampleFormat::I64, i64),
645 (SampleFormat::U8, u8),
646 (SampleFormat::U16, u16),
647 (SampleFormat::U32, u32),
648 (SampleFormat::U64, u64),
649 (SampleFormat::F64, f64)
650 )
651 }
652 .map_err(StartStreamError::BuildStreamError)?;
653
654 if let Err(e) = out_stream_handle.play() {
655 if let cpal::ErrorKind::RealtimeDenied = e.kind() {
656 #[cfg(any(feature = "log", feature = "tracing"))]
657 warn!(
658 "Failed to get realtime priority for output audio thread. This may increase latency and/or audio glitches"
659 );
660 } else {
661 return Err(StartStreamError::PlayStreamError(e));
662 }
663 }
664
665 let stream_info = CpalStreamInfo {
666 sample_rate: activate_info.sample_rate,
667 max_block_frames: activate_info.max_block_frames,
668 num_stream_in_channels: activate_info.num_stream_in_channels,
669 num_stream_out_channels: activate_info.num_stream_out_channels,
670 input_to_output_latency_seconds: activate_info.input_to_output_latency_seconds,
671 out_device_id,
672 in_device_id,
673 };
674
675 Ok(Self {
676 _out_stream_handle: out_stream_handle,
677 _in_stream_handle: in_stream_handle,
678 from_err_rx,
679 stream_info,
680 input_stream_running,
681 output_stream_running,
682 })
683 }
684
685 pub fn info(&self) -> &CpalStreamInfo {
687 &self.stream_info
688 }
689
690 pub fn poll_status(&mut self) -> mpsc::TryIter<'_, IoStreamError> {
696 if self._in_stream_handle.is_some() && !self.input_stream_ok() {
697 self._in_stream_handle = None;
698 }
699
700 self.from_err_rx.try_iter()
701 }
702
703 #[cfg(any(feature = "log", feature = "tracing"))]
708 pub fn log_status(&mut self) {
709 for e in self.from_err_rx.try_iter() {
710 error!("Audio stream error occurred: {}", e);
711 }
712 }
713
714 pub fn output_stream_ok(&self) -> bool {
720 self.output_stream_running.load(Ordering::Relaxed)
721 }
722
723 pub fn input_stream_ok(&self) -> bool {
730 self.input_stream_running
731 .as_ref()
732 .map(|r| r.load(Ordering::Relaxed))
733 .unwrap_or(true)
734 }
735
736 pub fn all_streams_ok(&self) -> bool {
742 self.output_stream_ok() && self.input_stream_ok()
743 }
744}
745
746impl Drop for CpalStream {
747 fn drop(&mut self) {
748 #[cfg(any(feature = "log", feature = "tracing"))]
750 self.log_status();
751 }
752}
753
754fn start_input_stream(
755 config: &CpalInputConfig,
756 output_sample_rate: cpal::SampleRate,
757 err_to_cx_tx: mpsc::Sender<IoStreamError>,
758) -> Result<StartInputStreamResult, StartStreamError> {
759 let host = if let Some(host_id) = config.host {
760 match cpal::host_from_id(host_id) {
761 Ok(host) => host,
762 Err(e) => {
763 #[cfg(any(feature = "log", feature = "tracing"))]
764 warn!(
765 "Requested audio host {:?} is not available: {}. Falling back to default host...",
766 &host_id, e
767 );
768 #[cfg(not(any(feature = "log", feature = "tracing")))]
769 let _ = e;
770 cpal::default_host()
771 }
772 }
773 } else {
774 cpal::default_host()
775 };
776
777 let mut in_device = None;
778 if let Some(device_id) = &config.device_id {
779 if let Some(device) = host.device_by_id(device_id)
780 && device.supports_input()
781 {
782 in_device = Some(device);
783 }
784
785 if in_device.is_none() {
786 if config.fallback {
787 #[cfg(any(feature = "log", feature = "tracing"))]
788 warn!(
789 "Could not find requested audio input device: {}. Falling back to default device...",
790 &device_id
791 );
792 } else {
793 #[cfg(any(feature = "log", feature = "tracing"))]
794 warn!(
795 "Could not find requested audio input device: {}. No input stream will be started.",
796 &device_id
797 );
798 return Ok(StartInputStreamResult::NotStarted);
799 }
800 }
801 }
802
803 if in_device.is_none() {
804 if let Some(default_device) = host.default_input_device() {
805 in_device = Some(default_device);
806 } else if config.fail_on_no_input {
807 return Err(StartStreamError::DefaultInputDeviceNotFound);
808 } else {
809 #[cfg(any(feature = "log", feature = "tracing"))]
810 warn!("No default audio input device found. Input stream will not be started.");
811 return Ok(StartInputStreamResult::NotStarted);
812 }
813 }
814 let in_device = in_device.unwrap();
815
816 let in_device_id = match in_device.id() {
817 Ok(id) => Some(id),
818 Err(e) => {
819 #[cfg(any(feature = "log", feature = "tracing"))]
820 warn!("Failed to get id of input audio device: {}", e);
821 #[cfg(not(any(feature = "log", feature = "tracing")))]
822 let _ = e;
823 None
824 }
825 };
826
827 let default_config = in_device
828 .default_input_config()
829 .map_err(StartStreamError::FailedToGetConfig)?;
830
831 #[cfg(not(target_os = "ios"))]
832 let desired_block_frames =
833 if let &cpal::SupportedBufferSize::Range { min, max } = default_config.buffer_size() {
834 config.desired_block_frames.map(|f| f.clamp(min, max))
835 } else {
836 None
837 };
838
839 #[cfg(target_os = "ios")]
843 let desired_block_frames: Option<u32> = None;
844
845 let supported_configs = in_device
846 .supported_input_configs()
847 .map_err(StartStreamError::FailedToGetConfig)?;
848
849 let mut min_sample_rate = u32::MAX;
850 let mut max_sample_rate = 0;
851 for config in supported_configs.into_iter() {
852 min_sample_rate = min_sample_rate.min(config.min_sample_rate());
853 max_sample_rate = max_sample_rate.max(config.max_sample_rate());
854 }
855 let sample_rate = output_sample_rate.clamp(min_sample_rate, max_sample_rate);
856
857 #[cfg(not(feature = "resample_inputs"))]
858 if sample_rate != output_sample_rate {
859 if config.fail_on_no_input {
860 return Err(StartStreamError::CouldNotMatchSampleRate(
861 output_sample_rate,
862 ));
863 } else {
864 #[cfg(any(feature = "log", feature = "tracing"))]
865 warn!(
866 "Could not use output sample rate {} for the input sample rate. Input stream will not be started",
867 output_sample_rate
868 );
869 return Ok(StartInputStreamResult::NotStarted);
870 }
871 }
872
873 let num_in_channels = default_config.channels() as usize;
874 assert_ne!(num_in_channels, 0);
875
876 let desired_buffer_size = if let Some(samples) = desired_block_frames {
877 cpal::BufferSize::Fixed(samples)
878 } else {
879 cpal::BufferSize::Default
880 };
881
882 let max_block_frames = match desired_buffer_size {
883 cpal::BufferSize::Default => DEFAULT_MAX_BLOCK_FRAMES as usize,
884 cpal::BufferSize::Fixed(f) => f as usize,
885 };
886
887 let stream_config = cpal::StreamConfig {
888 channels: num_in_channels as u16,
889 sample_rate,
890 buffer_size: desired_buffer_size,
891 };
892
893 let (prod, cons) = fixed_resample::resampling_channel::<f32>(
894 num_in_channels,
895 sample_rate,
896 output_sample_rate,
897 true,
898 config.channel_config,
899 );
900
901 let input_stream_running = Arc::new(AtomicBool::new(true));
902
903 #[cfg(any(feature = "log", feature = "tracing"))]
904 info!(
905 "Starting input audio stream with device \"{:?}\" with configuration {:?}",
906 &in_device_id, &stream_config
907 );
908
909 let mut callback = InputCallback {
910 prod,
911 err_to_cx_tx: err_to_cx_tx.clone(),
912 input_stream_running: Arc::clone(&input_stream_running),
913 };
914
915 let in_sample_format = default_config.sample_format();
916
917 let scratch_capacity = max_block_frames * num_in_channels;
918
919 #[cfg(any(
920 target_os = "linux",
921 target_os = "freebsd",
922 target_os = "dragonfly",
923 target_os = "netbsd"
924 ))]
925 let is_alsa = host.id() == HostId::Alsa;
926
927 macro_rules! build_input_stream {
928 ($sample_format:expr, $(($format:path, $primitive_type:ty)),*) => {
929 match $sample_format {
930 $($format => {
931 let mut scratch = scratch_vec(scratch_capacity);
932
933 in_device.build_input_stream(
934 stream_config,
935 move |input: &[$primitive_type], _info: &cpal::InputCallbackInfo| {
936 for in_chunk in input.chunks(scratch_capacity) {
937 for (o, &i) in scratch.iter_mut().zip(in_chunk.iter()) {
938 *o = <f32 as cpal::FromSample<$primitive_type>>::from_sample_(i);
939 }
940
941 callback.callback(&scratch[..in_chunk.len()]);
942 }
943 },
944 err_callback(
945 true,
946 input_stream_running.clone(),
947 err_to_cx_tx.clone(),
948 #[cfg(any(
949 target_os = "linux",
950 target_os = "freebsd",
951 target_os = "dragonfly",
952 target_os = "netbsd"
953 ))]
954 is_alsa
955 ),
956 Some(BUILD_STREAM_TIMEOUT),
957 )
958 },)*
959 _ => unreachable!(),
960 }
961 }
962 }
963
964 let stream_handle = if let SampleFormat::F32 = in_sample_format {
969 in_device.build_input_stream(
970 stream_config,
971 move |input: &[f32], _info: &cpal::InputCallbackInfo| {
972 callback.callback(input);
973 },
974 err_callback(
975 true,
976 input_stream_running.clone(),
977 err_to_cx_tx.clone(),
978 #[cfg(any(
979 target_os = "linux",
980 target_os = "freebsd",
981 target_os = "dragonfly",
982 target_os = "netbsd"
983 ))]
984 is_alsa,
985 ),
986 Some(BUILD_STREAM_TIMEOUT),
987 )
988 } else {
989 build_input_stream!(
990 in_sample_format,
991 (SampleFormat::I8, i8),
992 (SampleFormat::I16, i16),
993 (SampleFormat::I32, i32),
994 (SampleFormat::I64, i64),
995 (SampleFormat::U8, u8),
996 (SampleFormat::U16, u16),
997 (SampleFormat::U32, u32),
998 (SampleFormat::U64, u64),
999 (SampleFormat::F64, f64)
1000 )
1001 };
1002
1003 let stream_handle = match stream_handle {
1004 Ok(s) => s,
1005 Err(e) => {
1006 if config.fail_on_no_input {
1007 return Err(StartStreamError::BuildStreamError(e));
1008 } else {
1009 #[cfg(any(feature = "log", feature = "tracing"))]
1010 error!(
1011 "Failed to build input audio stream, input stream will not be started. {}",
1012 e
1013 );
1014 return Ok(StartInputStreamResult::NotStarted);
1015 }
1016 }
1017 };
1018
1019 if let Err(e) = stream_handle.play() {
1020 if let cpal::ErrorKind::RealtimeDenied = e.kind() {
1021 #[cfg(any(feature = "log", feature = "tracing"))]
1022 warn!(
1023 "Failed to get realtime priority for input audio thread. This may increase latency and/or audio glitches"
1024 );
1025 } else if config.fail_on_no_input {
1026 return Err(StartStreamError::PlayStreamError(e));
1027 } else {
1028 #[cfg(any(feature = "log", feature = "tracing"))]
1029 error!(
1030 "Failed to start input audio stream, input stream will not be started. {}",
1031 e
1032 );
1033 return Ok(StartInputStreamResult::NotStarted);
1034 }
1035 }
1036
1037 Ok(StartInputStreamResult::Started {
1038 stream_handle,
1039 cons,
1040 num_stream_in_channels: num_in_channels as u32,
1041 in_device_id,
1042 input_stream_running,
1043 })
1044}
1045
1046#[allow(clippy::large_enum_variant)]
1047enum StartInputStreamResult {
1048 NotStarted,
1049 Started {
1050 stream_handle: cpal::Stream,
1051 cons: fixed_resample::ResamplingCons<f32>,
1052 num_stream_in_channels: u32,
1053 in_device_id: Option<DeviceId>,
1054 input_stream_running: Arc<AtomicBool>,
1055 },
1056}
1057
1058struct InputCallback {
1059 prod: ResamplingProd<f32>,
1060 err_to_cx_tx: mpsc::Sender<IoStreamError>,
1061 input_stream_running: Arc<AtomicBool>,
1062}
1063
1064impl InputCallback {
1065 fn callback(&mut self, input: &[f32]) {
1066 let _ = self.prod.push_interleaved(input);
1067 }
1068}
1069
1070impl Drop for InputCallback {
1071 fn drop(&mut self) {
1072 self.input_stream_running.store(false, Ordering::Relaxed);
1073 let _ = self
1074 .err_to_cx_tx
1075 .send(IoStreamError::Input(cpal::Error::new(
1076 cpal::ErrorKind::StreamInvalidated,
1077 )));
1078 }
1079}
1080
1081struct OutputCallback {
1082 num_out_channels: usize,
1083 processor: FirewheelProcessor,
1084 sample_rate: u32,
1085 sample_rate_recip: f64,
1086 predicted_delta_time: Duration,
1087 prev_instant: Option<Instant>,
1088 stream_start_instant: Instant,
1089 input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
1090 input_buffer: Vec<f32>,
1091 err_to_cx_tx: mpsc::Sender<IoStreamError>,
1092 input_stream_running: Option<Arc<AtomicBool>>,
1093 output_stream_running: Arc<AtomicBool>,
1094}
1095
1096impl OutputCallback {
1097 #[allow(clippy::too_many_arguments)]
1098 fn new(
1099 num_out_channels: usize,
1100 max_block_frames: usize,
1101 sample_rate: u32,
1102 processor: FirewheelProcessor,
1103 input_stream_cons: Option<fixed_resample::ResamplingCons<f32>>,
1104 err_to_cx_tx: mpsc::Sender<IoStreamError>,
1105 input_stream_running: Option<Arc<AtomicBool>>,
1106 output_stream_running: Arc<AtomicBool>,
1107 ) -> Self {
1108 let stream_start_instant = Instant::now();
1109
1110 let input_buffer = if let Some(cons) = &input_stream_cons {
1111 scratch_vec(max_block_frames * cons.num_channels())
1112 } else {
1113 Vec::new()
1114 };
1115
1116 Self {
1117 num_out_channels,
1118 processor,
1119 sample_rate,
1120 sample_rate_recip: f64::from(sample_rate).recip(),
1121 predicted_delta_time: Duration::default(),
1122 prev_instant: None,
1123 stream_start_instant,
1124 input_stream_cons,
1125 input_buffer,
1126 err_to_cx_tx,
1127 input_stream_running,
1128 output_stream_running,
1129 }
1130 }
1131
1132 fn callback(&mut self, output: &mut [f32], info: &cpal::OutputCallbackInfo) {
1133 let process_timestamp = bevy_platform::time::Instant::now();
1134
1135 let frames = output.len() / self.num_out_channels;
1136
1137 let (underflow, dropped_frames) = if let Some(prev_instant) = self.prev_instant {
1138 let delta_time = process_timestamp - prev_instant;
1139
1140 let underflow = delta_time > self.predicted_delta_time;
1141
1142 let dropped_frames = if underflow {
1143 (delta_time.as_secs_f64() * self.sample_rate as f64).round() as u32
1144 } else {
1145 0
1146 };
1147
1148 (underflow, dropped_frames)
1149 } else {
1150 self.prev_instant = Some(process_timestamp);
1151 (false, 0)
1152 };
1153
1154 self.predicted_delta_time =
1159 Duration::from_secs_f64(frames as f64 * self.sample_rate_recip * 1.5);
1160
1161 let duration_since_stream_start =
1162 process_timestamp.duration_since(self.stream_start_instant);
1163
1164 let (num_in_channels, input_stream_status) = if let Some(cons) = &mut self.input_stream_cons
1218 {
1219 let num_in_channels = cons.num_channels();
1220 let num_input_samples = frames * num_in_channels;
1221
1222 if self
1223 .input_stream_running
1224 .as_ref()
1225 .unwrap()
1226 .load(Ordering::Relaxed)
1227 {
1228 let status =
1229 cons.read_interleaved(&mut self.input_buffer[..num_input_samples], false);
1230
1231 let status = match status {
1232 ReadStatus::UnderflowOccurred { num_frames_read: _ } => {
1233 StreamStatus::OUTPUT_UNDERFLOW
1234 }
1235 ReadStatus::OverflowCorrected {
1236 num_frames_discarded: _,
1237 } => StreamStatus::INPUT_OVERFLOW,
1238 _ => StreamStatus::empty(),
1239 };
1240
1241 (num_in_channels, status)
1242 } else {
1243 self.input_buffer[..num_input_samples].fill(0.0);
1244
1245 (num_in_channels, StreamStatus::CLOSED)
1246 }
1247 } else {
1248 (0, StreamStatus::empty())
1249 };
1250
1251 let mut output_stream_status = StreamStatus::empty();
1252 if underflow {
1253 output_stream_status.insert(StreamStatus::OUTPUT_UNDERFLOW);
1254 }
1255
1256 let timestamp = info.timestamp();
1257 let process_to_playback_delay = timestamp.playback.duration_since(timestamp.callback);
1258
1259 self.processor.process(
1260 &InterleavedSlice::new(
1261 &self.input_buffer[..frames * num_in_channels],
1262 num_in_channels,
1263 frames,
1264 )
1265 .unwrap(),
1266 &mut InterleavedSlice::new_mut(output, self.num_out_channels, frames).unwrap(),
1267 BackendProcessInfo {
1268 frames,
1269 process_timestamp: Some(process_timestamp),
1270 duration_since_stream_start,
1271 input_stream_status,
1272 output_stream_status,
1273 dropped_frames,
1274 process_to_playback_delay: Some(process_to_playback_delay),
1275 },
1276 );
1277 }
1278}
1279
1280impl Drop for OutputCallback {
1281 fn drop(&mut self) {
1282 self.output_stream_running.store(false, Ordering::Relaxed);
1283 let _ = self
1284 .err_to_cx_tx
1285 .send(IoStreamError::Input(cpal::Error::new(
1286 cpal::ErrorKind::StreamInvalidated,
1287 )));
1288 }
1289}
1290
1291fn err_callback(
1292 is_input: bool,
1293 is_running: Arc<AtomicBool>,
1294 err_to_cx_tx: mpsc::Sender<IoStreamError>,
1295 #[cfg(any(
1296 target_os = "linux",
1297 target_os = "freebsd",
1298 target_os = "dragonfly",
1299 target_os = "netbsd"
1300 ))]
1301 is_alsa: bool,
1302) -> impl FnMut(cpal::Error) + Send + 'static {
1303 let mut last_underrun_msg_instant: Option<Instant> = None;
1304
1305 move |err| {
1306 let do_send = match err.kind() {
1307 cpal::ErrorKind::Xrun => {
1308 let mut do_send = true;
1309 if let Some(instant) = last_underrun_msg_instant
1310 && instant.elapsed() < UNDERRUN_LOG_COOLDOWN
1311 {
1312 do_send = false;
1313 }
1314
1315 if do_send {
1316 last_underrun_msg_instant = Some(Instant::now());
1317 }
1318
1319 do_send
1320 }
1321 cpal::ErrorKind::DeviceNotAvailable
1322 | cpal::ErrorKind::HostUnavailable
1323 | cpal::ErrorKind::BackendError
1324 | cpal::ErrorKind::Other
1325 | cpal::ErrorKind::PermissionDenied
1326 | cpal::ErrorKind::StreamInvalidated => {
1327 is_running.store(false, Ordering::Relaxed);
1328 true
1329 }
1330 _ => false,
1331 };
1332
1333 #[cfg(any(
1335 target_os = "linux",
1336 target_os = "freebsd",
1337 target_os = "dragonfly",
1338 target_os = "netbsd"
1339 ))]
1340 let do_send = if do_send
1341 && is_alsa
1342 && err.kind() != cpal::ErrorKind::StreamInvalidated
1343 && let Some(msg) = err.message()
1344 && msg.contains("snd_pcm_avail_delay")
1346 {
1347 false
1348 } else {
1349 do_send
1350 };
1351
1352 if do_send
1353 && let Err(e) = err_to_cx_tx.send(if is_input {
1354 IoStreamError::Input(err)
1355 } else {
1356 IoStreamError::Output(err)
1357 })
1358 {
1359 #[cfg(any(feature = "log", feature = "tracing"))]
1361 error!("Audio stream error occurred: {}", e.0);
1362 #[cfg(not(any(feature = "log", feature = "tracing")))]
1363 let _ = e;
1364 }
1365 }
1366}
1367
1368#[derive(Debug, thiserror::Error)]
1370pub enum StartStreamError {
1371 #[error("Failed to activate Firewheel context: The Firewheel context is already active")]
1378 AlreadyActive,
1379 #[error("Failed to activate Firewheel context: Audio graph failed to compile: {0}")]
1381 GraphCompileError(#[from] CompileGraphError),
1382
1383 #[error("The requested audio input device was not found: {0}")]
1384 InputDeviceNotFound(String),
1385 #[error("The requested audio output device was not found: {0}")]
1386 OutputDeviceNotFound(String),
1387 #[error("Failed to get default input output device")]
1388 DefaultInputDeviceNotFound,
1389 #[error("Failed to get default audio output device")]
1390 DefaultOutputDeviceNotFound,
1391 #[error("Failed to get audio device config: {0}")]
1392 FailedToGetConfig(cpal::Error),
1393 #[error("Failed to build audio stream: {0}")]
1394 BuildStreamError(cpal::Error),
1395 #[error("Failed to play audio stream: {0}")]
1396 PlayStreamError(cpal::Error),
1397
1398 #[cfg(not(feature = "resample_inputs"))]
1399 #[error("Not able to use a sample rate of {0} for the input audio device")]
1400 CouldNotMatchSampleRate(u32),
1401}
1402
1403impl From<ActivateError> for StartStreamError {
1404 fn from(e: ActivateError) -> Self {
1405 match e {
1406 ActivateError::AlreadyActive => Self::AlreadyActive,
1407 ActivateError::GraphCompileError(e) => Self::GraphCompileError(e),
1408 }
1409 }
1410}
1411
1412#[derive(Debug, thiserror::Error)]
1413pub enum IoStreamError {
1414 #[error("Input audio stream error: {0}")]
1416 Input(cpal::Error),
1417 #[error("Output audio stream error: {0}")]
1419 Output(cpal::Error),
1420}
1421
1422fn scratch_vec(len: usize) -> Vec<f32> {
1423 let mut v = Vec::new();
1424 v.reserve_exact(len);
1425 v.resize(len, 0.0f32);
1426 v
1427}