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
19const 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#[derive(Debug, Clone, PartialEq)]
29pub struct CpalOutputConfig {
30 pub host: Option<cpal::HostId>,
33
34 pub device_name: Option<String>,
39
40 pub desired_sample_rate: Option<u32>,
45
46 pub desired_block_frames: Option<u32>,
56
57 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#[derive(Debug, Clone, PartialEq)]
78pub struct CpalInputConfig {
79 pub host: Option<cpal::HostId>,
82
83 pub device_name: Option<String>,
88
89 pub desired_block_frames: Option<u32>,
99
100 pub channel_config: ResamplingChannelConfig,
102
103 pub fallback: bool,
108
109 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#[derive(Debug, Clone, PartialEq)]
132pub struct CpalConfig {
133 pub output: CpalOutputConfig,
135
136 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
153pub 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 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 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 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 #[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 ..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 #[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 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 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 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 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 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#[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}