diff --git a/Cargo.toml b/Cargo.toml index fd831e9a8..b5daa78f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ lazy_static = "1.3" alsa = "0.4.3" nix = "0.15.0" libc = "0.2.65" +parking_lot = "0.11" jack = { version = "0.6.5", optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] diff --git a/src/host/alsa/enumerate.rs b/src/host/alsa/enumerate.rs index a269d7b8c..32f1894e8 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -1,5 +1,6 @@ use super::alsa; -use super::Device; +use super::parking_lot::Mutex; +use super::{Device, DeviceHandles}; use {BackendSpecificError, DevicesError}; /// ALSA implementation for `Devices`. @@ -26,43 +27,18 @@ impl Iterator for Devices { match self.hint_iter.next() { None => return None, Some(hint) => { - let name = hint.name; - - let io = hint.direction; - - if let Some(io) = io { - if io != alsa::Direction::Playback { - continue; - } - } - - let name = match name { - Some(name) => { - // Ignoring the `null` device. - if name == "null" { - continue; - } - name - } - _ => continue, - }; - - // See if the device has an available output stream. - let has_available_output = { - let playback_handle = - alsa::pcm::PCM::new(&name, alsa::Direction::Playback, true); - playback_handle.is_ok() - }; - - // See if the device has an available input stream. - let has_available_input = { - let capture_handle = - alsa::pcm::PCM::new(&name, alsa::Direction::Capture, true); - capture_handle.is_ok() + let name = match hint.name { + None => continue, + // Ignoring the `null` device. + Some(name) if name == "null" => continue, + Some(name) => name, }; - if has_available_output || has_available_input { - return Some(Device(name)); + if let Ok(handles) = DeviceHandles::open(&name) { + return Some(Device { + name, + handles: Mutex::new(handles), + }); } } } @@ -72,12 +48,18 @@ impl Iterator for Devices { #[inline] pub fn default_input_device() -> Option { - Some(Device("default".to_owned())) + Some(Device { + name: "default".to_owned(), + handles: Mutex::new(Default::default()), + }) } #[inline] pub fn default_output_device() -> Option { - Some(Device("default".to_owned())) + Some(Device { + name: "default".to_owned(), + handles: Mutex::new(Default::default()), + }) } impl From for DevicesError { diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index b11e3282a..d306519dd 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1,7 +1,9 @@ extern crate alsa; extern crate libc; +extern crate parking_lot; use self::alsa::poll::Descriptors; +use self::parking_lot::Mutex; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, @@ -163,8 +165,68 @@ impl Drop for TriggerReceiver { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Device(String); +#[derive(Default)] +struct DeviceHandles { + playback: Option, + capture: Option, +} + +impl DeviceHandles { + /// Create `DeviceHandles` for `name` and try to open a handle for both + /// directions. Returns `Ok` if either direction is opened successfully. + fn open(name: &str) -> Result { + let mut handles = Self::default(); + let playback_err = handles.try_open(name, alsa::Direction::Playback).err(); + let capture_err = handles.try_open(name, alsa::Direction::Capture).err(); + if let Some(err) = capture_err.and(playback_err) { + Err(err) + } else { + Ok(handles) + } + } + + /// Get a mutable reference to the `Option` for a specific `stream_type`. + /// If the `Option` is `None`, the `alsa::PCM` will be opened and placed in + /// the `Option` before returning. If `handle_mut()` returns `Ok` the contained + /// `Option` is guaranteed to be `Some(..)`. + fn try_open( + &mut self, + name: &str, + stream_type: alsa::Direction, + ) -> Result<&mut Option, alsa::Error> { + let handle = match stream_type { + alsa::Direction::Playback => &mut self.playback, + alsa::Direction::Capture => &mut self.capture, + }; + + if handle.is_none() { + *handle = Some(alsa::pcm::PCM::new(name, stream_type, true)?); + } + + Ok(handle) + } + + /// Get a mutable reference to the `alsa::PCM` handle for a specific `stream_type`. + /// If the handle is not yet opened, it will be opened and stored in `self`. + fn get_mut( + &mut self, + name: &str, + stream_type: alsa::Direction, + ) -> Result<&mut alsa::PCM, alsa::Error> { + Ok(self.try_open(name, stream_type)?.as_mut().unwrap()) + } + + /// Take ownership of the `alsa::PCM` handle for a specific `stream_type`. + /// If the handle is not yet opened, it will be opened and returned. + fn take(&mut self, name: &str, stream_type: alsa::Direction) -> Result { + Ok(self.try_open(name, stream_type)?.take().unwrap()) + } +} + +pub struct Device { + name: String, + handles: Mutex, +} impl Device { fn build_stream_inner( @@ -173,10 +235,13 @@ impl Device { sample_format: SampleFormat, stream_type: alsa::Direction, ) -> Result { - let name = &self.0; + let handle_result = self + .handles + .lock() + .take(&self.name, stream_type) + .map_err(|e| (e, e.errno())); - let handle = match alsa::pcm::PCM::new(name, stream_type, true).map_err(|e| (e, e.errno())) - { + let handle = match handle_result { Err((_, Some(nix::errno::Errno::EBUSY))) => { return Err(BuildStreamError::DeviceNotAvailable) } @@ -229,16 +294,19 @@ impl Device { #[inline] fn name(&self) -> Result { - Ok(self.0.clone()) + Ok(self.name.clone()) } fn supported_configs( &self, stream_t: alsa::Direction, ) -> Result, SupportedStreamConfigsError> { - let name = &self.0; + let mut guard = self.handles.lock(); + let handle_result = guard + .get_mut(&self.name, stream_t) + .map_err(|e| (e, e.errno())); - let handle = match alsa::pcm::PCM::new(name, stream_t, true).map_err(|e| (e, e.errno())) { + let handle = match handle_result { Err((_, Some(nix::errno::Errno::ENOENT))) | Err((_, Some(nix::errno::Errno::EBUSY))) => { return Err(SupportedStreamConfigsError::DeviceNotAvailable)