diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index fe0d9f467..2b9129a87 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -83,6 +83,17 @@ jobs: run: sudo apt-get install libasound2-dev - name: Install libjack run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 + - name: Install sndio + run: | + sudo apt-get install build-essential && + git clone https://caoua.org/git/sndio && + cd sndio && + ./configure && + make && + sudo mkdir /var/run/sndiod && + sudo useradd -r -g audio -s /sbin/nologin -d /var/run/sndiod sndiod && + sudo make install && + sudo ldconfig - name: Install stable uses: actions-rs/toolchain@v1 with: @@ -99,6 +110,11 @@ jobs: with: command: test args: --all --all-features --verbose + - name: Run with jack feature + uses: actions-rs/cargo@v1 + with: + command: test + args: --all --features jack --verbose linux-check-and-test-armv7: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 0289afe13..a2acc0f72 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .DS_Store recorded.wav rls*.log +rusty-tags.* diff --git a/Cargo.toml b/Cargo.toml index 79741c192..df76cd628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["audio", "sound"] [features] asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. +linux-sndio = ["sndio-sys", "libc"] # sndio on Linux (normally this is only used on OpenBSD) [dependencies] thiserror = "1.0.2" @@ -34,6 +35,14 @@ libc = "0.2.65" parking_lot = "0.11" jack = { version = "0.7.0", optional = true } +[target.'cfg(target_os = "linux")'.dependencies] +sndio-sys = { version = "0.0.*", optional = true } +libc = { version = "0.2.65", optional = true } + +[target.'cfg(target_os = "openbsd")'.dependencies] +sndio-sys = "0.0.*" +libc = "0.2.65" + [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s. mach = "0.3" # For access to mach_timebase type. diff --git a/README.md b/README.md index 474d97b69..38fa2e89d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Currently, supported hosts include: - macOS (via CoreAudio) - iOS (via CoreAudio) - Android (via Oboe) +- OpenBSD (via sndio) - Emscripten Note that on Linux, the ALSA development files are required. These are provided diff --git a/src/error.rs b/src/error.rs index 31a899329..5819a2eb0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -147,7 +147,7 @@ pub enum PauseStreamError { } /// Errors that might occur while a stream is running. -#[derive(Debug, Error)] +#[derive(Debug, Error, Clone)] pub enum StreamError { /// The device no longer exists. This can happen if the device is disconnected while the /// program is running. diff --git a/src/host/mod.rs b/src/host/mod.rs index 555b01022..b8f22b6ef 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -14,6 +14,11 @@ pub(crate) mod jack; pub(crate) mod null; #[cfg(target_os = "android")] pub(crate) mod oboe; +#[cfg(any( + target_os = "openbsd", + all(target_os = "linux", feature = "linux-sndio") +))] +pub(crate) mod sndio; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] diff --git a/src/host/sndio/adapters.rs b/src/host/sndio/adapters.rs new file mode 100644 index 000000000..e999e1035 --- /dev/null +++ b/src/host/sndio/adapters.rs @@ -0,0 +1,70 @@ +use crate::{Data, FrameCount, InputCallbackInfo, OutputCallbackInfo, Sample}; +use samples_formats::TypeSampleFormat; + +/// When given an input data callback that expects samples in the specified sample format, return +/// an input data callback that expects samples in the I16 sample format. The `buffer_size` is in +/// samples. +pub(super) fn input_adapter_callback( + mut original_data_callback: D, + buffer_size: FrameCount, +) -> Box +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + T: Sample + TypeSampleFormat + Copy + Send + Default + 'static, +{ + let mut adapted_buf = vec![T::default(); buffer_size as usize]; + + Box::new(move |data: &Data, info: &InputCallbackInfo| { + let data_slice: &[i16] = data.as_slice().unwrap(); // unwrap OK because data is always i16 + let adapted_slice = &mut adapted_buf; + assert_eq!(data_slice.len(), adapted_slice.len()); + for (adapted_ref, data_element) in adapted_slice.iter_mut().zip(data_slice.iter()) { + *adapted_ref = T::from(data_element); + } + + // Note: we construct adapted_data here instead of in the parent function because adapted_buf needs + // to be owned by the closure. + let adapted_data = unsafe { data_from_vec(&mut adapted_buf) }; + original_data_callback(&adapted_data, info); + }) +} + +/// When given an output data callback that expects a place to write samples in the specified +/// sample format, return an output data callback that expects a place to write samples in the I16 +/// sample format. The `buffer_size` is in samples. +pub(super) fn output_adapter_callback( + mut original_data_callback: D, + buffer_size: FrameCount, +) -> Box +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + T: Sample + TypeSampleFormat + Copy + Send + Default + 'static, +{ + let mut adapted_buf = vec![T::default(); buffer_size as usize]; + Box::new(move |data: &mut Data, info: &OutputCallbackInfo| { + // Note: we construct adapted_data here instead of in the parent function because + // adapted_buf needs to be owned by the closure. + let mut adapted_data = unsafe { data_from_vec(&mut adapted_buf) }; + + // Populate adapted_buf / adapted_data. + original_data_callback(&mut adapted_data, info); + + let data_slice: &mut [i16] = data.as_slice_mut().unwrap(); // unwrap OK because data is always i16 + let adapted_slice = &adapted_buf; + assert_eq!(data_slice.len(), adapted_slice.len()); + for (data_ref, adapted_element) in data_slice.iter_mut().zip(adapted_slice.iter()) { + *data_ref = adapted_element.to_i16(); + } + }) +} + +unsafe fn data_from_vec(adapted_buf: &mut Vec) -> Data +where + T: TypeSampleFormat, +{ + Data::from_parts( + adapted_buf.as_mut_ptr() as *mut _, + adapted_buf.len(), // TODO: this is converting a FrameCount to a number of samples; invalid for stereo! + T::sample_format(), + ) +} diff --git a/src/host/sndio/mod.rs b/src/host/sndio/mod.rs new file mode 100644 index 000000000..8ad32c054 --- /dev/null +++ b/src/host/sndio/mod.rs @@ -0,0 +1,1128 @@ +extern crate libc; +extern crate sndio_sys; + +mod adapters; +mod runner; +use self::adapters::{input_adapter_callback, output_adapter_callback}; +use self::runner::runner; + +use std::collections::HashMap; +use std::convert::From; +use std::mem::{self, MaybeUninit}; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; + +use thiserror::Error; + +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + DeviceNameError, DevicesError, FrameCount, HostUnavailable, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, + StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, +}; + +use traits::{DeviceTrait, HostTrait, StreamTrait}; + +pub type SupportedInputConfigs = ::std::vec::IntoIter; +pub type SupportedOutputConfigs = ::std::vec::IntoIter; + +/// Default multiple of the round field of a sio_par struct to use for the buffer size (in frames). +const DEFAULT_ROUND_MULTIPLE: u32 = 2; + +const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(48000); +const SUPPORTED_SAMPLE_RATES: &[SampleRate] = + &[SampleRate(8000), SampleRate(44100), SampleRate(48000)]; + +#[derive(Clone, Debug, Error)] +pub enum SndioError { + #[error("The requested device is no longer available. For example, it has been unplugged.")] + DeviceNotAvailable, + + #[error("{0}")] + BackendSpecific(BackendSpecificError), +} + +#[cfg(target_endian = "big")] +const IS_LITTLE_ENDIAN: u32 = 0; + +#[cfg(target_endian = "little")] +const IS_LITTLE_ENDIAN: u32 = 1; + +impl From for BuildStreamError { + fn from(e: SndioError) -> BuildStreamError { + match e { + SndioError::DeviceNotAvailable => BuildStreamError::DeviceNotAvailable, + SndioError::BackendSpecific(bse) => BuildStreamError::BackendSpecific { err: bse }, + } + } +} + +impl From for DefaultStreamConfigError { + fn from(e: SndioError) -> DefaultStreamConfigError { + match e { + SndioError::DeviceNotAvailable => DefaultStreamConfigError::DeviceNotAvailable, + SndioError::BackendSpecific(bse) => { + DefaultStreamConfigError::BackendSpecific { err: bse } + } + } + } +} + +impl From for PauseStreamError { + fn from(e: SndioError) -> PauseStreamError { + match e { + SndioError::DeviceNotAvailable => PauseStreamError::DeviceNotAvailable, + SndioError::BackendSpecific(bse) => PauseStreamError::BackendSpecific { err: bse }, + } + } +} + +impl From for StreamError { + fn from(e: SndioError) -> StreamError { + match e { + SndioError::DeviceNotAvailable => StreamError::DeviceNotAvailable, + SndioError::BackendSpecific(bse) => StreamError::BackendSpecific { err: bse }, + } + } +} + +impl From for SupportedStreamConfigsError { + fn from(e: SndioError) -> SupportedStreamConfigsError { + match e { + SndioError::DeviceNotAvailable => SupportedStreamConfigsError::DeviceNotAvailable, + SndioError::BackendSpecific(bse) => { + SupportedStreamConfigsError::BackendSpecific { err: bse } + } + } + } +} + +pub struct Devices(Option); + +impl Devices { + fn new() -> Self { + Devices(Some(Device::new())) + } +} + +impl Iterator for Devices { + type Item = Device; + fn next(&mut self) -> Option { + self.0.take() + } +} + +struct SioHdl(*mut sndio_sys::sio_hdl); + +impl SioHdl { + /// Returns a map of sample rates to sio_par by re-configuring the device. This should not be + /// performed while recording or playing, or even after configuring the device for this state! + fn get_sample_rate_map( + &mut self, + behavior: BufferXrunBehavior, + ) -> Result { + let mut sample_rate_map = SampleRateMap::new(); + for rate in SUPPORTED_SAMPLE_RATES { + let mut par = new_sio_par(); + + // Use I16 at 48KHz; mono playback & record + par.bits = 16; + par.sig = 1; + par.le = IS_LITTLE_ENDIAN; // Native byte order + par.rchan = 1; // mono record + par.pchan = 1; // mono playback + par.rate = rate.0; + par.xrun = match behavior { + BufferXrunBehavior::Ignore => 0, + BufferXrunBehavior::Sync => 1, + BufferXrunBehavior::Error => 2, + }; + + // Set it on device and get it back to see what is valid. + self.negotiate_params(&mut par)?; + + if par.rchan != 1 { + return Err(backend_specific_error(format!( + "unexpected number of record channels: {}", + par.rchan + ))); + } + + if par.pchan != 1 { + return Err(backend_specific_error(format!( + "unexpected number of playback channels: {}", + par.pchan + ))); + } + + if par.rate != rate.0 { + return Err(backend_specific_error(format!( + "unexpected sample rate (frames per second): expected {}, got {}", + rate.0, par.rate + ))); + } + + // TODO: more checks -- bits, bps, sig, le, msb + + sample_rate_map.insert(*rate, par); + } + Ok(sample_rate_map) + } + + /// Calls sio_setpar and sio_getpar on the passed in sio_par struct. Before calling this, the + /// caller should have initialized `par` with `new_sio_par` and then set the desired parameters + /// on it. After calling (assuming an error is not returned), the caller should check the + /// parameters to see if they are OK. + /// + /// This should not be called if the device is running! However, it will panic if the device is + /// not opened yet. + fn negotiate_params(&mut self, par: &mut sndio_sys::sio_par) -> Result<(), SndioError> { + // What follows is the suggested parameter negotiation from the man pages. + self.set_params(par)?; + + let status = unsafe { + // Retrieve the actual parameters of the device. + sndio_sys::sio_getpar(self.0, par as *mut _) + }; + if status != 1 { + return Err(backend_specific_error( + "failed to get device-supported parameters with sio_getpar", + ) + .into()); + } + + if par.bits != 16 || par.bps != 2 { + // We have to check both because of the possibility of padding (usually an issue with + // 24 bits not 16 though). + return Err(backend_specific_error(format!( + "unexpected sample size (not 16bit): bits/sample: {}, bytes/sample: {})", + par.bits, par.bps + )) + .into()); + } + + if par.sig != 1 { + return Err(backend_specific_error( + "sndio device does not support I16 but we need it to", + ) + .into()); + } + Ok(()) + } + + /// Calls sio_setpar on the passed in sio_par struct. This sets the device parameters. + fn set_params(&mut self, par: &sndio_sys::sio_par) -> Result<(), SndioError> { + let mut newpar = new_sio_par(); + // This is a little hacky -- testing indicates the __magic from sio_initpar needs to be + // preserved when calling sio_setpar. Unfortunately __magic is the wrong value after + // retrieval from sio_getpar. + newpar.bits = par.bits; + newpar.bps = par.bps; + newpar.sig = par.sig; + newpar.le = par.le; + newpar.msb = par.msb; + newpar.rchan = par.rchan; + newpar.pchan = par.pchan; + newpar.rate = par.rate; + newpar.appbufsz = par.appbufsz; + newpar.bufsz = par.bufsz; + newpar.round = par.round; + newpar.xrun = par.xrun; + let status = unsafe { + // Request the device using our parameters + sndio_sys::sio_setpar(self.0, &mut newpar as *mut _) + }; + if status != 1 { + return Err(backend_specific_error("failed to set parameters with sio_setpar").into()); + } + Ok(()) + } +} + +// It is necessary to add the Send marker trait to this struct because the Arc> +// which contains it needs to be passed to the runner thread. This is sound as long as the sio_hdl +// pointer is not copied out of its SioHdl and used while the mutex is unlocked. +unsafe impl Send for SioHdl {} + +struct SampleRateMap(pub HashMap); + +impl SampleRateMap { + fn new() -> Self { + SampleRateMap(HashMap::new()) + } + + fn get(&self, rate: SampleRate) -> Option<&sndio_sys::sio_par> { + self.0.get(&rate.0) + } + + fn insert(&mut self, rate: SampleRate, par: sndio_sys::sio_par) -> Option { + self.0.insert(rate.0, par) + } + + fn config_ranges(&self, num_channels: F) -> Vec + where + F: Fn(&sndio_sys::sio_par) -> u32, + { + self.0 + .values() + .map(|par| { + let config = supported_config_from_par(par, num_channels(par)); + SupportedStreamConfigRange { + channels: config.channels, + min_sample_rate: config.sample_rate, + max_sample_rate: config.sample_rate, + buffer_size: config.buffer_size, + sample_format: config.sample_format, + } + }) + .collect() + } +} + +struct CallbackPile { + index: usize, + store: HashMap, +} + +impl CallbackPile { + fn new() -> Self { + Self { + index: 0, + store: HashMap::new(), + } + } + + fn remove_at(&mut self, index: usize) -> T { + self.store.remove(&index).unwrap() + } + + fn empty(&self) -> bool { + self.store.len() == 0 + } + + fn append(&mut self, t: T) -> usize { + let index = self.index; + self.store.insert(index, t); + self.index = index + 1; + index + } +} + +/// The shared state between `Device` and `Stream`. Responsible for closing handle when dropped. +/// Upon `Device` creation, this is in the `Init` state. Calling `.open` transitions +/// this to the `Opened` state (this generally happens when getting input or output configs). +/// From there, the state can transition to `Running` once at least one `Stream` has been created +/// with the `build_input_stream_raw` or `build_output_stream_raw` functions. +enum InnerState { + Init { + /// Buffer overrun/underrun behavior -- ignore/sync/error? + behavior: BufferXrunBehavior, + }, + Opened { + /// Contains a handle returned from sio_open. Note that even though this is a pointer type + /// and so doesn't follow Rust's borrowing rules, we should be careful not to copy it out + /// because that may render Mutex ineffective in enforcing exclusive access. + hdl: SioHdl, + + /// Map of sample rate to parameters. + sample_rate_map: SampleRateMap, + }, + Running { + /// Contains a handle returned from sio_open. Note that even though this is a pointer type + /// and so doesn't follow Rust's borrowing rules, we should be careful not to copy it out + /// because that may render Mutex ineffective in enforcing exclusive access. + hdl: SioHdl, + + /// Contains the chosen buffer size, in elements not bytes. + buffer_size: FrameCount, + + /// Stores the sndio-configured parameters. + par: sndio_sys::sio_par, + + /// Map of sample rate to parameters. + sample_rate_map: SampleRateMap, + + /// Each input Stream that has not been dropped has its callbacks in an element of this Vec. + /// The last element is guaranteed to not be None. + input_callbacks: CallbackPile, + + /// Each output Stream that has not been dropped has its callbacks in an element of this Vec. + /// The last element is guaranteed to not be None. + output_callbacks: CallbackPile, + + /// Whether the runner thread was spawned yet. + thread_spawned: bool, + + /// Channel used for signalling that the runner thread should wakeup because there is now a + /// Stream. This will only be None if either 1) the runner thread has not yet started and + /// set this value, or 2) the runner thread has exited. + wakeup_sender: Option>, + }, +} + +struct InputCallbacks { + data_callback: Box, + error_callback: Box, +} + +struct OutputCallbacks { + data_callback: Box, + error_callback: Box, +} + +impl InnerState { + fn new() -> Self { + InnerState::Init { + behavior: BufferXrunBehavior::Sync, + } + } + + fn open(&mut self) -> Result<(), SndioError> { + match self { + InnerState::Opened { .. } | InnerState::Running { .. } => { + Err(backend_specific_error("device is already open")) + } + InnerState::Init { ref behavior } => { + let hdl = unsafe { + // The transmute is needed because this C string is *const u8 in one place but *const i8 in another place. + let devany_ptr = + mem::transmute::<_, *const i8>(sndio_sys::SIO_DEVANY as *const _); + let nonblocking = true as i32; + sndio_sys::sio_open( + devany_ptr, + sndio_sys::SIO_PLAY | sndio_sys::SIO_REC, + nonblocking, + ) + }; + if hdl.is_null() { + return Err(SndioError::DeviceNotAvailable); + } + + let mut hdl = SioHdl(hdl); + let sample_rate_map = hdl.get_sample_rate_map(*behavior)?; + *self = InnerState::Opened { + hdl, + sample_rate_map, + }; + Ok(()) + } + } + } + + fn start(&mut self) -> Result<(), SndioError> { + match self { + InnerState::Running { hdl, .. } => { + let status = unsafe { + // "The sio_start() function puts the device in a waiting state: the device + // will wait for playback data to be provided (using the sio_write() + // function). Once enough data is queued to ensure that play buffers will + // not underrun, actual playback is started automatically." + sndio_sys::sio_start(hdl.0) // Unwrap OK because of check above + }; + if status != 1 { + Err(backend_specific_error("failed to start stream")) + } else { + Ok(()) + } + } + _ => Err(backend_specific_error( + "cannot start a device that hasn't been opened yet", + )), + } + } + + fn stop(&mut self) -> Result<(), SndioError> { + match self { + InnerState::Running { hdl, .. } => { + let status = unsafe { + // The sio_stop() function puts the audio subsystem in the same state as before + // sio_start() is called. It stops recording, drains the play buffer and then stops + // playback. If samples to play are queued but playback hasn't started yet then + // playback is forced immediately; playback will actually stop once the buffer is + // drained. In no case are samples in the play buffer discarded. + sndio_sys::sio_stop(hdl.0) // Unwrap OK because of check above + }; + if status != 1 { + Err(backend_specific_error("error calling sio_stop")) + } else { + Ok(()) + } + } + _ => { + // Nothing to do -- device is not open. + Ok(()) + } + } + } + + // TODO: make these 4 methods generic (new CallbackSet where T is either InputCallbacks or OutputCallbacks) + /// Puts the supplied callbacks into the vector in the first free position, or at the end. The + /// index of insertion is returned. + fn add_output_callbacks(&mut self, callbacks: OutputCallbacks) -> Result { + match self { + InnerState::Running { + ref input_callbacks, + ref mut output_callbacks, + ref mut wakeup_sender, + .. + } => { + // If there were previously no callbacks, wakeup the runner thread. + if input_callbacks.empty() && output_callbacks.empty() { + wakeup_sender.as_ref().map(|sender| sender.send(())); + } + let index = output_callbacks.append(callbacks); + Ok(index) + } + _ => Err(backend_specific_error("device is not in a running state")), + } + } + + /// Removes the callbacks at specified index, returning them. Panics if the index is invalid + /// (out of range or there is a None element at that position). + fn remove_output_callbacks(&mut self, index: usize) -> Result { + match *self { + InnerState::Running { + ref mut output_callbacks, + .. + } => Ok(output_callbacks.remove_at(index)), + _ => Err(backend_specific_error("device is not in a running state")), + } + } + + /// Puts the supplied callbacks into the vector in the first free position, or at the end. The + /// index of insertion is returned. + fn add_input_callbacks(&mut self, callbacks: InputCallbacks) -> Result { + match self { + InnerState::Running { + ref mut input_callbacks, + ref output_callbacks, + ref mut wakeup_sender, + .. + } => { + // If there were previously no callbacks, wakeup the runner thread. + if input_callbacks.empty() && output_callbacks.empty() { + wakeup_sender.as_ref().map(|sender| sender.send(())); + } + let index = input_callbacks.append(callbacks); + Ok(index) + } + _ => Err(backend_specific_error("device is not in a running state")), + } + } + + /// Removes the callbacks at specified index, returning them. Panics if the index is invalid + /// (out of range or there is a None element at that position). + fn remove_input_callbacks(&mut self, index: usize) -> Result { + match *self { + InnerState::Running { + ref mut input_callbacks, + .. + } => Ok(input_callbacks.remove_at(index)), + _ => Err(backend_specific_error("device is not in a running state")), + } + } + + /// Send an error to all input and output error callbacks. + fn error(&mut self, e: impl Into) { + match *self { + InnerState::Running { + ref mut input_callbacks, + ref mut output_callbacks, + .. + } => { + let e = e.into(); + for cbs in input_callbacks.store.values_mut() { + (cbs.error_callback)(e.clone()); + } + for cbs in output_callbacks.store.values_mut() { + (cbs.error_callback)(e.clone()); + } + } + _ => {} // Drop the error + } + } + + /// Common code shared between build_input_stream_raw and build_output_stream_raw + fn setup_stream(&mut self, config: &StreamConfig) -> Result<(), BuildStreamError> { + // If not already open, make sure it's open + match self { + InnerState::Init { .. } => { + self.open()?; + } + _ => {} + } + + match self { + InnerState::Init { .. } => { + // Probably unreachable + Err(backend_specific_error("device was expected to be opened").into()) + } + InnerState::Opened { + hdl, + sample_rate_map, + } => { + // No running streams yet; we get to set the par. + let mut par; + if let Some(par_) = sample_rate_map.get(config.sample_rate) { + par = par_.clone(); + } else { + return Err(backend_specific_error(format!( + "no configuration for sample rate {}", + config.sample_rate.0 + )) + .into()); + } + + let buffer_size = determine_buffer_size(&config.buffer_size, par.round, None)?; + + // Transition to running + par.appbufsz = buffer_size as u32; + hdl.set_params(&par)?; + let mut tmp = SampleRateMap::new(); + mem::swap(&mut tmp, sample_rate_map); + + *self = InnerState::Running { + hdl: SioHdl(hdl.0), // Just this once, it's ok to copy this + buffer_size, + par, + sample_rate_map: tmp, + input_callbacks: CallbackPile::new(), + output_callbacks: CallbackPile::new(), + thread_spawned: false, + wakeup_sender: None, + }; + Ok(()) + } + InnerState::Running { + par, buffer_size, .. + } => { + // TODO: allow setting new par like above flow if input_callbacks and + // output_callbacks are both zero. + + // Perform some checks + if par.rate != config.sample_rate.0 as u32 { + return Err(backend_specific_error("sample rates don't match").into()); + } + determine_buffer_size(&config.buffer_size, par.round, Some(*buffer_size))?; + Ok(()) + } + } + } + + fn has_streams(&self) -> bool { + match self { + InnerState::Running { + ref input_callbacks, + ref output_callbacks, + .. + } => !input_callbacks.empty() || !output_callbacks.empty(), + _ => false, + } + } +} + +impl Drop for InnerState { + fn drop(&mut self) { + match self { + InnerState::Running { hdl, .. } => unsafe { + sndio_sys::sio_close(hdl.0); + }, + _ => {} + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub enum BufferXrunBehavior { + Ignore, // SIO_IGNORE + Sync, // SIO_SYNC + Error, // SIO_ERROR +} + +#[derive(Clone)] +pub struct Device { + inner_state: Arc>, +} + +impl Device { + pub fn new() -> Self { + Device { + inner_state: Arc::new(Mutex::new(InnerState::new())), + } + } + + pub fn set_xrun_behavior(&mut self, b: BufferXrunBehavior) -> Result<(), SndioError> { + let mut inner_state = self.inner_state.lock().map_err(|e| { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + match *inner_state { + InnerState::Init { + ref mut behavior, .. + } => { + *behavior = b; + Ok(()) + } + _ => Err(backend_specific_error( + "the xrun behavior can only be specified at initialization time", + )), + } + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + #[inline] + fn name(&self) -> Result { + Ok("sndio default device".to_owned()) + } + + #[inline] + fn supported_input_configs( + &self, + ) -> Result { + let mut inner_state = + self.inner_state + .lock() + .map_err(|e| -> SupportedStreamConfigsError { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + + match *inner_state { + InnerState::Init { .. } => { + inner_state.open()?; + } + _ => {} + } + + match *inner_state { + InnerState::Running { + ref sample_rate_map, + .. + } + | InnerState::Opened { + ref sample_rate_map, + .. + } => { + let config_ranges = sample_rate_map.config_ranges(|par| par.rchan); + Ok(config_ranges.into_iter()) + } + _ => Err(backend_specific_error("device has not yet been opened").into()), + } + } + + #[inline] + fn supported_output_configs( + &self, + ) -> Result { + let mut inner_state = + self.inner_state + .lock() + .map_err(|e| -> SupportedStreamConfigsError { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + + match *inner_state { + InnerState::Init { .. } => { + inner_state.open()?; + } + _ => {} + } + + match *inner_state { + InnerState::Running { + ref sample_rate_map, + .. + } + | InnerState::Opened { + ref sample_rate_map, + .. + } => { + let config_ranges = sample_rate_map.config_ranges(|par| par.pchan); + Ok(config_ranges.into_iter()) + } + _ => Err(backend_specific_error("device has not yet been opened").into()), + } + } + + #[inline] + fn default_input_config(&self) -> Result { + let mut inner_state = self + .inner_state + .lock() + .map_err(|e| -> DefaultStreamConfigError { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + + match *inner_state { + InnerState::Init { .. } => { + inner_state.open()?; + } + _ => {} + } + + match *inner_state { + InnerState::Running { + ref sample_rate_map, + .. + } + | InnerState::Opened { + ref sample_rate_map, + .. + } => { + let config = if let Some(par) = sample_rate_map.get(DEFAULT_SAMPLE_RATE) { + supported_config_from_par(par, par.rchan) + } else { + return Err(backend_specific_error( + "missing map of sample rates to sio_par structs!", + ) + .into()); + }; + + Ok(config) + } + _ => Err(backend_specific_error("device has not yet been opened").into()), + } + } + + #[inline] + fn default_output_config(&self) -> Result { + let mut inner_state = self + .inner_state + .lock() + .map_err(|e| -> DefaultStreamConfigError { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + + match *inner_state { + InnerState::Init { .. } => { + inner_state.open()?; + } + _ => {} + } + + match *inner_state { + InnerState::Running { + ref sample_rate_map, + .. + } + | InnerState::Opened { + ref sample_rate_map, + .. + } => { + let config = if let Some(par) = sample_rate_map.get(DEFAULT_SAMPLE_RATE) { + supported_config_from_par(par, par.pchan) + } else { + return Err(backend_specific_error( + "missing map of sample rates to sio_par structs!", + ) + .into()); + }; + + Ok(config) + } + _ => Err(backend_specific_error("device has not yet been opened").into()), + } + } + + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let inner_state_arc = self.inner_state.clone(); + + let mut inner_state = self.inner_state.lock().unwrap(); + + inner_state.setup_stream(config)?; + + let idx; + let boxed_data_cb; + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + return Err(backend_specific_error("stream was not properly setup").into()); + } + InnerState::Running { + ref buffer_size, .. + } => { + boxed_data_cb = match sample_format { + SampleFormat::F32 => { + input_adapter_callback::(data_callback, *buffer_size) + } + SampleFormat::U16 => { + input_adapter_callback::(data_callback, *buffer_size) + } + SampleFormat::I16 => Box::new(data_callback), + }; + } + } + + idx = inner_state.add_input_callbacks(InputCallbacks { + data_callback: boxed_data_cb, + error_callback: Box::new(error_callback), + })?; + + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => {} + InnerState::Running { + ref mut thread_spawned, + .. + } => { + if !*thread_spawned { + thread::spawn(move || runner(inner_state_arc)); + *thread_spawned = true; + } + } + } + + drop(inner_state); // Unlock + Ok(Stream { + inner_state: self.inner_state.clone(), + is_output: false, + index: idx, + }) + } + + /// Create an output stream. + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let inner_state_arc = self.inner_state.clone(); + + let mut inner_state = self.inner_state.lock().map_err(|e| -> BuildStreamError { + backend_specific_error(format!("InnerState unlock error: {:?}", e)).into() + })?; + + inner_state.setup_stream(config)?; + + let idx; + let boxed_data_cb; + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + return Err(backend_specific_error("stream was not properly setup").into()); + } + InnerState::Running { + ref buffer_size, .. + } => { + boxed_data_cb = match sample_format { + SampleFormat::F32 => { + output_adapter_callback::(data_callback, *buffer_size) + } + SampleFormat::U16 => { + output_adapter_callback::(data_callback, *buffer_size) + } + SampleFormat::I16 => Box::new(data_callback), + }; + } + } + + idx = inner_state.add_output_callbacks(OutputCallbacks { + data_callback: boxed_data_cb, + error_callback: Box::new(error_callback), + })?; + + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => {} + InnerState::Running { + ref mut thread_spawned, + .. + } => { + if !*thread_spawned { + thread::spawn(move || runner(inner_state_arc)); + *thread_spawned = true; + } + } + } + + drop(inner_state); // Unlock + Ok(Stream { + inner_state: self.inner_state.clone(), + is_output: true, + index: idx, + }) + } +} + +fn supported_config_from_par(par: &sndio_sys::sio_par, num_channels: u32) -> SupportedStreamConfig { + SupportedStreamConfig { + channels: num_channels as u16, // Conversion is courtesy of type mismatch between sndio and RustAudio. + sample_rate: SampleRate(par.rate), // TODO: actually frames per second, not samples per second. Important for adding multi-channel support + buffer_size: SupportedBufferSize::Range { + min: par.round, + max: 10 * par.round, // There isn't really a max. + // Also note that min and max hold frame counts not + // sample counts. This would matter if stereo was + // supported. + }, + sample_format: SampleFormat::I16, + } +} + +fn new_sio_par() -> sndio_sys::sio_par { + let mut par = MaybeUninit::::uninit(); + unsafe { + sndio_sys::sio_initpar(par.as_mut_ptr()); + par.assume_init() + } +} + +fn backend_specific_error(desc: impl Into) -> SndioError { + SndioError::BackendSpecific(BackendSpecificError { + description: desc.into(), + }) +} + +pub struct Host; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } + + pub fn default_output_device() -> Option { + Some(Device::new()) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume this host is always available on sndio. + true + } + + fn devices(&self) -> Result { + Ok(Devices::new()) + } + + fn default_input_device(&self) -> Option { + Some(Device::new()) + } + + fn default_output_device(&self) -> Option { + Some(Device::new()) + } +} + +pub struct Stream { + inner_state: Arc>, + + /// True if this is output; false if this is input. + is_output: bool, + + /// Index into input_callbacks or output_callbacks + index: usize, +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + // No-op since the stream was already started by build_output_stream_raw + Ok(()) + } + + // sndio doesn't support pausing. + fn pause(&self) -> Result<(), PauseStreamError> { + Err(backend_specific_error("pausing is not implemented").into()) + } +} + +impl Drop for Stream { + /// Requests a shutdown from the callback (runner) thread and waits for it to finish shutting down. + /// If the thread is already stopped, nothing happens. + fn drop(&mut self) { + let mut inner_state = self.inner_state.lock().unwrap(); + if self.is_output { + let _ = inner_state.remove_output_callbacks(self.index); + } else { + let _ = inner_state.remove_input_callbacks(self.index); + } + + match *inner_state { + InnerState::Running { + ref thread_spawned, + ref wakeup_sender, + .. + } => { + if !inner_state.has_streams() && *thread_spawned { + // Wake up runner thread so it can shut down + if let Some(ref sender) = wakeup_sender { + let _ = sender.send(()); + } + } + } + _ => {} + } + } +} + +impl Drop for Device { + fn drop(&mut self) { + let inner_state = self.inner_state.lock().unwrap(); + match *inner_state { + InnerState::Running { + ref thread_spawned, + ref wakeup_sender, + .. + } => { + if !inner_state.has_streams() && *thread_spawned { + // Wake up runner thread so it can shut down + if let Some(ref sender) = wakeup_sender { + let _ = sender.send(()); + } + } + } + _ => {} + } + } +} + +fn determine_buffer_size( + requested: &BufferSize, + round: FrameCount, + configured_size: Option, +) -> Result { + // Round up the buffer size the user selected to the next multiple of par.round. If there + // was already a stream created with a different buffer size, return an error (sorry). + // Note: if we want stereo support, this will need to change. + let desired_buffer_size = match requested { + BufferSize::Fixed(requested) => { + let requested = *requested; + if requested > 0 { + requested + round - ((requested - 1) % round) - 1 + } else { + round + } + } + BufferSize::Default => { + if let Some(bufsize) = configured_size { + bufsize + } else { + DEFAULT_ROUND_MULTIPLE * round + } + } + }; + + if configured_size.is_some() && configured_size != Some(desired_buffer_size) { + return Err(backend_specific_error("buffer sizes don't match").into()); + } + Ok(desired_buffer_size) +} diff --git a/src/host/sndio/runner.rs b/src/host/sndio/runner.rs new file mode 100644 index 000000000..73b6202c2 --- /dev/null +++ b/src/host/sndio/runner.rs @@ -0,0 +1,419 @@ +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant}; + +use super::{backend_specific_error, InnerState}; + +use crate::{ + Data, FrameCount, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, + OutputStreamTimestamp, SampleFormat, StreamInstant, +}; + +/// The runner thread handles playing and/or recording +pub(super) fn runner(inner_state_arc: Arc>) { + let buffer_size: FrameCount; + let start_time: Instant; + let latency: Duration; + let mut clear_output_buf_needed = false; + let (wakeup_sender, wakeup_receiver) = mpsc::channel(); + { + let mut inner_state = inner_state_arc.lock().unwrap(); + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + return; + } + InnerState::Running { + wakeup_sender: ref mut wakeup_sender_, + buffer_size: ref buffer_size_, + ref par, + .. + } => { + *wakeup_sender_ = Some(wakeup_sender); + + buffer_size = *buffer_size_; + if buffer_size == 0 { + // Probably unreachable + inner_state.error(backend_specific_error("could not determine buffer size")); + return; + } + + latency = Duration::from_secs(1) * buffer_size / par.rate; + } + } + + if let Err(err) = inner_state.start() { + inner_state.error(err); + return; + } + + start_time = Instant::now(); + } + + // TODO: this is converting a FrameCount to a number of samples; invalid for stereo! + let mut output_buf = [0i16].repeat(buffer_size as usize); // Allocate buffer of correct size + let mut input_buf = [0i16].repeat(buffer_size as usize); // Allocate buffer of correct size + let mut output_data = unsafe { + Data::from_parts( + output_buf.as_mut_ptr() as *mut (), + output_buf.len(), + SampleFormat::I16, + ) + }; + let input_data = unsafe { + Data::from_parts( + input_buf.as_mut_ptr() as *mut (), + input_buf.len(), + SampleFormat::I16, + ) + }; + let data_byte_size = output_data.len * output_data.sample_format.sample_size(); + + let mut output_offset_bytes_into_buf: u64 = 0; // Byte offset in output buf to sio_write + let mut input_offset_bytes_into_buf: u64 = 0; // Byte offset in input buf to sio_read + let mut paused = false; + loop { + // See if shutdown requested in inner_state.status; if so, break + let mut nfds; + let mut pollfds: Vec; + { + let mut inner_state = inner_state_arc.lock().unwrap(); + // If there's nothing to do, wait until that's no longer the case. + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + _ => {} + } + + if !inner_state.has_streams() { + if !paused { + if let Err(_) = inner_state.stop() { + // No callbacks to error with + break; + } + } + paused = true; + while let Ok(_) = wakeup_receiver.try_recv() {} // While the lock is still held, drain the channel. + + // Unlock to prevent deadlock + drop(inner_state); + + // Block until a callback has been added; unwrap OK because the sender is in the + // Arc so it won't get dropped while this thread is running. + wakeup_receiver.recv().unwrap(); + } + } + + // If there no Streams and no Device then there is nothing to do -- exit. Note: this is + // only correct if there are no Weak references to this InnerState anywhere. + if Arc::strong_count(&inner_state_arc) == 1 { + break; + } + + { + let mut inner_state = inner_state_arc.lock().unwrap(); + if paused { + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { .. } => { + if !inner_state.has_streams() { + // Spurious wakeup + continue; + } + } + } + + if let Err(err) = inner_state.start() { + inner_state.error(backend_specific_error(format!( + "failed to unpause after new Stream created: {:?}", + err + ))); + break; + } + paused = false; + } + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { ref mut hdl, .. } => { + nfds = unsafe { sndio_sys::sio_nfds(hdl.0) }; + if nfds <= 0 { + inner_state.error(backend_specific_error(format!( + "cannot allocate {} pollfd structs", + nfds + ))); + break; + } + pollfds = [libc::pollfd { + fd: 0, + events: 0, + revents: 0, + }] + .repeat(nfds as usize); + + // Populate pollfd structs with sndio_sys::sio_pollfd + nfds = unsafe { + sndio_sys::sio_pollfd( + hdl.0, + pollfds.as_mut_ptr(), + (libc::POLLOUT | libc::POLLIN) as i32, + ) + }; + if nfds <= 0 || nfds > pollfds.len() as i32 { + inner_state.error(backend_specific_error(format!( + "invalid pollfd count from sio_pollfd: {}", + nfds + ))); + break; + } + } + } + } + + // Poll (block until ready to write) + let status = unsafe { libc::poll(pollfds.as_mut_ptr(), nfds as u32, -1) }; + if status < 0 { + let mut inner_state = inner_state_arc.lock().unwrap(); + inner_state.error(backend_specific_error(format!( + "poll failed: returned {}", + status + ))); + break; + } + + let revents; + { + let mut inner_state = inner_state_arc.lock().unwrap(); + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { ref mut hdl, .. } => { + revents = unsafe { sndio_sys::sio_revents(hdl.0, pollfds.as_mut_ptr()) } as i16; + } + } + if revents & libc::POLLHUP != 0 { + inner_state.error(backend_specific_error("device disappeared")); + break; + } + } + + if revents & (libc::POLLOUT | libc::POLLIN) == 0 { + continue; + } + + let elapsed = Instant::now().duration_since(start_time); + if revents & libc::POLLOUT != 0 { + // At this point we know data can be written + let mut output_callback_info = OutputCallbackInfo { + timestamp: OutputStreamTimestamp { + callback: StreamInstant::new( + elapsed.as_secs() as i64, + elapsed.as_nanos() as u32, + ), + playback: StreamInstant::new(0, 0), // Set below + }, + }; + output_callback_info.timestamp.playback = output_callback_info + .timestamp + .callback + .add(latency) + .unwrap(); // TODO: figure out if overflow can happen + + { + let mut inner_state = inner_state_arc.lock().unwrap(); + + let bytes_written; + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { + ref mut hdl, + ref mut output_callbacks, + .. + } => { + if output_offset_bytes_into_buf == 0 { + // The whole output buffer has been written (or this is the first time). Fill it. + if output_callbacks.empty() { + if clear_output_buf_needed { + // There is probably nonzero data in the buffer from previous output + // Streams. Zero it out. + for sample in output_buf.iter_mut() { + *sample = 0; + } + clear_output_buf_needed = false; + } + } else { + for cbs in output_callbacks.store.values_mut() { + // Really we shouldn't have more than one output callback as they are + // stepping on each others' data. + // TODO: perhaps we should not call these callbacks while holding the lock + (cbs.data_callback)(&mut output_data, &output_callback_info); + } + clear_output_buf_needed = true; + } + } + + // unwrap OK because .open was called + bytes_written = unsafe { + sndio_sys::sio_write( + hdl.0, + (output_data.data as *const u8) + .add(output_offset_bytes_into_buf as usize) + as *const _, + data_byte_size as u64 - output_offset_bytes_into_buf, + ) + }; + } + } + + if bytes_written <= 0 { + inner_state.error(backend_specific_error("no bytes written; EOF?")); + break; + } + + output_offset_bytes_into_buf += bytes_written; + if output_offset_bytes_into_buf as usize > data_byte_size { + inner_state.error(backend_specific_error("too many bytes written!")); + break; + } + + if output_offset_bytes_into_buf as usize == data_byte_size { + // Everything written; need to call data callback again. + output_offset_bytes_into_buf = 0; + }; + } + } + + if revents & libc::POLLIN != 0 { + // At this point, we know data can be read + let mut input_callback_info = InputCallbackInfo { + timestamp: InputStreamTimestamp { + callback: StreamInstant::new( + elapsed.as_secs() as i64, + elapsed.as_nanos() as u32, + ), + capture: StreamInstant::new(0, 0), + }, + }; + if let Some(capture_instant) = input_callback_info.timestamp.callback.sub(latency) { + input_callback_info.timestamp.capture = capture_instant; + } else { + println!("cpal(sndio): Underflow while calculating capture timestamp"); // TODO: is this possible? Handle differently? + input_callback_info.timestamp.capture = input_callback_info.timestamp.callback; + } + + { + let mut inner_state = inner_state_arc.lock().unwrap(); + + // unwrap OK because .open was called + let bytes_read; + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { ref mut hdl, .. } => { + bytes_read = unsafe { + sndio_sys::sio_read( + hdl.0, + (input_data.data as *const u8) + .add(input_offset_bytes_into_buf as usize) + as *mut _, + data_byte_size as u64 - input_offset_bytes_into_buf, + ) + } + } + } + + if bytes_read <= 0 { + inner_state.error(backend_specific_error("no bytes read; EOF?")); + break; + } + + input_offset_bytes_into_buf += bytes_read; + if input_offset_bytes_into_buf as usize > data_byte_size { + inner_state.error(backend_specific_error("too many bytes read!")); + break; + } + + if input_offset_bytes_into_buf as usize == data_byte_size { + // Input buffer is full; need to call data callback again. + input_offset_bytes_into_buf = 0; + }; + + if input_offset_bytes_into_buf == 0 { + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // Unlikely error state + inner_state.error(backend_specific_error( + "inner state should be InnerState::Running", + )); + break; + } + InnerState::Running { + ref mut input_callbacks, + .. + } => { + for cbs in input_callbacks.store.values_mut() { + // TODO: perhaps we should not call these callbacks while holding the lock + (cbs.data_callback)(&input_data, &input_callback_info); + } + } + } + } + } + } + } + + { + let mut inner_state = inner_state_arc.lock().unwrap(); + if !paused { + let _ = inner_state.stop(); // Can't do anything with error since no error callbacks left + } + match *inner_state { + InnerState::Init { .. } | InnerState::Opened { .. } => { + // anlikely error state but nothing to do with error + return; + } + InnerState::Running { + ref mut wakeup_sender, + ref mut thread_spawned, + .. + } => { + *wakeup_sender = None; + *thread_spawned = false; + } + } + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index d75e9005e..d3f8843af 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -448,30 +448,68 @@ macro_rules! impl_platform_host { // TODO: Add pulseaudio and jack here eventually. #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] mod platform_impl { + #[cfg(not(feature = "linux-sndio"))] pub use crate::host::alsa::{ Device as AlsaDevice, Devices as AlsaDevices, Host as AlsaHost, Stream as AlsaStream, SupportedInputConfigs as AlsaSupportedInputConfigs, SupportedOutputConfigs as AlsaSupportedOutputConfigs, }; - #[cfg(feature = "jack")] + #[cfg(all(feature = "jack", not(feature = "linux-sndio")))] pub use crate::host::jack::{ Device as JackDevice, Devices as JackDevices, Host as JackHost, Stream as JackStream, SupportedInputConfigs as JackSupportedInputConfigs, SupportedOutputConfigs as JackSupportedOutputConfigs, }; - #[cfg(feature = "jack")] + #[cfg(all(feature = "jack", not(feature = "linux-sndio")))] impl_platform_host!(Jack jack "JACK", Alsa alsa "ALSA"); - #[cfg(not(feature = "jack"))] + #[cfg(all(not(feature = "jack"), not(feature = "linux-sndio")))] impl_platform_host!(Alsa alsa "ALSA"); /// The default host for the current compilation target platform. + #[cfg(not(feature = "linux-sndio"))] pub fn default_host() -> Host { AlsaHost::new() .expect("the default host should always be available") .into() } + + #[cfg(feature = "linux-sndio")] + pub use crate::host::sndio::{ + Device as SndioDevice, Devices as SndioDevices, Host as SndioHost, Stream as SndioStream, + SupportedInputConfigs as SndioSupportedInputConfigs, + SupportedOutputConfigs as SndioSupportedOutputConfigs, + }; + + #[cfg(feature = "linux-sndio")] + impl_platform_host!(Sndio sndio "sndio"); + + /// The default host for the current compilation target platform. + #[cfg(feature = "linux-sndio")] + pub fn default_host() -> Host { + SndioHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(target_os = "openbsd")] +mod platform_impl { + pub use crate::host::sndio::{ + Device as SndioDevice, Devices as SndioDevices, Host as SndioHost, Stream as SndioStream, + SupportedInputConfigs as SndioSupportedInputConfigs, + SupportedOutputConfigs as SndioSupportedOutputConfigs, + }; + + impl_platform_host!(Sndio sndio "sndio"); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + SndioHost::new() + .expect("the default host should always be available") + .into() + } } #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -579,6 +617,7 @@ mod platform_impl { target_os = "linux", target_os = "dragonfly", target_os = "freebsd", + target_os = "openbsd", target_os = "macos", target_os = "ios", target_os = "emscripten", diff --git a/src/samples_formats.rs b/src/samples_formats.rs index 480fc0b8b..304657938 100644 --- a/src/samples_formats.rs +++ b/src/samples_formats.rs @@ -23,6 +23,29 @@ impl SampleFormat { } } +/// Returns the SampleFormat for the given standard type. +pub trait TypeSampleFormat { + fn sample_format() -> SampleFormat; +} + +impl TypeSampleFormat for f32 { + fn sample_format() -> SampleFormat { + SampleFormat::F32 + } +} + +impl TypeSampleFormat for u16 { + fn sample_format() -> SampleFormat { + SampleFormat::U16 + } +} + +impl TypeSampleFormat for i16 { + fn sample_format() -> SampleFormat { + SampleFormat::I16 + } +} + /// Trait for containers that contain PCM data. pub unsafe trait Sample: Copy + Clone { /// The `SampleFormat` corresponding to this data type.