diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5b6a94f..255e74e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `DeviceBusy` error variant to `SupportedStreamConfigsError`, `DefaultStreamConfigError`, and - `BuildStreamError` for retryable device access errors (EBUSY, EAGAIN). +- `ErrorKind::DeviceBusy` for retryable device access errors (e.g. EBUSY, EAGAIN). +- `ErrorKind::PermissionDenied` for OS-level access denials. - `StreamConfig` now implements `Copy`. - `StreamTrait::buffer_size()` to query the stream's current buffer size in frames per callback. - `HostTrait::device_by_id()` is now dispatched to each backend's implementation, allowing @@ -23,8 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Public error enums are now marked `#[non_exhaustive]` to allow adding variants without - SemVer-breaking changes. +- Changed per-operation error types (`DevicesError`, `SupportedStreamConfigsError`, etc.) and + `HostUnavailable` into a unfied `Error`/`ErrorKind`. See [UPGRADING.md](UPGRADING.md). - `DeviceTrait::build_*_stream()` now takes `StreamConfig` by value instead of `&StreamConfig` - `HostId::name()` now returns a more human-friendly name instead of the raw backend identifier. - `StreamInstant` API changed and extended to mirror `std::time::Instant`/`Duration`. See @@ -37,8 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AAudio**: Bump MSRV to 1.85. - **AAudio**: Buffers with default sizes are now dynamically tuned. - **AAudio**: `SupportedBufferSize` now reports `min: 1`. -- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable` - instead of looping. +- **ALSA**: Device disconnection now stops the stream with `ErrorKind::DeviceNotAvailable`. - **ALSA**: Polling errors trigger underrun recovery instead of looping. - **ALSA**: Try to resume from hardware after a system suspend. - **ALSA**: Loop partial reads and writes to completion. @@ -46,25 +45,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`. - **ASIO**: Timestamps now include driver-reported hardware latency. - **ASIO**: Hardware latency is now re-queried when the driver reports `kAsioLatenciesChanged`. -- **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on - `kAsioResyncRequest`. -- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver +- **ASIO**: Stream error callback now receives `ErrorKind::Xrun` on `kAsioResyncRequest`. +- **ASIO**: Stream error callback now receives `ErrorKind::StreamInvalidated` when the driver reports a sample rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate. - **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`. - **CoreAudio**: Bump MSRV to 1.85. - **CoreAudio**: Bump `mach2` to 0.6 (uses `core::ffi` instead of `libc`, enables tvOS builds). - **CoreAudio**: Timestamps now include device latency and safety offset. -- **CoreAudio**: Poisoned stream mutex in stream functions now propagate panics. - **CoreAudio**: Physical stream format is now set directly on the hardware device. -- **CoreAudio**: Stream error callback now receives `StreamError::StreamInvalidated` on any sample +- **CoreAudio**: Stream error callback now receives `ErrorKind::StreamInvalidated` on any sample rate change on macOS, and on iOS on route changes that require a stream rebuild. -- **CoreAudio**: Stream error callback now receives `StreamError::DeviceNotAvailable` on iOS +- **CoreAudio**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on iOS when media services are lost. - **CoreAudio**: User timeouts are now respected when building a stream. - **JACK**: Timestamps now use the precise hardware deadline. - **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized without error. -- **JACK**: Server shutdown now fires `StreamError::DeviceNotAvailable`. +- **JACK**: Server shutdown now fires `ErrorKind::DeviceNotAvailable`. - **JACK**: Default client name now includes the process PID. - **JACK**: User timeouts are now respected when building a stream. - **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio, @@ -88,6 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AAudio**: Fix capture and playback timestamps falling back to time-zero on error. - **AAudio**: Fix capture and playback timestamp not accounting for audio pipeline buffer depth. - **AAudio**: Fix overflow in `buffer_capacity_in_frames` for large fixed buffer sizes. +- **AAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking. - **ALSA**: Fix capture stream hanging or spinning on overruns. - **ALSA**: Fix non-monotonic `StreamInstant` during stream startup. - **ALSA**: Fix spurious timestamp errors during stream startup. @@ -103,13 +101,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ASIO**: Poisoned error callback mutex no longer silently drops subsequent error notifications. - **ASIO**: Poisoned stream mutex in the buffer-size change handler no longer silently skips the update. +- **ASIO**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking. - **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation. +- **CoreAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of + panicking. - **JACK**: Fix input capture timestamp using callback execution time instead of cycle start. - **JACK**: Poisoned error callback mutex no longer silently drops subsequent error notifications. +- **PulseAudio**: Poisoned locks now exit the thread gracefully instead of panicking. - **JACK**: Port registration failure now fails stream creation instead of silently failing. - **JACK**: `activate_async()` failure now returns an error instead of panicking. - **JACK**: Sample rate is now validated against the live JACK server at stream creation time. - **JACK**: Underrun notification no longer blocks the notification thread. +- **WASAPI**: Poisoned locks now returns an error instead of panicking. - **WebAudio**: Fix duplicated callbacks on repeated `play()` calls. - **WebAudio**: Report errors through the callback instead of panicking. diff --git a/UPGRADING.md b/UPGRADING.md index ef4e0196d..09d0227a4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,8 +4,8 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH ## Breaking Changes Checklist -- [ ] Add wildcard arms to exhaustive `match` expressions on cpal error enums -- [ ] Optionally handle the new `DeviceBusy` variant for retryable device errors +- [ ] Replace per-operation error type matches with `e.kind()` on `ErrorKind` +- [ ] Replace `HostUnavailable` error type with `ErrorKind::HostUnavailable` on `Error` - [ ] Change `build_*_stream` call sites to pass `StreamConfig` by value (drop the `&`) - [ ] For custom hosts, change `DeviceTrait` implementations to accept `StreamConfig` by value. - [ ] Remove `instant.duration_since(e)` unwraps; it now returns `Duration` (saturating). @@ -16,12 +16,15 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH - [ ] Update `duration_since` call sites to pass by value (drop the `&`). - [ ] Migrate `wasm32-unknown-emscripten` to `wasm32-unknown-unknown` if possible. -## 1. Error enums are now `#[non_exhaustive]` +## 1. Unified `Error` and `ErrorKind` type -**What changed:** Public error enums in `cpal` are now marked `#[non_exhaustive]`. +**What changed:** All per-operation error types (`DevicesError`, `SupportedStreamConfigsError`, +`DefaultStreamConfigError`, `BuildStreamError`, `StreamError`, `PlayStreamError`, +`PauseStreamError`) and the `HostUnavailable` struct are replaced by a single `cpal::Error` struct +with getters for its `kind()` and optional `message()`. ```rust -// Before (v0.17) +// Before (v0.17): each operation returned its own error type match device.default_output_config() { Ok(config) => config, Err(DefaultStreamConfigError::DeviceNotAvailable) => panic!("device gone"), @@ -29,30 +32,43 @@ match device.default_output_config() { Err(DefaultStreamConfigError::BackendSpecific { err }) => panic!("{err}"), } -// After (v0.18) -loop { - match device.default_output_config() { - Ok(config) => break config, - Err(DefaultStreamConfigError::DeviceBusy) => { +// After (v0.18): all operations return cpal::Error; match on e.kind() +match device.default_output_config() { + Ok(config) => config, + Err(e) => match e.kind() { + cpal::ErrorKind::DeviceNotAvailable => panic!("device gone"), + cpal::ErrorKind::UnsupportedConfig => panic!("unsupported"), + cpal::ErrorKind::DeviceBusy => { std::thread::sleep(std::time::Duration::from_millis(100)); + // retry } - Err(DefaultStreamConfigError::DeviceNotAvailable) => panic!("device gone"), - Err(DefaultStreamConfigError::StreamTypeNotSupported) => panic!("unsupported"), - Err(DefaultStreamConfigError::BackendSpecific { err }) => panic!("{err}"), - Err(_) => panic!("unknown error"), - } + _ => panic!("{e}"), + }, } ``` -**Why:** This lets cpal add new variants in future minor releases without a SemVer-breaking change. +The `ErrorKind` variants and their equivalents from v0.17: -## 2. New `DeviceBusy` variant +| `ErrorKind` | Former equivalent | +|------------------------|------------------------------------------------------| +| `HostUnavailable` | `HostUnavailable` (struct) | +| `DeviceNotAvailable` | `DeviceNotAvailable` in most enums | +| `DeviceBusy` | - (new; previously mapped to `DeviceNotAvailable`) | +| `UnsupportedConfig` | `StreamConfigNotSupported`, `StreamTypeNotSupported` | +| `UnsupportedOperation` | - (new) | +| `InvalidInput` | - (new) | +| `StreamInvalidated` | `StreamError::StreamInvalidated` | +| `Xrun` | `StreamError::BufferUnderrun` | +| `PermissionDenied` | - (new) | +| `Other` | `BackendSpecific` | -**What changed:** On ALSA, `EBUSY`/`EAGAIN` errors from device open calls now produce `DeviceBusy` instead of `DeviceNotAvailable`. This may be added to other hosts in the future. +The `message()` getter on `Error` returns human-readable context (formerly in +`BackendSpecific::err`). -**Why:** Unlike `DeviceNotAvailable` (device is gone), `DeviceBusy` signals a transient condition. Retrying after a short delay may succeed, as shown in the example above. +**Why:** A single type simplifies error handling across all cpal operations and allows new +`ErrorKind` variants to be added without changing any return types. -## 3. `StreamConfig` is now passed by value +## 2. `StreamConfig` is now passed by value **What changed:** `StreamConfig` now implements `Copy`, and all `DeviceTrait` stream-building methods accept it by value. @@ -68,7 +84,7 @@ let stream = device.build_output_stream(config, data_fn, err_fn, None)?; If you implement `DeviceTrait` on your own type (via the `custom` feature), update your `build_input_stream_raw` and `build_output_stream_raw` signatures from `config: &StreamConfig` to `config: StreamConfig`. Any `config.clone()` calls before `move` closures can also be removed. -## 4. `StreamInstant` API overhaul +## 3. `StreamInstant` API overhaul The `StreamInstant` API has been aligned with `std::time::Instant` and `std::time::Duration`. @@ -135,7 +151,7 @@ StreamInstant::new(0_u64, 0); **Why:** All audio host clocks are positive and monotonic; they are never negative. -## 5. `wasm32-unknown-emscripten` target removed +## 4. `wasm32-unknown-emscripten` target removed **What changed:** The `emscripten` audio host and the `wasm32-unknown-emscripten` build target are no longer supported. diff --git a/examples/beep.rs b/examples/beep.rs index 457a11654..2d1cbf636 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -14,7 +14,7 @@ use clap::Parser; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - FromSample, HostUnavailable, Sample, SizedSample, I24, + Error, ErrorKind, FromSample, HostId, Sample, SizedSample, I24, }; #[derive(Parser, Debug)] @@ -40,14 +40,14 @@ struct Opt { fn main() -> anyhow::Result<()> { let opt = Opt::parse(); - // Jack/PulseAudio support must be enabled at compile time, and is + // JACK/PulseAudio support must be enabled at compile time, and is // only available on some platforms. #[allow(unused_mut, unused_assignments)] - let mut jack_host_id = Err(HostUnavailable); + let mut jack_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] - let mut pulseaudio_host_id = Err(HostUnavailable); + let mut pulseaudio_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] - let mut pipewire_host_id = Err(HostUnavailable); + let mut pipewire_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -57,16 +57,16 @@ fn main() -> anyhow::Result<()> { { #[cfg(feature = "jack")] { - jack_host_id = Ok(cpal::HostId::Jack); + jack_host_id = Ok(HostId::Jack); } #[cfg(feature = "pulseaudio")] { - pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); + pulseaudio_host_id = Ok(HostId::PulseAudio); } #[cfg(feature = "pipewire")] { - pipewire_host_id = Ok(cpal::HostId::PipeWire); + pipewire_host_id = Ok(HostId::PipeWire); } } diff --git a/examples/custom.rs b/examples/custom.rs index e35678446..73ed9bfcf 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -1,14 +1,19 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; use std::time::Instant; +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use cpal::OutputCallbackInfo; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - DeviceDescription, DeviceDescriptionBuilder, + ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, Error, ErrorKind, FrameCount, + FromSample, Sample, SampleFormat, StreamConfig, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, }; -use cpal::{FromSample, Sample}; #[allow(dead_code)] #[derive(Clone)] // Clone, Send+Sync are required @@ -33,8 +38,8 @@ struct StreamControls { pause: AtomicBool, } -const CHANNEL_COUNT: cpal::ChannelCount = 2; -const BUFFER_SIZE: cpal::FrameCount = 4096; +const CHANNEL_COUNT: ChannelCount = 2; +const BUFFER_SIZE: FrameCount = 4096; impl HostTrait for MyHost { type Device = MyDevice; @@ -44,7 +49,7 @@ impl HostTrait for MyHost { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Ok(std::iter::once(MyDevice)) } @@ -58,76 +63,68 @@ impl HostTrait for MyHost { } impl DeviceTrait for MyDevice { - type SupportedInputConfigs = std::iter::Empty; - type SupportedOutputConfigs = std::iter::Once; + type SupportedInputConfigs = std::iter::Empty; + type SupportedOutputConfigs = std::iter::Once; type Stream = MyStream; - fn name(&self) -> Result { + fn name(&self) -> Result { Ok(String::from("custom")) } - fn description(&self) -> Result { + fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new("Custom Device".to_string()).build()) } - fn id(&self) -> Result { - Err(cpal::DeviceIdError::UnsupportedPlatform) + fn id(&self) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Ok(std::iter::empty()) } - fn supported_output_configs( - &self, - ) -> Result { - Ok(std::iter::once(cpal::SupportedStreamConfigRange::new( + fn supported_output_configs(&self) -> Result { + Ok(std::iter::once(SupportedStreamConfigRange::new( CHANNEL_COUNT, 44100, 44100, - cpal::SupportedBufferSize::Range { + SupportedBufferSize::Range { min: BUFFER_SIZE, max: BUFFER_SIZE, }, - cpal::SampleFormat::F32, + SampleFormat::F32, ))) } - fn default_input_config( - &self, - ) -> Result { - Err(cpal::DefaultStreamConfigError::StreamTypeNotSupported) + fn default_input_config(&self) -> Result { + Err(Error::new(ErrorKind::UnsupportedConfig)) } - fn default_output_config( - &self, - ) -> Result { - Ok(cpal::SupportedStreamConfig::new( + fn default_output_config(&self) -> Result { + Ok(SupportedStreamConfig::new( CHANNEL_COUNT, 44100, - cpal::SupportedBufferSize::Range { + SupportedBufferSize::Range { min: BUFFER_SIZE, max: BUFFER_SIZE, }, - cpal::SampleFormat::I16, + SampleFormat::I16, )) } fn build_input_stream_raw( &self, - _: cpal::StreamConfig, - _: cpal::SampleFormat, + _: StreamConfig, + _: SampleFormat, _: D, _: E, - _: Option, - ) -> Result + _: Option, + ) -> Result where - D: FnMut(&cpal::Data, &cpal::InputCallbackInfo) + Send + 'static, - E: FnMut(cpal::StreamError) + Send + 'static, + D: FnMut(&Data, &cpal::InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - Err(cpal::BuildStreamError::StreamConfigNotSupported) + Err(Error::new(ErrorKind::UnsupportedConfig)) } // this is the meat of a custom device impl. @@ -136,15 +133,15 @@ impl DeviceTrait for MyDevice { // a proper impl would also check the stream config and sample format, as well as handle errors fn build_output_stream_raw( &self, - _: cpal::StreamConfig, - _: cpal::SampleFormat, + _: StreamConfig, + _: SampleFormat, mut data_callback: D, _: E, - _: Option, - ) -> Result + _: Option, + ) -> Result where - D: FnMut(&mut cpal::Data, &cpal::OutputCallbackInfo) + Send + 'static, - E: FnMut(cpal::StreamError) + Send + 'static, + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let controls = Arc::new(StreamControls { exit: AtomicBool::new(false), @@ -182,7 +179,7 @@ impl DeviceTrait for MyDevice { callback: stream_instant, playback: stream_instant, }; - data_callback(&mut data, &cpal::OutputCallbackInfo::new(timestamp)); + data_callback(&mut data, &OutputCallbackInfo::new(timestamp)); let avg = buffer.iter().sum::() / buffer.len() as f32; println!("avg: {avg}"); @@ -198,12 +195,12 @@ impl DeviceTrait for MyDevice { } impl StreamTrait for MyStream { - fn play(&self) -> Result<(), cpal::PlayStreamError> { + fn play(&self) -> Result<(), Error> { self.controls.pause.store(false, Ordering::Relaxed); Ok(()) } - fn pause(&self) -> Result<(), cpal::PauseStreamError> { + fn pause(&self) -> Result<(), Error> { self.controls.pause.store(true, Ordering::Relaxed); Ok(()) } @@ -213,7 +210,7 @@ impl StreamTrait for MyStream { cpal::StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos()) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(BUFFER_SIZE) } } @@ -320,7 +317,7 @@ impl Oscillator { pub fn make_stream( device: &cpal::Device, - config: cpal::StreamConfig, + config: StreamConfig, ) -> Result { let num_channels = config.channels as usize; let mut oscillator = Oscillator { diff --git a/examples/feedback.rs b/examples/feedback.rs index 3890466af..98918dee1 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -9,7 +9,7 @@ use clap::Parser; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - HostUnavailable, + Error, ErrorKind, HostId, }; use ringbuf::{ traits::{Consumer, Producer, Split}, @@ -43,12 +43,12 @@ struct Opt { fn main() -> anyhow::Result<()> { let opt = Opt::parse(); - // Jack/PulseAudio support must be enabled at compile time, and is + // JACK/PulseAudio support must be enabled at compile time, and is // only available on some platforms. #[allow(unused_mut, unused_assignments)] - let mut jack_host_id = Err(HostUnavailable); + let mut jack_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] - let mut pulseaudio_host_id = Err(HostUnavailable); + let mut pulseaudio_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[cfg(any( target_os = "linux", @@ -59,12 +59,12 @@ fn main() -> anyhow::Result<()> { { #[cfg(feature = "jack")] { - jack_host_id = Ok(cpal::HostId::Jack); + jack_host_id = Ok(HostId::Jack); } #[cfg(feature = "pulseaudio")] { - pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); + pulseaudio_host_id = Ok(HostId::PulseAudio); } } @@ -171,6 +171,6 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn err_fn(err: cpal::StreamError) { +fn err_fn(err: Error) { eprintln!("an error occurred on stream: {err}"); } diff --git a/examples/ios-feedback/src/feedback.rs b/examples/ios-feedback/src/feedback.rs index d113174f3..0badbc598 100644 --- a/examples/ios-feedback/src/feedback.rs +++ b/examples/ios-feedback/src/feedback.rs @@ -103,6 +103,6 @@ pub fn run_example() -> Result<(), anyhow::Error> { Ok(()) } -fn err_fn(err: cpal::StreamError) { +fn err_fn(err: cpal::Error) { eprintln!("an error occurred on stream: {}", err); } diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 2fd55ed5f..5a8ca9a2e 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -4,7 +4,7 @@ use clap::Parser; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{FromSample, HostUnavailable, Sample}; +use cpal::{Error, ErrorKind, FromSample, HostId, Sample}; use std::fs::File; use std::io::BufWriter; use std::sync::{Arc, Mutex}; @@ -39,11 +39,11 @@ fn main() -> Result<(), anyhow::Error> { // Jack/PulseAudio support must be enabled at compile time, and is // only available on some platforms. #[allow(unused_mut, unused_assignments)] - let mut jack_host_id = Err(HostUnavailable); + let mut jack_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] - let mut pulseaudio_host_id = Err(HostUnavailable); + let mut pulseaudio_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] - let mut pipewire_host_id = Err(HostUnavailable); + let mut pipewire_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -53,16 +53,16 @@ fn main() -> Result<(), anyhow::Error> { { #[cfg(feature = "jack")] { - jack_host_id = Ok(cpal::HostId::Jack); + jack_host_id = Ok(HostId::Jack); } #[cfg(feature = "pulseaudio")] { - pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); + pulseaudio_host_id = Ok(HostId::PulseAudio); } #[cfg(feature = "pipewire")] { - pipewire_host_id = Ok(cpal::HostId::PipeWire); + pipewire_host_id = Ok(HostId::PipeWire); } } diff --git a/src/error.rs b/src/error.rs index 317fd1518..5864e4621 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,354 +1,164 @@ -use std::error::Error; +use std::borrow::Cow; +use std::error::Error as StdError; use std::fmt::{Display, Formatter}; -/// The requested host, although supported on this platform, is unavailable. +/// A list specifying general categories of CPAL error. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct HostUnavailable; - -impl Display for HostUnavailable { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str("the requested host is unavailable") - } -} - -impl Error for HostUnavailable {} - -/// 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 unforeseen 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 at -/// with details about your use case, the backend you're using, and the error message. Or submit -/// a pull request with a patch that adds the necessary error variant to the appropriate error enum. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct BackendSpecificError { - pub description: String, -} - -impl Display for BackendSpecificError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "A backend-specific error has occurred: {}", - self.description - ) - } -} - -impl Error for BackendSpecificError {} - -/// An error that might occur while attempting to enumerate the available devices on a system. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] -pub enum DevicesError { - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, -} - -impl Display for DevicesError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - } - } -} - -impl Error for DevicesError {} - -impl From for DevicesError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} - -/// An error that may occur while attempting to retrieve a device ID. -#[derive(Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum DeviceIdError { - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { - err: BackendSpecificError, - }, - UnsupportedPlatform, -} - -impl Display for DeviceIdError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::UnsupportedPlatform => f.write_str("Device IDs are unsupported for this OS"), - } - } -} - -impl Error for DeviceIdError {} - -impl From for DeviceIdError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} - -/// An error that may occur while attempting to retrieve a device name. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum DeviceNameError { - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, -} - -impl Display for DeviceNameError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - } - } -} - -impl Error for DeviceNameError {} - -impl From for DeviceNameError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} - -/// Error that can happen when enumerating the list of supported formats. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum SupportedStreamConfigsError { - /// The device no longer exists. This can happen if the device is disconnected while the - /// program is running. - DeviceNotAvailable, +pub enum ErrorKind { /// The device is temporarily busy. This can happen when another application or stream - /// is using the device. Retrying may succeed. + /// is using the device. Retrying after a short delay may succeed. DeviceBusy, - /// We called something the C-Layer did not understand - InvalidArgument, - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, -} -impl Display for SupportedStreamConfigsError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."), - Self::DeviceBusy => f.write_str("The requested device is temporarily busy. Another application or stream may be using it."), - Self::InvalidArgument => f.write_str("Invalid argument passed to the backend. For example, this happens when trying to read capture capabilities when the device does not support it.") - } - } -} + /// The requested audio device is not available. + /// + /// This can happen if the device has been disconnected while the program is running, or if + /// the device identifier refers to a device that does not exist on this system. + DeviceNotAvailable, -impl Error for SupportedStreamConfigsError {} + /// The audio host (server or subsystem) is not available on this system. + /// + /// This is distinct from [`DeviceNotAvailable`]: when a host (e.g. PulseAudio, PipeWire, JACK, + /// or kernel subsystem) is absent or not running, no devices can be reached through it. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable + HostUnavailable, -impl From for SupportedStreamConfigsError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} + /// Invalid input or argument. + InvalidInput, -/// May occur when attempting to request the default input or output stream format from a [`Device`](crate::Device). -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum DefaultStreamConfigError { - /// The device no longer exists. This can happen if the device is disconnected while the - /// program is running. - DeviceNotAvailable, - /// The device is temporarily busy. This can happen when another application or stream - /// is using the device. Retrying after a short delay may succeed. - DeviceBusy, - /// Returned if e.g. the default input format was requested on an output-only audio device. - StreamTypeNotSupported, - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, -} + /// Access to the device or resource was denied by the operating system or audio subsystem. + /// + /// The device exists and may be functional, but the current process or user does not have + /// permission to use it. Common causes include microphone privacy settings (iOS, macOS), + /// missing audio group membership (Linux), or file permission errors. + /// + /// Unlike [`DeviceNotAvailable`], which signals absence, this variant signals an + /// authorization failure. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable + PermissionDenied, -impl Display for DefaultStreamConfigError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::DeviceNotAvailable => f.write_str( - "The requested device is no longer available. For example, it has been unplugged.", - ), - Self::DeviceBusy => f.write_str( - "The requested device is temporarily busy. Another application or stream may be using it.", - ), - Self::StreamTypeNotSupported => { - f.write_str("The requested stream type is not supported by the device.") - } - } - } -} + /// The stream configuration is no longer valid and must be rebuilt. + StreamInvalidated, -impl Error for DefaultStreamConfigError {} + /// The requested stream configuration is not supported. This includes unsupported sample + /// rates, channel counts, or sample formats. + UnsupportedConfig, -impl From for DefaultStreamConfigError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} -/// Error that can happen when creating a [`Stream`](crate::Stream). -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum BuildStreamError { - /// The device no longer exists. This can happen if the device is disconnected while the - /// program is running. - DeviceNotAvailable, - /// The device is temporarily busy. This can happen when another application or stream - /// is using the device. Retrying may succeed. - DeviceBusy, - /// The specified stream configuration is not supported. - StreamConfigNotSupported, - /// We called something the C-Layer did not understand + /// The requested operation is not supported. This includes unsupported stream directions + /// (e.g., requesting input on an output-only device), unavailable features, or operations + /// not implemented by the backend. + UnsupportedOperation, + + /// A buffer underrun or overrun occurred, causing a potential audio glitch. + Xrun, + + /// A catch-all for errors that do not fall under any other CPAL error kind. + /// + /// CPAL itself emits this variant only for genuinely unclassifiable conditions. Treat them as + /// permanent: no retry strategy is possible without host-specific knowledge. /// - /// On ALSA device functions called with a feature they do not support will yield this. E.g. - /// Trying to use capture capabilities on an output only format yields this. - InvalidArgument, - /// Occurs if adding a new Stream ID would cause an integer overflow. - StreamIdOverflow, - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, + /// New [`ErrorKind`] variants may be added in future releases to cover specific cases + /// currently reported as `Other`. + Other, } -impl Display for BuildStreamError { +impl Display for ErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::BackendSpecific { err } => err.fmt(f), + Self::HostUnavailable => f.write_str( + "The requested audio host is not available. The subsystem or daemon may not be installed or running.", + ), Self::DeviceNotAvailable => f.write_str( - "The requested device is no longer available. For example, it has been unplugged.", + "The requested audio device is not available. It may have been disconnected.", ), Self::DeviceBusy => f.write_str( "The requested device is temporarily busy. Another application or stream may be using it.", ), - Self::StreamConfigNotSupported => { - f.write_str("The requested stream configuration is not supported by the device.") + Self::UnsupportedConfig => f.write_str( + "The requested stream configuration is not supported by the device.", + ), + Self::UnsupportedOperation => f.write_str("The requested operation is not supported."), + Self::InvalidInput => f.write_str("Invalid input or argument."), + Self::StreamInvalidated => { + f.write_str("The stream configuration is no longer valid and must be rebuilt.") } - Self::InvalidArgument => f.write_str( - "The requested device does not support this capability (invalid argument)", + Self::Xrun => f.write_str("A buffer underrun or overrun occurred."), + Self::PermissionDenied => f.write_str( + "Permission denied. Grant the required access and retry.", ), - Self::StreamIdOverflow => f.write_str("Adding a new stream ID would cause an overflow"), + Self::Other => f.write_str("An error occurred."), } } } -impl Error for BuildStreamError {} - -impl From for BuildStreamError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } - } -} - -/// Errors that might occur when calling [`Stream::play()`](crate::traits::StreamTrait::play). -/// -/// As of writing this, only macOS may immediately return an error while calling this method. This -/// is because both the alsa and wasapi backends only enqueue these commands and do not process -/// them immediately. +/// Error type for all CPAL operations. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum PlayStreamError { - /// The device associated with the stream is no longer available. - DeviceNotAvailable, - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, +pub struct Error { + kind: ErrorKind, + message: Option>, } -impl Display for PlayStreamError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::DeviceNotAvailable => { - f.write_str("the device associated with the stream is no longer available") - } +impl Error { + /// Create a new error with the given kind and no message. + pub fn new(kind: ErrorKind) -> Self { + Self { + kind, + message: None, } } -} -impl Error for PlayStreamError {} + /// Create a new error with the given kind and a human-readable message. + pub fn with_message(kind: ErrorKind, message: impl Into>) -> Self { + Self { + kind, + message: Some(message.into()), + } + } -impl From for PlayStreamError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } + /// Returns the error kind. + pub fn kind(&self) -> ErrorKind { + self.kind } -} -/// Errors that might occur when calling [`Stream::pause()`](crate::traits::StreamTrait::pause). -/// -/// As of writing this, only macOS may immediately return an error while calling this method. This -/// is because both the alsa and wasapi backends only enqueue these commands and do not process -/// them immediately. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum PauseStreamError { - /// The device associated with the stream is no longer available. - DeviceNotAvailable, - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, + /// Returns the human-readable message, if any. + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } } -impl Display for PauseStreamError { +impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::DeviceNotAvailable => { - f.write_str("the device associated with the stream is no longer available") - } + match &self.message { + Some(msg) => f.write_str(msg), + None => write!(f, "{}", self.kind), } } } -impl Error for PauseStreamError {} +impl StdError for Error {} -impl From for PauseStreamError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Self::new(kind) } } -/// Errors that might occur while a stream is running. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum StreamError { - /// The device no longer exists. This can happen if the device is disconnected while the - /// program is running. - DeviceNotAvailable, - - /// The stream configuration is no longer valid and must be rebuilt. - StreamInvalidated, - - /// Buffer underrun or overrun occurred, causing a potential audio glitch. - BufferUnderrun, - - /// See the [`BackendSpecificError`] docs for more information about this error variant. - BackendSpecific { err: BackendSpecificError }, -} - -impl Display for StreamError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::BackendSpecific { err } => err.fmt(f), - Self::StreamInvalidated => { - f.write_str("The stream configuration is no longer valid and must be rebuilt.") - } - Self::BufferUnderrun => f.write_str("Buffer underrun/overrun occurred."), - Self::DeviceNotAvailable => f.write_str( - "The requested device is no longer available. For example, it has been unplugged.", - ), - } - } +/// Extension trait for attaching a context message to a [`Result`] whose error converts into +/// [`cpal::Error`]. +#[allow(dead_code)] +pub(crate) trait ResultExt { + /// Converts the error via [`Into`] and prepends `msg`, yielding + /// `": "` as the message. + fn context(self, msg: &'static str) -> Result; } -impl Error for StreamError {} - -impl From for StreamError { - fn from(err: BackendSpecificError) -> Self { - Self::BackendSpecific { err } +impl> ResultExt for Result { + fn context(self, msg: &'static str) -> Result { + self.map_err(|e| { + let e = e.into(); + Error::with_message(e.kind(), format!("{msg}: {e}")) + }) } } diff --git a/src/host/aaudio/convert.rs b/src/host/aaudio/convert.rs index ab7e44a83..0021bce1e 100644 --- a/src/host/aaudio/convert.rs +++ b/src/host/aaudio/convert.rs @@ -2,10 +2,7 @@ extern crate ndk; -use crate::{ - BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, StreamError, - StreamInstant, -}; +use crate::{Error, ErrorKind, StreamInstant}; /// Returns a [`StreamInstant`] for the current moment. pub fn now_stream_instant() -> StreamInstant { @@ -58,57 +55,29 @@ pub fn input_stream_instant(stream: &ndk::audio::AudioStream, sample_rate: u32) } } -impl From for StreamError { +impl From for Error { fn from(error: ndk::audio::AudioError) -> Self { - use self::ndk::audio::AudioError::*; + use ndk::audio::AudioError::*; match error { - Disconnected | Unavailable => Self::DeviceNotAvailable, - e => (BackendSpecificError { - description: e.to_string(), - }) - .into(), - } - } -} - -impl From for PlayStreamError { - fn from(error: ndk::audio::AudioError) -> Self { - use self::ndk::audio::AudioError::*; - match error { - Disconnected | Unavailable => Self::DeviceNotAvailable, - e => (BackendSpecificError { - description: e.to_string(), - }) - .into(), - } - } -} - -impl From for PauseStreamError { - fn from(error: ndk::audio::AudioError) -> Self { - use self::ndk::audio::AudioError::*; - match error { - Disconnected | Unavailable => Self::DeviceNotAvailable, - e => (BackendSpecificError { - description: e.to_string(), - }) - .into(), - } - } -} - -impl From for BuildStreamError { - fn from(error: ndk::audio::AudioError) -> Self { - use self::ndk::audio::AudioError::*; - match error { - Disconnected | Unavailable => Self::DeviceNotAvailable, - NoFreeHandles => Self::StreamIdOverflow, - InvalidFormat | InvalidRate => Self::StreamConfigNotSupported, - IllegalArgument => Self::InvalidArgument, - e => (BackendSpecificError { - description: e.to_string(), - }) - .into(), + Disconnected | Unavailable | NoService | InvalidHandle => { + Error::with_message(ErrorKind::DeviceNotAvailable, error.to_string()) + } + NoFreeHandles | NoMemory | WouldBlock | Timeout => { + Error::with_message(ErrorKind::DeviceBusy, error.to_string()) + } + InvalidFormat | InvalidRate => { + Error::with_message(ErrorKind::UnsupportedConfig, error.to_string()) + } + IllegalArgument | Null | OutOfRange => { + Error::with_message(ErrorKind::InvalidInput, error.to_string()) + } + Internal | InvalidState => { + Error::with_message(ErrorKind::StreamInvalidated, error.to_string()) + } + Unimplemented => { + Error::with_message(ErrorKind::UnsupportedOperation, error.to_string()) + } + _ => Error::with_message(ErrorKind::Other, error.to_string()), } } } diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 9dac62150..397b36da8 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -15,13 +15,12 @@ use convert::{input_stream_instant, now_stream_instant, output_stream_instant}; use java_interface::{AudioDeviceInfo, AudioManager}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{error::ResultExt, Error, ErrorKind}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, - DeviceNameError, DeviceType, DevicesError, FrameCount, InputCallbackInfo, InputStreamTimestamp, - InterfaceType, OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, - SampleFormat, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BufferSize, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, + DeviceType, FrameCount, InputCallbackInfo, InputStreamTimestamp, InterfaceType, + OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, StreamConfig, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, }; mod convert; @@ -164,7 +163,7 @@ pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; pub type Devices = std::vec::IntoIter; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } @@ -177,7 +176,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { if let Ok(devices) = AudioDeviceInfo::request(DeviceDirection::Duplex) { Ok(devices .into_iter() @@ -306,10 +305,10 @@ fn build_input_stream( mut error_callback: E, builder: ndk::audio::AudioStreamBuilder, sample_format: SampleFormat, -) -> Result +) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let builder = configure_for_device(builder, device, config); let channel_count = config.channels as i32; @@ -335,7 +334,7 @@ where ndk::audio::AudioCallbackResult::Continue })) .error_callback(Box::new(move |_stream, error| { - (error_callback)(StreamError::from(error)) + (error_callback)(Error::from(error)) })) .open_stream()?; @@ -356,10 +355,10 @@ fn build_output_stream( mut error_callback: E, builder: ndk::audio::AudioStreamBuilder, sample_format: SampleFormat, -) -> Result +) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let builder = configure_for_device(builder, device, config); let channel_count = config.channels as i32; @@ -432,7 +431,7 @@ where ndk::audio::AudioCallbackResult::Continue })) .error_callback(Box::new(move |_stream, error| { - (error_callback)(StreamError::from(error)) + (error_callback)(Error::from(error)) })) .open_stream()?; @@ -468,7 +467,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { + fn name(&self) -> Result { match &self.0 { None => Ok("default".to_string()), Some(info) => { @@ -485,7 +484,7 @@ impl DeviceTrait for Device { } } - fn description(&self) -> Result { + fn description(&self) -> Result { match &self.0 { None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()), Some(info) => { @@ -509,7 +508,7 @@ impl DeviceTrait for Device { } } - fn id(&self) -> Result { + fn id(&self) -> Result { let device_str = match &self.0 { None => "-1".to_string(), // Default device Some(info) => info.id.to_string(), @@ -517,17 +516,14 @@ impl DeviceTrait for Device { Ok(DeviceId(crate::platform::HostId::AAudio, device_str)) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { if let Some(info) = &self.0 { // Output-only devices do not support input if matches!(info.direction, DeviceDirection::Output) { - return Err(SupportedStreamConfigsError::BackendSpecific { - err: BackendSpecificError { - description: "output-only device does not support input".to_string(), - }, - }); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "output-only device does not support input", + )); } Ok(device_supported_configs(info)) } else { @@ -535,17 +531,14 @@ impl DeviceTrait for Device { } } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { if let Some(info) = &self.0 { // Input-only devices do not support output if matches!(info.direction, DeviceDirection::Input) { - return Err(SupportedStreamConfigsError::BackendSpecific { - err: BackendSpecificError { - description: "input-only device does not support output".to_string(), - }, - }); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "input-only device does not support output", + )); } Ok(device_supported_configs(info)) } else { @@ -553,24 +546,34 @@ impl DeviceTrait for Device { } } - fn default_input_config(&self) -> Result { - let mut configs: Vec<_> = self.supported_input_configs().unwrap().collect(); + fn default_input_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_input_configs()?.collect(); configs.sort_by(|a, b| b.cmp_default_heuristics(a)); let config = configs .into_iter() .next() - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)? + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "no supported input configuration", + ) + })? .with_max_sample_rate(); Ok(config) } - fn default_output_config(&self) -> Result { - let mut configs: Vec<_> = self.supported_output_configs().unwrap().collect(); + fn default_output_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_output_configs()?.collect(); configs.sort_by(|a, b| b.cmp_default_heuristics(a)); let config = configs .into_iter() .next() - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)? + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "no supported output configuration", + ) + })? .with_max_sample_rate(); Ok(config) } @@ -582,19 +585,19 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let format = match sample_format { SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, sample_format => { - return Err(BackendSpecificError { - description: format!("{} format is not supported on Android.", sample_format), - } - .into()) + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("{sample_format} format is not supported on Android"), + )) } }; let channel_count = match config.channels { @@ -602,13 +605,10 @@ impl DeviceTrait for Device { 2 => 2, channels => { // TODO: more channels available in native AAudio - return Err(BackendSpecificError { - description: format!( - "{} channels are not supported yet (only 1 or 2).", - channels - ), - } - .into()); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("{channels} channels are not supported yet (only 1 or 2)"), + )); } }; @@ -634,19 +634,19 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let format = match sample_format { SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, sample_format => { - return Err(BackendSpecificError { - description: format!("{} format is not supported on Android.", sample_format), - } - .into()) + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("{sample_format} format is not supported on Android"), + )) } }; let channel_count = match config.channels { @@ -654,13 +654,10 @@ impl DeviceTrait for Device { 2 => 2, channels => { // TODO: more channels available in native AAudio - return Err(BackendSpecificError { - description: format!( - "{} channels are not supported yet (only 1 or 2).", - channels - ), - } - .into()); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("{channels} channels are not supported yet (only 1 or 2)"), + )); } }; @@ -681,37 +678,45 @@ impl DeviceTrait for Device { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { - let stream = self.inner.lock().unwrap(); + fn play(&self) -> Result<(), Error> { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; - stream.request_start().map_err(PlayStreamError::from)?; + stream + .request_start() + .context("failed to start AAudio stream")?; stream .wait_for_state_change( ndk::audio::AudioStreamState::Starting, DEFAULT_TIMEOUT_NANOS, ) .map(|_| ()) - .map_err(PlayStreamError::from) + .context("failed to wait for AAudio stream to start") } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { match self.direction { DeviceDirection::Output => { - let stream = self.inner.lock().unwrap(); + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; - stream.request_pause().map_err(PauseStreamError::from)?; + stream + .request_pause() + .context("failed to pause AAudio stream")?; stream .wait_for_state_change( ndk::audio::AudioStreamState::Pausing, DEFAULT_TIMEOUT_NANOS, ) .map(|_| ()) - .map_err(PauseStreamError::from) - } - _ => Err(BackendSpecificError { - description: "Pause only supported on output streams.".to_owned(), + .context("failed to wait for AAudio stream to pause") } - .into()), + _ => Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "pause only supported on output streams", + )), } } @@ -719,8 +724,10 @@ impl StreamTrait for Stream { now_stream_instant() } - fn buffer_size(&self) -> Result { - let stream = self.inner.lock().unwrap(); + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; // frames_per_data_callback is only set for BufferSize::Fixed; for Default AAudio // schedules callbacks at the burst size, so that is the best available estimate. diff --git a/src/host/alsa/enumerate.rs b/src/host/alsa/enumerate.rs index 0b880df97..1e5405928 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use super::{alsa, Device, Host}; -use crate::{BackendSpecificError, DeviceDirection, DevicesError}; +use crate::{DeviceDirection, Error}; const HW_PREFIX: &str = "hw"; const PLUGHW_PREFIX: &str = "plughw"; @@ -24,7 +24,7 @@ impl Host { /// We enumerate both ALSA hints and physical devices because: /// - Hints provide virtual devices, user configs, and card-specific devices with metadata /// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility - pub(super) fn enumerate_devices(&self) -> Result { + pub(super) fn enumerate_devices(&self) -> Result { let mut devices = Vec::new(); let mut seen_pcm_ids = HashSet::new(); @@ -147,13 +147,6 @@ fn physical_devices() -> Vec { devices } -impl From for DevicesError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} - impl From for DeviceDirection { fn from(direction: alsa::Direction) -> Self { match direction { diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index d2eedb42b..d3a7e2be0 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -23,12 +23,10 @@ use crate::{ host::fill_with_equilibrium, iter::{SupportedInputConfigs, SupportedOutputConfigs}, traits::{DeviceTrait, HostTrait, StreamTrait}, - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, - DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, - OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, - StreamError, StreamInstant, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + SampleRate, StreamConfig, StreamInstant, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, }; mod enumerate; @@ -99,8 +97,10 @@ pub struct Host { } impl Host { - pub fn new() -> Result { - let inner = AlsaContext::new().map_err(|_| crate::HostUnavailable)?; + pub fn new() -> Result { + let inner = AlsaContext::new().map_err(|e| { + Error::with_message(ErrorKind::HostUnavailable, format!("ALSA unavailable: {e}")) + })?; Ok(Host { inner: Arc::new(inner), }) @@ -116,7 +116,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { self.enumerate_devices() } @@ -170,15 +170,15 @@ impl DeviceTrait for Device { type Stream = Stream; // ALSA overrides name() to return pcm_id directly instead of from description - fn name(&self) -> Result { + fn name(&self) -> Result { Device::name(self) } - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } @@ -201,23 +201,19 @@ impl DeviceTrait for Device { ) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -228,10 +224,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let stream_inner = self.build_stream_inner(conf, sample_format, alsa::Direction::Capture)?; @@ -251,10 +247,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let stream_inner = self.build_stream_inner(conf, sample_format, alsa::Direction::Playback)?; @@ -360,7 +356,7 @@ impl Device { conf: StreamConfig, sample_format: SampleFormat, stream_type: alsa::Direction, - ) -> Result { + ) -> Result { // Validate buffer size if Fixed is specified. This is necessary because // `set_period_size_near()` with `ValueOr::Nearest` will accept ANY value and return the // "nearest" supported value, which could be wildly different (e.g., requesting 4096 frames @@ -377,27 +373,18 @@ impl Device { if let Ok(config) = supported_config { if let SupportedBufferSize::Range { min, max } = config.buffer_size { if !(min..=max).contains(&requested_size) { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("buffer size {requested_size} is not in the supported range {min}..={max}"), + )); } } } } - let open_result = { + let handle = { let _guard = ALSA_OPEN_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); - alsa::pcm::PCM::new(&self.pcm_id, stream_type, true).map_err(|e| (e, e.errno())) - }; - let handle = match open_result { - Err((_, libc::ENOENT)) - | Err((_, libc::EPERM)) - | Err((_, libc::ENODEV)) - | Err((_, LIBC_ENOTSUPP)) => return Err(BuildStreamError::DeviceNotAvailable), - Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => { - return Err(BuildStreamError::DeviceBusy) - } - Err((_, libc::EINVAL)) => return Err(BuildStreamError::InvalidArgument), - Err((e, _)) => return Err(e.into()), - Ok(handle) => handle, + alsa::pcm::PCM::new(&self.pcm_id, stream_type, true)? }; let can_pause = set_hw_params_from_format(&handle, conf, sample_format)?; @@ -407,9 +394,10 @@ impl Device { let num_descriptors = handle.count(); if num_descriptors == 0 { - let description = "poll descriptor count for stream was 0".to_string(); - let err = BackendSpecificError { description }; - return Err(err.into()); + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "poll descriptor count for stream was 0", + )); } // Check to see if we can retrieve valid timestamps from the device. @@ -452,11 +440,11 @@ impl Device { Ok(stream_inner) } - fn name(&self) -> Result { + fn name(&self) -> Result { Ok(self.pcm_id.clone()) } - fn description(&self) -> Result { + fn description(&self) -> Result { let name = self .desc .as_ref() @@ -480,31 +468,17 @@ impl Device { Ok(builder.build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone())) } fn supported_configs( &self, stream_t: alsa::Direction, - ) -> Result, SupportedStreamConfigsError> { - let open_result = { + ) -> Result, Error> { + let pcm = { let _guard = ALSA_OPEN_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); - alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno())) - }; - let pcm = match open_result { - Err((_, libc::ENOENT)) - | Err((_, libc::EPERM)) - | Err((_, libc::ENODEV)) - | Err((_, LIBC_ENOTSUPP)) => { - return Err(SupportedStreamConfigsError::DeviceNotAvailable) - } - Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => { - return Err(SupportedStreamConfigsError::DeviceBusy) - } - Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument), - Err((e, _)) => return Err(e.into()), - Ok(pcm) => pcm, + alsa::pcm::PCM::new(&self.pcm_id, stream_t, true)? }; let hw_params = alsa::pcm::HwParams::any(&pcm)?; @@ -629,40 +603,27 @@ impl Device { Ok(output.into_iter()) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { self.supported_configs(alsa::Direction::Capture) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { self.supported_configs(alsa::Direction::Playback) } // ALSA does not offer default stream formats, so instead we compare all supported formats by // the `SupportedStreamConfigRange::cmp_default_heuristics` order and select the greatest. - fn default_config( - &self, - stream_t: alsa::Direction, - ) -> Result { + fn default_config(&self, stream_t: alsa::Direction) -> Result { let mut formats: Vec<_> = { match self.supported_configs(stream_t) { - Err(SupportedStreamConfigsError::DeviceNotAvailable) => { - return Err(DefaultStreamConfigError::DeviceNotAvailable); - } - Err(SupportedStreamConfigsError::DeviceBusy) => { - return Err(DefaultStreamConfigError::DeviceBusy); - } - Err(SupportedStreamConfigsError::InvalidArgument) => { - // this happens sometimes when querying for input and output capabilities, but - // the device supports only one - return Err(DefaultStreamConfigError::StreamTypeNotSupported); - } - Err(SupportedStreamConfigsError::BackendSpecific { err }) => { - return Err(err.into()); + // EINVAL when querying direction the device does not support (input-only or output-only) + Err(err) if err.kind() == ErrorKind::InvalidInput => { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support the requested direction", + )); } + Err(err) => return Err(err), Ok(fmts) => fmts.collect(), } }; @@ -680,15 +641,18 @@ impl Device { } Ok(format) } - None => Err(DefaultStreamConfigError::StreamTypeNotSupported), + None => Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "no supported configuration for this device", + )), } } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { self.default_config(alsa::Direction::Capture) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { self.default_config(alsa::Direction::Playback) } } @@ -833,7 +797,7 @@ fn input_stream_worker( rx: Arc, stream: &StreamInner, data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), - error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + error_callback: &mut (dyn FnMut(Error) + Send + 'static), timeout: Option, ) { boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); @@ -858,20 +822,20 @@ fn input_stream_worker( Err(err) => Err(err), }; if let Err(err) = result { - match err { - StreamError::BufferUnderrun => { - error_callback(StreamError::BufferUnderrun); + match err.kind() { + ErrorKind::Xrun => { + error_callback(err); if let Err(err) = stream.channel.prepare() { error_callback(err.into()); } else if let Err(err) = stream.channel.start() { error_callback(err.into()); } } - StreamError::DeviceNotAvailable => { - error_callback(StreamError::DeviceNotAvailable); + ErrorKind::DeviceNotAvailable => { + error_callback(err); return; } - err => error_callback(err), + _ => error_callback(err), } } } @@ -881,7 +845,7 @@ fn output_stream_worker( rx: Arc, stream: &StreamInner, data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), - error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + error_callback: &mut (dyn FnMut(Error) + Send + 'static), timeout: Option, ) { boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); @@ -907,9 +871,9 @@ fn output_stream_worker( Err(err) => Err(err), }; if let Err(err) = result { - match err { - StreamError::BufferUnderrun => { - error_callback(StreamError::BufferUnderrun); + match err.kind() { + ErrorKind::Xrun => { + error_callback(err); if let Err(err) = stream.channel.prepare() { error_callback(err.into()); } @@ -917,11 +881,11 @@ fn output_stream_worker( // ALSA automatically restarts them when the buffer is refilled // and the stream is triggered again. } - StreamError::DeviceNotAvailable => { - error_callback(StreamError::DeviceNotAvailable); + ErrorKind::DeviceNotAvailable => { + error_callback(err); return; } - err => error_callback(err), + _ => error_callback(err), } } } @@ -947,7 +911,7 @@ fn boost_current_thread_priority(buffer_size: BufferSize, sample_rate: SampleRat fn boost_current_thread_priority(_: BufferSize, _: SampleRate) {} /// Attempt hardware resume from a suspend event (`ESTRPIPE`). -fn try_resume(channel: &alsa::PCM) -> Result { +fn try_resume(channel: &alsa::PCM) -> Result { match channel.resume() { Ok(()) => { if channel @@ -969,7 +933,9 @@ fn try_resume(channel: &alsa::PCM) -> Result { // device is still resuming; poll again until it is ready. Err(e) if e.errno() == libc::EAGAIN => Ok(Poll::Pending), // hardware does not support soft resume - Err(e) if e.errno() == libc::ENOSYS => Err(StreamError::BufferUnderrun), + Err(e) if e.errno() == libc::ENOSYS => { + Err(Error::with_message(ErrorKind::Xrun, e.to_string())) + } Err(e) => Err(e.into()), } } @@ -987,7 +953,7 @@ fn poll_for_period( rx: &TriggerReceiver, stream: &StreamInner, ctxt: &mut StreamWorkerContext, -) -> Result { +) -> Result { let StreamWorkerContext { ref mut descriptors, ref poll_timeout, @@ -1014,7 +980,10 @@ fn poll_for_period( } // POLLHUP/POLLNVAL: the device has been disconnected. if revents.intersects(alsa::poll::Flags::HUP | alsa::poll::Flags::NVAL) { - return Err(StreamError::DeviceNotAvailable); + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "device disconnected", + )); } // POLLERR signals an xrun or suspend; avail() below returns EPIPE/ESTRPIPE accordingly. // POLLIN/POLLOUT: data is ready, fall through to process it. @@ -1022,7 +991,9 @@ fn poll_for_period( let status = stream.channel.status()?; let avail_frames = match stream.channel.avail() { // Xrun: recover via prepare() (+ start() for capture, handled by the worker). - Err(err) if err.errno() == libc::EPIPE => return Err(StreamError::BufferUnderrun), + Err(err) if err.errno() == libc::EPIPE => { + return Err(Error::with_message(ErrorKind::Xrun, err.to_string())) + } // Suspend: try hardware resume first; fall back to prepare() if unsupported. Err(err) if err.errno() == libc::ESTRPIPE => return try_resume(&stream.channel), res => res, @@ -1054,7 +1025,7 @@ fn process_input( status: alsa::pcm::Status, delay_frames: usize, data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), -) -> Result<(), StreamError> { +) -> Result<(), Error> { let mut frames_read = 0; while frames_read < stream.period_frames { match stream @@ -1069,17 +1040,18 @@ fn process_input( if frames_read == 0 { return Ok(()); } else { - return Err(StreamError::BufferUnderrun); + return Err(Error::with_message(ErrorKind::Xrun, err.to_string())); } } // EPIPE = xrun: full underrun recovery (prepare + start) required. - Err(err) if err.errno() == libc::EPIPE => return Err(StreamError::BufferUnderrun), + Err(err) if err.errno() == libc::EPIPE => { + return Err(Error::with_message(ErrorKind::Xrun, err.to_string())) + } // ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun // recovery if the hardware doesn't support it. Err(err) if err.errno() == libc::ESTRPIPE => { return try_resume(&stream.channel).map(|_| ()); } - Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable), Err(err) => return Err(err.into()), } } @@ -1109,7 +1081,7 @@ fn process_output( status: alsa::pcm::Status, delay_frames: usize, data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), -) -> Result<(), StreamError> { +) -> Result<(), Error> { // Buffer is always pre-filled with equilibrium, user overwrites what they want buffer.copy_from_slice(&stream.silence_template); { @@ -1144,17 +1116,18 @@ fn process_output( if frames_written == 0 { return Ok(()); } else { - return Err(StreamError::BufferUnderrun); + return Err(Error::with_message(ErrorKind::Xrun, err.to_string())); } } // EPIPE = xrun: full underrun recovery (prepare) required. - Err(err) if err.errno() == libc::EPIPE => return Err(StreamError::BufferUnderrun), + Err(err) if err.errno() == libc::EPIPE => { + return Err(Error::with_message(ErrorKind::Xrun, err.to_string())) + } // ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun // recovery if the hardware doesn't support it. Err(err) if err.errno() == libc::ESTRPIPE => { return try_resume(&stream.channel).map(|_| ()); } - Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable), Err(err) => return Err(err.into()), } } @@ -1165,9 +1138,7 @@ fn process_output( // // This ensures accurate timestamps based on actual hardware timing. #[inline] -fn stream_timestamp_hardware( - status: &alsa::pcm::Status, -) -> Result { +fn stream_timestamp_hardware(status: &alsa::pcm::Status) -> Result { let trigger_ts = status.get_trigger_htstamp(); // trigger_htstamp records when the PCM stream started. // On the first few callbacks, it might not have been set yet, @@ -1175,18 +1146,21 @@ fn stream_timestamp_hardware( // once it is set. Bail out and let the caller use the fallback. // See https://github.com/RustAudio/cpal/issues/710 if trigger_ts.tv_sec == 0 && trigger_ts.tv_nsec == 0 { - return Err(BackendSpecificError { - description: "trigger_htstamp not yet set".to_string(), - }); + return Err(Error::with_message( + ErrorKind::Other, + "trigger_htstamp not yet set", + )); } let ts = status.get_htstamp(); let nanos = timespec_diff_nanos(ts, trigger_ts); if nanos < 0 { - let description = format!( - "get_htstamp `{}.{}` was earlier than get_trigger_htstamp `{}.{}`", - ts.tv_sec, ts.tv_nsec, trigger_ts.tv_sec, trigger_ts.tv_nsec - ); - return Err(BackendSpecificError { description }); + return Err(Error::with_message( + ErrorKind::Other, + format!( + "get_htstamp `{}.{}` was earlier than get_trigger_htstamp `{}.{}`", + ts.tv_sec, ts.tv_nsec, trigger_ts.tv_sec, trigger_ts.tv_nsec + ), + )); } Ok(StreamInstant::from_nanos(nanos as u64)) } @@ -1195,9 +1169,7 @@ fn stream_timestamp_hardware( // // This ensures positive values that are compatible with our `StreamInstant` representation. #[inline] -fn stream_timestamp_fallback( - creation: std::time::Instant, -) -> Result { +fn stream_timestamp_fallback(creation: std::time::Instant) -> Result { let now = std::time::Instant::now(); let duration = now.duration_since(creation); Ok(StreamInstant::new( @@ -1239,7 +1211,7 @@ impl Stream { ) -> Stream where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let (tx, rx) = trigger(); let rx_thread = rx.clone(); @@ -1272,7 +1244,7 @@ impl Stream { ) -> Stream where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let (tx, rx) = trigger(); let rx_thread = rx.clone(); @@ -1309,11 +1281,11 @@ impl Drop for Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { self.inner.channel.pause(false).ok(); Ok(()) } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { self.inner.channel.pause(true).ok(); Ok(()) } @@ -1329,7 +1301,7 @@ impl StreamTrait for Stream { .expect("stream duration exceeded `StreamInstant` range") } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.inner.period_frames as FrameCount) } } @@ -1356,7 +1328,7 @@ fn init_hw_params<'a>( pcm_handle: &'a alsa::pcm::PCM, config: StreamConfig, sample_format: SampleFormat, -) -> Result, BackendSpecificError> { +) -> Result, Error> { let hw_params = alsa::pcm::HwParams::any(pcm_handle)?; hw_params.set_access(alsa::pcm::Access::RWInterleaved)?; @@ -1376,7 +1348,7 @@ fn init_hw_params<'a>( fn sample_format_to_alsa_format( hw_params: &alsa::pcm::HwParams, sample_format: SampleFormat, -) -> Result { +) -> Result { use alsa::pcm::Format; // For each sample format, define (native_endian_format, opposite_endian_format) pairs @@ -1425,9 +1397,10 @@ fn sample_format_to_alsa_format( #[cfg(target_endian = "big")] SampleFormat::DsdU32 => (Format::DSDU32BE, Format::DSDU32LE), _ => { - return Err(BackendSpecificError { - description: format!("Sample format '{sample_format}' is not supported"), - }) + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format '{sample_format}' is not supported"), + )) } }; @@ -1441,18 +1414,17 @@ fn sample_format_to_alsa_format( return Ok(opposite); } - Err(BackendSpecificError { - description: format!( - "Sample format '{sample_format}' is not supported by hardware in any endianness" - ), - }) + Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format '{sample_format}' is not supported by hardware in any endianness"), + )) } fn set_hw_params_from_format( pcm_handle: &alsa::pcm::PCM, config: StreamConfig, sample_format: SampleFormat, -) -> Result { +) -> Result { let hw_params = init_hw_params(pcm_handle, config, sample_format)?; // When BufferSize::Fixed(x) is specified, we configure double-buffering with @@ -1491,15 +1463,16 @@ fn set_sw_params_from_format( pcm_handle: &alsa::pcm::PCM, config: StreamConfig, stream_type: alsa::Direction, -) -> Result { +) -> Result { let sw_params = pcm_handle.sw_params_current()?; let period_samples = { let (buffer, period) = pcm_handle.get_params()?; if buffer == 0 { - return Err(BackendSpecificError { - description: "initialization resulted in a null buffer".to_string(), - }); + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "initialization resulted in a null buffer", + )); } let start_threshold = match stream_type { alsa::Direction::Playback => { @@ -1544,45 +1517,22 @@ fn canonical_pcm_id(pcm_id: &str) -> String { pcm_id.to_owned() } -impl From for BackendSpecificError { +impl From for Error { fn from(err: alsa::Error) -> Self { - Self { - description: err.to_string(), + match err.errno() { + libc::ENODEV | libc::ENOENT | LIBC_ENOTSUPP => { + Error::with_message(ErrorKind::DeviceNotAvailable, err.to_string()) + } + libc::EPERM | libc::EACCES => { + Error::with_message(ErrorKind::PermissionDenied, err.to_string()) + } + libc::EBUSY | libc::EAGAIN => { + Error::with_message(ErrorKind::DeviceBusy, err.to_string()) + } + libc::EINVAL => Error::with_message(ErrorKind::InvalidInput, err.to_string()), + libc::EPIPE => Error::with_message(ErrorKind::Xrun, err.to_string()), + libc::ENOSYS => Error::with_message(ErrorKind::UnsupportedOperation, err.to_string()), + _ => Error::with_message(ErrorKind::Other, err.to_string()), } } } - -impl From for BuildStreamError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} - -impl From for SupportedStreamConfigsError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} - -impl From for PlayStreamError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} - -impl From for PauseStreamError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} - -impl From for StreamError { - fn from(err: alsa::Error) -> Self { - let err: BackendSpecificError = err.into(); - err.into() - } -} diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 82e50d24d..75eb4ab30 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -3,20 +3,17 @@ pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use super::sys; use crate::host::com; use crate::ChannelCount; -use crate::DefaultStreamConfigError; use crate::DeviceDescription; use crate::DeviceDescriptionBuilder; use crate::DeviceId; -use crate::DeviceIdError; -use crate::DeviceNameError; -use crate::DevicesError; +use crate::Error; +use crate::ErrorKind; use crate::FrameCount; use crate::SampleFormat; use crate::SampleRate; use crate::SupportedBufferSize; use crate::SupportedStreamConfig; use crate::SupportedStreamConfigRange; -use crate::SupportedStreamConfigsError; use std::hash::{Hash, Hasher}; use std::sync::atomic::AtomicU32; @@ -66,7 +63,7 @@ impl Hash for Device { } impl Device { - pub fn description(&self) -> Result { + pub fn description(&self) -> Result { let direction = crate::device_description::direction_from_counts( Some(self.channels_in), Some(self.channels_out), @@ -78,41 +75,33 @@ impl Device { .build()) } - pub fn id(&self) -> Result { + pub fn id(&self) -> Result { Ok(DeviceId(crate::platform::HostId::Asio, self.name.clone())) } /// Gets the supported input configs. /// TODO currently only supports the default. /// Need to find all possible configs. - pub fn supported_input_configs( - &self, - ) -> Result { - let default = self - .default_input_config() - .map_err(|_| SupportedStreamConfigsError::DeviceNotAvailable)?; + pub fn supported_input_configs(&self) -> Result { + let default = self.default_input_config()?; Ok(self.configs_for(default).into_iter()) } /// Gets the supported output configs. /// TODO currently only supports the default. /// Need to find all possible configs. - pub fn supported_output_configs( - &self, - ) -> Result { - let default = self - .default_output_config() - .map_err(|_| SupportedStreamConfigsError::DeviceNotAvailable)?; + pub fn supported_output_configs(&self) -> Result { + let default = self.default_output_config()?; Ok(self.configs_for(default).into_iter()) } /// Returns the default input config - pub fn default_input_config(&self) -> Result { + pub fn default_input_config(&self) -> Result { self.default_config(self.channels_in, self.input_sample_format) } /// Returns the default output config - pub fn default_output_config(&self) -> Result { + pub fn default_output_config(&self) -> Result { self.default_config(self.channels_out, self.output_sample_format) } @@ -120,12 +109,19 @@ impl Device { &self, channels: ChannelCount, sample_format: Option, - ) -> Result { + ) -> Result { if channels == 0 { - return Err(DefaultStreamConfigError::StreamTypeNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "ASIO device reports no channels for this direction", + )); } - let sample_format = - sample_format.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; + let sample_format = sample_format.ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedOperation, + "no supported sample format for this ASIO device", + ) + })?; Ok(SupportedStreamConfig { channels, sample_rate: self.sample_rate, @@ -155,7 +151,7 @@ impl Device { } impl Devices { - pub fn new(asio: Arc) -> Result { + pub fn new(asio: Arc) -> Result { // Make sure that COM is initialized. com::com_initialized(); let drivers = asio.driver_names().into_iter(); diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 60e1ca4ef..0d6e52f85 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -8,10 +8,8 @@ extern crate asio_sys as sys; use crate::host::com; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, - SupportedStreamConfigsError, + Data, DeviceDescription, DeviceId, Error, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + StreamConfig, SupportedStreamConfig, }; pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; @@ -35,7 +33,7 @@ pub struct Host { } impl Host { - pub fn new() -> Result { + pub fn new() -> Result { com::com_initialized(); let asio = GLOBAL_ASIO .get_or_init(|| Arc::new(sys::Asio::new())) @@ -54,7 +52,7 @@ impl HostTrait for Host { //unimplemented!("check how to do this using asio-sys") } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new(self.asio.clone()) } @@ -74,31 +72,27 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -109,10 +103,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { Device::build_input_stream_raw( self, @@ -131,10 +125,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { Device::build_output_stream_raw( self, @@ -148,11 +142,11 @@ impl DeviceTrait for Device { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { Stream::play(self) } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { Stream::pause(self) } @@ -160,7 +154,7 @@ impl StreamTrait for Stream { Stream::now(self) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Stream::buffer_size(self) } } diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 4d65d068c..e09a54ad0 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -7,9 +7,8 @@ use crate::I24; use self::num_traits::{FromPrimitive, PrimInt}; use super::Device; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, InputCallbackInfo, - OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, - StreamInstant, + BufferSize, Data, Error, ErrorKind, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + StreamConfig, StreamInstant, }; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; @@ -73,18 +72,20 @@ impl Stream { self.time_base.to_stream_instant(ms as u64 * 1_000_000) } - pub fn play(&self) -> Result<(), PlayStreamError> { + pub fn play(&self) -> Result<(), Error> { self.playing.store(true, Ordering::Release); Ok(()) } - pub fn pause(&self) -> Result<(), PauseStreamError> { + pub fn pause(&self) -> Result<(), Error> { self.playing.store(false, Ordering::Release); Ok(()) } - pub fn buffer_size(&self) -> Result { - let streams = self.asio_streams.lock().unwrap(); + pub fn buffer_size(&self) -> Result { + let streams = self.asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; Ok(streams .output .as_ref() @@ -102,28 +103,38 @@ impl Device { mut data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { com::com_initialized(); - let description = self - .description() - .map_err(|_| BuildStreamError::DeviceNotAvailable)?; + let description = self.description()?; let driver = super::GLOBAL_ASIO .get() - .ok_or(BuildStreamError::DeviceNotAvailable)? + .ok_or_else(|| { + Error::with_message(ErrorKind::DeviceNotAvailable, "ASIO driver not initialized") + })? .load_driver(description.name()) .map_err(load_driver_err)?; let stream_type = driver.input_data_type().map_err(build_stream_err)?; // Ensure that the desired sample type is supported. - let expected_sample_format = super::device::convert_data_type(&stream_type) - .ok_or(BuildStreamError::StreamConfigNotSupported)?; + let expected_sample_format = + super::device::convert_data_type(&stream_type).ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("ASIO input data type {stream_type:?} is not supported"), + ) + })?; if sample_format != expected_sample_format { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample format {sample_format} does not match ASIO input format {expected_sample_format}" + ), + )); } let num_channels = config.channels; @@ -405,28 +416,38 @@ impl Device { mut data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { com::com_initialized(); - let description = self - .description() - .map_err(|_| BuildStreamError::DeviceNotAvailable)?; + let description = self.description()?; let driver = super::GLOBAL_ASIO .get() - .ok_or(BuildStreamError::DeviceNotAvailable)? + .ok_or_else(|| { + Error::with_message(ErrorKind::DeviceNotAvailable, "ASIO driver not initialized") + })? .load_driver(description.name()) .map_err(load_driver_err)?; let stream_type = driver.output_data_type().map_err(build_stream_err)?; // Ensure that the desired sample type is supported. - let expected_sample_format = super::device::convert_data_type(&stream_type) - .ok_or(BuildStreamError::StreamConfigNotSupported)?; + let expected_sample_format = + super::device::convert_data_type(&stream_type).ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("ASIO output data type {stream_type:?} is not supported"), + ) + })?; if sample_format != expected_sample_format { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample format {sample_format} does not match ASIO output format {expected_sample_format}" + ), + )); } let num_channels = config.channels; @@ -761,14 +782,13 @@ impl Device { driver: &sys::Driver, config: StreamConfig, sample_format: SampleFormat, - ) -> Result { - let num_asio_channels = self - .default_input_config() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)? - .channels; + ) -> Result { + let num_asio_channels = self.default_input_config()?.channels; check_config(driver, config, sample_format, num_asio_channels)?; let num_channels = config.channels as usize; - let mut streams = self.asio_streams.lock().unwrap(); + let mut streams = self.asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; let buffer_size = match config.buffer_size { BufferSize::Fixed(v) => Some(v as i32), @@ -791,7 +811,7 @@ impl Device { *streams = new_streams; bs }) - .map_err(|_| BuildStreamError::DeviceNotAvailable) + .map_err(build_stream_err) } } } @@ -804,14 +824,13 @@ impl Device { driver: &sys::Driver, config: StreamConfig, sample_format: SampleFormat, - ) -> Result { - let num_asio_channels = self - .default_output_config() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)? - .channels; + ) -> Result { + let num_asio_channels = self.default_output_config()?.channels; check_config(driver, config, sample_format, num_asio_channels)?; let num_channels = config.channels as usize; - let mut streams = self.asio_streams.lock().unwrap(); + let mut streams = self.asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; let buffer_size = match config.buffer_size { BufferSize::Fixed(v) => Some(v as i32), @@ -834,7 +853,7 @@ impl Device { *streams = new_streams; bs }) - .map_err(|_| BuildStreamError::DeviceNotAvailable) + .map_err(build_stream_err) } } } @@ -847,7 +866,7 @@ impl Device { is_input: bool, ) -> sys::DriverEventCallbackId where - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let error_callback_shared = Arc::new(Mutex::new(error_callback)); let configured_sample_rate = driver.sample_rate().ok().filter(|&r| r > 0.0); @@ -871,7 +890,10 @@ impl Device { error_callback_shared .lock() .unwrap_or_else(|e| e.into_inner())( - StreamError::StreamInvalidated + Error::with_message( + ErrorKind::StreamInvalidated, + "ASIO driver requested stream reset", + ), ); false } @@ -879,7 +901,10 @@ impl Device { error_callback_shared .lock() .unwrap_or_else(|e| e.into_inner())( - StreamError::BufferUnderrun + Error::with_message( + ErrorKind::Xrun, + "ASIO driver requested resynchronization", + ), ); false } @@ -918,7 +943,10 @@ impl Device { error_callback_shared .lock() .unwrap_or_else(|e| e.into_inner())( - StreamError::StreamInvalidated + Error::with_message( + ErrorKind::StreamInvalidated, + format!("ASIO driver changed sample rate to {new_rate} Hz"), + ), ); } } @@ -954,7 +982,7 @@ fn check_config( config: StreamConfig, sample_format: SampleFormat, num_asio_channels: u16, -) -> Result<(), BuildStreamError> { +) -> Result<(), Error> { let StreamConfig { channels, sample_rate, @@ -969,7 +997,13 @@ fn check_config( let range = driver.buffersize_range().map_err(build_stream_err)?; let requested_size_i32 = requested_size as i32; if !(range.min..=range.max).contains(&requested_size_i32) { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "buffer size {requested_size} is not in the supported range {}..={}", + range.min, range.max + ), + )); } } @@ -984,16 +1018,27 @@ fn check_config( .set_sample_rate(sample_rate) .map_err(build_stream_err)?; } else { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample rate {sample_rate} Hz is not supported by the ASIO driver"), + )); } } // unsigned formats are not supported by asio match sample_format { SampleFormat::I16 | SampleFormat::I24 | SampleFormat::I32 | SampleFormat::F32 => (), - _ => return Err(BuildStreamError::StreamConfigNotSupported), + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format {sample_format} is not supported by ASIO"), + )) + } } if channels > num_asio_channels { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("{channels} channels exceeds the ASIO device maximum of {num_asio_channels}"), + )); } Ok(()) } @@ -1047,25 +1092,24 @@ unsafe fn asio_channel_slice_mut( std::slice::from_raw_parts_mut(buff_ptr, channel_length) } -fn load_driver_err(e: sys::LoadDriverError) -> BuildStreamError { +fn load_driver_err(e: sys::LoadDriverError) -> Error { match e { sys::LoadDriverError::LoadDriverFailed | sys::LoadDriverError::DriverAlreadyExists => { - BuildStreamError::DeviceNotAvailable + Error::with_message(ErrorKind::DeviceNotAvailable, e.to_string()) } sys::LoadDriverError::InitializationFailed(asio_err) => build_stream_err(asio_err), } } -fn build_stream_err(e: sys::AsioError) -> BuildStreamError { +fn build_stream_err(e: sys::AsioError) -> Error { match e { sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { - BuildStreamError::DeviceNotAvailable + Error::with_message(ErrorKind::DeviceNotAvailable, e.to_string()) } - sys::AsioError::InvalidInput | sys::AsioError::BadMode => BuildStreamError::InvalidArgument, - err => { - let description = format!("{}", err); - BackendSpecificError { description }.into() + sys::AsioError::InvalidInput | sys::AsioError::BadMode => { + Error::with_message(ErrorKind::InvalidInput, e.to_string()) } + err => Error::with_message(ErrorKind::Other, err.to_string()), } } diff --git a/src/host/audioworklet/mod.rs b/src/host/audioworklet/mod.rs index 802c19da1..337a2d56b 100644 --- a/src/host/audioworklet/mod.rs +++ b/src/host/audioworklet/mod.rs @@ -15,11 +15,9 @@ use wasm_bindgen::prelude::*; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, - DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, SampleRate, StreamConfig, StreamError, StreamInstant, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, ErrorKind, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamInstant, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, }; use std::time::Duration; @@ -50,11 +48,14 @@ const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; const DEFAULT_RENDER_SIZE: u64 = 128; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { if Self::is_available() { Ok(Host) } else { - Err(crate::HostUnavailable) + Err(crate::Error::with_message( + ErrorKind::HostUnavailable, + "AudioWorklet API is not available", + )) } } } @@ -80,7 +81,7 @@ impl HostTrait for Host { } } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new() } @@ -95,7 +96,7 @@ impl HostTrait for Host { } impl Devices { - fn new() -> Result { + fn new() -> Result { Ok(Self::default()) } } @@ -106,14 +107,14 @@ impl DeviceTrait for Device { type Stream = Stream; #[inline] - fn description(&self) -> Result { + fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) .direction(crate::DeviceDirection::Output) .build()) } #[inline] - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId( crate::platform::HostId::AudioWorklet, "default".to_string(), @@ -121,17 +122,13 @@ impl DeviceTrait for Device { } #[inline] - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { // TODO Ok(Vec::new().into_iter()) } #[inline] - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { let buffer_size = SupportedBufferSize::Unknown; // In actuality the number of supported channels cannot be fully known until @@ -150,13 +147,15 @@ impl DeviceTrait for Device { } #[inline] - fn default_input_config(&self) -> Result { - // TODO - Err(DefaultStreamConfigError::StreamTypeNotSupported) + fn default_input_config(&self) -> Result { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "AudioWorklet does not support audio input", + )) } #[inline] - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { const EXPECT: &str = "expected at least one valid webaudio stream config"; let config = self .supported_output_configs() @@ -175,13 +174,15 @@ impl DeviceTrait for Device { _data_callback: D, _error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - // TODO - Err(BuildStreamError::StreamConfigNotSupported) + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "AudioWorklet does not support audio input", + )) } /// Create an output stream. @@ -204,13 +205,36 @@ impl DeviceTrait for Device { mut data_callback: D, mut error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - if !valid_config(config, sample_format) { - return Err(BuildStreamError::StreamConfigNotSupported); + if config.channels < MIN_CHANNELS || config.channels > MAX_CHANNELS { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "{} channels is not supported; AudioWorklet supports {} to {}", + config.channels, MIN_CHANNELS, MAX_CHANNELS + ), + )); + } + if config.sample_rate < MIN_SAMPLE_RATE || config.sample_rate > MAX_SAMPLE_RATE { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "{} Hz is not supported; AudioWorklet supports {} to {} Hz", + config.sample_rate, MIN_SAMPLE_RATE, MAX_SAMPLE_RATE + ), + )); + } + if sample_format != SUPPORTED_SAMPLE_FORMAT { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample format {sample_format} is not supported; AudioWorklet requires {SUPPORTED_SAMPLE_FORMAT}" + ), + )); } let stream_opts = web_sys::AudioContextOptions::new(); @@ -223,13 +247,8 @@ impl DeviceTrait for Device { ); } - let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( - |err| -> BuildStreamError { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; - err.into() - }, - )?; + let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts) + .map_err(|err| Error::with_message(ErrorKind::UnsupportedConfig, format!("{err:?}")))?; let destination = audio_context.destination(); @@ -317,15 +336,13 @@ impl DeviceTrait for Device { .await; if let Err(err) = result { - let description = if let Some(string_value) = err.as_string() { - string_value - } else { - format!("Browser error initializing stream: {err:?}") - }; - - error_callback(StreamError::BackendSpecific { - err: BackendSpecificError { description }, - }) + let message = err + .as_string() + .unwrap_or_else(|| format!("Browser error initializing stream: {err:?}")); + error_callback(Error::with_message( + ErrorKind::UnsupportedOperation, + message, + )) } }); @@ -337,29 +354,27 @@ impl DeviceTrait for Device { } impl StreamTrait for Stream { - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.buffer_size_frames.load(Ordering::Relaxed) as crate::FrameCount) } - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { match self.audio_context.resume() { Ok(_) => Ok(()), - Err(err) => { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; - Err(err.into()) - } + Err(err) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("{err:?}"), + )), } } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { match self.audio_context.suspend() { Ok(_) => Ok(()), - Err(err) => { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; - Err(err.into()) - } + Err(err) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("{err:?}"), + )), } } @@ -393,15 +408,6 @@ impl Iterator for Devices { } } -// Whether or not the given stream configuration is valid for building a stream. -fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool { - conf.channels <= MAX_CHANNELS - && conf.channels >= MIN_CHANNELS - && conf.sample_rate <= MAX_SAMPLE_RATE - && conf.sample_rate >= MIN_SAMPLE_RATE - && sample_format == SUPPORTED_SAMPLE_FORMAT -} - // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { let secsf = frames as f64 / rate as f64; diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 2d26846c6..c2a9ab87b 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -15,12 +15,10 @@ use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, StreamInstant, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + error::ResultExt, BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceId, Error, ErrorKind, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, + StreamConfig, StreamInstant, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, }; use self::enumerate::{ @@ -41,7 +39,7 @@ pub struct Device; pub struct Host; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } @@ -54,7 +52,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Ok(Devices::new()) } @@ -68,7 +66,7 @@ impl HostTrait for Host { } impl Device { - fn description(&self) -> Result { + fn description(&self) -> Result { // Query AVAudioSession to determine actual input/output availability // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton let direction = unsafe { @@ -84,39 +82,45 @@ impl Device { .build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId( crate::platform::HostId::CoreAudio, "default".to_string(), )) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Ok(get_supported_stream_configs(true)) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Ok(get_supported_stream_configs(false)) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { // Get the primary (exact channel count) config from supported configs get_supported_stream_configs(true) .next() .map(|range| range.with_max_sample_rate()) - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "no supported input configuration", + ) + }) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { // Get the maximum channel count config from supported configs get_supported_stream_configs(false) .last() .map(|range| range.with_max_sample_rate()) - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "no supported output configuration", + ) + }) } } @@ -125,31 +129,27 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -160,10 +160,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { // Configure buffer size and create audio unit let mut audio_unit = setup_stream_audio_unit(config, sample_format, true)?; @@ -207,10 +207,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { // Configure buffer size and create audio unit let mut audio_unit = setup_stream_audio_unit(config, sample_format, false)?; @@ -262,29 +262,29 @@ impl Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { - let mut stream = self.inner.lock().expect("stream lock poisoned"); - + fn play(&self) -> Result<(), Error> { + let mut stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; if !stream.playing { - if let Err(e) = stream.audio_unit.start() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + stream + .audio_unit + .start() + .context("failed to start audio unit")?; stream.playing = true; } Ok(()) } - fn pause(&self) -> Result<(), PauseStreamError> { - let mut stream = self.inner.lock().expect("stream lock poisoned"); - + fn pause(&self) -> Result<(), Error> { + let mut stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; if stream.playing { - if let Err(e) = stream.audio_unit.stop() { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + stream + .audio_unit + .stop() + .context("failed to stop audio unit")?; stream.playing = false; } Ok(()) @@ -295,7 +295,7 @@ impl StreamTrait for Stream { host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(get_device_buffer_frames() as crate::FrameCount) } } @@ -337,7 +337,7 @@ fn configure_for_recording(audio_unit: &mut AudioUnit) -> Result<(), coreaudio:: fn set_audio_session_buffer_size( buffer_size: u32, sample_rate: crate::SampleRate, -) -> Result<(), BuildStreamError> { +) -> Result<(), Error> { // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton let audio_session = unsafe { AVAudioSession::sharedInstance() }; @@ -349,7 +349,12 @@ fn set_audio_session_buffer_size( unsafe { audio_session .setPreferredIOBufferDuration_error(buffer_duration) - .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + .map_err(|e| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("failed to set preferred I/O buffer duration: {e}"), + ) + })?; } Ok(()) @@ -411,7 +416,7 @@ fn setup_stream_audio_unit( config: StreamConfig, sample_format: SampleFormat, is_input: bool, -) -> Result { +) -> Result { // Configure buffer size via AVAudioSession if let BufferSize::Fixed(buffer_size) = config.buffer_size { set_audio_session_buffer_size(buffer_size, config.sample_rate)?; @@ -487,10 +492,10 @@ fn setup_input_callback( device_buffer_frames: Option, mut data_callback: D, mut error_callback: E, -) -> Result<(), BuildStreamError> +) -> Result<(), Error> where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let bytes_per_channel = sample_format.sample_size(); type Args = render_callback::Args; @@ -502,7 +507,7 @@ where let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - error_callback(err.into()); + error_callback(err); return Err(()); } Ok(cb) => cb, @@ -532,10 +537,10 @@ fn setup_output_callback( device_buffer_frames: Option, mut data_callback: D, mut error_callback: E, -) -> Result<(), BuildStreamError> +) -> Result<(), Error> where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let bytes_per_channel = sample_format.sample_size(); type Args = render_callback::Args; @@ -547,7 +552,7 @@ where let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - error_callback(err.into()); + error_callback(err); return Err(()); } Ok(cb) => cb, diff --git a/src/host/coreaudio/ios/session_event_manager.rs b/src/host/coreaudio/ios/session_event_manager.rs index 7d1dd94e6..64a0c4572 100644 --- a/src/host/coreaudio/ios/session_event_manager.rs +++ b/src/host/coreaudio/ios/session_event_manager.rs @@ -12,11 +12,11 @@ use objc2_avf_audio::{ }; use objc2_foundation::{NSNotification, NSNotificationCenter, NSNumber, NSString}; -use crate::StreamError; +use crate::{Error, ErrorKind}; -pub(super) type ErrorCallbackMutex = Arc>>; +pub(super) type ErrorCallbackMutex = Arc>>; -unsafe fn route_change_error(notification: &NSNotification) -> Option { +unsafe fn route_change_error(notification: &NSNotification) -> Option { let user_info = notification.userInfo()?; let key = AVAudioSessionRouteChangeReasonKey?; let dict = unsafe { user_info.cast_unchecked::() }; @@ -27,13 +27,15 @@ unsafe fn route_change_error(notification: &NSNotification) -> Option { - Some(StreamError::StreamInvalidated) - } + | AVAudioSessionRouteChangeReason::RouteConfigurationChange => Some(Error::with_message( + ErrorKind::StreamInvalidated, + "audio route changed", + )), - AVAudioSessionRouteChangeReason::NoSuitableRouteForCategory => { - Some(StreamError::DeviceNotAvailable) - } + AVAudioSessionRouteChangeReason::NoSuitableRouteForCategory => Some(Error::with_message( + ErrorKind::DeviceNotAvailable, + "no suitable audio route for the session category", + )), _ => None, } @@ -76,7 +78,10 @@ impl SessionEventManager { let cb = error_callback.clone(); let block = RcBlock::new(move |_: NonNull| { if let Ok(mut cb) = cb.lock() { - cb(StreamError::DeviceNotAvailable); + cb(Error::with_message( + ErrorKind::DeviceNotAvailable, + "audio media services were lost", + )); } }); if let Some(name) = unsafe { AVAudioSessionMediaServicesWereLostNotification } { @@ -91,7 +96,10 @@ impl SessionEventManager { let cb = error_callback.clone(); let block = RcBlock::new(move |_: NonNull| { if let Ok(mut cb) = cb.lock() { - cb(StreamError::StreamInvalidated); + cb(Error::with_message( + ErrorKind::StreamInvalidated, + "audio media services were reset", + )); } }); if let Some(name) = unsafe { AVAudioSessionMediaServicesWereResetNotification } { diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index c63e7fa01..c2ab069e1 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -1,14 +1,12 @@ -use super::OSStatus; use super::Stream; use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; use crate::host::coreaudio::macos::loopback::LoopbackDevice; use crate::host::coreaudio::macos::StreamInner; use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, InputCallbackInfo, - OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + error::ResultExt, BufferSize, ChannelCount, Data, DeviceId, Error, ErrorKind, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, }; use coreaudio::audio_unit::audio_format::LinearPcmFlags; use coreaudio::audio_unit::macos_helpers::{ @@ -91,7 +89,7 @@ fn set_sample_rate( audio_device_id: AudioObjectID, target_sample_rate: SampleRate, timeout: Option, -) -> Result<(), BuildStreamError> { +) -> Result<(), Error> { // Get the current sample rate. let mut property_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyNominalSampleRate, @@ -150,7 +148,10 @@ fn set_sample_rate( .iter() .any(|r| sample_rate as f64 >= r.mMinimum && sample_rate as f64 <= r.mMaximum) { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample rate {sample_rate} Hz is not supported by this device"), + )); } // Register the listener before setting the property so we don't miss the notification. @@ -189,18 +190,16 @@ fn set_sample_rate( } } Err(RecvTimeoutError::Timeout) => { - return Err(BackendSpecificError { - description: "timeout waiting for sample rate update for device" - .to_string(), - } - .into()); + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timeout waiting for sample rate update for device", + )); } Err(RecvTimeoutError::Disconnected) => { - return Err(BackendSpecificError { - description: "sample rate listener channel disconnected unexpectedly" - .to_string(), - } - .into()); + return Err(Error::with_message( + ErrorKind::Other, + "sample rate listener channel disconnected unexpectedly", + )); } } remaining = remaining @@ -273,31 +272,27 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -308,10 +303,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { Device::build_input_stream_raw( self, @@ -330,10 +325,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { Device::build_output_stream_raw( self, @@ -396,14 +391,8 @@ impl Device { status == 0 && class_id == kAudioAggregateDeviceClassID } - fn description(&self) -> Result { - let name = get_device_name(self.audio_device_id).map_err(|err| { - DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: err.to_string(), - }, - } - })?; + fn description(&self) -> Result { + let name = get_device_name(self.audio_device_id).context("failed to get device name")?; let input_configs = self .supported_input_configs() @@ -427,7 +416,7 @@ impl Device { Ok(builder.build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { let property_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyDeviceUID, mScope: kAudioObjectPropertyScopeGlobal, @@ -458,11 +447,10 @@ impl Device { let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() }; Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string)) } else { - Err(DeviceIdError::BackendSpecific { - err: BackendSpecificError { - description: "Device UID is null".to_string(), - }, - }) + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "device UID is null", + )) } } @@ -471,7 +459,7 @@ impl Device { fn supported_configs( &self, scope: AudioObjectPropertyScope, - ) -> Result { + ) -> Result { let mut property_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyStreamConfiguration, mScope: scope, @@ -562,12 +550,15 @@ impl Device { #[allow(non_upper_case_globals)] let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!("unexpected scope (neither input nor output): {scope:?}"), - }), - }?; + kAudioObjectPropertyScopeInput => true, + kAudioObjectPropertyScopeOutput => false, + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + format!("unexpected scope (neither input nor output): {scope:?}"), + )) + } + }; let audio_unit = audio_unit_from_device(self, input)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; @@ -607,48 +598,18 @@ impl Device { } } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { self.supported_configs(kAudioObjectPropertyScopeInput) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { self.supported_configs(kAudioObjectPropertyScopeOutput) } fn default_config( &self, scope: AudioObjectPropertyScope, - ) -> Result { - fn default_config_error_from_os_status( - status: OSStatus, - ) -> Result<(), DefaultStreamConfigError> { - let err = match coreaudio::Error::from_os_status(status) { - Err(err) => err, - Ok(_) => return Ok(()), - }; - match err { - coreaudio::Error::AudioUnit( - coreaudio::error::AudioUnitError::FormatNotSupported, - ) - | coreaudio::Error::AudioCodec(_) - | coreaudio::Error::AudioFormat(_) => { - Err(DefaultStreamConfigError::StreamTypeNotSupported) - } - coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => { - Err(DefaultStreamConfigError::DeviceNotAvailable) - } - err => { - let description = format!("{err}"); - let err = BackendSpecificError { description }; - Err(err.into()) - } - } - } - + ) -> Result { let property_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyStreamFormat, mScope: scope, @@ -666,37 +627,49 @@ impl Device { NonNull::from(&mut data_size), NonNull::from(&mut asbd).cast(), ); - default_config_error_from_os_status(status)?; - - let sample_format = { - let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( - asbd.mFormatID, - Some(asbd.mFormatFlags), - ); - let flags = match audio_format { - Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, - _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), - }; - let maybe_sample_format = - coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( - flags, - asbd.mBitsPerChannel, + check_os_status(status)?; + + let sample_format = + { + let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( + asbd.mFormatID, + Some(asbd.mFormatFlags), ); - match maybe_sample_format { - Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, - Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, - _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), - } - }; + let flags = match audio_format { + Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "device audio format is not linear PCM", + )) + } + }; + let maybe_sample_format = + coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( + flags, + asbd.mBitsPerChannel, + ); + match maybe_sample_format { + Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, + Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, + _ => return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "device sample format is not supported; only F32 and I16 are supported", + )), + } + }; #[allow(non_upper_case_globals)] let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!("unexpected scope (neither input nor output): {scope:?}"), - }), - }?; + kAudioObjectPropertyScopeInput => true, + kAudioObjectPropertyScopeOutput => false, + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + format!("unexpected scope (neither input nor output): {scope:?}"), + )) + } + }; let audio_unit = audio_unit_from_device(self, input)?; let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; @@ -710,11 +683,11 @@ impl Device { } } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { self.default_config(kAudioObjectPropertyScopeInput) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { self.default_config(kAudioObjectPropertyScopeOutput) } @@ -747,10 +720,10 @@ impl Device { mut data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { // The scope and element for working with a device's input stream. let scope = Scope::Output; @@ -806,7 +779,7 @@ impl Device { let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - invoke_error_callback(&error_callback, err.into()); + invoke_error_callback(&error_callback, err); return Err(()); } Ok(cb) => cb, @@ -827,10 +800,10 @@ impl Device { // Create error callback for stream - either dummy or real based on device type let error_callback_for_stream: super::ErrorCallback = if is_default_input_device(self) { - Box::new(|_: StreamError| {}) + Box::new(|_: Error| {}) } else { let error_callback_clone = error_callback_disconnect.clone(); - Box::new(move |err: StreamError| { + Box::new(move |err: Error| { invoke_error_callback(&error_callback_clone, err); }) }; @@ -848,7 +821,7 @@ impl Device { stream .inner .lock() - .expect("stream lock poisoned") + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned"))? .audio_unit .start()?; @@ -862,10 +835,10 @@ impl Device { mut data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { // Best-effort: set the physical stream format (bit depth + sample rate) on the hardware. // This avoids unnecessary conversions, especially on aggregate devices. Not an error if @@ -915,7 +888,7 @@ impl Device { let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { Err(err) => { - invoke_error_callback(&error_callback, err.into()); + invoke_error_callback(&error_callback, err); return Err(()); } Ok(cb) => cb, @@ -935,10 +908,10 @@ impl Device { // Create error callback for stream - either dummy or real based on device type let error_callback_for_stream: super::ErrorCallback = if is_default_output_device(self) { - Box::new(|_: StreamError| {}) + Box::new(|_: Error| {}) } else { let error_callback_clone = error_callback_disconnect.clone(); - Box::new(move |err: StreamError| { + Box::new(move |err: Error| { invoke_error_callback(&error_callback_clone, err); }) }; @@ -956,7 +929,7 @@ impl Device { stream .inner .lock() - .expect("stream lock poisoned") + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned"))? .audio_unit .start()?; @@ -975,7 +948,7 @@ fn configure_stream_format_and_buffer( sample_format: SampleFormat, scope: Scope, element: Element, -) -> Result<(), BuildStreamError> { +) -> Result<(), Error> { // Set the stream format using stream-specific scope/element // - Input streams: scope=Output, element=Input (configuring output format of input element) // - Output streams: scope=Input, element=Output (configuring input format of output element) diff --git a/src/host/coreaudio/macos/enumerate.rs b/src/host/coreaudio/macos/enumerate.rs index 01dc57351..5e133a2dc 100644 --- a/src/host/coreaudio/macos/enumerate.rs +++ b/src/host/coreaudio/macos/enumerate.rs @@ -1,5 +1,5 @@ -use super::{Device, OSStatus}; -use crate::{BackendSpecificError, DevicesError}; +use super::{check_os_status, Device}; +use crate::Error; use objc2_core_audio::{ kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice, kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices, @@ -11,7 +11,7 @@ use std::mem; use std::ptr::{null, NonNull}; use std::vec::IntoIter as VecIntoIter; -unsafe fn audio_devices() -> Result, OSStatus> { +unsafe fn audio_devices() -> Result, Error> { let property_address = AudioObjectPropertyAddress { mSelector: kAudioHardwarePropertyDevices, mScope: kAudioObjectPropertyScopeGlobal, @@ -21,7 +21,7 @@ unsafe fn audio_devices() -> Result, OSStatus> { macro_rules! try_status_or_return { ($status:expr) => { if $status != kAudioHardwareNoError as i32 { - return Err($status); + return Err(check_os_status($status).unwrap_err()); } }; } @@ -58,17 +58,8 @@ 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()); - } - } - }; + pub fn new() -> Result { + let devices = unsafe { audio_devices() }?; Ok(Devices(devices.into_iter())) } } diff --git a/src/host/coreaudio/macos/loopback.rs b/src/host/coreaudio/macos/loopback.rs index 875aeea0d..1f4f4164f 100644 --- a/src/host/coreaudio/macos/loopback.rs +++ b/src/host/coreaudio/macos/loopback.rs @@ -1,7 +1,7 @@ //! Manages loopback recording (recording system audio output) use super::device::Device; -use crate::{host::coreaudio::check_os_status, BackendSpecificError, BuildStreamError}; +use crate::{host::coreaudio::check_os_status, Error, ErrorKind}; use objc2::{rc::Retained, AnyThread}; use objc2_core_audio::{ kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey, @@ -27,7 +27,7 @@ use std::{ type CFStringRef = *mut std::os::raw::c_void; impl Device { - fn uid(&self) -> Result, BackendSpecificError> { + fn uid(&self) -> Result, Error> { let mut cfstring: CFStringRef = std::ptr::null_mut(); let mut size = std::mem::size_of::() as u32; @@ -50,9 +50,10 @@ impl Device { check_os_status(status)?; if cfstring.is_null() { - return Err(BackendSpecificError { - description: "Device uid is null".to_string(), - }); + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "device UID is null", + )); } let ns_string: Retained = unsafe { @@ -80,7 +81,7 @@ pub struct LoopbackDevice { impl LoopbackDevice { /// Create a [`LoopbackDevice`] that records the sound /// output of `device`. - pub fn from_device(device: &Device) -> Result { + pub fn from_device(device: &Device) -> Result { // 1 - Create tap // Empty list of processes as we want to record all processes diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index dfc81f435..87a231e37 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -4,7 +4,7 @@ use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_ use super::OSStatus; use crate::host::coreaudio::macos::loopback::LoopbackDevice; use crate::traits::{HostTrait, StreamTrait}; -use crate::{BackendSpecificError, DevicesError, PauseStreamError, PlayStreamError}; +use crate::{error::ResultExt, Error, ErrorKind}; use coreaudio::audio_unit::AudioUnit; use objc2_core_audio::AudioDeviceID; use std::sync::{mpsc, Arc, Mutex, Weak}; @@ -28,7 +28,7 @@ pub use device::Device; pub struct Host; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } @@ -42,7 +42,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new() } @@ -56,14 +56,14 @@ impl HostTrait for Host { } /// Type alias for the error callback to reduce complexity -type ErrorCallback = Box; +type ErrorCallback = Box; /// Invoke error callback, recovering from poisoned mutex if needed. /// Returns true if callback was invoked, false if skipped due to WouldBlock. #[inline] -fn invoke_error_callback(error_callback: &Arc>, err: crate::StreamError) -> bool +fn invoke_error_callback(error_callback: &Arc>, err: Error) -> bool where - E: FnMut(crate::StreamError) + Send, + E: FnMut(Error) + Send, { match error_callback.try_lock() { Ok(mut cb) => { @@ -88,7 +88,7 @@ where /// /// When a device disconnects, this manager: /// 1. Attempts to pause the stream to stop audio I/O -/// 2. Calls the error callback with `StreamError::DeviceNotAvailable` +/// 2. Calls the error callback with `ErrorKind::DeviceNotAvailable` /// /// The dedicated thread architecture ensures `Stream` can implement `Send`. struct DisconnectManager { @@ -101,9 +101,9 @@ impl DisconnectManager { device_id: AudioDeviceID, stream_weak: Weak>, error_callback: Arc>, - ) -> Result { + ) -> Result { let (shutdown_tx, shutdown_rx) = mpsc::channel(); - let (disconnect_tx, disconnect_rx) = mpsc::channel::(); + let (disconnect_tx, disconnect_rx) = mpsc::channel::(); let (ready_tx, ready_rx) = mpsc::channel(); // Spawn a dedicated thread to own both listeners. CoreAudio requires that @@ -118,7 +118,10 @@ impl DisconnectManager { }; let alive_listener = AudioObjectPropertyListener::new(device_id, alive_address, move || { - let _ = disconnect_tx_alive.send(crate::StreamError::DeviceNotAvailable); + let _ = disconnect_tx_alive.send(Error::with_message( + ErrorKind::DeviceNotAvailable, + "device disconnected", + )); }); let rate_address = AudioObjectPropertyAddress { @@ -128,7 +131,10 @@ impl DisconnectManager { }; let rate_listener = AudioObjectPropertyListener::new(device_id, rate_address, move || { - let _ = disconnect_tx_rate.send(crate::StreamError::StreamInvalidated); + let _ = disconnect_tx_rate.send(Error::with_message( + ErrorKind::StreamInvalidated, + "device sample rate changed", + )); }); match (alive_listener, rate_listener) { @@ -144,13 +150,12 @@ impl DisconnectManager { }); // Wait for listener creation to complete or fail - ready_rx - .recv() - .map_err(|_| crate::BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: "Disconnect listener thread terminated unexpectedly".to_string(), - }, - })??; + ready_rx.recv().map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "disconnect listener thread terminated unexpectedly", + ) + })??; // Handle disconnect events on the main thread pool let stream_weak_clone = stream_weak.clone(); @@ -189,25 +194,21 @@ struct StreamInner { } impl StreamInner { - fn play(&mut self) -> Result<(), PlayStreamError> { + fn play(&mut self) -> Result<(), Error> { if !self.playing { - if let Err(e) = self.audio_unit.start() { - let description = format!("{e}"); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + self.audio_unit + .start() + .context("failed to start audio unit")?; self.playing = true; } Ok(()) } - fn pause(&mut self) -> Result<(), PauseStreamError> { + fn pause(&mut self) -> Result<(), Error> { if self.playing { - if let Err(e) = self.audio_unit.stop() { - let description = format!("{e}"); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + self.audio_unit + .stop() + .context("failed to stop audio unit")?; self.playing = false; } Ok(()) @@ -222,10 +223,7 @@ pub struct Stream { } impl Stream { - fn new( - inner: StreamInner, - error_callback: ErrorCallback, - ) -> Result { + fn new(inner: StreamInner, error_callback: ErrorCallback) -> Result { let device_id = inner.device_id; let inner_arc = Arc::new(Mutex::new(inner)); let weak_inner = Arc::downgrade(&inner_arc); @@ -241,12 +239,18 @@ impl Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { - self.inner.lock().expect("stream lock poisoned").play() + fn play(&self) -> Result<(), Error> { + self.inner + .lock() + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned"))? + .play() } - fn pause(&self) -> Result<(), PauseStreamError> { - self.inner.lock().expect("stream lock poisoned").pause() + fn pause(&self) -> Result<(), Error> { + self.inner + .lock() + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned"))? + .pause() } fn now(&self) -> crate::StreamInstant { @@ -254,12 +258,13 @@ impl StreamTrait for Stream { host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") } - fn buffer_size(&self) -> Result { - let stream = self.inner.lock().unwrap(); - + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "stream lock poisoned") + })?; device::get_device_buffer_frame_size(&stream.audio_unit) .map(|size| size as crate::FrameCount) - .map_err(|_| crate::StreamError::DeviceNotAvailable) + .context("failed to get buffer frame size") } } diff --git a/src/host/coreaudio/macos/property_listener.rs b/src/host/coreaudio/macos/property_listener.rs index ffb638c8f..24f36f597 100644 --- a/src/host/coreaudio/macos/property_listener.rs +++ b/src/host/coreaudio/macos/property_listener.rs @@ -7,7 +7,7 @@ use objc2_core_audio::{ }; use super::OSStatus; -use crate::BuildStreamError; +use crate::Error; /// A double-indirection to be able to pass a closure (a fat pointer) /// via a single c_void. @@ -28,7 +28,7 @@ impl AudioObjectPropertyListener { audio_object_id: AudioObjectID, property_address: AudioObjectPropertyAddress, callback: F, - ) -> Result { + ) -> Result { let callback = Box::new(PropertyListenerCallbackWrapper(Box::new(callback))); unsafe { coreaudio::Error::from_os_status(AudioObjectAddPropertyListener( @@ -50,11 +50,11 @@ impl AudioObjectPropertyListener { /// Use this method if you need to explicitly handle failure to remove /// the property listener. #[allow(dead_code)] - pub fn remove(mut self) -> Result<(), BuildStreamError> { + pub fn remove(mut self) -> Result<(), Error> { self.remove_inner() } - fn remove_inner(&mut self) -> Result<(), BuildStreamError> { + fn remove_inner(&mut self) -> Result<(), Error> { unsafe { coreaudio::Error::from_os_status(AudioObjectRemovePropertyListener( self.audio_object_id, diff --git a/src/host/coreaudio/mod.rs b/src/host/coreaudio/mod.rs index 5bf24a058..51eee2553 100644 --- a/src/host/coreaudio/mod.rs +++ b/src/host/coreaudio/mod.rs @@ -7,10 +7,7 @@ use objc2_core_audio_types::{ kAudioFormatLinearPCM, AudioStreamBasicDescription, }; -use crate::DefaultStreamConfigError; -use crate::{BuildStreamError, SupportedStreamConfigsError}; - -use crate::{BackendSpecificError, SampleFormat, StreamConfig}; +use crate::{Error, ErrorKind, SampleFormat, StreamConfig}; // iOS and tvOS share the same CoreAudio / AudioUnit surface (RemoteIO, // AVAudioSession), so both target the `ios` submodule. @@ -31,14 +28,8 @@ pub use self::macos::{Host, Stream}; // Common helper methods used by both macOS and iOS -fn check_os_status(os_status: OSStatus) -> Result<(), BackendSpecificError> { - match coreaudio::Error::from_os_status(os_status) { - Ok(()) => Ok(()), - Err(err) => { - let description = err.to_string(); - Err(BackendSpecificError { description }) - } - } +fn check_os_status(os_status: OSStatus) -> Result<(), Error> { + coreaudio::Error::from_os_status(os_status).map_err(Error::from) } // Create a coreaudio AudioStreamBasicDescription from a CPAL Format. @@ -76,16 +67,13 @@ fn asbd_from_config( } #[inline] -fn host_time_to_stream_instant( - m_host_time: u64, -) -> Result { +fn host_time_to_stream_instant(m_host_time: u64) -> Result { let mut info: mach2::mach_time::mach_timebase_info = Default::default(); let res = unsafe { mach2::mach_time::mach_timebase_info(&mut info) }; check_os_status(res)?; let nanos = m_host_time as u128 * info.numer as u128 / info.denom as u128; - let secs = u64::try_from(nanos / 1_000_000_000).map_err(|_| BackendSpecificError { - description: "mach absolute time overflow".to_string(), - })?; + let secs = u64::try_from(nanos / 1_000_000_000) + .map_err(|_| Error::with_message(ErrorKind::Other, "mach absolute time overflow"))?; let subsec_nanos = (nanos % 1_000_000_000) as u32; Ok(crate::StreamInstant::new(secs, subsec_nanos)) } @@ -99,38 +87,65 @@ fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Dura std::time::Duration::new(secs, nanos) } -// TODO need stronger error identification -impl From for BuildStreamError { - fn from(err: coreaudio::Error) -> BuildStreamError { +impl From for Error { + fn from(err: coreaudio::Error) -> Self { + use coreaudio::error::{AudioCodecError, AudioError, AudioFormatError, AudioUnitError}; + let msg = format!("{err}"); match err { coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat | coreaudio::Error::NoKnownSubtype - | coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::FormatNotSupported) - | coreaudio::Error::AudioCodec(_) - | coreaudio::Error::AudioFormat(_) => BuildStreamError::StreamConfigNotSupported, - _ => BuildStreamError::DeviceNotAvailable, + | coreaudio::Error::UnsupportedSampleRate + | coreaudio::Error::UnsupportedStreamFormat + | coreaudio::Error::NonInterleavedInputOnlySupportsMono + | coreaudio::Error::AudioUnit(AudioUnitError::FormatNotSupported) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidPropertyValue) + | coreaudio::Error::AudioUnit(AudioUnitError::TooManyFramesToProcess) + | coreaudio::Error::AudioCodec(AudioCodecError::UnsupportedFormat) + | coreaudio::Error::AudioFormat(AudioFormatError::UnsupportedDataFormat) + | coreaudio::Error::AudioFormat(AudioFormatError::UnknownFormat) => { + Error::with_message(ErrorKind::UnsupportedConfig, msg) + } + + coreaudio::Error::SystemSoundClientMessageTimedOut + | coreaudio::Error::NoMatchingDefaultAudioUnitFound + | coreaudio::Error::AudioUnit(AudioUnitError::NoConnection) + | coreaudio::Error::AudioUnit(AudioUnitError::FailedInitialization) + | coreaudio::Error::Audio(AudioError::FileNotFound) => { + Error::with_message(ErrorKind::DeviceNotAvailable, msg) + } + + coreaudio::Error::AudioUnit(AudioUnitError::Unauthorized) + | coreaudio::Error::Audio(AudioError::FilePermission) => { + Error::with_message(ErrorKind::PermissionDenied, msg) + } + + coreaudio::Error::AudioUnit(AudioUnitError::InvalidProperty) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidParameter) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidElement) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidScope) + | coreaudio::Error::AudioUnit(AudioUnitError::PropertyNotInUse) + | coreaudio::Error::AudioCodec(AudioCodecError::UnknownProperty) + | coreaudio::Error::AudioCodec(AudioCodecError::BadPropertySize) + | coreaudio::Error::AudioFormat(AudioFormatError::UnsupportedProperty) + | coreaudio::Error::AudioFormat(AudioFormatError::BadPropertySize) + | coreaudio::Error::AudioFormat(AudioFormatError::BadSpecifierSize) + | coreaudio::Error::Audio(AudioError::Param) + | coreaudio::Error::Audio(AudioError::BadFilePath) => { + Error::with_message(ErrorKind::InvalidInput, msg) + } + + coreaudio::Error::Audio(AudioError::Unimplemented) + | coreaudio::Error::AudioUnit(AudioUnitError::PropertyNotWritable) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidOfflineRender) + | coreaudio::Error::AudioCodec(AudioCodecError::IllegalOperation) => { + Error::with_message(ErrorKind::UnsupportedOperation, msg) + } + + _ => Error::with_message(ErrorKind::Other, msg), } } } -impl From for SupportedStreamConfigsError { - fn from(err: coreaudio::Error) -> SupportedStreamConfigsError { - let description = format!("{err}"); - let err = BackendSpecificError { description }; - // Check for possible DeviceNotAvailable variant - SupportedStreamConfigsError::BackendSpecific { err } - } -} - -impl From for DefaultStreamConfigError { - fn from(err: coreaudio::Error) -> DefaultStreamConfigError { - let description = format!("{err}"); - let err = BackendSpecificError { description }; - // Check for possible DeviceNotAvailable variant - DefaultStreamConfigError::BackendSpecific { err } - } -} - pub(crate) type OSStatus = i32; // Compile-time assertion that Stream is Send and Sync diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index ec874013f..ade8f653d 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -5,10 +5,8 @@ use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, StreamConfig, StreamError, StreamInstant, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + Data, DeviceDescription, DeviceId, Error, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + StreamConfig, StreamInstant, SupportedStreamConfig, SupportedStreamConfigRange, }; use core::time::Duration; @@ -29,8 +27,8 @@ pub struct Host(Box); impl Host { // this only exists for impl_platform_host, which requires it - pub(crate) fn new() -> Result { - Err(crate::HostUnavailable) + pub(crate) fn new() -> Result { + Err(crate::Error::new(crate::ErrorKind::HostUnavailable)) } /// Construct a custom host from an arbitrary [`HostTrait`] implementation. @@ -104,7 +102,7 @@ impl Stream { type Devices = Box>; trait HostErased: Send + Sync { - fn devices(&self) -> Result; + fn devices(&self) -> Result; fn default_input_device(&self) -> Option; fn default_output_device(&self) -> Option; } @@ -140,20 +138,20 @@ impl Clone for SupportedConfigs { } } -type ErrorCallback = Box; +type ErrorCallback = Box; type InputCallback = Box; type OutputCallback = Box; trait DeviceErased: Send + Sync { - fn name(&self) -> Result; - fn description(&self) -> Result; - fn id(&self) -> Result; + fn name(&self) -> Result; + fn description(&self) -> Result; + fn id(&self) -> Result; fn supports_input(&self) -> bool; fn supports_output(&self) -> bool; - fn supported_input_configs(&self) -> Result; - fn supported_output_configs(&self) -> Result; - fn default_input_config(&self) -> Result; - fn default_output_config(&self) -> Result; + fn supported_input_configs(&self) -> Result; + fn supported_output_configs(&self) -> Result; + fn default_input_config(&self) -> Result; + fn default_output_config(&self) -> Result; fn build_input_stream_raw( &self, config: StreamConfig, @@ -161,7 +159,7 @@ trait DeviceErased: Send + Sync { data_callback: InputCallback, error_callback: ErrorCallback, timeout: Option, - ) -> Result; + ) -> Result; fn build_output_stream_raw( &self, config: StreamConfig, @@ -169,16 +167,16 @@ trait DeviceErased: Send + Sync { data_callback: OutputCallback, error_callback: ErrorCallback, timeout: Option, - ) -> Result; + ) -> Result; // Required because `DeviceInner` is clone fn clone(&self) -> Device; } trait StreamErased: Send + Sync { - fn play(&self) -> Result<(), PlayStreamError>; - fn pause(&self) -> Result<(), PauseStreamError>; + fn play(&self) -> Result<(), Error>; + fn pause(&self) -> Result<(), Error>; fn now(&self) -> StreamInstant; - fn buffer_size(&self) -> Result; + fn buffer_size(&self) -> Result; } fn device_to_erased(d: impl DeviceErased + 'static) -> Device { @@ -191,7 +189,7 @@ where T::Devices: 'static, T::Device: DeviceErased + 'static, { - fn devices(&self) -> Result { + fn devices(&self) -> Result { let iter = ::devices(self)?; let erased = Box::new(iter.map(device_to_erased)); Ok(erased) @@ -224,15 +222,15 @@ where T::Stream: Send + Sync + 'static, { #[allow(deprecated)] - fn name(&self) -> Result { + fn name(&self) -> Result { ::name(self) } - fn description(&self) -> Result { + fn description(&self) -> Result { ::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { ::id(self) } @@ -244,19 +242,19 @@ where ::supports_output(self) } - fn supported_input_configs(&self) -> Result { + fn supported_input_configs(&self) -> Result { ::supported_input_configs(self).map(supported_configs_to_erased) } - fn supported_output_configs(&self) -> Result { + fn supported_output_configs(&self) -> Result { ::supported_output_configs(self).map(supported_configs_to_erased) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { ::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { ::default_output_config(self) } @@ -267,7 +265,7 @@ where data_callback: InputCallback, error_callback: ErrorCallback, timeout: Option, - ) -> Result { + ) -> Result { ::build_input_stream_raw( self, config, @@ -286,7 +284,7 @@ where data_callback: OutputCallback, error_callback: ErrorCallback, timeout: Option, - ) -> Result { + ) -> Result { ::build_output_stream_raw( self, config, @@ -307,11 +305,11 @@ impl StreamErased for T where T: StreamTrait + Send + Sync, { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { ::play(self) } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { ::pause(self) } @@ -319,7 +317,7 @@ where ::now(self) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { ::buffer_size(self) } } @@ -334,7 +332,7 @@ impl HostTrait for Host { false } - fn devices(&self) -> Result { + fn devices(&self) -> Result { self.0.devices() } @@ -354,15 +352,15 @@ impl DeviceTrait for Device { type Stream = Stream; - fn name(&self) -> Result { + fn name(&self) -> Result { self.0.name() } - fn description(&self) -> Result { + fn description(&self) -> Result { self.0.description() } - fn id(&self) -> Result { + fn id(&self) -> Result { self.0.id() } @@ -374,23 +372,19 @@ impl DeviceTrait for Device { self.0.supports_output() } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { self.0.supported_input_configs() } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { self.0.supported_output_configs() } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { self.0.default_input_config() } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { self.0.default_output_config() } @@ -401,10 +395,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { self.0.build_input_stream_raw( config, @@ -422,10 +416,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { self.0.build_output_stream_raw( config, @@ -438,11 +432,11 @@ impl DeviceTrait for Device { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { self.0.play() } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { self.0.pause() } @@ -450,7 +444,7 @@ impl StreamTrait for Stream { self.0.now() } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { self.0.buffer_size() } } diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index c1563384d..166d3af2d 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,10 +1,8 @@ use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, - DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, - InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, + Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, Error, ErrorKind, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, }; use std::hash::{Hash, Hasher}; use std::time::Duration; @@ -33,7 +31,7 @@ impl Device { connect_ports_automatically: bool, start_server_automatically: bool, direction: DeviceDirection, - ) -> Result { + ) -> Result { let client_options = super::get_client_options(start_server_automatically); // Create a dummy client to find out the sample rate of the server to be able to provide it @@ -56,7 +54,7 @@ impl Device { }) } - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId(crate::platform::HostId::Jack, self.name.clone())) } @@ -64,7 +62,7 @@ impl Device { name: &str, connect_ports_automatically: bool, start_server_automatically: bool, - ) -> Result { + ) -> Result { let output_client_name = format!("{}_out", name); Device::new_device( output_client_name, @@ -78,7 +76,7 @@ impl Device { name: &str, connect_ports_automatically: bool, start_server_automatically: bool, - ) -> Result { + ) -> Result { let input_client_name = format!("{}_in", name); Device::new_device( input_client_name, @@ -88,7 +86,7 @@ impl Device { ) } - pub fn default_config(&self) -> Result { + pub fn default_config(&self) -> Result { let channels = DEFAULT_NUM_CHANNELS; let sample_rate = self.sample_rate; let buffer_size = self.buffer_size; @@ -138,39 +136,35 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new(self.name.clone()) .direction(self.direction) .build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Ok(self.supported_configs().into_iter()) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Ok(self.supported_configs().into_iter()) } /// Returns the default input config /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. /// The sample rate is set by the JACK server. - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { self.default_config() } /// Returns the default output config /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. /// The sample rate is set by the JACK server. - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { self.default_config() } @@ -181,34 +175,50 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { if self.is_output() { - // Trying to create an input stream from an output device - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support input", + )); } if sample_format != JACK_SAMPLE_FORMAT { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format {sample_format} is not supported; JACK requires {JACK_SAMPLE_FORMAT}"), + )); } let name = self.name.clone(); let start_server_automatically = self.start_server_automatically; let connect_ports_automatically = self.connect_ports_automatically; - let build = move || -> Result { - // Create a fresh client to validate against live server state. + let build = move || -> Result { let client_options = super::get_client_options(start_server_automatically); - let client = super::get_client(&name, client_options) - .map_err(|err| BuildStreamError::BackendSpecific { err })?; + let client = super::get_client(&name, client_options)?; if conf.sample_rate != client.sample_rate() { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample rate {} Hz does not match JACK server rate {} Hz", + conf.sample_rate, + client.sample_rate() + ), + )); } if let crate::BufferSize::Fixed(size) = conf.buffer_size { if size != client.buffer_size() { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "buffer size {size} does not match JACK server buffer size {}", + client.buffer_size() + ), + )); } } let mut stream = @@ -226,11 +236,10 @@ impl DeviceTrait for Device { }); match rx.recv_timeout(dur) { Ok(result) => result, - Err(_) => Err(BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: "timed out waiting for JACK server".into(), - }, - }), + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for JACK server", + )), } } else { build() @@ -244,34 +253,51 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { if self.is_input() { - // Trying to create an output stream from an input device - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support output", + )); } if sample_format != JACK_SAMPLE_FORMAT { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format {sample_format} is not supported; JACK requires {JACK_SAMPLE_FORMAT}"), + )); } let name = self.name.clone(); let start_server_automatically = self.start_server_automatically; let connect_ports_automatically = self.connect_ports_automatically; - let build = move || -> Result { + let build = move || -> Result { // Create a fresh client to validate against live server state. let client_options = super::get_client_options(start_server_automatically); - let client = super::get_client(&name, client_options) - .map_err(|err| BuildStreamError::BackendSpecific { err })?; + let client = super::get_client(&name, client_options)?; if conf.sample_rate != client.sample_rate() { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample rate {} Hz does not match JACK server rate {} Hz", + conf.sample_rate, + client.sample_rate() + ), + )); } if let crate::BufferSize::Fixed(size) = conf.buffer_size { if size != client.buffer_size() { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "buffer size {size} does not match JACK server buffer size {}", + client.buffer_size() + ), + )); } } let mut stream = @@ -289,11 +315,10 @@ impl DeviceTrait for Device { }); match rx.recv_timeout(dur) { Ok(result) => result, - Err(_) => Err(BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: "timed out waiting for JACK server".into(), - }, - }), + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for JACK server", + )), } } else { build() diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index 6c235659a..2bef1e2ee 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -5,7 +5,7 @@ extern crate jack; use crate::traits::HostTrait; -use crate::{BackendSpecificError, DevicesError, SampleFormat}; +use crate::{Error, ErrorKind, SampleFormat}; mod device; mod stream; @@ -43,7 +43,7 @@ pub struct Host { } impl Host { - pub fn new() -> Result { + pub fn new() -> Result { let mut host = Host { name: format!("cpal_client_{}", std::process::id()), connect_ports_automatically: true, @@ -93,11 +93,8 @@ impl Host { self.start_server_automatically, ); - match in_device_res { - Ok(device) => self.devices_created.push(device), - Err(err) => { - println!("{}", err.description); - } + if let Ok(device) = in_device_res { + self.devices_created.push(device); } let out_device_res = Device::default_output_device( @@ -105,11 +102,8 @@ impl Host { self.connect_ports_automatically, self.start_server_automatically, ); - match out_device_res { - Ok(device) => self.devices_created.push(device), - Err(err) => { - println!("{}", err.description); - } + if let Ok(device) = out_device_res { + self.devices_created.push(device); } } } @@ -133,7 +127,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Ok(self.devices_created.clone().into_iter()) } @@ -165,45 +159,78 @@ fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions { client_options } -fn get_client( - name: &str, - client_options: jack::ClientOptions, -) -> Result { - let err = |description: &str| BackendSpecificError { - description: description.to_owned(), - }; - match jack::Client::new(name, client_options) { - Ok((client, status)) => { - if status.intersects(jack::ClientStatus::SERVER_ERROR) { - Err(err( - "There was an error communicating with the JACK server!", - )) - } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { - Err(err("Could not connect to the JACK server!")) - } else if status.intersects(jack::ClientStatus::VERSION_ERROR) { - Err(err( - "Error connecting to JACK server: Client's protocol version does not match!", - )) - } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { - Err(err( - "Error connecting to JACK server: Unable to initialize client!", - )) - } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { - Err(err( - "Error connecting to JACK server: Unable to access shared memory!", - )) - } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { - Err(err( - "Error connecting to JACK server: Requested client does not exist!", - )) - } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { - Err(err("Error connecting to JACK server: The operation contained an invalid or unsupported option!")) - } else { - Ok(client) +impl From for Error { + fn from(err: jack::Error) -> Self { + let msg = format!("{err}"); + match err { + jack::Error::ClientError(_) + | jack::Error::ClientActivationError + | jack::Error::ClientDeactivationError + | jack::Error::LibraryError(_) + | jack::Error::WeakFunctionNotFound(_) + | jack::Error::RingbufferCreateFailed => { + Error::with_message(ErrorKind::DeviceNotAvailable, msg) + } + + jack::Error::ClientIsNoLongerAlive | jack::Error::ClientPanicked => { + Error::with_message(ErrorKind::StreamInvalidated, msg) + } + + jack::Error::SetBufferSizeError | jack::Error::NotEnoughSpace => { + Error::with_message(ErrorKind::UnsupportedConfig, msg) + } + + jack::Error::InvalidDeactivation | jack::Error::FreewheelError => { + Error::with_message(ErrorKind::UnsupportedOperation, msg) + } + + jack::Error::PortNamingError | jack::Error::PortAliasError => { + Error::with_message(ErrorKind::InvalidInput, msg) } + + _ => Error::with_message(ErrorKind::Other, msg), } - Err(e) => Err(BackendSpecificError { - description: format!("Failed to open client because of error: {:?}", e), - }), + } +} + +fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { + let (client, status) = jack::Client::new(name, client_options)?; + if status.intersects(jack::ClientStatus::VERSION_ERROR) { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "client protocol version does not match the JACK server", + )) + } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "JACK client operation contained an invalid or unsupported option", + )) + } else if status.intersects(jack::ClientStatus::SERVER_ERROR) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "error communicating with the JACK server", + )) + } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "could not connect to the JACK server", + )) + } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "unable to initialize JACK client", + )) + } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "unable to access JACK shared memory", + )) + } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "requested JACK client does not exist", + )) + } else { + Ok(client) } } diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 4dcec87c7..57218a4e3 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -4,13 +4,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use crate::{ - BackendSpecificError, BuildStreamError, Data, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleRate, StreamError, StreamInstant, + Data, Error, ErrorKind, InputCallbackInfo, OutputCallbackInfo, SampleRate, StreamInstant, }; use super::JACK_SAMPLE_FORMAT; -type ErrorCallbackPtr = Arc>; +type ErrorCallbackPtr = Arc>; pub struct Stream { // TODO: It might be faster to send a message when playing/pausing than to check this every iteration @@ -31,20 +30,21 @@ impl Stream { channels: ChannelCount, data_callback: D, error_callback: E, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let mut ports = vec![]; let mut port_names: Vec = vec![]; for i in 0..channels { let port = client .register_port(&format!("in_{}", i), jack::AudioIn::default()) - .map_err(|e| BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to register input port {}: {}", i, e), - }, + .map_err(|e| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("failed to register input port {i}: {e}"), + ) })?; if let Ok(port_name) = port.name() { port_names.push(port_name); @@ -69,10 +69,11 @@ impl Stream { let async_client = client .activate_async(notification_handler, input_process_handler) - .map_err(|e| BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to activate JACK client: {:?}", e), - }, + .map_err(|e| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("failed to activate JACK client: {e:?}"), + ) })?; Ok(Self { @@ -88,20 +89,21 @@ impl Stream { channels: ChannelCount, data_callback: D, error_callback: E, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let mut ports = vec![]; let mut port_names: Vec = vec![]; for i in 0..channels { let port = client .register_port(&format!("out_{}", i), jack::AudioOut::default()) - .map_err(|e| BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to register output port {}: {}", i, e), - }, + .map_err(|e| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("failed to register output port {i}: {e}"), + ) })?; if let Ok(port_name) = port.name() { port_names.push(port_name); @@ -126,10 +128,11 @@ impl Stream { let async_client = client .activate_async(notification_handler, output_process_handler) - .map_err(|e| BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to activate JACK client: {:?}", e), - }, + .map_err(|e| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("failed to activate JACK client: {e:?}"), + ) })?; Ok(Self { @@ -194,12 +197,12 @@ impl Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { self.playing.store(true, Ordering::Relaxed); Ok(()) } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { self.playing.store(false, Ordering::Relaxed); Ok(()) } @@ -208,7 +211,7 @@ impl StreamTrait for Stream { micros_to_stream_instant(self.async_client.as_client().time()) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.async_client.as_client().buffer_size() as crate::FrameCount) } } @@ -404,13 +407,16 @@ impl JackNotificationHandler { } impl jack::NotificationHandler for JackNotificationHandler { - unsafe fn shutdown(&mut self, _status: jack::ClientStatus, _reason: &str) { + unsafe fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) { self.error_callback_ptr .lock() - .unwrap_or_else(|e| e.into_inner())(StreamError::DeviceNotAvailable); + .unwrap_or_else(|e| e.into_inner())(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("JACK server shut down: {reason}"), + )); } - fn sample_rate(&mut self, _: &jack::Client, _srate: jack::Frames) -> jack::Control { + fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { match self.init_sample_rate_flag.load(Ordering::Relaxed) { false => { // One of these notifications is sent every time a client is started. @@ -422,9 +428,10 @@ impl jack::NotificationHandler for JackNotificationHandler { // The stream configuration must be rebuilt with the new sample rate. self.error_callback_ptr .lock() - .unwrap_or_else(|e| e.into_inner())( - StreamError::StreamInvalidated - ); + .unwrap_or_else(|e| e.into_inner())(Error::with_message( + ErrorKind::StreamInvalidated, + format!("JACK server changed sample rate to {srate} Hz"), + )); jack::Control::Quit } } @@ -432,9 +439,9 @@ impl jack::NotificationHandler for JackNotificationHandler { fn xrun(&mut self, _: &jack::Client) -> jack::Control { match self.error_callback_ptr.try_lock() { - Ok(mut cb) => cb(StreamError::BufferUnderrun), + Ok(mut cb) => cb(Error::with_message(ErrorKind::Xrun, "JACK xrun detected")), Err(std::sync::TryLockError::Poisoned(e)) => { - e.into_inner()(StreamError::BufferUnderrun) + e.into_inner()(Error::with_message(ErrorKind::Xrun, "JACK xrun detected")) } Err(std::sync::TryLockError::WouldBlock) => {} } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 16e7a2457..4284a24f1 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -6,10 +6,9 @@ use std::time::Duration; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, - DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, StreamConfig, SupportedStreamConfig, + SupportedStreamConfigRange, }; #[derive(Default)] @@ -34,13 +33,13 @@ pub struct SupportedOutputConfigs; impl Host { #[allow(dead_code)] - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } impl Devices { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Devices) } } @@ -50,35 +49,27 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Ok("null".to_string()) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new("Null Device".to_string()).build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId(crate::platform::HostId::Null, String::new())) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { unimplemented!() } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { unimplemented!() } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { unimplemented!() } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { unimplemented!() } @@ -89,10 +80,10 @@ impl DeviceTrait for Device { _data_callback: D, _error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { unimplemented!() } @@ -105,10 +96,10 @@ impl DeviceTrait for Device { _data_callback: D, _error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { unimplemented!() } @@ -122,7 +113,7 @@ impl HostTrait for Host { false } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new() } @@ -136,11 +127,11 @@ impl HostTrait for Host { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { unimplemented!() } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { unimplemented!() } @@ -148,7 +139,7 @@ impl StreamTrait for Stream { unimplemented!() } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { unimplemented!() } } diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index fae18223e..40cfef6e2 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -152,14 +152,14 @@ impl DeviceTrait for Device { type SupportedInputConfigs = SupportedInputConfigs; type SupportedOutputConfigs = SupportedOutputConfigs; - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(crate::DeviceId( crate::HostId::PipeWire, self.node_name.clone(), )) } - fn description(&self) -> Result { + fn description(&self) -> Result { let mut builder = crate::DeviceDescriptionBuilder::new(&self.nick_name) .direction(self.direction) .device_type(self.device_type()) @@ -190,9 +190,7 @@ impl DeviceTrait for Device { ) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { if !self.supports_input() { return Ok(vec![].into_iter()); } @@ -220,9 +218,7 @@ impl DeviceTrait for Device { .collect::>() .into_iter()) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { if !self.supports_output() { return Ok(vec![].into_iter()); } @@ -250,11 +246,12 @@ impl DeviceTrait for Device { .collect::>() .into_iter()) } - fn default_input_config( - &self, - ) -> Result { + fn default_input_config(&self) -> Result { if !self.supports_input() { - return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported); + return Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedOperation, + "device does not support input", + )); } Ok(crate::SupportedStreamConfig { channels: self.channels, @@ -267,11 +264,12 @@ impl DeviceTrait for Device { }) } - fn default_output_config( - &self, - ) -> Result { + fn default_output_config(&self) -> Result { if !self.supports_output() { - return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported); + return Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedOperation, + "device does not support output", + )); } Ok(crate::SupportedStreamConfig { channels: self.channels, @@ -291,10 +289,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, - E: FnMut(crate::StreamError) + Send + 'static, + E: FnMut(crate::Error) + Send + 'static, { let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); @@ -307,6 +305,7 @@ impl DeviceTrait for Device { }; let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); + let start = std::time::Instant::now(); let handle = thread::Builder::new() .name("pw_in".to_owned()) .spawn(move || { @@ -324,6 +323,7 @@ impl DeviceTrait for Device { data_callback, error_callback, last_quantum_clone, + start, ) else { let _ = pw_init_tx.send(false); @@ -345,23 +345,27 @@ impl DeviceTrait for Device { drop(listener); drop(context); }) - .map_err(|e| crate::BuildStreamError::BackendSpecific { - err: crate::BackendSpecificError { - description: format!("failed to create thread: {e}"), - }, + .map_err(|e| { + crate::Error::with_message( + crate::ErrorKind::Other, + format!("failed to create thread: {e}"), + ) })?; match pw_init_rx.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, last_quantum, + start, }), - Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), - Err(_) => Err(crate::BuildStreamError::BackendSpecific { - err: crate::BackendSpecificError { - description: "pipewire timeout".to_owned(), - }, - }), + Ok(false) => Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedConfig, + "stream configuration rejected by PipeWire", + )), + Err(_) => Err(crate::Error::with_message( + crate::ErrorKind::DeviceNotAvailable, + "PipeWire timed out", + )), } } @@ -372,10 +376,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, - E: FnMut(crate::StreamError) + Send + 'static, + E: FnMut(crate::Error) + Send + 'static, { let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); @@ -388,6 +392,7 @@ impl DeviceTrait for Device { }; let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); + let start = std::time::Instant::now(); let handle = thread::Builder::new() .name("pw_out".to_owned()) .spawn(move || { @@ -406,6 +411,7 @@ impl DeviceTrait for Device { data_callback, error_callback, last_quantum_clone, + start, ) else { let _ = pw_init_tx.send(false); @@ -428,23 +434,27 @@ impl DeviceTrait for Device { drop(listener); drop(context); }) - .map_err(|e| crate::BuildStreamError::BackendSpecific { - err: crate::BackendSpecificError { - description: format!("failed to create thread: {e}"), - }, + .map_err(|e| { + crate::Error::with_message( + crate::ErrorKind::Other, + format!("failed to create thread: {e}"), + ) })?; match pw_init_rx.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, last_quantum, + start, }), - Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), - Err(_) => Err(crate::BuildStreamError::BackendSpecific { - err: crate::BackendSpecificError { - description: "pipewire timeout".to_owned(), - }, - }), + Ok(false) => Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedConfig, + "stream configuration rejected by PipeWire", + )), + Err(_) => Err(crate::Error::with_message( + crate::ErrorKind::DeviceNotAvailable, + "PipeWire timed out", + )), } } } diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index c00c69dc0..bdec290e6 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -14,9 +14,14 @@ pub struct Host { } impl Host { - pub fn new() -> Result { + pub fn new() -> Result { let _pw = PwInitGuard::new(); - let devices = init_devices().ok_or(crate::HostUnavailable)?; + let devices = init_devices().ok_or_else(|| { + crate::Error::with_message( + crate::ErrorKind::HostUnavailable, + "PipeWire host initialization failed", + ) + })?; Ok(Host { _pw, devices }) } } @@ -27,7 +32,7 @@ impl HostTrait for Host { fn is_available() -> bool { utils::find_socket_path().is_some() } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Ok(self.devices.clone().into_iter()) } diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index b3e1b0612..08853693e 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -4,11 +4,12 @@ use std::{ Arc, Mutex, }, thread::JoinHandle, + time::Instant, }; use crate::{ - host::fill_with_equilibrium, traits::StreamTrait, BackendSpecificError, InputCallbackInfo, - OutputCallbackInfo, SampleFormat, StreamConfig, StreamError, StreamInstant, + host::fill_with_equilibrium, traits::StreamTrait, Error, ErrorKind, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, StreamConfig, StreamInstant, }; use pipewire::{ self as pw, @@ -68,6 +69,7 @@ pub struct Stream { pub(crate) handle: Option>, pub(crate) controller: pw::channel::Sender, pub(crate) last_quantum: Arc, + pub(crate) start: Instant, } impl Drop for Stream { @@ -78,32 +80,34 @@ impl Drop for Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), crate::PlayStreamError> { + fn play(&self) -> Result<(), crate::Error> { self.controller .send(StreamCommand::Toggle(true)) - .map_err(|_| crate::PlayStreamError::BackendSpecific { - err: BackendSpecificError { - description: "Cannot send message".to_owned(), - }, + .map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) })?; Ok(()) } - fn pause(&self) -> Result<(), crate::PauseStreamError> { + fn pause(&self) -> Result<(), crate::Error> { self.controller .send(StreamCommand::Toggle(false)) - .map_err(|_| crate::PauseStreamError::BackendSpecific { - err: BackendSpecificError { - description: "Cannot send message".to_owned(), - }, + .map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) })?; Ok(()) } fn now(&self) -> crate::StreamInstant { - monotonic_stream_instant().expect("clock_gettime failed") + monotonic_stream_instant().unwrap_or_else(|| stream_instant_from_start(self.start)) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.last_quantum.load(Ordering::Relaxed) as _) } } @@ -176,17 +180,16 @@ pub struct UserData { sample_format: SampleFormat, format: pw::spa::param::audio::AudioInfoRaw, last_quantum: Arc, + start: Instant, } impl UserData where - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { fn state_changed(&mut self, new: StreamState) { match new { pipewire::stream::StreamState::Error(e) => { - (self.error_callback)(StreamError::BackendSpecific { - err: BackendSpecificError { description: e }, - }) + (self.error_callback)(Error::with_message(ErrorKind::StreamInvalidated, e)) } // TODO: maybe we need to log information when every new state comes? pipewire::stream::StreamState::Paused => {} @@ -232,14 +235,9 @@ fn pw_stream_time(stream: &pw::stream::Stream) -> Option { impl UserData where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - fn publish_data_in( - &mut self, - stream: &pw::stream::Stream, - frames: usize, - data: &Data, - ) -> Result<(), BackendSpecificError> { + fn publish_data_in(&mut self, stream: &pw::stream::Stream, frames: usize, data: &Data) { self.last_quantum.store(frames as u64, Ordering::Relaxed); let (callback, capture) = match pw_stream_time(stream) { Some(PwTime { now_ns, delay_ns }) => ( @@ -247,9 +245,8 @@ where StreamInstant::from_nanos((now_ns - delay_ns.max(0)) as u64), ), None => { - let cb = monotonic_stream_instant().ok_or_else(|| BackendSpecificError { - description: "clock_gettime failed".to_owned(), - })?; + let cb = monotonic_stream_instant() + .unwrap_or_else(|| stream_instant_from_start(self.start)); let capture = cb .checked_sub(frames_to_duration(frames, self.format.rate())) .unwrap_or(crate::StreamInstant::ZERO); @@ -259,20 +256,14 @@ where let timestamp = crate::InputStreamTimestamp { callback, capture }; let info = InputCallbackInfo { timestamp }; (self.data_callback)(data, &info); - Ok(()) } } impl UserData where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - fn publish_data_out( - &mut self, - stream: &pw::stream::Stream, - frames: usize, - data: &mut Data, - ) -> Result<(), BackendSpecificError> { + fn publish_data_out(&mut self, stream: &pw::stream::Stream, frames: usize, data: &mut Data) { self.last_quantum.store(frames as u64, Ordering::Relaxed); let (callback, playback) = match pw_stream_time(stream) { Some(PwTime { now_ns, delay_ns }) => ( @@ -280,9 +271,8 @@ where StreamInstant::from_nanos((now_ns + delay_ns.max(0)) as u64), ), None => { - let cb = monotonic_stream_instant().ok_or_else(|| BackendSpecificError { - description: "clock_gettime failed".to_owned(), - })?; + let cb = monotonic_stream_instant() + .unwrap_or_else(|| stream_instant_from_start(self.start)); let pl = cb + frames_to_duration(frames, self.format.rate()); (cb, pl) } @@ -290,7 +280,6 @@ where let timestamp = crate::OutputStreamTimestamp { callback, playback }; let info = OutputCallbackInfo { timestamp }; (self.data_callback)(data, &info); - Ok(()) } } pub struct StreamData { @@ -300,6 +289,12 @@ pub struct StreamData { pub context: ContextRc, } +/// Fallback timestamp using elapsed time since stream creation. +fn stream_instant_from_start(start: Instant) -> StreamInstant { + let elapsed = start.elapsed(); + StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos()) +} + /// Read `clock_gettime` and return it as a [`StreamInstant`]. /// /// This is the same clock used by `pw_stream_get_time_n` (`pw_time.now`), so values @@ -341,10 +336,11 @@ pub fn connect_output( data_callback: D, error_callback: E, last_quantum: Arc, + start: Instant, ) -> Result, pw::Error> where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let mainloop = pw::main_loop::MainLoopRc::new(None)?; let context = pw::context::ContextRc::new(&mainloop, None)?; @@ -356,6 +352,7 @@ where sample_format, format: Default::default(), last_quantum, + start, }; let channels = config.channels as _; let rate = config.sample_rate as _; @@ -392,18 +389,16 @@ where || current_rate != rate || current_fmt != expected_fmt; if mismatch { - (user_data.error_callback)(StreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("negotiated format mismatch: expected channels={channels} rate={rate} format={expected_fmt:?}, got channels={current_channels} rate={current_rate} format={current_fmt:?}"), - }, - }); + (user_data.error_callback)(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("negotiated format mismatch: expected channels={channels} rate={rate} format={expected_fmt:?}, got channels={current_channels} rate={current_rate} format={current_fmt:?}"), + )); // if the format does not match, we stop the stream if let Err(e) = stream.set_active(false) { - (user_data.error_callback)(StreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("failed to stop the stream, reason: {e}"), - }, - }); + (user_data.error_callback)(Error::with_message( + ErrorKind::Other, + format!("failed to stop the stream, reason: {e}"), + )); } } @@ -446,9 +441,7 @@ where let data = active.as_mut_ptr() as *mut (); let mut data = unsafe { Data::from_parts(data, n_samples, user_data.sample_format) }; - if let Err(err) = user_data.publish_data_out(stream, frames, &mut data) { - (user_data.error_callback)(StreamError::BackendSpecific { err }); - } + user_data.publish_data_out(stream, frames, &mut data); let chunk = buf_data.chunk_mut(); *chunk.offset_mut() = 0; *chunk.stride_mut() = stride as i32; @@ -501,10 +494,11 @@ pub fn connect_input( data_callback: D, error_callback: E, last_quantum: Arc, + start: Instant, ) -> Result, pw::Error> where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let mainloop = pw::main_loop::MainLoopRc::new(None)?; let context = pw::context::ContextRc::new(&mainloop, None)?; @@ -516,6 +510,7 @@ where sample_format, format: Default::default(), last_quantum, + start, }; let channels = config.channels as _; @@ -555,18 +550,16 @@ where || current_rate != rate || current_fmt != expected_fmt; if mismatch { - (user_data.error_callback)(StreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("negotiated format mismatch: expected channels={channels} rate={rate} format={expected_fmt:?}, got channels={current_channels} rate={current_rate} format={current_fmt:?}"), - }, - }); + (user_data.error_callback)(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("negotiated format mismatch: expected channels={channels} rate={rate} format={expected_fmt:?}, got channels={current_channels} rate={current_rate} format={current_fmt:?}"), + )); // if the format does not match, we stop the stream if let Err(e) = stream.set_active(false) { - (user_data.error_callback)(StreamError::BackendSpecific { - err: BackendSpecificError { - description: format!("failed to stop the stream, reason: {e}"), - }, - }); + (user_data.error_callback)(Error::with_message( + ErrorKind::Other, + format!("failed to stop the stream, reason: {e}"), + )); } } } @@ -594,9 +587,7 @@ where let data = samples.as_mut_ptr() as *mut (); let data = unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; - if let Err(err) = user_data.publish_data_in(stream, frames as usize, &data) { - (user_data.error_callback)(StreamError::BackendSpecific { err }); - } + user_data.publish_data_in(stream, frames as usize, &data); } }) .register()?; diff --git a/src/host/pulseaudio/mod.rs b/src/host/pulseaudio/mod.rs index ee9bb1059..e800ad8f9 100644 --- a/src/host/pulseaudio/mod.rs +++ b/src/host/pulseaudio/mod.rs @@ -7,12 +7,11 @@ mod stream; pub use stream::Stream; use crate::{ + error::ResultExt, traits::{DeviceTrait, HostTrait}, - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, - DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, FrameCount, HostId, HostUnavailable, InputCallbackInfo, OutputCallbackInfo, - SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, Error, ErrorKind, + FrameCount, HostId, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, + StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, }; const MIN_SAMPLE_RATE: SampleRate = 8000; @@ -67,10 +66,50 @@ impl TryFrom for protocol::SampleFormat { } } -impl From for BackendSpecificError { +impl From for Error { fn from(err: pulseaudio::ClientError) -> Self { - BackendSpecificError { - description: err.to_string(), + use pulseaudio::ClientError::*; + + fn pulse_error_kind(e: protocol::PulseError) -> ErrorKind { + use protocol::PulseError::*; + match e { + AccessDenied | AuthKey => ErrorKind::PermissionDenied, + ConnectionRefused | InvalidServer | ModInitFailed => ErrorKind::HostUnavailable, + NoEntity => ErrorKind::DeviceNotAvailable, + Timeout | Busy => ErrorKind::DeviceBusy, + NotSupported | Obsolete | NotImplemented | Version | NoExtension => { + ErrorKind::UnsupportedOperation + } + Invalid | Command | TooLarge | Exist | Forked => ErrorKind::InvalidInput, + ConnectionTerminated | Killed | Protocol | BadState | Io => { + ErrorKind::StreamInvalidated + } + NoData => ErrorKind::Xrun, + _ => ErrorKind::Other, + } + } + + match err { + ServerUnavailable => { + Error::with_message(ErrorKind::HostUnavailable, "PulseAudio server unavailable") + } + UnexpectedSequenceNumber | Disconnected => Error::with_message( + ErrorKind::StreamInvalidated, + "PulseAudio client disconnected", + ), + Io(e) => Error::with_message(ErrorKind::StreamInvalidated, format!("I/O error: {e}")), + ServerError(e) => Error::with_message(pulse_error_kind(e), format!("{e}")), + Protocol(e) => { + use protocol::ProtocolError::*; + let kind = match &e { + UnsupportedVersion(_) | Unimplemented(..) => ErrorKind::UnsupportedOperation, + Invalid(_) => ErrorKind::InvalidInput, + Timeout => ErrorKind::DeviceBusy, + UnexpectedCommand(_) | Io(_) => ErrorKind::StreamInvalidated, + ServerError(e) => pulse_error_kind(*e), + }; + Error::with_message(kind, format!("{e}")) + } } } } @@ -82,9 +121,13 @@ pub struct Host { } impl Host { - pub fn new() -> Result { - let client = - pulseaudio::Client::from_env(c"cpal-pulseaudio").map_err(|_| HostUnavailable)?; + pub fn new() -> Result { + let client = pulseaudio::Client::from_env(c"cpal-pulseaudio").map_err(|e| { + Error::with_message( + ErrorKind::HostUnavailable, + format!("PulseAudio unavailable: {e}"), + ) + })?; Ok(Self { client }) } @@ -98,14 +141,12 @@ impl HostTrait for Host { pulseaudio::socket_path_from_env().is_some() } - fn devices(&self) -> Result { - let sinks = block_on(self.client.list_sinks()).map_err(|err| BackendSpecificError { - description: format!("Failed to list sinks: {err}"), - })?; + fn devices(&self) -> Result { + let sinks = + block_on(self.client.list_sinks()).context("failed to list PulseAudio sinks")?; - let sources = block_on(self.client.list_sources()).map_err(|err| BackendSpecificError { - description: format!("Failed to list sources: {err}"), - })?; + let sources = + block_on(self.client.list_sources()).context("failed to list PulseAudio sources")?; Ok(sinks .into_iter() @@ -185,11 +226,16 @@ fn supported_config_ranges() -> Vec { fn default_config_from_spec( sample_spec: &protocol::SampleSpec, channel_map: &protocol::ChannelMap, -) -> Result { - let sample_format: SampleFormat = sample_spec - .format - .try_into() - .map_err(|_| DefaultStreamConfigError::StreamTypeNotSupported)?; +) -> Result { + let sample_format: SampleFormat = sample_spec.format.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "PulseAudio sample format {:?} is not supported", + sample_spec.format + ), + ) + })?; let bytes_per_frame = channel_map.num_channels() as usize * sample_format.sample_size(); let max_frames = (protocol::MAX_MEMBLOCKQ_LENGTH / bytes_per_frame) as u32; Ok(SupportedStreamConfig { @@ -208,7 +254,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = std::vec::IntoIter; type Stream = Stream; - fn name(&self) -> Result { + fn name(&self) -> Result { let name = match self { Device::Sink { info, .. } => &info.name, Device::Source { info, .. } => &info.name, @@ -217,34 +263,36 @@ impl DeviceTrait for Device { Ok(String::from_utf8_lossy(name.as_bytes()).into_owned()) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { let Device::Source { .. } = self else { return Ok(vec![].into_iter()); }; Ok(supported_config_ranges().into_iter()) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { let Device::Sink { .. } = self else { return Ok(vec![].into_iter()); }; Ok(supported_config_ranges().into_iter()) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { let Device::Source { info, .. } = self else { - return Err(DefaultStreamConfigError::StreamTypeNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support input", + )); }; default_config_from_spec(&info.sample_spec, &info.channel_map) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { let Device::Sink { info, .. } = self else { - return Err(DefaultStreamConfigError::StreamTypeNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support output", + )); }; default_config_from_spec(&info.sample_spec, &info.channel_map) } @@ -256,18 +304,24 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let Device::Source { client, info } = self else { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support input", + )); }; - let format: protocol::SampleFormat = sample_format - .try_into() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + let format: protocol::SampleFormat = sample_format.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format {sample_format} is not supported by PulseAudio"), + ) + })?; let sample_spec = make_sample_spec(config, format); let channel_map = make_channel_map(config); @@ -306,11 +360,10 @@ impl DeviceTrait for Device { }); match rx.recv_timeout(dur) { Ok(result) => result, - Err(_) => Err(BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: "timed out waiting for PulseAudio server".into(), - }, - }), + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for PulseAudio server", + )), } } else { stream::Stream::new_record(client, params, data_callback, error_callback) @@ -324,18 +377,24 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let Device::Sink { client, info } = self else { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support output", + )); }; - let format: protocol::SampleFormat = sample_format - .try_into() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + let format: protocol::SampleFormat = sample_format.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("sample format {sample_format} is not supported by PulseAudio"), + ) + })?; let sample_spec = make_sample_spec(config, format); let channel_map = make_channel_map(config); @@ -374,18 +433,17 @@ impl DeviceTrait for Device { }); match rx.recv_timeout(dur) { Ok(result) => result, - Err(_) => Err(BuildStreamError::BackendSpecific { - err: BackendSpecificError { - description: "timed out waiting for PulseAudio server".into(), - }, - }), + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for PulseAudio server", + )), } } else { stream::Stream::new_playback(client, params, data_callback, error_callback) } } - fn description(&self) -> Result { + fn description(&self) -> Result { let (name, description, direction) = match self { Device::Sink { info, .. } => (&info.name, &info.description, DeviceDirection::Output), Device::Source { info, .. } => (&info.name, &info.description, DeviceDirection::Input), @@ -400,7 +458,7 @@ impl DeviceTrait for Device { Ok(builder.build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { let id = match self { Device::Sink { info, .. } => info.index, Device::Source { info, .. } => info.index, diff --git a/src/host/pulseaudio/stream.rs b/src/host/pulseaudio/stream.rs index a1bf235e1..5705ea1e0 100644 --- a/src/host/pulseaudio/stream.rs +++ b/src/host/pulseaudio/stream.rs @@ -10,9 +10,8 @@ use futures::executor::block_on; use pulseaudio::{protocol, AsPlaybackSource}; use crate::{ - traits::StreamTrait, BackendSpecificError, BuildStreamError, Data, FrameCount, - InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, - PlayStreamError, SampleFormat, StreamError, StreamInstant, + traits::StreamTrait, Data, Error, ErrorKind, FrameCount, InputCallbackInfo, + InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, StreamInstant, }; const LATENCY_MAX_INTERVAL: Duration = Duration::from_millis(100); @@ -65,27 +64,27 @@ impl Drop for Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { match &self.0 { StreamInner::Playback(stream, _, handle) => { - block_on(stream.uncork()).map_err(Into::::into)?; + block_on(stream.uncork()).map_err(Error::from)?; handle.notify(); } StreamInner::Record(stream, _, handle) => { - block_on(stream.uncork()).map_err(Into::::into)?; - block_on(stream.started()).map_err(Into::::into)?; + block_on(stream.uncork()).map_err(Error::from)?; + block_on(stream.started()).map_err(Error::from)?; handle.notify(); } } Ok(()) } - fn pause(&self) -> Result<(), crate::PauseStreamError> { + fn pause(&self) -> Result<(), Error> { let res = match &self.0 { StreamInner::Playback(stream, _, _) => block_on(stream.cork()), StreamInner::Record(stream, _, _) => block_on(stream.cork()), }; - res.map_err(Into::::into)?; + res.map_err(Error::from)?; match &self.0 { StreamInner::Playback(_, _, handle) | StreamInner::Record(_, _, handle) => { handle.notify() @@ -102,7 +101,7 @@ impl StreamTrait for Stream { StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos()) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { let (spec, bytes) = match &self.0 { StreamInner::Playback(s, _, _) => ( s.sample_spec(), @@ -123,10 +122,10 @@ impl Stream { params: protocol::PlaybackStreamParams, mut data_callback: D, error_callback: E, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let start = Instant::now(); @@ -138,10 +137,15 @@ impl Stream { let poll_clone = last_poll_micros.clone(); let sample_spec = params.sample_spec; - let format: SampleFormat = sample_spec - .format - .try_into() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + let format: SampleFormat = sample_spec.format.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "PulseAudio sample format {:?} is not supported", + sample_spec.format + ), + ) + })?; // Silence for unsigned formats is the midpoint, not zero. Among // PulseAudio's supported formats, only U8 is unsigned and has a @@ -207,7 +211,7 @@ impl Stream { }; let stream = block_on(client.create_playback_stream(params, callback.as_playback_source())) - .map_err(Into::::into)?; + .map_err(Error::from)?; // Share the error callback between the worker and latency threads so // both can surface errors to the user. @@ -221,11 +225,7 @@ impl Stream { if let Err(e) = block_on(stream_clone.play_all()) { error_callback_clone .lock() - .unwrap_or_else(|e| e.into_inner())(StreamError::from( - BackendSpecificError { - description: e.to_string(), - }, - )); + .unwrap_or_else(|e| e.into_inner())(Error::from(e)); } }); @@ -243,11 +243,7 @@ impl Stream { let timing_info = match block_on(stream_clone.timing_info()) { Ok(timing_info) => timing_info, Err(e) => { - error_callback.lock().unwrap_or_else(|e| e.into_inner())(StreamError::from( - BackendSpecificError { - description: e.to_string(), - }, - )); + error_callback.lock().unwrap_or_else(|e| e.into_inner())(Error::from(e)); break; } }; @@ -266,10 +262,10 @@ impl Stream { // Wait until woken by a write/play/pause/drop event or until LATENCY_MAX_INTERVAL. let (lock, cvar) = &*update_thread; - let guard = lock.lock().unwrap(); + let Ok(guard) = lock.lock() else { break }; let (mut guard, _) = cvar .wait_timeout_while(guard, LATENCY_MAX_INTERVAL, |notified| !*notified) - .unwrap(); + .unwrap_or_else(|e| e.into_inner()); *guard = false; }); @@ -281,10 +277,10 @@ impl Stream { params: protocol::RecordStreamParams, mut data_callback: D, mut error_callback: E, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let start = Instant::now(); @@ -296,10 +292,15 @@ impl Stream { let poll_clone = last_poll_micros.clone(); let sample_spec = params.sample_spec; - let format: SampleFormat = sample_spec - .format - .try_into() - .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; + let format: SampleFormat = sample_spec.format.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "PulseAudio sample format {:?} is not supported", + sample_spec.format + ), + ) + })?; let handle = LatencyHandle::new(); let update_callback = handle.update.clone(); @@ -348,8 +349,8 @@ impl Stream { cvar.notify_one(); }; - let stream = block_on(client.create_record_stream(params, callback)) - .map_err(Into::::into)?; + let stream = + block_on(client.create_record_stream(params, callback)).map_err(Error::from)?; // Spawn a thread to monitor the stream's latency in a loop. let cancel_thread = handle.cancel.clone(); @@ -365,9 +366,7 @@ impl Stream { let timing_info = match block_on(stream_clone.timing_info()) { Ok(timing_info) => timing_info, Err(e) => { - error_callback(StreamError::from(BackendSpecificError { - description: e.to_string(), - })); + error_callback(Error::from(e)); break; } }; @@ -386,10 +385,10 @@ impl Stream { // Wait until woken by a read/play/pause/drop event or until LATENCY_MAX_INTERVAL. let (lock, cvar) = &*update_thread; - let guard = lock.lock().unwrap(); + let Ok(guard) = lock.lock() else { break }; let (mut guard, _) = cvar .wait_timeout_while(guard, LATENCY_MAX_INTERVAL, |notified| !*notified) - .unwrap(); + .unwrap_or_else(|e| e.into_inner()); *guard = false; }); diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index c51c02570..81d4c040e 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,9 +1,8 @@ use crate::{ - BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceDescription, - DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, - DeviceType, DevicesError, FrameCount, InputCallbackInfo, InterfaceType, OutputCallbackInfo, - SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, + error::ResultExt, BufferSize, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceDirection, DeviceId, DeviceType, Error, ErrorKind, FrameCount, InputCallbackInfo, + InterfaceType, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, COMMON_SAMPLE_RATES, }; impl From for DeviceDirection { @@ -27,7 +26,6 @@ use std::sync::OnceLock; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; -use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; use crate::host::com; use windows::core::Interface; use windows::core::GUID; @@ -42,7 +40,7 @@ use windows::Win32::System::Variant::{VT_LPWSTR, VT_UI4}; use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore; use super::stream::{AudioClientFlow, Stream, StreamInner}; -use crate::{traits::DeviceTrait, BuildStreamError, StreamError}; +use crate::traits::DeviceTrait; pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; @@ -84,11 +82,11 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } @@ -100,23 +98,19 @@ impl DeviceTrait for Device { self.data_flow() == Audio::eRender } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -127,10 +121,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let stream_inner = self.build_input_stream_raw_inner(config, sample_format)?; Ok(Stream::new_input( @@ -147,10 +141,10 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let stream_inner = self.build_output_stream_raw_inner(config, sample_format)?; Ok(Stream::new_output( @@ -192,7 +186,7 @@ unsafe fn data_flow_from_immendpoint(endpoint: &Audio::IMMEndpoint) -> Audio::ED pub unsafe fn is_format_supported( _client: &Audio::IAudioClient, _waveformatex_ptr: *const Audio::WAVEFORMATEX, -) -> Result { +) -> Result { // Checking formats is not needed for shared mode with auto-conversion, therefore this check has been removed until someone implements WASAPI exclusive mode support // I used an NAudio issue as reference: https://github.com/naudio/NAudio/issues/819 @@ -332,7 +326,7 @@ fn enumerator_to_interface_type(enumerator: &str) -> Option Result { + pub fn description(&self) -> Result { unsafe { // Open the device's property store. let property_store = self @@ -372,14 +366,12 @@ impl Device { ); // Prefer FriendlyName for name (e.g., "Speakers (XYZ Audio Adapter)"), fall back to DeviceDesc - let name = - friendly_name - .or(device_desc) - .ok_or_else(|| DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: "failed to retrieve device name".to_string(), - }, - })?; + let name = friendly_name.or(device_desc).ok_or_else(|| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + "failed to retrieve device name", + ) + })?; // Get direction from data flow (eCapture = Input, eRender = Output) let direction = self.data_flow().into(); @@ -420,18 +412,17 @@ impl Device { } } - fn id(&self) -> Result { + fn id(&self) -> Result { unsafe { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { Ok(id_str) => Ok(DeviceId(crate::platform::HostId::Wasapi, id_str)), - Err(e) => Err(DeviceIdError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to convert device ID to string: {}", e), - }, - }), + Err(e) => Err(Error::with_message( + ErrorKind::Other, + format!("failed to convert device ID to string: {e}"), + )), }, - Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), + Err(e) => Err(Error::from(e)), } } } @@ -451,7 +442,10 @@ impl Device { fn ensure_future_audio_client( &self, ) -> Result>, windows::core::Error> { - let mut lock = self.future_audio_client.lock().unwrap(); + let mut lock = self + .future_audio_client + .lock() + .map_err(|_| windows::core::Error::from(windows::Win32::Foundation::E_UNEXPECTED))?; if lock.is_some() { return Ok(lock); } @@ -483,22 +477,14 @@ impl Device { // number of channels seems to be supported. Any, more or less returns an invalid // parameter error. Thus, we just assume that the default number of channels is the only // number supported. - fn supported_formats(&self) -> Result { + fn supported_formats(&self) -> Result { // initializing COM because we call `CoTaskMemFree` to release the format. com::com_initialized(); // Retrieve the `IAudioClient`. - let lock = match self.ensure_future_audio_client() { - Ok(lock) => lock, - Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { - return Err(SupportedStreamConfigsError::DeviceNotAvailable) - } - Err(e) => { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - }; + let lock = self + .ensure_future_audio_client() + .context("failed to get audio client")?; let client = &lock.as_ref().unwrap().0; unsafe { @@ -506,24 +492,23 @@ impl Device { let default_waveformatex_ptr = client .GetMixFormat() .map(WaveFormatExPtr) - .map_err(windows_err_to_cpal_err::)?; + .context("failed to get mix format")?; // If the default format can't succeed we have no hope of finding other formats. if !is_format_supported(client, default_waveformatex_ptr.0)? { - let description = - "Could not determine support for default `WAVEFORMATEX`".to_string(); - let err = BackendSpecificError { description }; - return Err(err.into()); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "could not determine support for default WAVEFORMATEX", + )); } let format = match format_from_waveformatex_ptr(default_waveformatex_ptr.0, client) { Some(fmt) => fmt, None => { - let description = - "could not create a `cpal::SupportedStreamConfig` from a `WAVEFORMATEX`" - .to_string(); - let err = BackendSpecificError { description }; - return Err(err.into()); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "could not create a SupportedStreamConfig from WAVEFORMATEX", + )); } }; @@ -572,9 +557,7 @@ impl Device { } } - pub fn supported_input_configs( - &self, - ) -> Result { + pub fn supported_input_configs(&self) -> Result { if self.data_flow() == Audio::eCapture { self.supported_formats() // If it's an output device, assume no input formats. @@ -583,9 +566,7 @@ impl Device { } } - pub fn supported_output_configs( - &self, - ) -> Result { + pub fn supported_output_configs(&self) -> Result { if self.data_flow() == Audio::eRender { self.supported_formats() // If it's an input device, assume no output formats. @@ -598,31 +579,27 @@ impl Device { // processor to mix them together. // // One format is guaranteed to be supported, the one returned by `GetMixFormat`. - fn default_format(&self) -> Result { + fn default_format(&self) -> Result { // initializing COM because we call `CoTaskMemFree` com::com_initialized(); - let lock = match self.ensure_future_audio_client() { - Ok(lock) => lock, - Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { - return Err(DefaultStreamConfigError::DeviceNotAvailable) - } - Err(e) => { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - }; + let lock = self + .ensure_future_audio_client() + .context("failed to get audio client")?; let client = &lock.as_ref().unwrap().0; unsafe { let format_ptr = client .GetMixFormat() .map(WaveFormatExPtr) - .map_err(windows_err_to_cpal_err::)?; + .context("failed to get mix format")?; - format_from_waveformatex_ptr(format_ptr.0, client) - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + format_from_waveformatex_ptr(format_ptr.0, client).ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "device audio format could not be mapped to a supported CPAL format", + ) + }) } } @@ -631,20 +608,26 @@ impl Device { endpoint.data_flow() } - pub fn default_input_config(&self) -> Result { + pub fn default_input_config(&self) -> Result { if self.data_flow() == Audio::eCapture { self.default_format() } else { - Err(DefaultStreamConfigError::StreamTypeNotSupported) + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support input", + )) } } - pub fn default_output_config(&self) -> Result { + pub fn default_output_config(&self) -> Result { let data_flow = self.data_flow(); if data_flow == Audio::eRender { self.default_format() } else { - Err(DefaultStreamConfigError::StreamTypeNotSupported) + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support output", + )) } } @@ -652,24 +635,16 @@ impl Device { &self, config: StreamConfig, sample_format: SampleFormat, - ) -> Result { + ) -> Result { unsafe { // Making sure that COM is initialized. // It's not actually sure that this is required, but when in doubt do it. com::com_initialized(); // Obtaining a `IAudioClient`. - let audio_client = match self.build_audioclient() { - Ok(client) => client, - Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { - return Err(BuildStreamError::DeviceNotAvailable) - } - Err(e) => { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - }; + let audio_client = self + .build_audioclient() + .context("failed to build audio client")?; // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. @@ -684,36 +659,37 @@ impl Device { // Computing the format and initializing the device. let waveformatex = { let format_attempt = config_to_waveformatextensible(config, sample_format) - .ok_or(BuildStreamError::StreamConfigNotSupported)?; + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "stream config could not be converted to a WASAPI-compatible format", + ) + })?; let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; // Ensure the format is supported. match super::device::is_format_supported(&audio_client, &format_attempt.Format) { - Ok(false) => return Err(BuildStreamError::StreamConfigNotSupported), - Err(_) => return Err(BuildStreamError::DeviceNotAvailable), + Ok(false) => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "stream config is not supported by this WASAPI device in shared mode", + )) + } + Err(e) => return Err(e), _ => (), } // Finally, initializing the audio client - let hresult = audio_client.Initialize( - share_mode, - stream_flags, - buffer_duration, - 0, - &format_attempt.Format, - None, - ); - match hresult { - Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { - return Err(BuildStreamError::DeviceNotAvailable); - } - Err(e) => { - let description = format!("{}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - Ok(()) => (), - }; + audio_client + .Initialize( + share_mode, + stream_flags, + buffer_duration, + 0, + &format_attempt.Format, + None, + ) + .context("failed to initialize audio client")?; format_attempt.Format }; @@ -721,39 +697,24 @@ impl Device { // obtaining the size of the samples buffer in number of frames let max_frames_in_buffer = audio_client .GetBufferSize() - .map_err(windows_err_to_cpal_err::)?; + .context("failed to get buffer size")?; let period_frames = shared_mode_period_frames(&audio_client, config.sample_rate, max_frames_in_buffer); // Creating the event that will be signalled whenever we need to submit some samples. - let event = { - let event = - Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) - .map_err(|e| { - let description = format!("failed to create event: {}", e); - let err = BackendSpecificError { description }; - BuildStreamError::from(err) - })?; - - if let Err(e) = audio_client.SetEventHandle(event) { - let description = format!("failed to call SetEventHandle: {}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + let event = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + .context("failed to create event")?; - event - }; + audio_client + .SetEventHandle(event) + .context("failed to set event handle")?; // Building a `IAudioCaptureClient` that will be used to read captured samples. let capture_client = audio_client .GetService::() - .map_err(|e| { - windows_err_to_cpal_err_message::( - e, - "failed to build capture client: ", - ) - })?; + .context("failed to get capture client")?; // Once we built the `StreamInner`, we add a command that will be picked up by the // `run()` method and added to the `RunContext`. @@ -762,12 +723,9 @@ impl Device { let audio_clock = get_audio_clock(&audio_client)?; let stream_latency = { - let hns = audio_client.GetStreamLatency().map_err(|e| { - windows_err_to_cpal_err_message::( - e, - "failed to get stream latency: ", - ) - })?; + let hns = audio_client + .GetStreamLatency() + .context("failed to get stream latency")?; Duration::from_nanos(hns.max(0) as u64 * 100) }; @@ -791,7 +749,7 @@ impl Device { &self, config: StreamConfig, sample_format: SampleFormat, - ) -> Result { + ) -> Result { unsafe { // Making sure that COM is initialized. // It's not actually sure that this is required, but when in doubt do it. @@ -800,7 +758,7 @@ impl Device { // Obtaining a `IAudioClient`. let audio_client = self .build_audioclient() - .map_err(windows_err_to_cpal_err::)?; + .context("failed to build audio client")?; // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. @@ -809,13 +767,23 @@ impl Device { // Computing the format and initializing the device. let waveformatex = { let format_attempt = config_to_waveformatextensible(config, sample_format) - .ok_or(BuildStreamError::StreamConfigNotSupported)?; + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "stream config could not be converted to a WASAPI-compatible format", + ) + })?; let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; // Ensure the format is supported. match super::device::is_format_supported(&audio_client, &format_attempt.Format) { - Ok(false) => return Err(BuildStreamError::StreamConfigNotSupported), - Err(_) => return Err(BuildStreamError::DeviceNotAvailable), + Ok(false) => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "stream config is not supported by this WASAPI device in shared mode", + )) + } + Err(e) => return Err(e), _ => (), } @@ -829,37 +797,24 @@ impl Device { &format_attempt.Format, None, ) - .map_err(windows_err_to_cpal_err::)?; + .context("failed to initialize audio client")?; format_attempt.Format }; // Creating the event that will be signalled whenever we need to submit some samples. - let event = { - let event = - Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) - .map_err(|e| { - let description = format!("failed to create event: {}", e); - let err = BackendSpecificError { description }; - BuildStreamError::from(err) - })?; - - if let Err(e) = audio_client.SetEventHandle(event) { - let description = format!("failed to call SetEventHandle: {}", e); - let err = BackendSpecificError { description }; - return Err(err.into()); - } + let event = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + .context("failed to create event")?; - event - }; + audio_client + .SetEventHandle(event) + .context("failed to set event handle")?; // obtaining the size of the samples buffer in number of frames - let max_frames_in_buffer = audio_client.GetBufferSize().map_err(|e| { - windows_err_to_cpal_err_message::( - e, - "failed to obtain buffer size: ", - ) - })?; + let max_frames_in_buffer = audio_client + .GetBufferSize() + .context("failed to get buffer size")?; let period_frames = shared_mode_period_frames(&audio_client, config.sample_rate, max_frames_in_buffer); @@ -867,12 +822,7 @@ impl Device { // Building a `IAudioRenderClient` that will be used to fill the samples buffer. let render_client = audio_client .GetService::() - .map_err(|e| { - windows_err_to_cpal_err_message::( - e, - "failed to build render client: ", - ) - })?; + .context("failed to get render client")?; // Once we built the `StreamInner`, we add a command that will be picked up by the // `run()` method and added to the `RunContext`. @@ -881,12 +831,9 @@ impl Device { let audio_clock = get_audio_clock(&audio_client)?; let stream_latency = { - let hns = audio_client.GetStreamLatency().map_err(|e| { - windows_err_to_cpal_err_message::( - e, - "failed to get stream latency: ", - ) - })?; + let hns = audio_client + .GetStreamLatency() + .context("failed to get stream latency")?; Duration::from_nanos(hns.max(0) as u64 * 100) }; @@ -1102,15 +1049,17 @@ pub struct Devices { } impl Devices { - pub fn new() -> Result { + pub fn new() -> Result { unsafe { // can fail because of wrong parameters (should never happen) or out of memory let collection = get_enumerator() .0 .EnumAudioEndpoints(Audio::eAll, Audio::DEVICE_STATE_ACTIVE) - .map_err(BackendSpecificError::from)?; + .context("failed to enumerate audio endpoints")?; - let count = collection.GetCount().map_err(BackendSpecificError::from)?; + let count = collection + .GetCount() + .context("failed to get device count")?; Ok(Devices { collection, @@ -1166,14 +1115,10 @@ pub fn default_output_device() -> Option { } /// Get the audio clock used to produce `StreamInstant`s. -unsafe fn get_audio_clock( - audio_client: &Audio::IAudioClient, -) -> Result { +unsafe fn get_audio_clock(audio_client: &Audio::IAudioClient) -> Result { audio_client .GetService::() - .map_err(|e| { - windows_err_to_cpal_err_message::(e, "failed to build audio clock: ") - }) + .context("failed to get audio clock") } // Turns a `Format` into a `WAVEFORMATEXTENSIBLE`. diff --git a/src/host/wasapi/mod.rs b/src/host/wasapi/mod.rs index 617a2e905..194a97bde 100644 --- a/src/host/wasapi/mod.rs +++ b/src/host/wasapi/mod.rs @@ -10,8 +10,7 @@ pub use self::device::{ #[allow(unused_imports)] pub use self::stream::Stream; use crate::traits::HostTrait; -use crate::BackendSpecificError; -use crate::DevicesError; +use crate::{Error, ErrorKind}; use std::io::Error as IoError; use windows::Win32::Media::Audio; @@ -27,7 +26,7 @@ mod stream; pub struct Host; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } @@ -41,7 +40,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new() } @@ -54,58 +53,33 @@ impl HostTrait for Host { } } -impl From for BackendSpecificError { - fn from(error: windows::core::Error) -> Self { - BackendSpecificError { - description: format!("{}", IoError::from(error)), - } - } -} +impl From for Error { + fn from(e: windows::core::Error) -> Self { + let kind = match e.code() { + Audio::AUDCLNT_E_SERVICE_NOT_RUNNING => ErrorKind::HostUnavailable, -trait ErrDeviceNotAvailable: From { - fn device_not_available() -> Self; -} + Audio::AUDCLNT_E_DEVICE_INVALIDATED | Audio::AUDCLNT_E_ENDPOINT_CREATE_FAILED => { + ErrorKind::DeviceNotAvailable + } -impl ErrDeviceNotAvailable for crate::BuildStreamError { - fn device_not_available() -> Self { - Self::DeviceNotAvailable - } -} + Audio::AUDCLNT_E_DEVICE_IN_USE => ErrorKind::DeviceBusy, -impl ErrDeviceNotAvailable for crate::SupportedStreamConfigsError { - fn device_not_available() -> Self { - Self::DeviceNotAvailable - } -} + Audio::AUDCLNT_E_RESOURCES_INVALIDATED => ErrorKind::StreamInvalidated, -impl ErrDeviceNotAvailable for crate::DefaultStreamConfigError { - fn device_not_available() -> Self { - Self::DeviceNotAvailable - } -} - -impl ErrDeviceNotAvailable for crate::StreamError { - fn device_not_available() -> Self { - Self::DeviceNotAvailable - } -} + Audio::AUDCLNT_E_UNSUPPORTED_FORMAT + | Audio::AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED + | Audio::AUDCLNT_E_BUFFER_SIZE_ERROR + | Audio::AUDCLNT_E_INVALID_DEVICE_PERIOD + | Audio::AUDCLNT_E_EXCLUSIVE_MODE_ONLY + | Audio::AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => ErrorKind::UnsupportedConfig, -fn windows_err_to_cpal_err(e: windows::core::Error) -> E { - windows_err_to_cpal_err_message::(e, "") -} + Audio::AUDCLNT_E_WRONG_ENDPOINT_TYPE + | Audio::AUDCLNT_E_ALREADY_INITIALIZED + | Audio::AUDCLNT_E_NOT_INITIALIZED + | Audio::AUDCLNT_E_NOT_STOPPED => ErrorKind::UnsupportedOperation, -fn windows_err_to_cpal_err_message( - e: windows::core::Error, - message: &str, -) -> E { - match e.code() { - Audio::AUDCLNT_E_DEVICE_INVALIDATED | Audio::AUDCLNT_E_DEVICE_IN_USE => { - E::device_not_available() - } - _ => { - let description = format!("{}{}", message, e); - let err = BackendSpecificError { description }; - err.into() - } + _ => ErrorKind::Other, + }; + Error::with_message(kind, IoError::from(e).to_string()) } } diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 90d00954e..5400396ad 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -1,8 +1,7 @@ -use super::windows_err_to_cpal_err; use crate::traits::StreamTrait; use crate::{ - BackendSpecificError, BufferSize, Data, FrameCount, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamError, StreamInstant, + error::ResultExt, BufferSize, Data, Error, ErrorKind, FrameCount, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, SampleRate, StreamInstant, }; use std::mem; use std::ptr; @@ -119,7 +118,7 @@ impl Stream { ) -> Stream where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let pending_scheduled_event = unsafe { Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) @@ -162,7 +161,7 @@ impl Stream { ) -> Stream where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { let pending_scheduled_event = unsafe { Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) @@ -221,15 +220,23 @@ impl Drop for Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { - self.push_command(Command::PlayStream) - .map_err(|_| crate::error::PlayStreamError::DeviceNotAvailable)?; + fn play(&self) -> Result<(), Error> { + self.push_command(Command::PlayStream).map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) + })?; Ok(()) } - fn pause(&self) -> Result<(), PauseStreamError> { - self.push_command(Command::PauseStream) - .map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?; + fn pause(&self) -> Result<(), Error> { + self.push_command(Command::PauseStream).map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) + })?; Ok(()) } @@ -250,7 +257,7 @@ impl StreamTrait for Stream { ) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.period_frames) } } @@ -265,7 +272,7 @@ impl Drop for StreamInner { // Process any pending commands that are queued within the `RunContext`. // Returns `true` if the loop should continue running, `false` if it should terminate. -fn process_commands(run_context: &mut RunContext) -> Result { +fn process_commands(run_context: &mut RunContext) -> Result { // Process the pending commands. for command in run_context.commands.try_iter() { match command { @@ -275,7 +282,7 @@ fn process_commands(run_context: &mut RunContext) -> Result { .stream .audio_client .Start() - .map_err(windows_err_to_cpal_err::)?; + .context("failed to start audio client")?; run_context.stream.playing = true; } }, @@ -285,7 +292,7 @@ fn process_commands(run_context: &mut RunContext) -> Result { .stream .audio_client .Stop() - .map_err(windows_err_to_cpal_err::)?; + .context("failed to stop audio client")?; run_context.stream.playing = false; } }, @@ -305,7 +312,7 @@ fn process_commands(run_context: &mut RunContext) -> Result { // This is called when the `run` thread is ready to wait for the next event. The // next event might be some command submitted by the user (the first handle) or // might indicate that one of the streams is ready to deliver or receive audio. -fn wait_for_handle_signal(handles: &[Foundation::HANDLE]) -> Result { +fn wait_for_handle_signal(handles: &[Foundation::HANDLE]) -> Result { debug_assert!(handles.len() <= SystemServices::MAXIMUM_WAIT_OBJECTS as usize); let result = unsafe { Threading::WaitForMultipleObjectsEx( @@ -317,9 +324,10 @@ fn wait_for_handle_signal(handles: &[Foundation::HANDLE]) -> Result Result Result { +fn get_available_frames(stream: &StreamInner) -> Result { unsafe { let padding = stream .audio_client .GetCurrentPadding() - .map_err(windows_err_to_cpal_err::)?; + .context("failed to get current padding")?; Ok(stream.max_frames_in_buffer - padding) } } @@ -340,7 +348,7 @@ fn get_available_frames(stream: &StreamInner) -> Result fn run_input( mut run_ctxt: RunContext, data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo), - error_callback: &mut dyn FnMut(StreamError), + error_callback: &mut dyn FnMut(Error), ) { boost_current_thread_priority( run_ctxt.stream.config.buffer_size, @@ -372,7 +380,7 @@ fn run_input( fn run_output( mut run_ctxt: RunContext, data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo), - error_callback: &mut dyn FnMut(StreamError), + error_callback: &mut dyn FnMut(Error), ) { boost_current_thread_priority( run_ctxt.stream.config.buffer_size, @@ -434,7 +442,7 @@ enum ControlFlow { fn process_commands_and_await_signal( run_context: &mut RunContext, - error_callback: &mut dyn FnMut(StreamError), + error_callback: &mut dyn FnMut(Error), ) -> Option { // Process queued commands. match process_commands(run_context) { @@ -450,7 +458,7 @@ fn process_commands_and_await_signal( let handle_idx = match wait_for_handle_signal(&run_context.handles) { Ok(idx) => idx, Err(err) => { - error_callback(err.into()); + error_callback(err); return Some(ControlFlow::Break); } }; @@ -469,7 +477,7 @@ fn process_input( stream: &StreamInner, capture_client: Audio::IAudioCaptureClient, data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo), - error_callback: &mut dyn FnMut(StreamError), + error_callback: &mut dyn FnMut(Error), ) -> ControlFlow { unsafe { // Get the available data in the shared buffer. @@ -480,7 +488,7 @@ fn process_input( Ok(0) => return ControlFlow::Continue, Ok(f) => f, Err(err) => { - error_callback(windows_err_to_cpal_err(err)); + error_callback(Error::from(err)); return ControlFlow::Break; } }; @@ -497,7 +505,7 @@ fn process_input( // TODO: Can this happen? Err(e) if e.code() == Audio::AUDCLNT_S_BUFFER_EMPTY => continue, Err(e) => { - error_callback(windows_err_to_cpal_err(e)); + error_callback(Error::from(e)); return ControlFlow::Break; } Ok(_) => (), @@ -524,7 +532,7 @@ fn process_input( // Release the buffer. let result = capture_client .ReleaseBuffer(frames_available) - .map_err(windows_err_to_cpal_err); + .context("failed to release capture buffer"); if let Err(err) = result { error_callback(err); return ControlFlow::Break; @@ -538,7 +546,7 @@ fn process_output( stream: &StreamInner, render_client: Audio::IAudioRenderClient, data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo), - error_callback: &mut dyn FnMut(StreamError), + error_callback: &mut dyn FnMut(Error), ) -> ControlFlow { // The number of frames available for writing. let frames_available = match get_available_frames(stream) { @@ -554,7 +562,7 @@ fn process_output( let buffer = match render_client.GetBuffer(frames_available) { Ok(b) => b, Err(e) => { - error_callback(windows_err_to_cpal_err(e)); + error_callback(Error::from(e)); return ControlFlow::Break; } }; @@ -577,7 +585,7 @@ fn process_output( data_callback(&mut data, &info); if let Err(err) = render_client.ReleaseBuffer(frames_available, 0) { - error_callback(windows_err_to_cpal_err(err)); + error_callback(Error::from(err)); return ControlFlow::Break; } } @@ -596,14 +604,14 @@ fn frames_to_duration(frames: FrameCount, rate: SampleRate) -> Duration { /// Use the stream's `IAudioClock` to produce the current stream instant. /// /// Uses the QPC position produced via the `GetPosition` method. -fn stream_instant(stream: &StreamInner) -> Result { +fn stream_instant(stream: &StreamInner) -> Result { let mut position: u64 = 0; let mut qpc_position: u64 = 0; unsafe { stream .audio_clock .GetPosition(&mut position, Some(&mut qpc_position)) - .map_err(windows_err_to_cpal_err::)?; + .context("failed to get clock position")?; }; // The `qpc_position` is in 100-nanosecond units. let nanos = qpc_position as u128 * 100; @@ -622,7 +630,7 @@ fn stream_instant(stream: &StreamInner) -> Result { fn input_timestamp( stream: &StreamInner, buffer_qpc_position: u64, -) -> Result { +) -> Result { // The `qpc_position` is in 100-nanosecond units. let nanos = buffer_qpc_position as u128 * 100; let capture = StreamInstant::new( @@ -643,7 +651,7 @@ fn output_timestamp( stream: &StreamInner, frames_available: FrameCount, sample_rate: SampleRate, -) -> Result { +) -> Result { let callback = stream_instant(stream)?; // `padding` is the number of frames already queued in the endpoint buffer ahead of the // frames we are about to write. Those frames must drain before ours are heard. diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index e0007cfd4..e19e8cdcf 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -11,11 +11,9 @@ use self::wasm_bindgen::JsCast; use self::web_sys::{AudioContext, AudioContextOptions}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, SampleRate, StreamConfig, StreamError, StreamInstant, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BufferSize, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, ErrorKind, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamInstant, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, }; use std::ops::DerefMut; use std::sync::atomic::{AtomicBool, Ordering}; @@ -62,7 +60,7 @@ const DEFAULT_BUFFER_SIZE: usize = 2048; const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; impl Host { - pub fn new() -> Result { + pub fn new() -> Result { Ok(Host) } } @@ -76,7 +74,7 @@ impl HostTrait for Host { true } - fn devices(&self) -> Result { + fn devices(&self) -> Result { Devices::new() } @@ -90,35 +88,31 @@ impl HostTrait for Host { } impl Devices { - fn new() -> Result { + fn new() -> Result { Ok(Self::default()) } } impl Device { - fn description(&self) -> Result { + fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) .direction(crate::DeviceDirection::Output) .build()) } - fn id(&self) -> Result { + fn id(&self) -> Result { Ok(DeviceId( crate::platform::HostId::WebAudio, "default".to_string(), )) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { // TODO Ok(Vec::new().into_iter()) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { let buffer_size = SupportedBufferSize::Range { min: MIN_BUFFER_SIZE, max: MAX_BUFFER_SIZE, @@ -135,12 +129,14 @@ impl Device { Ok(configs.into_iter()) } - fn default_input_config(&self) -> Result { - // TODO - Err(DefaultStreamConfigError::StreamTypeNotSupported) + fn default_input_config(&self) -> Result { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "WebAudio does not support audio input", + )) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { const EXPECT: &str = "expected at least one valid webaudio stream config"; let config = self .supported_output_configs() @@ -158,31 +154,27 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } - fn id(&self) -> Result { + fn id(&self) -> Result { Device::id(self) } - fn supported_input_configs( - &self, - ) -> Result { + fn supported_input_configs(&self) -> Result { Device::supported_input_configs(self) } - fn supported_output_configs( - &self, - ) -> Result { + fn supported_output_configs(&self) -> Result { Device::supported_output_configs(self) } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { Device::default_input_config(self) } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -193,13 +185,15 @@ impl DeviceTrait for Device { _data_callback: D, _error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { - // TODO - Err(BuildStreamError::StreamConfigNotSupported) + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "WebAudio does not support audio input", + )) } /// Create an output stream. @@ -210,13 +204,19 @@ impl DeviceTrait for Device { data_callback: D, error_callback: E, _timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { if !valid_config(config, sample_format) { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "sample format {sample_format} or channel count {} is not supported by WebAudio", + config.channels + ), + )); } let n_channels = config.channels as usize; @@ -224,7 +224,12 @@ impl DeviceTrait for Device { let buffer_size_frames = match config.buffer_size { BufferSize::Fixed(v) => { if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { - return Err(BuildStreamError::StreamConfigNotSupported); + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "buffer size {v} is out of the supported range {MIN_BUFFER_SIZE}..={MAX_BUFFER_SIZE}" + ), + )); } v as usize } @@ -235,20 +240,15 @@ impl DeviceTrait for Device { let data_callback = Arc::new(Mutex::new(Box::new(data_callback))); let error_callback = Arc::new(Mutex::new( - Box::new(error_callback) as Box + Box::new(error_callback) as Box )); let is_started = Arc::new(AtomicBool::new(false)); // Create the WebAudio stream. let stream_opts = AudioContextOptions::new(); stream_opts.set_sample_rate(config.sample_rate as f32); - let ctx = AudioContext::new_with_context_options(&stream_opts).map_err( - |err| -> BuildStreamError { - let description = format!("{:?}", err); - let err = BackendSpecificError { description }; - err.into() - }, - )?; + let ctx = AudioContext::new_with_context_options(&stream_opts) + .map_err(|err| Error::with_message(ErrorKind::UnsupportedConfig, format!("{err:?}")))?; let destination = ctx.destination(); @@ -305,10 +305,8 @@ impl DeviceTrait for Device { buffer_size_frames as u32, config.sample_rate as f32, ) - .map_err(|err| -> BuildStreamError { - let description = format!("{:?}", err); - let err = BackendSpecificError { description }; - err.into() + .map_err(|err| { + Error::with_message(ErrorKind::UnsupportedConfig, format!("{err:?}")) })?; // A self reference to this closure for passing to future audio event calls. @@ -343,30 +341,44 @@ impl DeviceTrait for Device { let len = temporary_buffer.len(); let data = temporary_buffer.as_mut_ptr() as *mut (); let mut data = unsafe { Data::from_parts(data, len, sample_format) }; - let mut data_callback = data_callback_handle.lock().unwrap(); - // outputLatency can change at runtime, so read it each callback. - let output_latency_secs = js_sys::Reflect::get( - ctx_handle.as_ref(), - &JsValue::from("outputLatency"), - ) - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - let total_hw_latency_secs = { - let sum = base_latency_secs + output_latency_secs; - if sum.is_finite() { - sum.max(0.0) - } else { - 0.0 + match data_callback_handle.lock() { + Ok(mut data_callback) => { + // outputLatency can change at runtime, so read it each callback. + let output_latency_secs = js_sys::Reflect::get( + ctx_handle.as_ref(), + &JsValue::from("outputLatency"), + ) + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let total_hw_latency_secs = { + let sum = base_latency_secs + output_latency_secs; + if sum.is_finite() { + sum.max(0.0) + } else { + 0.0 + } + }; + let callback = StreamInstant::from_secs_f64(now); + let playback = StreamInstant::from_secs_f64( + time_at_start_of_buffer + total_hw_latency_secs, + ); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + (data_callback.deref_mut())(&mut data, &info); } - }; - let callback = StreamInstant::from_secs_f64(now); - let playback = StreamInstant::from_secs_f64( - time_at_start_of_buffer + total_hw_latency_secs, - ); - let timestamp = crate::OutputStreamTimestamp { callback, playback }; - let info = OutputCallbackInfo { timestamp }; - (data_callback.deref_mut())(&mut data, &info); + Err(_) => { + (error_callback_handle + .lock() + .unwrap_or_else(|e| e.into_inner()))( + Error::with_message( + ErrorKind::StreamInvalidated, + "data callback lock poisoned", + ), + ); + return; + } + } } // Deinterleave the sample data and copy into the audio context buffer. @@ -383,12 +395,13 @@ impl DeviceTrait for Device { if let Err(err) = ctx_buffer .copy_to_channel(&temporary_channel_buffer, channel as i32) { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("{err:?}"), + ), ); return; } @@ -406,12 +419,13 @@ impl DeviceTrait for Device { .unchecked_ref::() .copy_to_channel(&temporary_channel_array_view, channel as i32) { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("{err:?}"), + ), ); return; } @@ -423,24 +437,24 @@ impl DeviceTrait for Device { let source = match ctx_handle.create_buffer_source() { Ok(s) => s, Err(err) => { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; + // create_buffer_source is documented not to throw; defensive only. (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("{err:?}"), + ), ); return; } }; source.set_buffer(Some(&ctx_buffer)); if let Err(err) = source.connect_with_audio_node(&ctx_handle.destination()) { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message(ErrorKind::StreamInvalidated, format!("{err:?}")), ); return; } @@ -454,22 +468,20 @@ impl DeviceTrait for Device { .as_ref() .unchecked_ref(), ) { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; + // addEventListener is documented not to throw; defensive only. (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message(ErrorKind::StreamInvalidated, format!("{err:?}")), ); return; } if let Err(err) = source.start_with_when(time_at_start_of_buffer) { - let description = format!("{err:?}"); - let err = BackendSpecificError { description }; + // InvalidStateError (already started) is the expected failure mode. (error_callback_handle .lock() .unwrap_or_else(|e| e.into_inner()))( - StreamError::BackendSpecific { err }, + Error::with_message(ErrorKind::StreamInvalidated, format!("{err:?}")), ); return; } @@ -500,7 +512,7 @@ impl Stream { } impl StreamTrait for Stream { - fn play(&self) -> Result<(), PlayStreamError> { + fn play(&self) -> Result<(), Error> { let window = web_sys::window().unwrap(); match self.ctx.resume() { Ok(_) => { @@ -535,22 +547,20 @@ impl StreamTrait for Stream { } Ok(()) } - Err(err) => { - let description = format!("{:?}", err); - let err = BackendSpecificError { description }; - Err(err.into()) - } + Err(err) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("{err:?}"), + )), } } - fn pause(&self) -> Result<(), PauseStreamError> { + fn pause(&self) -> Result<(), Error> { match self.ctx.suspend() { Ok(_) => Ok(()), - Err(err) => { - let description = format!("{:?}", err); - let err = BackendSpecificError { description }; - Err(err.into()) - } + Err(err) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("{err:?}"), + )), } } @@ -558,7 +568,7 @@ impl StreamTrait for Stream { StreamInstant::from_secs_f64(self.ctx.current_time()) } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { Ok(self.buffer_size_frames as crate::FrameCount) } } diff --git a/src/lib.rs b/src/lib.rs index ee28fd695..8543d6fb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,19 +246,30 @@ impl std::fmt::Display for DeviceId { } impl std::str::FromStr for DeviceId { - type Err = DeviceIdError; + type Err = Error; + /// Parse a device identifier from its string representation (e.g. `"alsa:hw:0,0"`). + /// + /// The format is `":"` where `` is the lowercase host name (see + /// [`HostId`]) and `` is the device-specific identifier string. + /// + /// # Errors + /// + /// - [`ErrorKind::InvalidInput`] if the string is not in `"host:device"` format. + /// - [`ErrorKind::UnsupportedOperation`] if the host portion names a host not available on this + /// platform. + /// + /// [`ErrorKind::InvalidInput`]: ErrorKind::InvalidInput + /// [`ErrorKind::UnsupportedOperation`]: ErrorKind::UnsupportedOperation fn from_str(s: &str) -> Result { - let (host_str, device_str) = s.split_once(':').ok_or(DeviceIdError::BackendSpecific { - err: BackendSpecificError { - description: format!( - "Failed to parse device ID from: {s}\nCheck if format matches \"host:device_id\"" - ), - }, + let (host_str, device_str) = s.split_once(':').ok_or_else(|| { + Error::with_message( + ErrorKind::InvalidInput, + format!("failed to parse device ID \"{s}\": expected \"host:device_id\" format"), + ) })?; - let host_id = crate::platform::HostId::from_str(host_str) - .map_err(|_| DeviceIdError::UnsupportedPlatform)?; + let host_id = crate::platform::HostId::from_str(host_str)?; Ok(DeviceId(host_id, device_str.to_string())) } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 365b89c6d..0440a678f 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -203,8 +203,21 @@ macro_rules! impl_platform_host { } impl std::str::FromStr for HostId { - type Err = crate::HostUnavailable; - + type Err = crate::Error; + + /// Parse a host identifier from its string representation (e.g. `"alsa"`, + /// `"coreaudio"`). + /// + /// The comparison is case-insensitive. Only hosts compiled in for the current platform + /// are recognized; a host string that is valid on another platform is still an error + /// here. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedOperation`] if the string does not name a host available + /// on this platform. + /// + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation fn from_str(s: &str) -> Result { $( $(#[cfg($feat)])? @@ -212,7 +225,10 @@ macro_rules! impl_platform_host { return Ok(HostId::$HostVariant); } )* - Err(crate::HostUnavailable) + Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedOperation, + format!("host \"{s}\" is not supported on this platform"), + )) } } @@ -376,7 +392,7 @@ macro_rules! impl_platform_host { type Stream = Stream; #[allow(deprecated)] - fn name(&self) -> Result { + fn name(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -385,7 +401,7 @@ macro_rules! impl_platform_host { } } - fn description(&self) -> Result { + fn description(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -394,7 +410,7 @@ macro_rules! impl_platform_host { } } - fn id(&self) -> Result { + fn id(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -421,7 +437,7 @@ macro_rules! impl_platform_host { } } - fn supported_input_configs(&self) -> Result { + fn supported_input_configs(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -434,7 +450,7 @@ macro_rules! impl_platform_host { } } - fn supported_output_configs(&self) -> Result { + fn supported_output_configs(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -447,7 +463,7 @@ macro_rules! impl_platform_host { } } - fn default_input_config(&self) -> Result { + fn default_input_config(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -456,7 +472,7 @@ macro_rules! impl_platform_host { } } - fn default_output_config(&self) -> Result { + fn default_output_config(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -472,10 +488,10 @@ macro_rules! impl_platform_host { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, - E: FnMut(crate::StreamError) + Send + 'static, + E: FnMut(crate::Error) + Send + 'static, { match self.0 { $( @@ -501,10 +517,10 @@ macro_rules! impl_platform_host { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, - E: FnMut(crate::StreamError) + Send + 'static, + E: FnMut(crate::Error) + Send + 'static, { match self.0 { $( @@ -536,7 +552,7 @@ macro_rules! impl_platform_host { false } - fn devices(&self) -> Result { + fn devices(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -582,7 +598,7 @@ macro_rules! impl_platform_host { } impl crate::traits::StreamTrait for Stream { - fn play(&self) -> Result<(), crate::PlayStreamError> { + fn play(&self) -> Result<(), crate::Error> { match self.0 { $( $(#[cfg($feat)])? @@ -593,7 +609,7 @@ macro_rules! impl_platform_host { } } - fn pause(&self) -> Result<(), crate::PauseStreamError> { + fn pause(&self) -> Result<(), crate::Error> { match self.0 { $( $(#[cfg($feat)])? @@ -604,7 +620,7 @@ macro_rules! impl_platform_host { } } - fn buffer_size(&self) -> Result { + fn buffer_size(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? @@ -694,7 +710,16 @@ macro_rules! impl_platform_host { } /// Given a unique host identifier, initialise and produce the host if it is available. - pub fn host_from_id(id: HostId) -> Result { + /// + /// # Errors + /// + /// - [`ErrorKind::HostUnavailable`] if the host identified by `id` is not currently + /// reachable (e.g. the audio daemon is not running). + /// - [`ErrorKind::Other`] for unclassifiable initialization failures. + /// + /// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable + /// [`ErrorKind::Other`]: crate::ErrorKind::Other + pub fn host_from_id(id: HostId) -> Result { match id { $( $(#[cfg($feat)])? diff --git a/src/traits.rs b/src/traits.rs index 0e99a9340..0ea9f0c54 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -8,11 +8,9 @@ use std::time::Duration; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, - OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, - StreamError, StreamInstant, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + Data, DeviceDescription, DeviceId, Error, InputCallbackInfo, InputDevices, OutputCallbackInfo, + OutputDevices, SampleFormat, SizedSample, StreamConfig, StreamInstant, SupportedStreamConfig, + SupportedStreamConfigRange, }; /// A [`Host`] provides access to the available audio devices on the system. @@ -48,7 +46,16 @@ pub trait HostTrait { /// An iterator yielding all [`Device`](DeviceTrait)s currently available to the host on the system. /// /// Can be empty if the system does not support audio in general. - fn devices(&self) -> Result; + /// + /// # Errors + /// + /// - [`ErrorKind::HostUnavailable`] if the host has become unreachable (e.g. the audio + /// daemon crashed or was stopped). + /// - [`ErrorKind::Other`] for unclassifiable backend failures. + /// + /// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable + /// [`ErrorKind::Other`]: crate::ErrorKind::Other + fn devices(&self) -> Result; /// Fetches a [`Device`](DeviceTrait) based on a [`DeviceId`] if available /// @@ -73,7 +80,11 @@ pub trait HostTrait { /// input stream formats. /// /// Can be empty if the system does not support audio input. - fn input_devices(&self) -> Result, DevicesError> { + /// + /// # Errors + /// + /// Propagates errors from [`devices`](Self::devices). + fn input_devices(&self) -> Result, Error> { Ok(self.devices()?.filter(DeviceTrait::supports_input)) } @@ -81,7 +92,11 @@ pub trait HostTrait { /// output stream formats. /// /// Can be empty if the system does not support audio output. - fn output_devices(&self) -> Result, DevicesError> { + /// + /// # Errors + /// + /// Propagates errors from [`devices`](Self::devices). + fn output_devices(&self) -> Result, Error> { Ok(self.devices()?.filter(DeviceTrait::supports_output)) } } @@ -108,7 +123,7 @@ pub trait DeviceTrait { manufacturer, and device type. Use `id()` for a unique, stable device identifier \ that persists across reboots and reconnections." )] - fn name(&self) -> Result { + fn name(&self) -> Result { self.description().map(|desc| desc.name().to_string()) } @@ -120,13 +135,25 @@ pub trait DeviceTrait { /// /// For simple string representation, use `device.description().to_string()` or /// `device.description().name()`. - fn description(&self) -> Result; + /// + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + fn description(&self) -> Result; /// The ID of the device. /// /// This ID uniquely identifies the device on the host. It should be stable across program /// runs, device disconnections, and system reboots where possible. - fn id(&self) -> Result; + /// + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + fn id(&self) -> Result; /// True if the device supports audio input, otherwise false fn supports_input(&self) -> bool { @@ -140,25 +167,53 @@ pub trait DeviceTrait { .is_ok_and(|mut iter| iter.next().is_some()) } - /// An iterator yielding formats that are supported by the backend. + /// An iterator yielding input stream configurations that are supported by the device. /// - /// Can return an error if the device is no longer valid (e.g. it has been disconnected). - fn supported_input_configs( - &self, - ) -> Result; + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support input. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + fn supported_input_configs(&self) -> Result; - /// An iterator yielding output stream formats that are supported by the device. + /// An iterator yielding output stream configurations that are supported by the device. /// - /// Can return an error if the device is no longer valid (e.g. it has been disconnected). - fn supported_output_configs( - &self, - ) -> Result; + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support output. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + fn supported_output_configs(&self) -> Result; - /// The default input stream format for the device. - fn default_input_config(&self) -> Result; + /// The default input stream configuration for the device. + /// + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::UnsupportedConfig`] if the device has no default input configuration. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support input. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + fn default_input_config(&self) -> Result; - /// The default output stream format for the device. - fn default_output_config(&self) -> Result; + /// The default output stream configuration for the device. + /// + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::UnsupportedConfig`] if the device has no default output configuration. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support output. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + fn default_output_config(&self) -> Result; /// Create an input stream. /// @@ -170,17 +225,34 @@ pub trait DeviceTrait { /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedConfig`] if the sample rate, channel count, buffer size, or + /// sample format is not supported by the device. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support input streams. + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::DeviceBusy`] if the device is temporarily in use by another application. + /// - [`ErrorKind::PermissionDenied`] if the process lacks permission to access the device. + /// - [`ErrorKind::InvalidInput`] if the configuration parameters are invalid. + /// + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::DeviceBusy`]: crate::ErrorKind::DeviceBusy + /// [`ErrorKind::PermissionDenied`]: crate::ErrorKind::PermissionDenied + /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput fn build_input_stream( &self, config: StreamConfig, mut data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where T: SizedSample, D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { self.build_input_stream_raw( config, @@ -208,17 +280,34 @@ pub trait DeviceTrait { /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedConfig`] if the sample rate, channel count, buffer size, or + /// sample format is not supported by the device. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support output streams. + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::DeviceBusy`] if the device is temporarily in use by another application. + /// - [`ErrorKind::PermissionDenied`] if the process lacks permission to access the device. + /// - [`ErrorKind::InvalidInput`] if the configuration parameters are invalid. + /// + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::DeviceBusy`]: crate::ErrorKind::DeviceBusy + /// [`ErrorKind::PermissionDenied`]: crate::ErrorKind::PermissionDenied + /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput fn build_output_stream( &self, config: StreamConfig, mut data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where T: SizedSample, D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, + E: FnMut(Error) + Send + 'static, { self.build_output_stream_raw( config, @@ -249,6 +338,23 @@ pub trait DeviceTrait { /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedConfig`] if the sample rate, channel count, buffer size, or + /// sample format is not supported by the device. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support input streams. + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::DeviceBusy`] if the device is temporarily in use by another application. + /// - [`ErrorKind::PermissionDenied`] if the process lacks permission to access the device. + /// - [`ErrorKind::InvalidInput`] if the configuration parameters are invalid. + /// + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::DeviceBusy`]: crate::ErrorKind::DeviceBusy + /// [`ErrorKind::PermissionDenied`]: crate::ErrorKind::PermissionDenied + /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput fn build_input_stream_raw( &self, config: StreamConfig, @@ -256,10 +362,10 @@ pub trait DeviceTrait { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static; + E: FnMut(Error) + Send + 'static; /// Create a dynamically typed output stream. /// @@ -276,6 +382,23 @@ pub trait DeviceTrait { /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedConfig`] if the sample rate, channel count, buffer size, or + /// sample format is not supported by the device. + /// - [`ErrorKind::UnsupportedOperation`] if the device does not support output streams. + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::DeviceBusy`] if the device is temporarily in use by another application. + /// - [`ErrorKind::PermissionDenied`] if the process lacks permission to access the device. + /// - [`ErrorKind::InvalidInput`] if the configuration parameters are invalid. + /// + /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::DeviceBusy`]: crate::ErrorKind::DeviceBusy + /// [`ErrorKind::PermissionDenied`]: crate::ErrorKind::PermissionDenied + /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput fn build_output_stream_raw( &self, config: StreamConfig, @@ -283,10 +406,10 @@ pub trait DeviceTrait { data_callback: D, error_callback: E, timeout: Option, - ) -> Result + ) -> Result where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static; + E: FnMut(Error) + Send + 'static; } /// A stream created from [`Device`](DeviceTrait), with methods to control playback. @@ -295,14 +418,34 @@ pub trait StreamTrait { /// /// Note: Not all platforms automatically run the stream upon creation, so it is important to /// call `play` after creation if it is expected that the stream should run immediately. - fn play(&self) -> Result<(), PlayStreamError>; + /// + /// # Errors + /// + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::StreamInvalidated`] if the stream configuration has changed and the stream + /// must be rebuilt. + /// + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::StreamInvalidated`]: crate::ErrorKind::StreamInvalidated + fn play(&self) -> Result<(), Error>; /// Some devices support pausing the audio stream. This can be useful for saving energy in /// moments of silence. /// /// Note: Not all devices support suspending the stream at the hardware level. This method may /// fail in these cases. - fn pause(&self) -> Result<(), PauseStreamError>; + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedOperation`] if the backend does not support pausing streams. + /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. + /// - [`ErrorKind::StreamInvalidated`] if the stream configuration has changed and the stream + /// must be rebuilt. + /// + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable + /// [`ErrorKind::StreamInvalidated`]: crate::ErrorKind::StreamInvalidated + fn pause(&self) -> Result<(), Error>; /// Returns the backend's best available estimate of the number of frames per callback. /// @@ -310,7 +453,10 @@ pub trait StreamTrait { /// the negotiated hardware size; for default buffer sizes this is the backend's configured /// default. The value is updated when it changes during the lifetime of the stream. /// - /// Returns `Err` if the backend cannot retrieve the buffer size. + /// # Errors + /// + /// - [`ErrorKind::UnsupportedOperation`] if the backend cannot query the buffer size. + /// - [`ErrorKind::Other`] for unclassifiable backend failures. /// /// # Implementation notes /// @@ -320,7 +466,10 @@ pub trait StreamTrait { /// `buffer_size()` is primarily intended for sizing pre-allocated buffers, but must not be /// trusted as a guaranteed bound. An incorrect implementation of `buffer_size()` should not /// lead to memory safety violations. - fn buffer_size(&self) -> Result; + /// + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::Other`]: crate::ErrorKind::Other + fn buffer_size(&self) -> Result; /// Returns a [`StreamInstant`] representing the current moment on the stream's clock. ///