From 42fc702f5314fdce3145e8365f7b400901657191 Mon Sep 17 00:00:00 2001 From: mitchmindtree Date: Thu, 20 Jun 2019 22:37:36 +0200 Subject: [PATCH] Add `BackendSpecificError`. Add new `DevicesError`. See the documentation for both new errors for details. The new `DevicesError` has been added to allow for returning errors when enumerating devices. This has allowed to remove multiple potential `panic!`s in each of the alsa, coreaudio and wasapi backends. --- examples/enumerate.rs | 2 +- src/alsa/enumerate.rs | 41 +++++++++++++++------------- src/coreaudio/enumerate.rs | 27 ++++++++++++------- src/emscripten/mod.rs | 8 ++++++ src/lib.rs | 49 ++++++++++++++++++++++++++++----- src/null/mod.rs | 7 +++++ src/wasapi/device.rs | 55 ++++++++++++++++++++------------------ src/wasapi/mod.rs | 14 ++++++++-- 8 files changed, 140 insertions(+), 63 deletions(-) diff --git a/examples/enumerate.rs b/examples/enumerate.rs index 9164b9c..01e0052 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -4,7 +4,7 @@ fn main() { println!("Default Input Device:\n {:?}", cpal::default_input_device().map(|e| e.name())); println!("Default Output Device:\n {:?}", cpal::default_output_device().map(|e| e.name())); - let devices = cpal::devices(); + let devices = cpal::devices().expect("failed to enumerate devices"); println!("Devices: "); for (device_index, device) in devices.enumerate() { println!("{}. \"{}\"", diff --git a/src/alsa/enumerate.rs b/src/alsa/enumerate.rs index 3f18f57..1c9ed04 100644 --- a/src/alsa/enumerate.rs +++ b/src/alsa/enumerate.rs @@ -1,3 +1,4 @@ +use {BackendSpecificError, DevicesError}; use super::Device; use super::alsa; use super::check_errors; @@ -13,6 +14,28 @@ pub struct Devices { next_str: *const *const u8, } +impl Devices { + pub fn new() -> Result { + unsafe { + // TODO: check in which situation this can fail. + let card = -1; // -1 means all cards. + let iface = b"pcm\0"; // Interface identification. + let mut hints = mem::uninitialized(); // Array of device name hints. + let res = alsa::snd_device_name_hint(card, iface.as_ptr() as *const _, &mut hints); + if let Err(description) = check_errors(res) { + let err = BackendSpecificError { description }; + return Err(err.into()); + } + let hints = hints as *const *const u8; + let devices = Devices { + global_list: hints, + next_str: hints, + }; + Ok(devices) + } + } +} + unsafe impl Send for Devices { } unsafe impl Sync for Devices { @@ -27,24 +50,6 @@ impl Drop for Devices { } } -impl Default for Devices { - fn default() -> Devices { - unsafe { - // TODO: check in which situation this can fail. - let card = -1; // -1 means all cards. - let iface = b"pcm\0"; // Interface identification. - let mut hints = mem::uninitialized(); // Array of device name hints. - let res = alsa::snd_device_name_hint(card, iface.as_ptr() as *const _, &mut hints); - check_errors(res).unwrap(); - let hints = hints as *const *const u8; - Devices { - global_list: hints, - next_str: hints, - } - } - } -} - impl Iterator for Devices { type Item = Device; diff --git a/src/coreaudio/enumerate.rs b/src/coreaudio/enumerate.rs index 32c2a38..96de9bc 100644 --- a/src/coreaudio/enumerate.rs +++ b/src/coreaudio/enumerate.rs @@ -1,4 +1,4 @@ -use SupportedFormat; +use {BackendSpecificError, DevicesError, SupportedFormat}; use std::mem; use std::ptr::null; use std::vec::IntoIter as VecIntoIter; @@ -64,20 +64,27 @@ unsafe fn audio_devices() -> Result, OSStatus> { pub struct Devices(VecIntoIter); +impl Devices { + pub fn new() -> Result { + let devices = unsafe { + match audio_devices() { + Ok(devices) => devices, + Err(os_status) => { + let description = format!("{}", os_status); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + } + }; + Ok(Devices(devices.into_iter())) + } +} + unsafe impl Send for Devices { } unsafe impl Sync for Devices { } -impl Default for Devices { - fn default() -> Self { - let devices = unsafe { - audio_devices().expect("failed to get audio output devices") - }; - Devices(devices.into_iter()) - } -} - impl Iterator for Devices { type Item = Device; fn next(&mut self) -> Option { diff --git a/src/emscripten/mod.rs b/src/emscripten/mod.rs index d20dfb9..3f5067b 100644 --- a/src/emscripten/mod.rs +++ b/src/emscripten/mod.rs @@ -10,6 +10,7 @@ use stdweb::web::set_timeout; use BuildStreamError; use DefaultFormatError; +use DevicesError; use Format; use SupportedFormatsError; use StreamData; @@ -183,6 +184,13 @@ fn is_webaudio_available() -> bool { // Content is false if the iterator is empty. pub struct Devices(bool); + +impl Devices { + pub fn new() -> Result { + Ok(Self::default()) + } +} + impl Default for Devices { fn default() -> Devices { // We produce an empty iterator if the WebAudio API isn't available. diff --git a/src/lib.rs b/src/lib.rs index 4e47939..926dcaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -279,6 +279,37 @@ pub struct SupportedInputFormats(cpal_impl::SupportedInputFormats); /// See [`Device::supported_output_formats()`](struct.Device.html#method.supported_output_formats). pub struct SupportedOutputFormats(cpal_impl::SupportedOutputFormats); +/// Some error has occurred that is specific to the backend from which it was produced. +/// +/// This error is often used as a catch-all in cases where: +/// +/// - It is unclear exactly what error might be produced by the backend API. +/// - It does not make sense to add a variant to the enclosing error type. +/// - No error was expected to occur at all, but we return an error to avoid the possibility of a +/// `panic!` caused by some unforseen or unknown reason. +/// +/// **Note:** If you notice a `BackendSpecificError` that you believe could be better handled in a +/// cross-platform manner, please create an issue or submit a pull request with a patch that adds +/// the necessary error variant to the appropriate error enum. +#[derive(Debug, Fail)] +#[fail(display = "A backend-specific error has occurred: {}", description)] +pub struct BackendSpecificError { + pub description: String +} + +/// An error that might occur while attempting to enumerate the available devices on a system. +#[derive(Debug, Fail)] +pub enum DevicesError { + /// Some error that is specific to the backend from which it was produced. + /// + /// Note: This error is often used when + #[fail(display = "{}", err)] + BackendSpecific { + #[fail(cause)] + err: BackendSpecificError, + } +} + /// Error that can happen when enumerating the list of supported formats. #[derive(Debug, Fail)] pub enum SupportedFormatsError { @@ -331,34 +362,34 @@ pub enum BuildStreamError { /// /// Can be empty if the system does not support audio in general. #[inline] -pub fn devices() -> Devices { - Devices(Default::default()) +pub fn devices() -> Result { + Ok(Devices(cpal_impl::Devices::new()?)) } /// An iterator yielding all `Device`s currently available to the system that support one or more /// input stream formats. /// /// Can be empty if the system does not support audio input. -pub fn input_devices() -> InputDevices { +pub fn input_devices() -> Result { fn supports_input(device: &Device) -> bool { device.supported_input_formats() .map(|mut iter| iter.next().is_some()) .unwrap_or(false) } - devices().filter(supports_input) + Ok(devices()?.filter(supports_input)) } /// An iterator yielding all `Device`s currently available to the system that support one or more /// output stream formats. /// /// Can be empty if the system does not support audio output. -pub fn output_devices() -> OutputDevices { +pub fn output_devices() -> Result { fn supports_output(device: &Device) -> bool { device.supported_output_formats() .map(|mut iter| iter.next().is_some()) .unwrap_or(false) } - devices().filter(supports_output) + Ok(devices()?.filter(supports_output)) } /// The default input audio device on the system. @@ -704,6 +735,12 @@ impl Iterator for SupportedOutputFormats { } } +impl From for DevicesError { + fn from(err: BackendSpecificError) -> Self { + DevicesError::BackendSpecific { err } + } +} + // If a backend does not provide an API for retrieving supported formats, we query it with a bunch // of commonly used rates. This is always the case for wasapi and is sometimes the case for alsa. // diff --git a/src/null/mod.rs b/src/null/mod.rs index 0596b2e..e7a5b3c 100644 --- a/src/null/mod.rs +++ b/src/null/mod.rs @@ -4,6 +4,7 @@ use std::marker::PhantomData; use BuildStreamError; use DefaultFormatError; +use DevicesError; use Format; use SupportedFormatsError; use StreamData; @@ -56,6 +57,12 @@ pub struct StreamId; #[derive(Default)] pub struct Devices; +impl Devices { + pub fn new() -> Result { + Ok(Devices) + } +} + impl Iterator for Devices { type Item = Device; diff --git a/src/wasapi/device.rs b/src/wasapi/device.rs index a565adc..a927d93 100644 --- a/src/wasapi/device.rs +++ b/src/wasapi/device.rs @@ -9,7 +9,9 @@ use std::ptr; use std::slice; use std::sync::{Arc, Mutex, MutexGuard}; +use BackendSpecificError; use DefaultFormatError; +use DevicesError; use Format; use SupportedFormatsError; use SampleFormat; @@ -18,6 +20,7 @@ use SupportedFormat; use COMMON_SAMPLE_RATES; use super::check_result; +use super::check_result_backend_specific; use super::com; use super::winapi::Interface; use super::winapi::ctypes::c_void; @@ -688,6 +691,32 @@ pub struct Devices { next_item: u32, } +impl Devices { + pub fn new() -> Result { + unsafe { + let mut collection: *mut IMMDeviceCollection = mem::uninitialized(); + // can fail because of wrong parameters (should never happen) or out of memory + check_result_backend_specific( + (*ENUMERATOR.0).EnumAudioEndpoints( + eAll, + DEVICE_STATE_ACTIVE, + &mut collection, + ) + )?; + + let mut count = mem::uninitialized(); + // can fail if the parameter is null, which should never happen + check_result_backend_specific((*collection).GetCount(&mut count))?; + + Devices { + collection: collection, + total_count: count, + next_item: 0, + } + } + } +} + unsafe impl Send for Devices { } unsafe impl Sync for Devices { @@ -702,32 +731,6 @@ impl Drop for Devices { } } -impl Default for Devices { - fn default() -> Devices { - unsafe { - let mut collection: *mut IMMDeviceCollection = mem::uninitialized(); - // can fail because of wrong parameters (should never happen) or out of memory - check_result( - (*ENUMERATOR.0).EnumAudioEndpoints( - eAll, - DEVICE_STATE_ACTIVE, - &mut collection, - ) - ).unwrap(); - - let mut count = mem::uninitialized(); - // can fail if the parameter is null, which should never happen - check_result((*collection).GetCount(&mut count)).unwrap(); - - Devices { - collection: collection, - total_count: count, - next_item: 0, - } - } - } -} - impl Iterator for Devices { type Item = Device; diff --git a/src/wasapi/mod.rs b/src/wasapi/mod.rs index 82026e7..7fe9de8 100644 --- a/src/wasapi/mod.rs +++ b/src/wasapi/mod.rs @@ -1,10 +1,10 @@ extern crate winapi; +use BackendSpecificError; +use self::winapi::um::winnt::HRESULT; use std::io::Error as IoError; - pub use self::device::{Device, Devices, SupportedInputFormats, SupportedOutputFormats, default_input_device, default_output_device}; pub use self::stream::{EventLoop, StreamId}; -use self::winapi::um::winnt::HRESULT; mod com; mod device; @@ -18,3 +18,13 @@ fn check_result(result: HRESULT) -> Result<(), IoError> { Ok(()) } } + +fn check_result_backend_specific(result: HRESULT) -> Result<(), BackendSpecificError> { + match check_result(result) { + Ok(()) => Ok(()) + Err(err) => { + let description = format!("{}", err); + return BackendSpecificError { description } + } + } +}