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.
This commit is contained in:
mitchmindtree 2019-06-20 22:37:36 +02:00
parent cf84ab906f
commit 42fc702f53
8 changed files with 140 additions and 63 deletions

View File

@ -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!("{}. \"{}\"",

View File

@ -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<Self, DevicesError> {
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;

View File

@ -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<Vec<AudioDeviceID>, OSStatus> {
pub struct Devices(VecIntoIter<AudioDeviceID>);
impl Devices {
pub fn new() -> Result<Self, DevicesError> {
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<Device> {

View File

@ -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<Self, DevicesError> {
Ok(Self::default())
}
}
impl Default for Devices {
fn default() -> Devices {
// We produce an empty iterator if the WebAudio API isn't available.

View File

@ -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<Devices, DevicesError> {
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<InputDevices, DevicesError> {
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<OutputDevices, DevicesError> {
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<BackendSpecificError> 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.
//

View File

@ -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<Self, DevicesError> {
Ok(Devices)
}
}
impl Iterator for Devices {
type Item = Device;

View File

@ -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<Self, DevicesError> {
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;

View File

@ -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 }
}
}
}