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