diff --git a/CHANGELOG.md b/CHANGELOG.md index e522d1849..ea35943ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,23 @@ # Unreleased +- Add `audio_thread_priority` feature flag for real-time thread priority on ALSA/WASAPI. - Add `Sample::bits_per_sample` method. -- ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. +- Add `StreamConfigBuilder` with platform-specific options. +- Add device-tied stream building methods: `SupportedStreamConfig::build_input_stream()` and `build_output_stream()`. +- `BufferSize` now impls `Default`. +- Remove deprecated `oboe-shared-stdcxx` feature flag. +- ALSA: Add `AlsaStreamConfig` for periods and access types. - ALSA: Fix `BufferSize::Fixed` by selecting the nearest supported frame count. - ALSA: Change `BufferSize::Default` to use the device defaults. - ALSA: Change card enumeration to work like `aplay -L` does. +- ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. - ASIO: Fix linker flags for MinGW cross-compilation. - CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values. - CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration. - iOS: Fix example by properly activating audio session. +- JACK: Add `jack` feature flag to enable JACK audio backend support. +- JACK: Add `JackStreamConfig` for client names and port connections. +- WASAPI: Add `WasapiStreamConfig` for exclusive mode support. - WASAPI: Expose `IMMDevice` from WASAPI host Device. # Version 0.16.0 (2025-06-07) diff --git a/Cargo.toml b/Cargo.toml index 3014e1562..163d71863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,18 @@ edition = "2021" rust-version = "1.70" [features] -asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. +# ASIO backend (Windows only). See README for setup instructions. +asio = ["dep:asio-sys", "dep:num-traits"] -# Deprecated, the `oboe` backend has been removed -oboe-shared-stdcxx = [] +# Real-time thread priority for audio threads on ALSA/WASAPI +audio_thread_priority = ["dep:audio_thread_priority"] + +# JACK Audio backend +jack = ["dep:jack"] [dependencies] dasp_sample = "0.11" +jack = { version = "0.13.0", optional = true } [dev-dependencies] anyhow = "1.0" @@ -46,7 +51,6 @@ num-traits = { version = "0.2.6", optional = true } alsa = "0.9" libc = "0.2" audio_thread_priority = { version = "0.33.0", optional = true } -jack = { version = "0.13.0", optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] mach2 = "0.4" # For access to mach_timebase type. diff --git a/README.md b/README.md index b82cbd2aa..80b65e8c7 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ This library currently supports the following: Currently, supported hosts include: - Linux (via ALSA or JACK) -- Windows (via WASAPI by default, see ASIO instructions below) -- macOS (via CoreAudio) +- Windows (via WASAPI by default, JACK or ASIO, see instructions below) +- macOS (via CoreAudio or JACK) - iOS (via CoreAudio) - Android (via AAudio) - Emscripten @@ -27,16 +27,30 @@ Note that on Linux, the ALSA development files are required. These are provided as part of the `libasound2-dev` package on Debian and Ubuntu distributions and `alsa-lib-devel` on Fedora. +For JACK support, install the JACK development libraries on Linux +(`libjack-jackd2-dev` on Debian/Ubuntu) or download JACK from +[jackaudio.org](https://jackaudio.org/downloads/) for macOS/Windows. + ## Compiling for Web Assembly -If you are interested in using CPAL with WASM, please see [this guide](https://github.com/RustAudio/cpal/wiki/Setting-up-a-new-CPAL-WASM-project) in our Wiki which walks through setting up a new project from scratch. +If you are interested in using CPAL with WASM, please see +[this guide](https://github.com/RustAudio/cpal/wiki/Setting-up-a-new-CPAL-WASM-project) +in our Wiki which walks through setting up a new project from scratch. ## Feature flags for audio backends -Some audio backends are optional and will only be compiled with a [feature flag](https://doc.rust-lang.org/cargo/reference/features.html). +Some audio backends and features are optional and will only be compiled with a +[feature flag](https://doc.rust-lang.org/cargo/reference/features.html). + +### Audio Backend Features + +- **ASIO**: `asio` - Enable ASIO support (Windows only) +- **JACK**: `jack` - Enable JACK audio support + +### Additional Features -- JACK (on Linux): `jack` -- ASIO (on Windows): `asio` +- **Real-time Thread Priority**: `audio_thread_priority` - Enable real-time +thread priority for ALSA/WASAPI audio threads (may require elevated privileges) ## ASIO on Windows @@ -51,13 +65,20 @@ WASAPI. ### Locating the ASIO SDK -The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` environment variable. +The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` +environment variable. -The build script will try to find the ASIO SDK by following these steps in order: +The build script will try to find the ASIO SDK by following these steps in +order: 1. Check if `CPAL_ASIO_DIR` is set and if so use the path to point to the SDK. -2. Check if the ASIO SDK is already installed in the temporary directory, if so use that and set the path of `CPAL_ASIO_DIR` to the output of `std::env::temp_dir().join("asio_sdk")`. -3. If the ASIO SDK is not already installed, download it from and install it in the temporary directory. The path of `CPAL_ASIO_DIR` will be set to the output of `std::env::temp_dir().join("asio_sdk")`. +2. Check if the ASIO SDK is already installed in the temporary directory, if so +use that and set the path of `CPAL_ASIO_DIR` to the output of +`std::env::temp_dir().join("asio_sdk")`. +3. If the ASIO SDK is not already installed, download it from + and install it in the temporary directory. +The path of `CPAL_ASIO_DIR` will be set to the output of +`std::env::temp_dir().join("asio_sdk")`. In an ideal situation you don't need to worry about this step. @@ -70,22 +91,25 @@ In an ideal situation you don't need to worry about this step. 2. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If you installed LLVM to the default directory, this should work in the command prompt: - ``` + ```cmd setx LIBCLANG_PATH "C:\Program Files\LLVM\bin" ``` 3. If you don't have any ASIO devices or drivers available, you can [**download and install ASIO4ALL**](http://www.asio4all.org/). Be sure to enable the "offline" feature during installation despite what the installer says about it being useless. -4. Our build script assumes that Microsoft Visual Studio is installed if the host OS for compilation is Windows. The script will try to find `vcvarsall.bat` - and execute it with the right host and target machine architecture regardless of the Microsoft Visual Studio version. +4. Our build script assumes that Microsoft Visual Studio is installed if the + host OS for compilation is Windows. The script will try to find + `vcvarsall.bat` and execute it with the right host and target machine + architecture regardless of the Microsoft Visual Studio version. If there are any errors encountered in this process which is unlikely, - you may find the `vcvarsall.bat` manually and execute it with your machine architecture as an argument. + you may find the `vcvarsall.bat` manually and execute it with your machine + architecture as an argument. The script will detect this and skip the step. A manually executed command example for 64 bit machines: - ``` + ```cmd "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 ``` @@ -106,7 +130,7 @@ In an ideal situation you don't need to worry about this step. 6. Make sure to enable the `asio` feature when building CPAL: - ``` + ```shell cargo build --features "asio" ``` @@ -121,18 +145,26 @@ _Updated as of ASIO version 2.3.3._ ### Cross compilation -When Windows is the host and the target OS, the build script of `asio-sys` supports all cross compilation targets -which are supported by the MSVC compiler. An exhaustive list of combinations could be found [here](https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax) with the addition of undocumented `arm64`, `arm64_x86`, `arm64_amd64` and `arm64_arm` targets. (5.11.2023) +When Windows is the host and the target OS, the build script of `asio-sys` +supports all cross compilation targets which are supported by the MSVC +compiler. An exhaustive list of combinations could be found [here](https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax) +with the addition of undocumented `arm64`, `arm64_x86`, `arm64_amd64` and +`arm64_arm` targets. (5.11.2023) -It is also possible to compile Windows applications with ASIO support on Linux and macOS. +It is also possible to compile Windows applications with ASIO support on Linux +and macOS. -For both platforms the common way to do this is to use the [MinGW-w64](https://www.mingw-w64.org/) toolchain. +For both platforms the common way to do this is to use the +[MinGW-w64](https://www.mingw-w64.org/) toolchain. -Make sure that you have included the `MinGW-w64` include directory in your `CPLUS_INCLUDE_PATH` environment variable. -Make sure that LLVM is installed and include directory is also included in your `CPLUS_INCLUDE_PATH` environment variable. +Make sure that you have included the `MinGW-w64` include directory in your +`CPLUS_INCLUDE_PATH` environment variable. +Make sure that LLVM is installed and include directory is also included in your +`CPLUS_INCLUDE_PATH` environment variable. -Example for macOS for the target of `x86_64-pc-windows-gnu` where `mingw-w64` is installed via brew: +Example for macOS for the target of `x86_64-pc-windows-gnu` where `mingw-w64` +is installed via brew: -``` +```shell export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/opt/homebrew/Cellar/mingw-w64/11.0.1/toolchain-x86_64/x86_64-w64-mingw32/include" ``` diff --git a/examples/synth_tones.rs b/examples/synth_tones.rs index 8dab746d6..1c267668d 100644 --- a/examples/synth_tones.rs +++ b/examples/synth_tones.rs @@ -125,7 +125,7 @@ pub fn host_device_setup( let config = device.default_output_config()?; println!("Default output config : {config:?}"); - Ok((host, device, config)) + Ok((host, device, config.into())) } pub fn make_stream( diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index b1a29f348..5fd70ba08 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -25,6 +25,107 @@ use crate::{ pub type SupportedInputConfigs = VecIntoIter; pub type SupportedOutputConfigs = VecIntoIter; +/// ALSA access types for audio data. +/// +/// Different access types provide different trade-offs between performance, +/// latency, and compatibility with audio hardware and applications. +#[derive(Clone, Copy, Debug, Default)] +pub enum AlsaAccessType { + /// Interleaved read/write access (default). + /// + /// Audio samples from different channels are stored consecutively in memory + /// (e.g., L-R-L-R for stereo). This is the most common and compatible mode. + #[default] + RwInterleaved, + + /// Non-interleaved read/write access. + /// + /// Audio samples from different channels are stored in separate blocks + /// (e.g., LLLL-RRRR for stereo). Some professional applications prefer this. + RwNonInterleaved, + + /// Memory-mapped interleaved access. + /// + /// Uses memory-mapped I/O with interleaved samples. Can provide lower latency + /// but may not be supported by all hardware or system configurations. + MmapInterleaved, + + /// Memory-mapped non-interleaved access. + /// + /// Uses memory-mapped I/O with non-interleaved samples. Combines the benefits + /// of both memory-mapped access and channel separation. + MmapNonInterleaved, +} + +/// Platform-specific configuration for ALSA streams. +/// +/// This configuration allows fine-tuning ALSA-specific parameters that affect +/// audio latency and stability. These settings are only applied when using the +/// ALSA backend on Linux and other supported systems. +#[derive(Clone, Debug, Default)] +pub struct AlsaStreamConfig { + /// Number of periods for the ALSA buffer. + /// + /// ALSA divides the audio buffer into periods. The number of periods affects + /// both latency and stability: + /// - Fewer periods (2-3) = lower latency but potentially less stable + /// - More periods (4-8) = higher latency but more stable playback + /// + /// If not set, ALSA will use its default value (typically 2-4 periods). + pub periods: Option, + + /// ALSA access type for audio data transfer. + /// + /// Controls how audio data is organized in memory and accessed by the hardware. + /// Different access types can affect performance and compatibility: + /// - `RwInterleaved` (default): Standard interleaved access, most compatible + /// - `RwNonInterleaved`: Non-interleaved access, preferred by some pro apps + /// - `MmapInterleaved`: Memory-mapped interleaved, potentially lower latency + /// - `MmapNonInterleaved`: Memory-mapped non-interleaved + /// + /// If not set, defaults to `RwInterleaved`. + pub access_type: Option, +} + +impl AlsaStreamConfig { + /// Set the number of periods for the ALSA buffer. + /// + /// # Arguments + /// * `periods` - Number of periods (typically 2-8, common values are 2, 3, or 4) + /// + /// # Example + /// ```no_run + /// use cpal::{StreamConfig, SampleRate, BufferSize}; + /// + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Fixed(512)) + /// .on_alsa(|alsa| alsa.periods(2)); + /// ``` + pub fn periods(mut self, periods: u32) -> Self { + self.periods = Some(periods); + self + } + + /// Set the ALSA access type for audio data transfer. + /// + /// # Arguments + /// * `access_type` - The access type to use for audio data transfer + /// + /// # Example + /// ```no_run + /// # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))] + /// # { + /// use cpal::{StreamConfig, SampleRate, BufferSize, config::alsa::AlsaAccessType}; + /// + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .on_alsa(|alsa| alsa.access_type(AlsaAccessType::MmapInterleaved)); + /// # } + /// ``` + pub fn access_type(mut self, access_type: AlsaAccessType) -> Self { + self.access_type = Some(access_type); + self + } +} + mod enumerate; /// The default linux, dragonfly, freebsd and netbsd host type. @@ -101,7 +202,7 @@ impl DeviceTrait for Device { E: FnMut(StreamError) + Send + 'static, { let stream_inner = - self.build_stream_inner(conf, sample_format, alsa::Direction::Capture)?; + self.build_stream_inner(conf, sample_format, alsa::Direction::Capture, None, None)?; let stream = Stream::new_input( Arc::new(stream_inner), data_callback, @@ -124,7 +225,73 @@ impl DeviceTrait for Device { E: FnMut(StreamError) + Send + 'static, { let stream_inner = - self.build_stream_inner(conf, sample_format, alsa::Direction::Playback)?; + self.build_stream_inner(conf, sample_format, alsa::Direction::Playback, None, None)?; + let stream = Stream::new_output( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } +} + +impl Device { + /// Build input stream with optional ALSA-specific configuration + pub fn build_input_stream_raw_with_alsa_config( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + alsa_config: Option<&AlsaStreamConfig>, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let periods = alsa_config.and_then(|c| c.periods); + let access_type = alsa_config.and_then(|c| c.access_type); + let stream_inner = self.build_stream_inner( + conf, + sample_format, + alsa::Direction::Capture, + periods, + access_type, + )?; + let stream = Stream::new_input( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } + + /// Build output stream with optional ALSA-specific configuration + pub fn build_output_stream_raw_with_alsa_config( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + alsa_config: Option<&AlsaStreamConfig>, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let periods = alsa_config.and_then(|c| c.periods); + let access_type = alsa_config.and_then(|c| c.access_type); + let stream_inner = self.build_stream_inner( + conf, + sample_format, + alsa::Direction::Playback, + periods, + access_type, + )?; let stream = Stream::new_output( Arc::new(stream_inner), data_callback, @@ -250,6 +417,8 @@ impl Device { conf: &StreamConfig, sample_format: SampleFormat, stream_type: alsa::Direction, + periods: Option, + access_type: Option, ) -> Result { let handle_result = self .handles @@ -264,7 +433,8 @@ impl Device { Err((e, _)) => return Err(e.into()), Ok(handle) => handle, }; - let can_pause = set_hw_params_from_format(&handle, conf, sample_format)?; + let can_pause = + set_hw_params_from_format(&handle, conf, sample_format, periods, access_type)?; let period_len = set_sw_params_from_format(&handle, conf, stream_type)?; handle.prepare()?; @@ -1064,9 +1234,19 @@ fn set_hw_params_from_format( pcm_handle: &alsa::pcm::PCM, config: &StreamConfig, sample_format: SampleFormat, + periods: Option, + access_type: Option, ) -> Result { let hw_params = alsa::pcm::HwParams::any(pcm_handle)?; - hw_params.set_access(alsa::pcm::Access::RWInterleaved)?; + + // Set the access type based on configuration + let alsa_access = match access_type.unwrap_or_default() { + AlsaAccessType::RwInterleaved => alsa::pcm::Access::RWInterleaved, + AlsaAccessType::RwNonInterleaved => alsa::pcm::Access::RWNonInterleaved, + AlsaAccessType::MmapInterleaved => alsa::pcm::Access::MMapInterleaved, + AlsaAccessType::MmapNonInterleaved => alsa::pcm::Access::MMapNonInterleaved, + }; + hw_params.set_access(alsa_access)?; let sample_format = if cfg!(target_endian = "big") { match sample_format { @@ -1123,8 +1303,13 @@ fn set_hw_params_from_format( hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; hw_params.set_channels(config.channels as u32)?; + // Set periods if requested + if let Some(periods_count) = periods { + hw_params.set_periods(periods_count, alsa::ValueOr::Nearest)?; + } + // Set buffer size if requested. ALSA will calculate the period size from this buffer size as: - // period_size = nearest_set_buffer_size / default_periods + // period_size = nearest_set_buffer_size / periods // // If not requested, ALSA will calculate the period size from the device defaults: // period_size = default_buffer_size / default_periods diff --git a/src/host/alsa/mod.rs.working b/src/host/alsa/mod.rs.working new file mode 100644 index 000000000..060a383b3 --- /dev/null +++ b/src/host/alsa/mod.rs.working @@ -0,0 +1,1388 @@ +extern crate alsa; +extern crate libc; + +use std::{ + cell::Cell, + cmp, fmt, + ops::RangeInclusive, + sync::{Arc, Mutex}, + thread::{self, JoinHandle}, + time::Duration, + vec::IntoIter as VecIntoIter, +}; + +use self::alsa::poll::Descriptors; +pub use self::enumerate::{default_input_device, default_output_device, Devices}; + +use crate::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, + StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, +}; + +pub type SupportedInputConfigs = VecIntoIter; +pub type SupportedOutputConfigs = VecIntoIter; + +mod enumerate; + +const VALID_BUFFER_SIZE: RangeInclusive = + 1..=FrameCount::MAX as alsa::pcm::Frames; + +const FALLBACK_PERIOD_TIME: u32 = 25_000; +const PREFERRED_PERIOD_COUNT: u32 = 2; + +/// The default linux, dragonfly, freebsd and netbsd host type. +#[derive(Debug)] +pub struct Host; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume ALSA is always available on linux/dragonfly/freebsd/netbsd. + true + } + + fn devices(&self) -> Result { + Devices::new() + } + + fn default_input_device(&self) -> Option { + default_input_device() + } + + fn default_output_device(&self) -> Option { + default_output_device() + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + Device::name(self) + } + + fn supported_input_configs( + &self, + ) -> Result { + Device::supported_input_configs(self) + } + + fn supported_output_configs( + &self, + ) -> Result { + Device::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Device::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Device::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let stream_inner = + self.build_stream_inner(conf, sample_format, alsa::Direction::Capture)?; + let stream = Stream::new_input( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } + + fn build_output_stream_raw( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let stream_inner = + self.build_stream_inner(conf, sample_format, alsa::Direction::Playback)?; + let stream = Stream::new_output( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } +} + +struct TriggerSender(libc::c_int); + +struct TriggerReceiver(libc::c_int); + +impl TriggerSender { + fn wakeup(&self) { + let buf = 1u64; + let ret = unsafe { libc::write(self.0, &buf as *const u64 as *const _, 8) }; + assert_eq!(ret, 8); + } +} + +impl TriggerReceiver { + fn clear_pipe(&self) { + let mut out = 0u64; + let ret = unsafe { libc::read(self.0, &mut out as *mut u64 as *mut _, 8) }; + assert_eq!(ret, 8); + } +} + +fn trigger() -> (TriggerSender, TriggerReceiver) { + let mut fds = [0, 0]; + match unsafe { libc::pipe(fds.as_mut_ptr()) } { + 0 => (TriggerSender(fds[1]), TriggerReceiver(fds[0])), + _ => panic!("Could not create pipe"), + } +} + +impl Drop for TriggerSender { + fn drop(&mut self) { + unsafe { + libc::close(self.0); + } + } +} + +impl Drop for TriggerReceiver { + fn drop(&mut self) { + unsafe { + libc::close(self.0); + } + } +} + +#[derive(Default)] +struct DeviceHandles { + playback: Option, + capture: Option, +} + +impl DeviceHandles { + /// Create `DeviceHandles` for `name` and try to open a handle for both + /// directions. Returns `Ok` if either direction is opened successfully. + fn open(pcm_id: &str) -> Result { + let mut handles = Self::default(); + let playback_err = handles.try_open(pcm_id, alsa::Direction::Playback).err(); + let capture_err = handles.try_open(pcm_id, alsa::Direction::Capture).err(); + if let Some(err) = capture_err.and(playback_err) { + Err(err) + } else { + Ok(handles) + } + } + + /// Get a mutable reference to the `Option` for a specific `stream_type`. + /// If the `Option` is `None`, the `alsa::PCM` will be opened and placed in + /// the `Option` before returning. If `handle_mut()` returns `Ok` the contained + /// `Option` is guaranteed to be `Some(..)`. + fn try_open( + &mut self, + pcm_id: &str, + stream_type: alsa::Direction, + ) -> Result<&mut Option, alsa::Error> { + let handle = match stream_type { + alsa::Direction::Playback => &mut self.playback, + alsa::Direction::Capture => &mut self.capture, + }; + + if handle.is_none() { + *handle = Some(alsa::pcm::PCM::new(pcm_id, stream_type, true)?); + } + + Ok(handle) + } + + /// Get a mutable reference to the `alsa::PCM` handle for a specific `stream_type`. + /// If the handle is not yet opened, it will be opened and stored in `self`. + fn get_mut( + &mut self, + pcm_id: &str, + stream_type: alsa::Direction, + ) -> Result<&mut alsa::PCM, alsa::Error> { + Ok(self.try_open(pcm_id, stream_type)?.as_mut().unwrap()) + } + + /// Take ownership of the `alsa::PCM` handle for a specific `stream_type`. + /// If the handle is not yet opened, it will be opened and returned. + fn take(&mut self, name: &str, stream_type: alsa::Direction) -> Result { + Ok(self.try_open(name, stream_type)?.take().unwrap()) + } +} + +#[derive(Clone)] +pub struct Device { + pcm_id: String, + desc: Option, + handles: Arc>, +} + +impl Device { + fn build_stream_inner( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + stream_type: alsa::Direction, + ) -> Result { + let handle_result = self + .handles + .lock() + .unwrap() + .take(&self.pcm_id, stream_type) + .map_err(|e| (e, e.errno())); + + let handle = match handle_result { + Err((_, libc::EBUSY)) => return Err(BuildStreamError::DeviceNotAvailable), + Err((_, libc::EINVAL)) => return Err(BuildStreamError::InvalidArgument), + Err((e, _)) => return Err(e.into()), + Ok(handle) => handle, + }; + let can_pause = set_hw_params_from_format(&handle, conf, sample_format)?; + let period_len = set_sw_params_from_format(&handle, conf, stream_type)?; + + handle.prepare()?; + + 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()); + } + + // Check to see if we can retrieve valid timestamps from the device. + // Related: https://bugs.freedesktop.org/show_bug.cgi?id=88503 + let ts = handle.status()?.get_htstamp(); + let creation_instant = match (ts.tv_sec, ts.tv_nsec) { + (0, 0) => Some(std::time::Instant::now()), + _ => None, + }; + + if let alsa::Direction::Capture = stream_type { + handle.start()?; + } + + let stream_inner = StreamInner { + dropping: Cell::new(false), + channel: handle, + sample_format, + num_descriptors, + conf: conf.clone(), + period_len, + can_pause, + creation_instant, + }; + + Ok(stream_inner) + } + + #[inline] + fn name(&self) -> Result { + Ok(self.to_string()) + } + + fn supported_configs( + &self, + stream_t: alsa::Direction, + ) -> Result, SupportedStreamConfigsError> { + let mut guard = self.handles.lock().unwrap(); + let handle_result = guard + .get_mut(&self.pcm_id, stream_t) + .map_err(|e| (e, e.errno())); + + let handle = match handle_result { + Err((_, libc::ENOENT)) | Err((_, libc::EBUSY)) => { + return Err(SupportedStreamConfigsError::DeviceNotAvailable) + } + Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument), + Err((e, _)) => return Err(e.into()), + Ok(handle) => handle, + }; + + let hw_params = alsa::pcm::HwParams::any(handle)?; + + // TODO: check endianness + const FORMATS: [(SampleFormat, alsa::pcm::Format); 8] = [ + (SampleFormat::I8, alsa::pcm::Format::S8), + (SampleFormat::U8, alsa::pcm::Format::U8), + (SampleFormat::I16, alsa::pcm::Format::S16LE), + //SND_PCM_FORMAT_S16_BE, + (SampleFormat::U16, alsa::pcm::Format::U16LE), + //SND_PCM_FORMAT_U16_BE, + //SND_PCM_FORMAT_S24_LE, + //SND_PCM_FORMAT_S24_BE, + //SND_PCM_FORMAT_U24_LE, + //SND_PCM_FORMAT_U24_BE, + (SampleFormat::I32, alsa::pcm::Format::S32LE), + //SND_PCM_FORMAT_S32_BE, + (SampleFormat::U32, alsa::pcm::Format::U32LE), + //SND_PCM_FORMAT_U32_BE, + (SampleFormat::F32, alsa::pcm::Format::FloatLE), + //SND_PCM_FORMAT_FLOAT_BE, + (SampleFormat::F64, alsa::pcm::Format::Float64LE), + //SND_PCM_FORMAT_FLOAT64_BE, + //SND_PCM_FORMAT_IEC958_SUBFRAME_LE, + //SND_PCM_FORMAT_IEC958_SUBFRAME_BE, + //SND_PCM_FORMAT_MU_LAW, + //SND_PCM_FORMAT_A_LAW, + //SND_PCM_FORMAT_IMA_ADPCM, + //SND_PCM_FORMAT_MPEG, + //SND_PCM_FORMAT_GSM, + //SND_PCM_FORMAT_SPECIAL, + //SND_PCM_FORMAT_S24_3LE, + //SND_PCM_FORMAT_S24_3BE, + //SND_PCM_FORMAT_U24_3LE, + //SND_PCM_FORMAT_U24_3BE, + //SND_PCM_FORMAT_S20_3LE, + //SND_PCM_FORMAT_S20_3BE, + //SND_PCM_FORMAT_U20_3LE, + //SND_PCM_FORMAT_U20_3BE, + //SND_PCM_FORMAT_S18_3LE, + //SND_PCM_FORMAT_S18_3BE, + //SND_PCM_FORMAT_U18_3LE, + //SND_PCM_FORMAT_U18_3BE, + ]; + + let mut supported_formats = Vec::new(); + for &(sample_format, alsa_format) in FORMATS.iter() { + if hw_params.test_format(alsa_format).is_ok() { + supported_formats.push(sample_format); + } + } + + let min_rate = hw_params.get_rate_min()?; + let max_rate = hw_params.get_rate_max()?; + + let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() { + vec![(min_rate, max_rate)] + } else { + const RATES: [libc::c_uint; 13] = [ + 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, + 192000, + ]; + + let mut rates = Vec::new(); + for &rate in RATES.iter() { + if hw_params.test_rate(rate).is_ok() { + rates.push((rate, rate)); + } + } + + if rates.is_empty() { + vec![(min_rate, max_rate)] + } else { + rates + } + }; + + let min_channels = hw_params.get_channels_min()?; + let max_channels = hw_params.get_channels_max()?; + + let max_channels = cmp::min(max_channels, 32); // TODO: limiting to 32 channels or too much stuff is returned + let supported_channels = (min_channels..max_channels + 1) + .filter_map(|num| { + if hw_params.test_channels(num).is_ok() { + Some(num as ChannelCount) + } else { + None + } + }) + .collect::>(); + + let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(&hw_params); + let buffer_size_range = SupportedBufferSize::Range { + min: min_buffer_size, + max: max_buffer_size, + }; + + let mut output = Vec::with_capacity( + supported_formats.len() * supported_channels.len() * sample_rates.len(), + ); + for &sample_format in supported_formats.iter() { + for &channels in supported_channels.iter() { + for &(min_rate, max_rate) in sample_rates.iter() { + output.push(SupportedStreamConfigRange { + channels, + min_sample_rate: SampleRate(min_rate), + max_sample_rate: SampleRate(max_rate), + buffer_size: buffer_size_range, + sample_format, + }); + } + } + } + + Ok(output.into_iter()) + } + + fn supported_input_configs( + &self, + ) -> Result { + self.supported_configs(alsa::Direction::Capture) + } + + 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 { + let mut formats: Vec<_> = { + match self.supported_configs(stream_t) { + Err(SupportedStreamConfigsError::DeviceNotAvailable) => { + return Err(DefaultStreamConfigError::DeviceNotAvailable); + } + 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()); + } + Ok(fmts) => fmts.collect(), + } + }; + + formats.sort_by(|a, b| a.cmp_default_heuristics(b)); + + match formats.into_iter().next_back() { + Some(f) => { + let min_r = f.min_sample_rate; + let max_r = f.max_sample_rate; + let mut format = f.with_max_sample_rate(); + const HZ_44100: SampleRate = SampleRate(44_100); + if min_r <= HZ_44100 && HZ_44100 <= max_r { + format.sample_rate = HZ_44100; + } + Ok(format) + } + None => Err(DefaultStreamConfigError::StreamTypeNotSupported), + } + } + + fn default_input_config(&self) -> Result { + self.default_config(alsa::Direction::Capture) + } + + fn default_output_config(&self) -> Result { + self.default_config(alsa::Direction::Playback) + } +} + +struct StreamInner { + // Flag used to check when to stop polling, regardless of the state of the stream + // (e.g. broken due to a disconnected device). + dropping: Cell, + + // The ALSA channel. + channel: alsa::pcm::PCM, + + // When converting between file descriptors and `snd_pcm_t`, this is the number of + // file descriptors that this `snd_pcm_t` uses. + num_descriptors: usize, + + // Format of the samples. + sample_format: SampleFormat, + + // The configuration used to open this stream. + conf: StreamConfig, + + // Minimum number of samples to put in the buffer. + period_len: usize, + + #[allow(dead_code)] + // Whether or not the hardware supports pausing the stream. + // TODO: We need an API to expose this. See #197, #284. + can_pause: bool, + + // In the case that the device does not return valid timestamps via `get_htstamp`, this field + // will be `Some` and will contain an `Instant` representing the moment the stream was created. + // + // If this field is `Some`, then the stream will use the duration since this instant as a + // source for timestamps. + // + // If this field is `None` then the elapsed duration between `get_trigger_htstamp` and + // `get_htstamp` is used. + creation_instant: Option, +} + +// Assume that the ALSA library is built with thread safe option. +unsafe impl Sync for StreamInner {} + +#[derive(Debug, Eq, PartialEq)] +enum StreamType { + Input, + Output, +} + +pub struct Stream { + /// The high-priority audio processing thread calling callbacks. + /// Option used for moving out in destructor. + thread: Option>, + + /// Handle to the underlying stream for playback controls. + inner: Arc, + + /// Used to signal to stop processing. + trigger: TriggerSender, +} + +struct StreamWorkerContext { + descriptors: Vec, + buffer: Vec, + poll_timeout: i32, +} + +impl StreamWorkerContext { + fn new(poll_timeout: &Option) -> Self { + let poll_timeout: i32 = if let Some(d) = poll_timeout { + d.as_millis().try_into().unwrap() + } else { + -1 + }; + + Self { + descriptors: Vec::new(), + buffer: Vec::new(), + poll_timeout, + } + } +} + +fn input_stream_worker( + rx: TriggerReceiver, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + timeout: Option, +) { + boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); + + let mut ctxt = StreamWorkerContext::new(&timeout); + loop { + let flow = + poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| { + error_callback(err.into()); + PollDescriptorsFlow::Continue + }); + + match flow { + PollDescriptorsFlow::Continue => { + continue; + } + PollDescriptorsFlow::XRun => { + if let Err(err) = stream.channel.prepare() { + error_callback(err.into()); + } + continue; + } + PollDescriptorsFlow::Return => return, + PollDescriptorsFlow::Ready { + status, + avail_frames: _, + delay_frames, + stream_type, + } => { + assert_eq!( + stream_type, + StreamType::Input, + "expected input stream, but polling descriptors indicated output", + ); + if let Err(err) = process_input( + stream, + &mut ctxt.buffer, + status, + delay_frames, + data_callback, + ) { + error_callback(err.into()); + } + } + } + } +} + +fn output_stream_worker( + rx: TriggerReceiver, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + timeout: Option, +) { + boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); + + let mut ctxt = StreamWorkerContext::new(&timeout); + loop { + let flow = + poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| { + error_callback(err.into()); + PollDescriptorsFlow::Continue + }); + + match flow { + PollDescriptorsFlow::Continue => continue, + PollDescriptorsFlow::XRun => { + if let Err(err) = stream.channel.prepare() { + error_callback(err.into()); + } + continue; + } + PollDescriptorsFlow::Return => return, + PollDescriptorsFlow::Ready { + status, + avail_frames, + delay_frames, + stream_type, + } => { + assert_eq!( + stream_type, + StreamType::Output, + "expected output stream, but polling descriptors indicated input", + ); + if let Err(err) = process_output( + stream, + &mut ctxt.buffer, + status, + avail_frames, + delay_frames, + data_callback, + error_callback, + ) { + error_callback(err.into()); + } + } + } + } +} + +#[cfg(feature = "audio_thread_priority")] +fn boost_current_thread_priority(buffer_size: BufferSize, sample_rate: SampleRate) { + use audio_thread_priority::promote_current_thread_to_real_time; + + let buffer_size = if let BufferSize::Fixed(buffer_size) = buffer_size { + buffer_size + } else { + // if the buffer size isn't fixed, let audio_thread_priority choose a sensible default value + 0 + }; + + if let Err(err) = promote_current_thread_to_real_time(buffer_size, sample_rate.0) { + eprintln!("Failed to promote audio thread to real-time priority: {err}"); + } +} + +#[cfg(not(feature = "audio_thread_priority"))] +fn boost_current_thread_priority(_: BufferSize, _: SampleRate) {} + +enum PollDescriptorsFlow { + Continue, + Return, + Ready { + stream_type: StreamType, + status: alsa::pcm::Status, + avail_frames: usize, + delay_frames: usize, + }, + XRun, +} + +// This block is shared between both input and output stream worker functions. +fn poll_descriptors_and_prepare_buffer( + rx: &TriggerReceiver, + stream: &StreamInner, + ctxt: &mut StreamWorkerContext, +) -> Result { + if stream.dropping.get() { + // The stream has been requested to be destroyed. + rx.clear_pipe(); + return Ok(PollDescriptorsFlow::Return); + } + + let StreamWorkerContext { + ref mut descriptors, + ref mut buffer, + ref poll_timeout, + } = *ctxt; + + descriptors.clear(); + + // Add the self-pipe for signaling termination. + descriptors.push(libc::pollfd { + fd: rx.0, + events: libc::POLLIN, + revents: 0, + }); + + // Add ALSA polling fds. + let len = descriptors.len(); + descriptors.resize( + stream.num_descriptors + len, + libc::pollfd { + fd: 0, + events: 0, + revents: 0, + }, + ); + let filled = stream.channel.fill(&mut descriptors[len..])?; + debug_assert_eq!(filled, stream.num_descriptors); + + // Don't timeout, wait forever. + let res = alsa::poll::poll(descriptors, *poll_timeout)?; + if res == 0 { + let description = String::from("`alsa::poll()` spuriously returned"); + return Err(BackendSpecificError { description }); + } + + if descriptors[0].revents != 0 { + // The stream has been requested to be destroyed. + rx.clear_pipe(); + return Ok(PollDescriptorsFlow::Return); + } + + let revents = stream.channel.revents(&descriptors[1..])?; + if revents.contains(alsa::poll::Flags::ERR) { + let description = String::from("`alsa::poll()` returned POLLERR"); + return Err(BackendSpecificError { description }); + } + let stream_type = match revents { + alsa::poll::Flags::OUT => StreamType::Output, + alsa::poll::Flags::IN => StreamType::Input, + _ => { + // Nothing to process, poll again + return Ok(PollDescriptorsFlow::Continue); + } + }; + + let status = stream.channel.status()?; + let avail_frames = match stream.channel.avail() { + Err(err) if err.errno() == libc::EPIPE => return Ok(PollDescriptorsFlow::XRun), + res => res, + }? as usize; + let delay_frames = match status.get_delay() { + // Buffer underrun. TODO: Notify the user. + d if d < 0 => 0, + d => d as usize, + }; + let available_samples = avail_frames * stream.conf.channels as usize; + + // Only go on if there is at least `stream.period_len` samples. + if available_samples < stream.period_len { + return Ok(PollDescriptorsFlow::Continue); + } + + // Prepare the data buffer. + let buffer_size = stream.sample_format.sample_size() * available_samples; + buffer.resize(buffer_size, 0u8); + + Ok(PollDescriptorsFlow::Ready { + stream_type, + status, + avail_frames, + delay_frames, + }) +} + +// Read input data from ALSA and deliver it to the user. +fn process_input( + stream: &StreamInner, + buffer: &mut [u8], + status: alsa::pcm::Status, + delay_frames: usize, + data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), +) -> Result<(), BackendSpecificError> { + stream.channel.io_bytes().readi(buffer)?; + let sample_format = stream.sample_format; + let data = buffer.as_mut_ptr() as *mut (); + let len = buffer.len() / sample_format.sample_size(); + let data = unsafe { Data::from_parts(data, len, sample_format) }; + let callback = stream_timestamp(&status, stream.creation_instant)?; + let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate); + let capture = callback + .sub(delay_duration) + .expect("`capture` is earlier than representation supported by `StreamInstant`"); + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = crate::InputCallbackInfo { timestamp }; + data_callback(&data, &info); + + Ok(()) +} + +// Request data from the user's function and write it via ALSA. +// +// Returns `true` +fn process_output( + stream: &StreamInner, + buffer: &mut [u8], + status: alsa::pcm::Status, + available_frames: usize, + delay_frames: usize, + data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + error_callback: &mut dyn FnMut(StreamError), +) -> Result<(), BackendSpecificError> { + { + // We're now sure that we're ready to write data. + let sample_format = stream.sample_format; + let data = buffer.as_mut_ptr() as *mut (); + let len = buffer.len() / sample_format.sample_size(); + let mut data = unsafe { Data::from_parts(data, len, sample_format) }; + let callback = stream_timestamp(&status, stream.creation_instant)?; + let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate); + let playback = callback + .add(delay_duration) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = crate::OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + } + loop { + match stream.channel.io_bytes().writei(buffer) { + Err(err) if err.errno() == libc::EPIPE => { + // ALSA underrun or overrun. + // See https://github.com/alsa-project/alsa-lib/blob/b154d9145f0e17b9650e4584ddfdf14580b4e0d7/src/pcm/pcm.c#L8767-L8770 + // Even if these recover successfully, they still may cause audible glitches. + + // TODO: + // Should we notify the user about successfully recovered errors? + // Should we notify the user about failures in try_recover, rather than ignoring them? + // (Both potentially not real-time-safe) + _ = stream.channel.try_recover(err, true); + } + Err(err) => { + error_callback(err.into()); + continue; + } + Ok(result) if result != available_frames => { + let description = format!( + "unexpected number of frames written: expected {available_frames}, \ + result {result} (this should never happen)" + ); + error_callback(BackendSpecificError { description }.into()); + continue; + } + _ => { + break; + } + } + } + Ok(()) +} + +// Use the elapsed duration since the start of the stream. +// +// This ensures positive values that are compatible with our `StreamInstant` representation. +fn stream_timestamp( + status: &alsa::pcm::Status, + creation_instant: Option, +) -> Result { + match creation_instant { + None => { + let trigger_ts = status.get_trigger_htstamp(); + 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 }); + } + Ok(crate::StreamInstant::from_nanos(nanos)) + } + Some(creation) => { + let now = std::time::Instant::now(); + let duration = now.duration_since(creation); + crate::StreamInstant::from_nanos_i128(duration.as_nanos() as i128).ok_or( + BackendSpecificError { + description: "stream duration has exceeded `StreamInstant` representation" + .to_string(), + }, + ) + } + } +} + +// Adapted from `timestamp2ns` here: +// https://fossies.org/linux/alsa-lib/test/audio_time.c +fn timespec_to_nanos(ts: libc::timespec) -> i64 { + let nanos = ts.tv_sec * 1_000_000_000 + ts.tv_nsec; + #[cfg(target_pointer_width = "64")] + return nanos; + #[cfg(not(target_pointer_width = "64"))] + return nanos.into(); +} + +// Adapted from `timediff` here: +// https://fossies.org/linux/alsa-lib/test/audio_time.c +fn timespec_diff_nanos(a: libc::timespec, b: libc::timespec) -> i64 { + timespec_to_nanos(a) - timespec_to_nanos(b) +} + +// 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.0 as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +impl Stream { + fn new_input( + inner: Arc, + mut data_callback: D, + mut error_callback: E, + timeout: Option, + ) -> Stream + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let (tx, rx) = trigger(); + // Clone the handle for passing into worker thread. + let stream = inner.clone(); + let thread = thread::Builder::new() + .name("cpal_alsa_in".to_owned()) + .spawn(move || { + input_stream_worker( + rx, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + Stream { + thread: Some(thread), + inner, + trigger: tx, + } + } + + fn new_output( + inner: Arc, + mut data_callback: D, + mut error_callback: E, + timeout: Option, + ) -> Stream + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let (tx, rx) = trigger(); + // Clone the handle for passing into worker thread. + let stream = inner.clone(); + let thread = thread::Builder::new() + .name("cpal_alsa_out".to_owned()) + .spawn(move || { + output_stream_worker( + rx, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + Stream { + thread: Some(thread), + inner, + trigger: tx, + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + self.inner.dropping.set(true); + self.trigger.wakeup(); + self.thread.take().unwrap().join().unwrap(); + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.inner.channel.pause(false).ok(); + Ok(()) + } + fn pause(&self) -> Result<(), PauseStreamError> { + self.inner.channel.pause(true).ok(); + Ok(()) + } +} + +// Overly safe clamp because alsa Frames are i64 +fn clamp_frame_count(buffer_size: alsa::pcm::Frames) -> FrameCount { + buffer_size.clamp(1, FrameCount::MAX as _) as _ +} + +fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) { + let min_buf = hw_params + .get_buffer_size_min() + .map(clamp_frame_count) + .unwrap_or(1); + let max_buf = hw_params + .get_buffer_size_max() + .map(clamp_frame_count) + .unwrap_or(FrameCount::MAX); + (min_buf, max_buf) +} + +fn set_hw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: &StreamConfig, + sample_format: SampleFormat, +) -> Result { + let hw_params = alsa::pcm::HwParams::any(pcm_handle)?; + hw_params.set_access(alsa::pcm::Access::RWInterleaved)?; + + let sample_format = if cfg!(target_endian = "big") { + match sample_format { + SampleFormat::I8 => alsa::pcm::Format::S8, + SampleFormat::I16 => alsa::pcm::Format::S16BE, + // SampleFormat::I24 => alsa::pcm::Format::S24BE, + SampleFormat::I32 => alsa::pcm::Format::S32BE, + // SampleFormat::I48 => alsa::pcm::Format::S48BE, + // SampleFormat::I64 => alsa::pcm::Format::S64BE, + SampleFormat::U8 => alsa::pcm::Format::U8, + SampleFormat::U16 => alsa::pcm::Format::U16BE, + // SampleFormat::U24 => alsa::pcm::Format::U24BE, + SampleFormat::U32 => alsa::pcm::Format::U32BE, + // SampleFormat::U48 => alsa::pcm::Format::U48BE, + // SampleFormat::U64 => alsa::pcm::Format::U64BE, + SampleFormat::F32 => alsa::pcm::Format::FloatBE, + SampleFormat::F64 => alsa::pcm::Format::Float64BE, + sample_format => { + return Err(BackendSpecificError { + description: format!( + "Sample format '{sample_format}' is not supported by this backend" + ), + }) + } + } + } else { + match sample_format { + SampleFormat::I8 => alsa::pcm::Format::S8, + SampleFormat::I16 => alsa::pcm::Format::S16LE, + // SampleFormat::I24 => alsa::pcm::Format::S24LE, + SampleFormat::I32 => alsa::pcm::Format::S32LE, + // SampleFormat::I48 => alsa::pcm::Format::S48LE, + // SampleFormat::I64 => alsa::pcm::Format::S64LE, + SampleFormat::U8 => alsa::pcm::Format::U8, + SampleFormat::U16 => alsa::pcm::Format::U16LE, + // SampleFormat::U24 => alsa::pcm::Format::U24LE, + SampleFormat::U32 => alsa::pcm::Format::U32LE, + // SampleFormat::U48 => alsa::pcm::Format::U48LE, + // SampleFormat::U64 => alsa::pcm::Format::U64LE, + SampleFormat::F32 => alsa::pcm::Format::FloatLE, + SampleFormat::F64 => alsa::pcm::Format::Float64LE, + sample_format => { + return Err(BackendSpecificError { + description: format!( + "Sample format '{sample_format}' is not supported by this backend" + ), + }) + } + } + }; + + hw_params.set_format(sample_format)?; + hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; + hw_params.set_channels(config.channels as u32)?; + + if !set_hw_params_periods(&hw_params, config.buffer_size) { + return Err(BackendSpecificError { + description: format!( + "Buffer size '{:?}' is not supported by this backend", + config.buffer_size + ), + }); + } + + pcm_handle.hw_params(&hw_params)?; + + Ok(hw_params.can_pause()) +} + +/// Returns true if the periods were reasonably set. A false result indicates the device default +/// configuration is being used. +fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSize) -> bool { + // TIMING IMPROVEMENT 2: Use consistent ValueOr::Nearest for all ALSA calls + // TODO: When the API is made available, this could rely on snd_pcm_hw_params_get_periods_min + // and snd_pcm_hw_params_get_periods_max + hw_params + .set_periods(PREFERRED_PERIOD_COUNT, alsa::ValueOr::Nearest) + .unwrap_or_default(); + + let Some(actual_period_count) = hw_params + .get_periods() + .ok() + .filter(|&period_count| period_count > 0) + else { + return false; + }; + + // TIMING IMPROVEMENT 1: Adapt to actual period count from ALSA + // Rather than rejecting different period counts, use what ALSA gives us + // and ensure all subsequent calculations use the actual period count + let period_count = actual_period_count; + + // Basic sanity check - reject only truly unreasonable values + if period_count < 1 || period_count > 16 { + return false; + } + + /// Returns true if the buffer size was reasonably set. + /// + /// The buffer is a ring buffer. The buffer size always has to be greater than one period size. + /// Commonly this is 2*period size, but some hardware can do 8 periods per buffer. It is also + /// possible for the buffer size to not be an integer multiple of the period size. + /// + /// See: https://www.alsa-project.org/wiki/FramesPeriods + fn set_hw_params_buffer_size( + hw_params: &alsa::pcm::HwParams, + period_count: u32, + mut buffer_size: FrameCount, + ) -> bool { + buffer_size = { + let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(hw_params); + buffer_size.clamp(min_buffer_size, max_buffer_size) + }; + + // TIMING IMPROVEMENT 1: Use time-based period configuration for device compatibility + // TIMING IMPROVEMENT 2: Use consistent ValueOr::Nearest for all ALSA calls + let Ok(_) = hw_params.set_period_time_near(FALLBACK_PERIOD_TIME, alsa::ValueOr::Nearest) + else { + return false; + }; + + // TIMING IMPROVEMENT 4: Validate period size is within reasonable bounds + let Ok(actual_period_size) = hw_params.get_period_size() else { + return false; + }; + + // Ensure period size is reasonable (not too small or too large) + if let Ok(sample_rate) = hw_params.get_rate() { + let expected_period_size = (FALLBACK_PERIOD_TIME * sample_rate) / 1_000_000; + let period_size_ratio = actual_period_size as f64 / expected_period_size as f64; + + // Allow reasonable deviation but reject extreme values that cause timing issues + if period_size_ratio < 0.5 || period_size_ratio > 2.0 { + return false; + } + } + + // Set buffer size to requested size + let Ok(actual_buffer_size) = hw_params.set_buffer_size_near(buffer_size as _) else { + return false; + }; + + // TIMING IMPROVEMENT 5: Validate final configuration makes sense + let final_period_count = hw_params.get_periods().unwrap_or(period_count); + let final_period_size = hw_params.get_period_size().unwrap_or(actual_period_size); + + // Check buffer/period relationship is reasonable + let calculated_buffer = final_period_count as alsa::pcm::Frames * final_period_size; + let buffer_ratio = actual_buffer_size as f64 / calculated_buffer as f64; + + // Allow some flexibility but reject configurations that will cause timing problems + if buffer_ratio < 0.8 || buffer_ratio > 1.2 { + return false; + } + + // Double-check the set size is within the CPAL range + VALID_BUFFER_SIZE.contains(&actual_buffer_size) + } + + if let BufferSize::Fixed(val) = buffer_size { + return set_hw_params_buffer_size(hw_params, period_count, val); + } + + // Default path: Use stable period time for consistent timing + // TIMING IMPROVEMENT 2: Use consistent ValueOr::Nearest + if hw_params + .set_period_time_near(FALLBACK_PERIOD_TIME, alsa::ValueOr::Nearest) + .is_err() + { + return false; + } + + let Ok(actual_period_size) = hw_params.get_period_size() else { + return false; + }; + + // TIMING IMPROVEMENT 4: Validate period size is within reasonable bounds + if let Ok(sample_rate) = hw_params.get_rate() { + let expected_period_size = (FALLBACK_PERIOD_TIME * sample_rate) / 1_000_000; + let period_size_ratio = actual_period_size as f64 / expected_period_size as f64; + + // Allow reasonable deviation but reject extreme values that cause timing issues + if period_size_ratio < 0.5 || period_size_ratio > 2.0 { + return false; + } + } + + let period_size = actual_period_size; + + // We should not fail if the driver is unhappy here. + // `default` pcm sometimes fails here, but there no reason to as we attempt to provide a size or + // minimum number of periods. + let Ok(actual_buffer_size) = + hw_params.set_buffer_size_near(period_size * period_count as alsa::pcm::Frames) + else { + return hw_params.set_buffer_size_min(1).is_ok() + && hw_params.set_buffer_size_max(FrameCount::MAX as _).is_ok(); + }; + + // TIMING IMPROVEMENT 5: Validate final configuration makes sense + let final_period_count = hw_params.get_periods().unwrap_or(period_count); + let final_period_size = hw_params.get_period_size().unwrap_or(period_size); + + // Check buffer/period relationship is reasonable + let calculated_buffer = final_period_count as alsa::pcm::Frames * final_period_size; + let buffer_ratio = actual_buffer_size as f64 / calculated_buffer as f64; + + // Allow some flexibility but reject configurations that will cause timing problems + if buffer_ratio < 0.8 || buffer_ratio > 1.2 { + return false; + } + + // Double-check the set size is within the CPAL range + VALID_BUFFER_SIZE.contains(&actual_buffer_size) +} + +fn set_sw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: &StreamConfig, + stream_type: alsa::Direction, +) -> Result { + let sw_params = pcm_handle.sw_params_current()?; + + let period_len = { + let (buffer, period) = pcm_handle.get_params()?; + if buffer == 0 { + return Err(BackendSpecificError { + description: "initialization resulted in a null buffer".to_string(), + }); + } + sw_params.set_avail_min(period as alsa::pcm::Frames)?; + + let start_threshold = match stream_type { + alsa::Direction::Playback => buffer - period, + + // For capture streams, the start threshold is irrelevant and ignored, + // because build_stream_inner() starts the stream before process_input() + // reads from it. Set it anyway I guess, since it's better than leaving + // it at an unspecified default value. + alsa::Direction::Capture => 1, + }; + sw_params.set_start_threshold(start_threshold.try_into().unwrap())?; + + period as usize * config.channels as usize + }; + + sw_params.set_tstamp_mode(true)?; + sw_params.set_tstamp_type(alsa::pcm::TstampType::MonotonicRaw)?; + + // tstamp_type param cannot be changed after the device is opened. + // The default tstamp_type value on most Linux systems is "monotonic", + // let's try to use it if setting the tstamp_type fails. + if pcm_handle.sw_params(&sw_params).is_err() { + sw_params.set_tstamp_type(alsa::pcm::TstampType::Monotonic)?; + pcm_handle.sw_params(&sw_params)?; + } + + Ok(period_len) +} + +impl From for BackendSpecificError { + fn from(err: alsa::Error) -> Self { + BackendSpecificError { + description: 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() + } +} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(desc) = &self.desc { + write!(f, "{} ({})", self.pcm_id, desc.replace('\n', ", ")) + } else { + write!(f, "{}", self.pcm_id) + } + } +} diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 73d936d99..334805e55 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -9,6 +9,7 @@ use crate::{ pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; pub use self::stream::Stream; + use std::sync::Arc; use std::time::Duration; @@ -131,7 +132,6 @@ impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { Stream::play(self) } - fn pause(&self) -> Result<(), PauseStreamError> { Stream::pause(self) } diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 7a911dce4..acec65b47 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -252,6 +252,114 @@ impl DeviceTrait for Device { } } +impl Device { + /// Build input stream with optional JACK-specific configuration + pub fn build_input_stream_raw_with_jack_config( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + jack_config: Option<&super::JackStreamConfig>, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if let DeviceType::OutputDevice = &self.device_type { + // Trying to create an input stream from an output device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + // Use configuration from jack_config if provided, otherwise use device defaults + let client_name = jack_config + .and_then(|config| config.client_name.as_ref()) + .unwrap_or(&self.name); + let start_server = jack_config + .and_then(|config| config.start_server_automatically) + .unwrap_or(self.start_server_automatically); + let connect_ports = jack_config + .and_then(|config| config.connect_ports_automatically) + .unwrap_or(self.connect_ports_automatically); + + // The settings should be fine, create a Client + let client_options = super::get_client_options(start_server); + let client; + match super::get_client(client_name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { description: e }, + }) + } + }; + let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback); + + if connect_ports { + stream.connect_to_system_inputs(); + } + + Ok(stream) + } + + /// Build output stream with optional JACK-specific configuration + pub fn build_output_stream_raw_with_jack_config( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + jack_config: Option<&super::JackStreamConfig>, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if let DeviceType::InputDevice = &self.device_type { + // Trying to create an output stream from an input device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + // Use configuration from jack_config if provided, otherwise use device defaults + let client_name = jack_config + .and_then(|config| config.client_name.as_ref()) + .unwrap_or(&self.name); + let start_server = jack_config + .and_then(|config| config.start_server_automatically) + .unwrap_or(self.start_server_automatically); + let connect_ports = jack_config + .and_then(|config| config.connect_ports_automatically) + .unwrap_or(self.connect_ports_automatically); + + // The settings should be fine, create a Client + let client_options = super::get_client_options(start_server); + let client; + match super::get_client(client_name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { description: e }, + }) + } + }; + let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback); + + if connect_ports { + stream.connect_to_system_outputs(); + } + + Ok(stream) + } +} + impl PartialEq for Device { fn eq(&self, other: &Self) -> bool { // Device::name() can never fail in this implementation diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index d4a28ecea..77ee78a65 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -5,7 +5,7 @@ use crate::{DevicesError, SampleFormat, SupportedStreamConfigRange}; mod device; pub use self::device::Device; -pub use self::stream::Stream; + mod stream; const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; @@ -14,6 +14,74 @@ pub type SupportedInputConfigs = std::vec::IntoIter; pub type SupportedOutputConfigs = std::vec::IntoIter; pub type Devices = std::vec::IntoIter; +/// Platform-specific configuration for JACK streams. +/// +/// This configuration allows customizing JACK client behavior, including +/// client naming and connection policies. These settings are only applied +/// when using the JACK backend. +#[derive(Clone, Debug, Default)] +pub struct JackStreamConfig { + /// Custom client name for JACK. + /// + /// If not set, defaults to "cpal_client_in" for input streams and + /// "cpal_client_out" for output streams. + pub client_name: Option, + /// Whether to automatically connect ports to system inputs/outputs. + /// + /// If not set, uses the default behavior (typically true). + pub connect_ports_automatically: Option, + /// Whether to automatically start the JACK server if it's not running. + /// + /// If not set, uses the default behavior (typically false). + pub start_server_automatically: Option, +} + +impl JackStreamConfig { + /// Set a custom client name for JACK. + /// + /// This name will appear in JACK connection managers and routing tools. + /// Client names must be unique within a JACK session. + /// + /// # Arguments + /// * `name` - The desired client name + /// + /// # Example + /// ```no_run + /// use cpal::{StreamConfig, SampleRate, BufferSize}; + /// + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .on_jack(|jack| jack.client_name("my_audio_app".to_string())); + /// ``` + pub fn client_name>(mut self, name: S) -> Self { + self.client_name = Some(name.into()); + self + } + + /// Set whether to automatically connect ports to system inputs/outputs. + /// + /// When enabled, output streams will connect to system playback ports + /// and input streams will connect to system capture ports automatically. + /// + /// # Arguments + /// * `connect` - Whether to auto-connect ports + pub fn connect_ports_automatically(mut self, connect: bool) -> Self { + self.connect_ports_automatically = Some(connect); + self + } + + /// Set whether to automatically start the JACK server if it's not running. + /// + /// When enabled, CPAL will attempt to start the JACK server if it's not + /// already running. This requires proper JACK configuration on the system. + /// + /// # Arguments + /// * `start` - Whether to auto-start the JACK server + pub fn start_server_automatically(mut self, start: bool) -> Self { + self.start_server_automatically = Some(start); + self + } +} + /// The JACK Host type #[derive(Debug)] pub struct Host { diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 7e927702a..0d48530aa 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -241,7 +241,7 @@ struct LocalProcessHandler { } impl LocalProcessHandler { - #[allow(too_many_arguments)] + #[allow(clippy::too_many_arguments)] fn new( out_ports: Vec>, in_ports: Vec>, diff --git a/src/host/mod.rs b/src/host/mod.rs index 0c61a5910..60425d48b 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -13,15 +13,7 @@ pub(crate) mod asio; pub(crate) mod coreaudio; #[cfg(target_os = "emscripten")] pub(crate) mod emscripten; -#[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "jack" -))] +#[cfg(feature = "jack")] pub(crate) mod jack; pub(crate) mod null; #[cfg(windows)] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 4e4cf14b8..bc503f164 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -537,6 +537,15 @@ impl Device { &self, config: &StreamConfig, sample_format: SampleFormat, + ) -> Result { + self.build_input_stream_raw_inner_with_config(config, sample_format, None) + } + + pub(crate) fn build_input_stream_raw_inner_with_config( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + wasapi_config: Option<&super::WasapiStreamConfig>, ) -> Result { unsafe { // Making sure that COM is initialized. @@ -569,7 +578,14 @@ impl Device { let waveformatex = { let format_attempt = config_to_waveformatextensible(config, sample_format) .ok_or(BuildStreamError::StreamConfigNotSupported)?; - let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; + let share_mode = if wasapi_config + .and_then(|c| c.exclusive_mode) + .unwrap_or(false) + { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; // Ensure the format is supported. match super::device::is_format_supported(&audio_client, &format_attempt.Format) { @@ -660,6 +676,15 @@ impl Device { &self, config: &StreamConfig, sample_format: SampleFormat, + ) -> Result { + self.build_output_stream_raw_inner_with_config(config, sample_format, None) + } + + pub(crate) fn build_output_stream_raw_inner_with_config( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + wasapi_config: Option<&super::WasapiStreamConfig>, ) -> Result { unsafe { // Making sure that COM is initialized. @@ -678,7 +703,14 @@ impl Device { let waveformatex = { let format_attempt = config_to_waveformatextensible(config, sample_format) .ok_or(BuildStreamError::StreamConfigNotSupported)?; - let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; + let share_mode = if wasapi_config + .and_then(|c| c.exclusive_mode) + .unwrap_or(false) + { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; // Ensure the format is supported. match super::device::is_format_supported(&audio_client, &format_attempt.Format) { @@ -758,6 +790,52 @@ impl Device { }) } } + + /// Build input stream with optional WASAPI-specific configuration + pub fn build_input_stream_raw_with_wasapi_config( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + wasapi_config: Option<&super::WasapiStreamConfig>, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let stream_inner = + self.build_input_stream_raw_inner_with_config(config, sample_format, wasapi_config)?; + Ok(Stream::new_input( + stream_inner, + data_callback, + error_callback, + )) + } + + /// Build output stream with optional WASAPI-specific configuration + pub fn build_output_stream_raw_with_wasapi_config( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + wasapi_config: Option<&super::WasapiStreamConfig>, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let stream_inner = + self.build_output_stream_raw_inner_with_config(config, sample_format, wasapi_config)?; + Ok(Stream::new_output( + stream_inner, + data_callback, + error_callback, + )) + } } impl PartialEq for Device { diff --git a/src/host/wasapi/mod.rs b/src/host/wasapi/mod.rs index e80760267..ec9db5c64 100644 --- a/src/host/wasapi/mod.rs +++ b/src/host/wasapi/mod.rs @@ -3,6 +3,27 @@ pub use self::device::{ SupportedOutputConfigs, }; pub use self::stream::Stream; + +/// Configuration for WASAPI streams. +#[derive(Clone, Debug, Default)] +pub struct WasapiStreamConfig { + /// Whether to use exclusive mode (true) or shared mode (false, default). + pub exclusive_mode: Option, +} + +impl WasapiStreamConfig { + /// Create a new WASAPI stream configuration with default values. + pub fn new() -> Self { + Self::default() + } + + /// Set exclusive mode. When true, the stream will attempt to use exclusive mode + /// which can provide lower latency but prevents other applications from using the device. + pub fn exclusive_mode(mut self, exclusive: bool) -> Self { + self.exclusive_mode = Some(exclusive); + self + } +} use crate::traits::HostTrait; use crate::BackendSpecificError; use crate::DevicesError; diff --git a/src/lib.rs b/src/lib.rs index efea3a379..ae2209e7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,48 @@ //! [Device] will run your stream before you can create one. Often, a default device can be //! retrieved via the [Host]. //! +//! ## Quick Start +//! +//! The easiest way to create an audio stream is using the builder API: +//! +//! ```no_run +//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! +//! let host = cpal::default_host(); +//! let device = host.default_output_device().expect("no output device available"); +//! +//! // Create stream with device defaults +//! let stream = device.default_output_config()? +//! .build_output_stream( +//! |data: &mut [f32], _info: &cpal::OutputCallbackInfo| { +//! // Fill buffer with audio data +//! for sample in data.iter_mut() { +//! *sample = 0.0; // Fill with silence +//! } +//! }, +//! |err| eprintln!("Stream error: {}", err), +//! None, // None=blocking, Some(Duration)=timeout +//! )?; +//! +//! stream.play()?; +//! # Ok::<(), Box>(()) +//! ``` +//! +//! ## Manual Configuration (Advanced) +//! +//! For more control, you can manually configure the stream parameters: +//! +//! ```no_run +//! use cpal::traits::{DeviceTrait, HostTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! let mut supported_configs_range = device.supported_output_configs() +//! .expect("error while querying configs"); +//! let supported_config = supported_configs_range.next() +//! .expect("no supported config?!") +//! .with_max_sample_rate(); +//! ``` +//! //! The first step is to initialise the [`Host`]: //! //! ``` @@ -32,9 +74,8 @@ //! let device = host.default_output_device().expect("no output device available"); //! ``` //! -//! Before we can create a stream, we must decide what the configuration of the audio stream is -//! going to be. -//! You can query all the supported configurations with the +//! Before we can create a stream manually, we must decide what the configuration of the audio +//! stream is going to be. You can query all the supported configurations with the //! [`supported_input_configs()`] and [`supported_output_configs()`] methods. //! These produce a list of [`SupportedStreamConfigRange`] structs which can later be turned into //! actual [`SupportedStreamConfig`] structs. @@ -46,51 +87,7 @@ //! > **Note**: the `supported_input/output_configs()` methods //! > could return an error for example if the device has been disconnected. //! -//! ```no_run -//! use cpal::traits::{DeviceTrait, HostTrait}; -//! # let host = cpal::default_host(); -//! # let device = host.default_output_device().unwrap(); -//! let mut supported_configs_range = device.supported_output_configs() -//! .expect("error while querying configs"); -//! let supported_config = supported_configs_range.next() -//! .expect("no supported config?!") -//! .with_max_sample_rate(); -//! ``` -//! -//! Now that we have everything for the stream, we are ready to create it from our selected device: -//! -//! ```no_run -//! use cpal::Data; -//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -//! # let host = cpal::default_host(); -//! # let device = host.default_output_device().unwrap(); -//! # let config = device.default_output_config().unwrap().into(); -//! let stream = device.build_output_stream( -//! &config, -//! move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { -//! // react to stream events and read or write stream data here. -//! }, -//! move |err| { -//! // react to errors here. -//! }, -//! None // None=blocking, Some(Duration)=timeout -//! ); -//! ``` -//! -//! While the stream is running, the selected audio device will periodically call the data callback -//! that was passed to the function. The callback is passed an instance of either [`&Data` or -//! `&mut Data`](Data) depending on whether the stream is an input stream or output stream respectively. -//! -//! > **Note**: Creating and running a stream will *not* block the thread. On modern platforms, the -//! > given callback is called by a dedicated, high-priority thread responsible for delivering -//! > audio data to the system's audio device in a timely manner. On older platforms that only -//! > provide a blocking API (e.g. ALSA), CPAL will create a thread in order to consistently -//! > provide non-blocking behaviour (currently this is a thread per stream, but this may change to -//! > use a single thread for all streams). *If this is an issue for your platform or design, -//! > please share your issue and use-case with the CPAL team on the GitHub issue tracker for -//! > consideration.* -//! -//! In this example, we simply fill the given output buffer with silence. +//! For manual stream creation with explicit sample format handling: //! //! ```no_run //! use cpal::{Data, Sample, SampleFormat, FromSample}; @@ -115,8 +112,26 @@ //! } //! ``` //! +//! While the stream is running, the selected audio device will periodically call the data callback +//! that was passed to the function. The callback is passed an instance of either [`&Data` or +//! `&mut Data`](Data) depending on whether the stream is an input stream or output stream +//! respectively. +//! +//! For most use cases, the callback closures can simply capture variables by value. However, if +//! you need to capture variables that implement `FnOnce` traits or transfer ownership of complex +//! state, you may need to use `move` in your closure. +//! +//! > **Note**: Creating and running a stream will *not* block the thread. On modern platforms, the +//! > given callback is called by a dedicated, high-priority thread responsible for delivering +//! > audio data to the system's audio device in a timely manner. On older platforms that only +//! > provide a blocking API (e.g. ALSA), CPAL will create a thread in order to consistently +//! > provide non-blocking behaviour (currently this is a thread per stream, but this may change to +//! > use a single thread for all streams). *If this is an issue for your platform or design, +//! > please share your issue and use-case with the CPAL team on the GitHub issue tracker for +//! > consideration.* +//! //! Not all platforms automatically run the stream upon creation. To ensure the stream has started, -//! we can use [`Stream::play`](traits::StreamTrait::play). +//! we must use [`Stream::play`](traits::StreamTrait::play). //! //! ```no_run //! # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; @@ -147,6 +162,71 @@ //! stream.pause().unwrap(); //! ``` //! +//! ## Cross-Platform Stream Configuration with Platform-Specific Optimizations +//! +//! The builder API provides true cross-platform compatibility while allowing platform-specific +//! optimizations. The same code compiles and runs on all platforms without conditional compilation: +//! +//! ```no_run +//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! use cpal::BufferSize; +//! +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! // This EXACT code works on ALL platforms - no #[cfg(...)] needed! +//! let stream = device.default_output_config()? +//! .with_buffer_size(BufferSize::Fixed(512)) +//! .on_alsa(|alsa| alsa.periods(2)) // ALSA optimization +//! .on_jack(|jack| jack.client_name("MyApp".into())) // JACK integration +//! .build_output_stream( +//! move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| { +//! // Generate audio - move is often needed for complex state +//! for sample in data.iter_mut() { +//! *sample = 0.0; // Fill with silence +//! } +//! }, +//! move |err| eprintln!("Stream error: {}", err), +//! None, // None=blocking, Some(Duration)=timeout +//! )?; +//! +//! stream.play()?; +//! # Ok::<(), Box>(()) +//! ``` +//! +//! You can also build configurations from scratch with explicit parameters: +//! +//! ```no_run +//! use cpal::{BufferSize, SampleFormat, SampleRate, StreamConfigBuilder}; +//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! let stream = StreamConfigBuilder::new() +//! .channels(2) +//! .sample_rate(SampleRate(48_000)) +//! .sample_format(SampleFormat::F32) +//! .buffer_size(BufferSize::Fixed(1024)) +//! .on_alsa(|alsa| alsa.periods(4)) // More periods for stability +//! .on_jack(|jack| { +//! jack.client_name("CustomApp".into()) +//! .connect_ports_automatically(true) +//! }) +//! .build_output_stream( +//! &device, +//! move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| { +//! // Audio processing logic here +//! for sample in data.iter_mut() { +//! *sample = 0.0; +//! } +//! }, +//! move |err| eprintln!("Stream error: {}", err), +//! None +//! )?; +//! +//! stream.play()?; +//! # Ok::<(), Box>(()) +//! ``` +//! //! [`default_input_device()`]: traits::HostTrait::default_input_device //! [`default_output_device()`]: traits::HostTrait::default_output_device //! [`devices()`]: traits::HostTrait::devices @@ -173,6 +253,52 @@ pub use samples_formats::{FromSample, Sample, SampleFormat, SizedSample, I24, I4 use std::convert::TryInto; use std::ops::{Div, Mul}; use std::time::Duration; + +/// Extension methods for Device to provide improved API +impl Device { + /// The default output stream configuration tied to this device. + /// + /// This returns a `DeviceSupportedStreamConfig` that combines the device with its default + /// output configuration, eliminating the need to pass the device separately when + /// building streams. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let stream = device.default_output_config()? + /// .on_alsa(|alsa| alsa.periods(2)) + /// .build_output_stream::( + /// |data, _| { /* audio callback */ }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn default_output_config( + &self, + ) -> Result { + use crate::traits::DeviceTrait; + let config = DeviceTrait::default_output_config(self)?; + Ok(DeviceSupportedStreamConfig::new(self.clone(), config)) + } + + /// The default input stream configuration tied to this device. + /// + /// This returns a `DeviceSupportedStreamConfig` that combines the device with its default + /// input configuration, eliminating the need to pass the device separately when + /// building streams. + pub fn default_input_config( + &self, + ) -> Result { + use crate::traits::DeviceTrait; + let config = DeviceTrait::default_input_config(self)?; + Ok(DeviceSupportedStreamConfig::new(self.clone(), config)) + } +} + #[cfg(target_os = "emscripten")] use wasm_bindgen::prelude::*; @@ -182,6 +308,40 @@ pub mod platform; mod samples_formats; pub mod traits; +/// Platform-specific configurations and types. +/// +/// This module provides access to platform-specific audio configuration +/// types that can be used with the builder pattern for fine-tuned control +/// over audio streams. +pub mod config { + /// ALSA-specific configuration types. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))] + pub mod alsa { + pub use crate::host::alsa::{AlsaAccessType, AlsaStreamConfig}; + } + + /// JACK-specific configuration types. + #[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) + ))] + pub mod jack { + pub use crate::host::jack::JackStreamConfig; + } +} + /// A host's device iterator yielding only *input* devices. pub type InputDevices = std::iter::Filter::Item) -> bool>; @@ -201,6 +361,7 @@ where u32: Mul, { type Output = Self; + fn mul(self, rhs: T) -> Self { SampleRate(self.0 * rhs) } @@ -211,6 +372,7 @@ where u32: Div, { type Output = Self; + fn div(self, rhs: T) -> Self { SampleRate(self.0 / rhs) } @@ -230,8 +392,9 @@ pub type FrameCount = u32; /// [`Default`]: BufferSize::Default /// [`Fixed(FrameCount)`]: BufferSize::Fixed /// [`SupportedStreamConfig`]: SupportedStreamConfig::buffer_size -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum BufferSize { + #[default] Default, Fixed(FrameCount), } @@ -265,6 +428,504 @@ pub struct StreamConfig { pub buffer_size: BufferSize, } +impl StreamConfig { + /// Create a new `StreamConfig` with the given parameters. + pub fn new(channels: ChannelCount, sample_rate: SampleRate, buffer_size: BufferSize) -> Self { + Self { + channels, + sample_rate, + buffer_size, + } + } + + /// Configure ALSA-specific options directly. + /// + /// This method provides direct access to ALSA configuration without requiring + /// an explicit `.builder()` call. It's safe to call on all platforms - on + /// non-ALSA platforms, the configuration is simply ignored. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, SampleRate, BufferSize}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .on_alsa(|alsa| alsa.periods(2)); + /// ``` + pub fn on_alsa(self, f: F) -> StreamConfigBuilder + where + F: FnOnce(AlsaStreamConfigWrapper) -> AlsaStreamConfigWrapper, + { + StreamConfigBuilder::from_stream_config(&self).on_alsa(f) + } + + /// Configure JACK-specific options directly. + /// + /// This method provides direct access to JACK configuration without requiring + /// an explicit `.builder()` call. It's safe to call on all platforms - on + /// non-JACK platforms, the configuration is simply ignored. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, SampleRate, BufferSize}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .on_jack(|jack| jack.client_name("my_app".to_string())); + /// ``` + pub fn on_jack(self, f: F) -> StreamConfigBuilder + where + F: FnOnce(JackStreamConfigWrapper) -> JackStreamConfigWrapper, + { + StreamConfigBuilder::from_stream_config(&self).on_jack(f) + } + + /// Set buffer size configuration directly. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, SampleRate, BufferSize}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .with_buffer_size(BufferSize::Fixed(512)); + /// ``` + pub fn with_buffer_size(mut self, buffer_size: BufferSize) -> Self { + self.buffer_size = buffer_size; + self + } + + /// Set the number of channels. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, SampleRate, BufferSize}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .with_channels(6); // 5.1 surround + /// ``` + pub fn with_channels(mut self, channels: ChannelCount) -> Self { + self.channels = channels; + self + } + + /// Set the sample rate. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, SampleRate, BufferSize}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default) + /// .with_sample_rate(SampleRate(48000)); + /// ``` + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> Self { + self.sample_rate = sample_rate; + self + } + + /// Build an output stream directly with typed samples. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds an output stream. Note that you must specify the + /// sample format since `StreamConfig` doesn't contain this information. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + /// # use cpal::{StreamConfig, SampleRate, BufferSize, SampleFormat}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default); + /// let stream = config.build_output_stream::( + /// &device, + /// SampleFormat::F32, + /// |data, _| { + /// for sample in data.iter_mut() { + /// *sample = 0.0; + /// } + /// }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// stream.play()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn build_output_stream( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + StreamConfigBuilder::from_stream_config(&self) + .sample_format(sample_format) + .build_output_stream(device, data_callback, error_callback, timeout) + } + + /// Build a raw output stream directly. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds a raw output stream. + pub fn build_output_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + StreamConfigBuilder::from_stream_config(&self) + .sample_format(sample_format) + .build_output_stream_raw( + device, + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + /// Build an input stream directly with typed samples. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds an input stream. + pub fn build_input_stream( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + StreamConfigBuilder::from_stream_config(&self) + .sample_format(sample_format) + .build_input_stream(device, data_callback, error_callback, timeout) + } + + /// Build a raw input stream directly. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds a raw input stream. + pub fn build_input_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + StreamConfigBuilder::from_stream_config(&self) + .sample_format(sample_format) + .build_input_stream_raw( + device, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +/// Builder for creating [`StreamConfig`] with platform-specific options. +/// +/// This builder provides a **truly cross-platform** way to configure audio streams. +/// Platform-specific methods like `.on_alsa()` and `.on_jack()` work on **ALL platforms** +/// without any conditional compilation - they're simply no-ops where not supported. +/// +/// Key advantages: +/// +/// - **No arbitrary defaults**: Unlike `StreamConfig::default()`, the builder requires +/// you to explicitly set essential parameters or derive them from a supported configuration. +/// - **Sample format preservation**: When building from [`SupportedStreamConfig`], the +/// sample format is preserved, which is critical for correct stream creation. +/// - **True cross-platform compatibility**: Write once, run anywhere. No `#[cfg(...)]` needed. +/// - **Type safety**: The builder prevents creation of invalid configurations by requiring +/// all essential fields before building. +/// +/// # Usage Patterns +/// +/// ## 1. Building from SupportedStreamConfig (Recommended) +/// +/// This is the preferred approach as it preserves device capabilities: +/// +/// ```no_run +/// use cpal::traits::{DeviceTrait, HostTrait}; +/// use cpal::BufferSize; +/// +/// let host = cpal::default_host(); +/// let device = host.default_output_device().unwrap(); +/// let supported_config = device.default_output_config().unwrap(); +/// +/// // Create builder from supported config - preserves sample format! +/// let builder = supported_config.builder() +/// .buffer_size(BufferSize::Fixed(512)); +/// +/// let (config, platform_config) = builder.build(); +/// ``` +/// +/// ## 2. Building from Scratch +/// +/// Use this when you need specific parameters that may differ from device defaults: +/// +/// ```no_run +/// use cpal::{StreamConfigBuilder, SampleRate, BufferSize, SampleFormat}; +/// +/// let builder = StreamConfigBuilder::new() +/// .channels(2) +/// .sample_rate(SampleRate(48_000)) +/// .sample_format(SampleFormat::F32) +/// .buffer_size(BufferSize::Fixed(1024)); +/// +/// let (config, platform_config) = builder.build(); +/// ``` +/// +/// ## 3. Platform-Specific Configuration +/// +/// Set platform-specific options that are safely ignored on unsupported platforms: +/// +/// ```no_run +/// # use cpal::traits::{DeviceTrait, HostTrait}; +/// # let host = cpal::default_host(); +/// # let device = host.default_output_device().unwrap(); +/// # let supported_config = device.default_output_config().unwrap(); +/// let builder = supported_config.builder() +/// .on_alsa(|alsa| alsa.periods(2)) // Works on ALL platforms! +/// .on_jack(|jack| jack.client_name("MyApp".into())); // Works on ALL platforms! +/// +/// let (config, platform_config) = builder.build(); +/// // No conditional compilation needed - works everywhere! +/// ``` +/// +/// ## 4. Direct Stream Creation +/// +/// The builder can create streams directly, handling platform detection automatically: +/// +/// ```no_run +/// # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +/// # use std::time::Duration; +/// # let host = cpal::default_host(); +/// # let device = host.default_output_device().unwrap(); +/// # let supported_config = device.default_output_config().unwrap(); +/// // This code works on ALL platforms - no conditional compilation needed! +/// let stream = supported_config.builder() +/// .on_alsa(|alsa| alsa.periods(2)) // Works on ALL platforms! +/// .on_jack(|jack| jack.client_name("MyApp".into())) // Works on ALL platforms! +/// .build_output_stream::( +/// &device, +/// |data, _info| { /* audio callback */ }, +/// |err| eprintln!("Error: {}", err), +/// None +/// ).unwrap(); +/// +/// stream.play().unwrap(); +/// ``` +/// +/// # Error Handling +/// +/// The builder uses type-safe error handling: +/// +/// ```no_run +/// use cpal::StreamConfigBuilder; +/// +/// let incomplete = StreamConfigBuilder::new() +/// .channels(2); // Missing sample_rate and sample_format! +/// +/// match incomplete.try_build() { +/// Some((config, platform_config)) => { +/// println!("Config: {:?}", config); +/// } +/// None => { +/// println!("Missing required configuration (sample_rate, sample_format)"); +/// } +/// } +/// ``` +#[derive(Clone, Debug, Default)] +pub struct StreamConfigBuilder { + channels: Option, + sample_rate: Option, + sample_format: Option, + buffer_size: BufferSize, + // Always include platform configs - they're just ignored on unsupported platforms + alsa_config: Option, + jack_config: Option, + wasapi_config: Option, +} + +/// Wrapper for ALSA configuration that exists on all platforms but only functions on ALSA platforms +#[derive(Clone, Debug, Default)] +pub struct AlsaStreamConfigWrapper { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))] + pub(crate) inner: crate::host::alsa::AlsaStreamConfig, +} + +/// Real implementation on ALSA platforms +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" +))] +impl AlsaStreamConfigWrapper { + pub fn periods(mut self, periods: u32) -> Self { + self.inner.periods = Some(periods); + self + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))] + pub fn access_type(mut self, access_type: crate::config::alsa::AlsaAccessType) -> Self { + self.inner.access_type = Some(access_type); + self + } +} + +/// Stub implementation on non-ALSA platforms +#[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" +)))] +impl AlsaStreamConfigWrapper { + pub fn periods(self, _periods: u32) -> Self { + self // No-op on non-ALSA platforms + } +} + +/// Wrapper for JACK configuration that exists on all platforms but only functions on JACK platforms +#[derive(Clone, Debug, Default)] +pub struct JackStreamConfigWrapper { + #[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) + ))] + pub(crate) inner: crate::host::jack::JackStreamConfig, +} + +/// Real implementation on JACK platforms +#[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) +))] +impl JackStreamConfigWrapper { + pub fn client_name(mut self, name: String) -> Self { + self.inner.client_name = Some(name); + self + } + + pub fn connect_ports_automatically(mut self, connect: bool) -> Self { + self.inner.connect_ports_automatically = Some(connect); + self + } + + pub fn start_server_automatically(mut self, start: bool) -> Self { + self.inner.start_server_automatically = Some(start); + self + } +} + +/// Stub implementation on non-JACK platforms +#[cfg(not(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) +)))] +impl JackStreamConfigWrapper { + pub fn client_name(self, _name: String) -> Self { + self // No-op on non-JACK platforms + } + + pub fn connect_ports_automatically(self, _connect: bool) -> Self { + self // No-op on non-JACK platforms + } + + pub fn start_server_automatically(self, _start: bool) -> Self { + self // No-op on non-JACK platforms + } +} + +/// Platform-specific configuration bundle for stream creation. +/// +/// This struct contains all platform-specific configurations that may be +/// relevant for the current platform. Only the configurations for the +/// active platform will be used. +#[derive(Clone, Debug, Default)] +pub struct PlatformStreamConfig { + pub alsa: Option, + pub jack: Option, + pub wasapi: Option, +} + +/// Wrapper for WASAPI configuration that exists on all platforms but only functions on Windows +#[derive(Clone, Debug, Default)] +pub struct WasapiStreamConfigWrapper { + #[cfg(target_os = "windows")] + pub(crate) inner: crate::host::wasapi::WasapiStreamConfig, +} + +/// Real implementation on Windows +#[cfg(target_os = "windows")] +impl WasapiStreamConfigWrapper { + pub fn exclusive_mode(mut self, exclusive: bool) -> Self { + self.inner.exclusive_mode = Some(exclusive); + self + } +} + +/// Stub implementation on non-Windows platforms +#[cfg(not(target_os = "windows"))] +impl WasapiStreamConfigWrapper { + pub fn exclusive_mode(self, _exclusive: bool) -> Self { + self // No-op on non-Windows platforms + } +} + /// Describes the minimum and maximum supported buffer size for the device #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SupportedBufferSize { @@ -315,63 +976,316 @@ pub struct Data { sample_format: SampleFormat, } -/// A monotonic time instance associated with a stream, retrieved from either: -/// -/// 1. A timestamp provided to the stream's underlying audio data callback or -/// 2. The same time source used to generate timestamps for a stream's underlying audio data -/// callback. -/// -/// `StreamInstant` represents a duration since some unspecified origin occurring either before -/// or equal to the moment the stream from which it was created begins. -/// -/// ## Host `StreamInstant` Sources -/// -/// | Host | Source | -/// | ---- | ------ | -/// | alsa | `snd_pcm_status_get_htstamp` | -/// | coreaudio | `mach_absolute_time` | -/// | wasapi | `QueryPerformanceCounter` | -/// | asio | `timeGetTime` | -/// | emscripten | `AudioContext.getOutputTimestamp` | -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct StreamInstant { - secs: i64, - nanos: u32, +/// A monotonic time instance associated with a stream, retrieved from either: +/// +/// 1. A timestamp provided to the stream's underlying audio data callback or +/// 2. The same time source used to generate timestamps for a stream's underlying audio data +/// callback. +/// +/// `StreamInstant` represents a duration since some unspecified origin occurring either before +/// or equal to the moment the stream from which it was created begins. +/// +/// ## Host `StreamInstant` Sources +/// +/// | Host | Source | +/// | ---- | ------ | +/// | alsa | `snd_pcm_status_get_htstamp` | +/// | coreaudio | `mach_absolute_time` | +/// | wasapi | `QueryPerformanceCounter` | +/// | asio | `timeGetTime` | +/// | emscripten | `AudioContext.getOutputTimestamp` | +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct StreamInstant { + secs: i64, + nanos: u32, +} + +/// A timestamp associated with a call to an input stream's data callback. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct InputStreamTimestamp { + /// The instant the stream's data callback was invoked. + pub callback: StreamInstant, + /// The instant that data was captured from the device. + /// + /// E.g. The instant data was read from an ADC. + pub capture: StreamInstant, +} + +/// A timestamp associated with a call to an output stream's data callback. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct OutputStreamTimestamp { + /// The instant the stream's data callback was invoked. + pub callback: StreamInstant, + /// The predicted instant that data written will be delivered to the device for playback. + /// + /// E.g. The instant data will be played by a DAC. + pub playback: StreamInstant, +} + +/// Information relevant to a single call to the user's input stream data callback. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputCallbackInfo { + timestamp: InputStreamTimestamp, +} + +/// Information relevant to a single call to the user's output stream data callback. +#[cfg_attr(target_os = "emscripten", wasm_bindgen)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutputCallbackInfo { + timestamp: OutputStreamTimestamp, +} + +/// A stream configuration tied to a specific device. +/// +/// This type combines a device with its supported stream configuration, +/// eliminating the need to pass the device separately when building streams. +/// It provides all the same configuration methods as `SupportedStreamConfig` +/// but with the device already captured. +#[derive(Clone)] +pub struct DeviceSupportedStreamConfig { + device: crate::Device, + config: SupportedStreamConfig, +} + +impl DeviceSupportedStreamConfig { + /// Create a new `DeviceSupportedStreamConfig` from a device and its configuration. + pub fn new(device: crate::Device, config: SupportedStreamConfig) -> Self { + Self { device, config } + } + + /// Get a reference to the device. + pub fn device(&self) -> &crate::Device { + &self.device + } + + /// Get a reference to the configuration. + pub fn config(&self) -> &SupportedStreamConfig { + &self.config + } + + /// Configure ALSA-specific options directly. + /// + /// This method provides direct access to ALSA configuration. It's safe to call + /// on all platforms - on non-ALSA platforms, the configuration is simply ignored. + pub fn on_alsa(self, f: F) -> Self + where + F: FnOnce(AlsaStreamConfigWrapper) -> AlsaStreamConfigWrapper, + { + let builder = self.config.builder().on_alsa(f); + let (new_config, _) = builder.build(); + // Convert back to SupportedStreamConfig + let supported_config = SupportedStreamConfig::new( + new_config.channels, + new_config.sample_rate, + self.config.buffer_size(), + self.config.sample_format(), + ); + Self { + device: self.device, + config: supported_config, + } + } + + /// Configure JACK-specific options directly. + /// + /// This method provides direct access to JACK configuration. It's safe to call + /// on all platforms - on non-JACK platforms, the configuration is simply ignored. + pub fn on_jack(self, f: F) -> Self + where + F: FnOnce(JackStreamConfigWrapper) -> JackStreamConfigWrapper, + { + let builder = self.config.builder().on_jack(f); + let (new_config, _) = builder.build(); + // Convert back to SupportedStreamConfig + let supported_config = SupportedStreamConfig::new( + new_config.channels, + new_config.sample_rate, + self.config.buffer_size(), + self.config.sample_format(), + ); + Self { + device: self.device, + config: supported_config, + } + } + + /// Set buffer size configuration directly. + pub fn with_buffer_size(mut self, _buffer_size: BufferSize) -> Self { + // Create a new config with the updated buffer size + let supported_config = SupportedStreamConfig::new( + self.config.channels(), + self.config.sample_rate(), + self.config.buffer_size(), + self.config.sample_format(), + ); + self.config = supported_config; + self + } + + /// Build an output stream directly with typed samples. + /// + /// This method builds a stream without requiring the device to be passed again. + pub fn build_output_stream( + self, + data_callback: DataCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result + where + T: SizedSample, + DataCallback: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, + ErrorCallback: FnMut(StreamError) + Send + 'static, + { + use crate::traits::DeviceTrait; + self.device.build_output_stream( + &self.config.config(), + data_callback, + error_callback, + timeout, + ) + } + + /// Build a raw output stream directly. + /// + /// This method builds a raw stream without requiring the device to be passed again. + pub fn build_output_stream_raw( + self, + sample_format: SampleFormat, + data_callback: DataCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result + where + DataCallback: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + ErrorCallback: FnMut(StreamError) + Send + 'static, + { + use crate::traits::DeviceTrait; + self.device.build_output_stream_raw( + &self.config.config(), + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + /// Build an input stream directly with typed samples. + pub fn build_input_stream( + self, + data_callback: DataCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result + where + T: SizedSample, + DataCallback: FnMut(&[T], &InputCallbackInfo) + Send + 'static, + ErrorCallback: FnMut(StreamError) + Send + 'static, + { + use crate::traits::DeviceTrait; + self.device.build_input_stream( + &self.config.config(), + data_callback, + error_callback, + timeout, + ) + } + + /// Build a raw input stream directly. + pub fn build_input_stream_raw( + self, + sample_format: SampleFormat, + data_callback: DataCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result + where + DataCallback: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + ErrorCallback: FnMut(StreamError) + Send + 'static, + { + use crate::traits::DeviceTrait; + self.device.build_input_stream_raw( + &self.config.config(), + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + /// Get the underlying builder for advanced configuration. + pub fn builder(self) -> StreamConfigBuilder { + self.config.builder() + } + + /// Build the configuration and platform config. + pub fn build(self) -> (StreamConfig, PlatformStreamConfig) { + self.config.builder().build() + } + + /// Get the channels count from the configuration. + pub fn channels(&self) -> ChannelCount { + self.config.channels() + } + + /// Get the sample rate from the configuration. + pub fn sample_rate(&self) -> SampleRate { + self.config.sample_rate() + } + + /// Get the buffer size from the configuration. + pub fn buffer_size(&self) -> SupportedBufferSize { + self.config.buffer_size() + } + + /// Get the sample format from the configuration. + pub fn sample_format(&self) -> SampleFormat { + self.config.sample_format() + } + + /// Get the basic StreamConfig. + pub fn stream_config(&self) -> StreamConfig { + self.config.config() + } +} + +impl From for StreamConfig { + fn from(config: DeviceSupportedStreamConfig) -> Self { + config.config.config() + } +} + +impl From<&DeviceSupportedStreamConfig> for StreamConfig { + fn from(config: &DeviceSupportedStreamConfig) -> Self { + config.config.config() + } } -/// A timestamp associated with a call to an input stream's data callback. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub struct InputStreamTimestamp { - /// The instant the stream's data callback was invoked. - pub callback: StreamInstant, - /// The instant that data was captured from the device. - /// - /// E.g. The instant data was read from an ADC. - pub capture: StreamInstant, +impl From for SupportedStreamConfig { + fn from(config: DeviceSupportedStreamConfig) -> Self { + config.config + } } -/// A timestamp associated with a call to an output stream's data callback. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub struct OutputStreamTimestamp { - /// The instant the stream's data callback was invoked. - pub callback: StreamInstant, - /// The predicted instant that data written will be delivered to the device for playback. - /// - /// E.g. The instant data will be played by a DAC. - pub playback: StreamInstant, +impl From<&DeviceSupportedStreamConfig> for SupportedStreamConfig { + fn from(config: &DeviceSupportedStreamConfig) -> Self { + config.config.clone() + } } -/// Information relevant to a single call to the user's input stream data callback. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InputCallbackInfo { - timestamp: InputStreamTimestamp, +impl std::fmt::Debug for DeviceSupportedStreamConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeviceSupportedStreamConfig") + .field("config", &self.config) + .finish() + } } -/// Information relevant to a single call to the user's output stream data callback. -#[cfg_attr(target_os = "emscripten", wasm_bindgen)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OutputCallbackInfo { - timestamp: OutputStreamTimestamp, +impl std::ops::Deref for DeviceSupportedStreamConfig { + type Target = SupportedStreamConfig; + + fn deref(&self) -> &Self::Target { + &self.config + } } impl SupportedStreamConfig { @@ -397,8 +1311,8 @@ impl SupportedStreamConfig { self.sample_rate } - pub fn buffer_size(&self) -> &SupportedBufferSize { - &self.buffer_size + pub fn buffer_size(&self) -> SupportedBufferSize { + self.buffer_size } pub fn sample_format(&self) -> SampleFormat { @@ -412,6 +1326,240 @@ impl SupportedStreamConfig { buffer_size: BufferSize::Default, } } + + /// Create a [`StreamConfigBuilder`] from this supported configuration. + /// + /// This is the recommended way to create a builder as it preserves all device + /// capabilities including the sample format, which is critical for correct + /// stream creation. The resulting builder can be further customized with + /// buffer size settings and platform-specific options. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let supported_config = device.default_output_config().unwrap(); + /// + /// // Create builder with device-optimal settings + /// let builder = supported_config.builder() + /// .buffer_size(cpal::BufferSize::Fixed(512)) + /// .on_alsa(|alsa| alsa.periods(2)); + /// + /// let (config, platform_config) = builder.build(); + /// ``` + pub fn builder(&self) -> StreamConfigBuilder { + StreamConfigBuilder::from_supported_config(self) + } + + /// Configure ALSA-specific options directly. + /// + /// This method provides direct access to ALSA configuration without requiring + /// an explicit `.builder()` call. It's safe to call on all platforms - on + /// non-ALSA platforms, the configuration is simply ignored. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let stream = device.default_output_config()? + /// .on_alsa(|alsa| alsa.periods(2)) + /// .build_output_stream::( + /// |data, _| { /* audio callback */ }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn on_alsa(self, f: F) -> StreamConfigBuilder + where + F: FnOnce(AlsaStreamConfigWrapper) -> AlsaStreamConfigWrapper, + { + self.builder().on_alsa(f) + } + + /// Configure JACK-specific options directly. + /// + /// This method provides direct access to JACK configuration without requiring + /// an explicit `.builder()` call. It's safe to call on all platforms - on + /// non-JACK platforms, the configuration is simply ignored. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let stream = device.default_output_config()? + /// .on_jack(|jack| jack.client_name("my_audio_app".to_string())) + /// .build_output_stream::( + /// |data, _| { /* audio callback */ }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn on_jack(self, f: F) -> StreamConfigBuilder + where + F: FnOnce(JackStreamConfigWrapper) -> JackStreamConfigWrapper, + { + self.builder().on_jack(f) + } + + /// Set buffer size configuration directly. + /// + /// This method provides direct access to buffer size configuration without + /// requiring an explicit `.builder()` call. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let stream = device.default_output_config()? + /// .with_buffer_size(cpal::BufferSize::Fixed(512)) + /// .build_output_stream::( + /// |data, _| { /* audio callback */ }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn with_buffer_size(self, buffer_size: BufferSize) -> StreamConfigBuilder { + self.builder().buffer_size(buffer_size) + } + + /// Build an output stream directly with typed samples. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds an output stream. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let stream = device.default_output_config()? + /// .build_output_stream::( + /// |data, _| { + /// for sample in data.iter_mut() { + /// *sample = 0.0; + /// } + /// }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// stream.play()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn build_output_stream( + self, + device: &crate::Device, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.builder() + .build_output_stream(device, data_callback, error_callback, timeout) + } + + /// Build a raw output stream directly. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds a raw output stream. + pub fn build_output_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.builder().build_output_stream_raw( + device, + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + /// Build an input stream directly with typed samples. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds an input stream. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_input_device().unwrap(); + /// let stream = device.default_input_config()? + /// .build_input_stream::( + /// |data, _| { + /// println!("Received {} samples", data.len()); + /// }, + /// |err| eprintln!("Stream error: {}", err), + /// None, + /// )?; + /// stream.play()?; + /// # Ok::<(), Box>(()) + /// ``` + pub fn build_input_stream( + self, + device: &crate::Device, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.builder() + .build_input_stream(device, data_callback, error_callback, timeout) + } + + /// Build a raw input stream directly. + /// + /// This is a convenience method that creates a builder internally and + /// immediately builds a raw input stream. + pub fn build_input_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.builder().build_input_stream_raw( + device, + sample_format, + data_callback, + error_callback, + timeout, + ) + } } impl StreamInstant { @@ -824,6 +1972,605 @@ impl From for StreamConfig { } } +impl StreamConfigBuilder { + /// Create a new builder with no defaults set. + /// + /// When using this constructor, you must call [`channels`](Self::channels), + /// [`sample_rate`](Self::sample_rate), and [`sample_format`](Self::sample_format) + /// before calling [`build`](Self::build) or the build will panic. + /// + /// # Recommendation + /// + /// Consider using [`SupportedStreamConfig::builder`] instead, which automatically + /// sets appropriate values based on device capabilities and preserves the sample format. + /// + /// # Examples + /// + /// ```no_run + /// use cpal::{StreamConfigBuilder, SampleRate, SampleFormat, BufferSize}; + /// + /// let builder = StreamConfigBuilder::new() + /// .channels(2) + /// .sample_rate(SampleRate(44_100)) + /// .sample_format(SampleFormat::F32) + /// .buffer_size(BufferSize::Default); + /// + /// let (config, platform_config) = builder.build(); + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Create a builder from a [`SupportedStreamConfig`]. + /// + /// This is the recommended way to create a builder as it preserves all the + /// essential configuration including sample format, which is critical for + /// correct stream creation. All device capabilities are preserved and you + /// can further customize buffer size and platform-specific options. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # use cpal::{BufferSize, StreamConfigBuilder}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// let supported_config = device.default_output_config().unwrap(); + /// + /// let builder = StreamConfigBuilder::from_supported_config(&supported_config) + /// .buffer_size(BufferSize::Fixed(1024)); + /// ``` + pub fn from_supported_config(config: &SupportedStreamConfig) -> Self { + Self { + channels: Some(config.channels()), + sample_rate: Some(config.sample_rate()), + sample_format: Some(config.sample_format()), + buffer_size: BufferSize::Default, + alsa_config: None, + jack_config: None, + wasapi_config: None, + } + } + + /// Create a builder from a [`StreamConfig`]. + /// + /// This creates a builder from an existing stream configuration, allowing + /// you to add platform-specific options or modify settings. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::{StreamConfig, StreamConfigBuilder, SampleRate, BufferSize, SampleFormat}; + /// let config = StreamConfig::new(2, SampleRate(44100), BufferSize::Default); + /// let builder = StreamConfigBuilder::from_stream_config(&config) + /// .sample_format(SampleFormat::F32); + /// ``` + pub fn from_stream_config(config: &StreamConfig) -> Self { + Self { + channels: Some(config.channels), + sample_rate: Some(config.sample_rate), + sample_format: None, // StreamConfig doesn't contain sample format + buffer_size: config.buffer_size, + alsa_config: None, + jack_config: None, + wasapi_config: None, + } + } + + /// Set the number of channels. + pub fn channels(mut self, channels: ChannelCount) -> Self { + self.channels = Some(channels); + self + } + + /// Set the sample rate. + pub fn sample_rate(mut self, sample_rate: SampleRate) -> Self { + self.sample_rate = Some(sample_rate); + self + } + + /// Set the sample format. + pub fn sample_format(mut self, sample_format: SampleFormat) -> Self { + self.sample_format = Some(sample_format); + self + } + + /// Set the buffer size. + pub fn buffer_size(mut self, buffer_size: BufferSize) -> Self { + self.buffer_size = buffer_size; + self + } + + /// Configure ALSA-specific options. + /// + /// **This method works on ALL platforms** without any conditional compilation needed. + /// On Linux/BSD systems with ALSA, the configuration takes effect. On other platforms, + /// it's safely ignored (no-op). No `#[cfg(...)]` attributes required! + /// + /// # Examples + /// + /// ```no_run + /// use cpal::StreamConfigBuilder; + /// // This code works everywhere - no platform checks needed! + /// let builder = StreamConfigBuilder::new() + /// .on_alsa(|alsa| alsa.periods(2)); // Safe on ALL platforms + /// ``` + pub fn on_alsa(mut self, f: F) -> Self + where + F: FnOnce(AlsaStreamConfigWrapper) -> AlsaStreamConfigWrapper, + { + let config = self.alsa_config.unwrap_or_default(); + self.alsa_config = Some(f(config)); + self + } + + /// Configure JACK-specific options. + /// + /// **This method works on ALL platforms** without any conditional compilation needed. + /// On systems with JACK installed and the jack feature enabled, the configuration + /// takes effect. On platforms without JACK support, this configuration is safely ignored. + /// + /// # Examples + /// + /// ```no_run + /// use cpal::StreamConfigBuilder; + /// let builder = StreamConfigBuilder::new() + /// .on_jack(|jack| jack.client_name("my_audio_app".to_string())); // Safe on all platforms + /// ``` + pub fn on_jack(mut self, f: F) -> Self + where + F: FnOnce(JackStreamConfigWrapper) -> JackStreamConfigWrapper, + { + let config = self.jack_config.unwrap_or_default(); + self.jack_config = Some(f(config)); + self + } + + /// Configure WASAPI-specific options. + /// + /// **This method works on ALL platforms** without any conditional compilation needed. + /// On Windows systems, the configuration takes effect. On other platforms, + /// this configuration is safely ignored. + /// + /// # Examples + /// + /// ```no_run + /// use cpal::StreamConfigBuilder; + /// let builder = StreamConfigBuilder::new() + /// .on_wasapi(|wasapi| wasapi.exclusive_mode(true)); // Safe on all platforms + /// ``` + pub fn on_wasapi(mut self, f: F) -> Self + where + F: FnOnce(WasapiStreamConfigWrapper) -> WasapiStreamConfigWrapper, + { + let config = self.wasapi_config.unwrap_or_default(); + self.wasapi_config = Some(f(config)); + self + } + + /// Build the stream configuration. + /// + /// Returns a tuple of (`StreamConfig`, `PlatformStreamConfig`) that can be + /// used to create streams or passed to platform-specific build methods. + /// The `StreamConfig` contains the standard audio parameters, while + /// `PlatformStreamConfig` contains any platform-specific options. + /// + /// # Panics + /// + /// Panics if any of the required fields (channels, sample_rate, sample_format) + /// have not been set. Use [`try_build`](Self::try_build) for a non-panicking version. + /// + /// # Examples + /// + /// ```no_run + /// # use cpal::traits::{DeviceTrait, HostTrait}; + /// # let host = cpal::default_host(); + /// # let device = host.default_output_device().unwrap(); + /// # let supported_config = device.default_output_config().unwrap(); + /// let builder = supported_config.builder(); + /// let (stream_config, platform_config) = builder.build(); + /// + /// println!("Channels: {}", stream_config.channels); + /// println!("Sample rate: {}", stream_config.sample_rate.0); + /// ``` + pub fn build(self) -> (StreamConfig, PlatformStreamConfig) { + self.try_build() + .expect("StreamConfigBuilder is missing required fields") + } + + /// Try to build the stream configuration. + /// + /// Returns `None` if any required fields (channels, sample_rate, sample_format) + /// are missing. This is the safe alternative to [`build`](Self::build) that + /// doesn't panic. + /// + /// # Examples + /// + /// ```no_run + /// use cpal::StreamConfigBuilder; + /// + /// let incomplete = StreamConfigBuilder::new().channels(2); + /// + /// match incomplete.try_build() { + /// Some((config, platform_config)) => { + /// println!("Built successfully: {:?}", config); + /// } + /// None => { + /// println!("Missing required configuration (sample_rate, sample_format)"); + /// } + /// } + /// ``` + pub fn try_build(self) -> Option<(StreamConfig, PlatformStreamConfig)> { + let channels = self.channels?; + let sample_rate = self.sample_rate?; + let _sample_format = self.sample_format?; + + let stream_config = StreamConfig { + channels, + sample_rate, + buffer_size: self.buffer_size, + }; + + let platform_config = PlatformStreamConfig { + alsa: self.alsa_config, + jack: self.jack_config, + wasapi: self.wasapi_config, + }; + + Some((stream_config, platform_config)) + } + + /// Build an input stream using this configuration. + /// + /// This is a convenience method that handles platform-specific configuration + /// automatically. It detects the active audio backend at runtime and applies + /// the appropriate platform-specific settings, falling back to standard stream + /// creation if no platform-specific configuration is set. + pub fn build_input_stream( + self, + device: &crate::Device, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let sample_format = self + .sample_format + .expect("sample_format must be set before building stream"); + + self.build_input_stream_raw( + device, + sample_format, + move |data, info| { + data_callback( + data.as_slice() + .expect("host supplied incorrect sample type"), + info, + ) + }, + error_callback, + timeout, + ) + } + + /// Build a raw input stream using this configuration. + /// + /// This is a convenience method that handles platform-specific configuration + /// automatically. Unlike [`build_input_stream`](Self::build_input_stream), this + /// method works with dynamically typed audio data and requires you to specify + /// the sample format explicitly. + pub fn build_input_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let (config, platform_config) = self.build(); + + // Try platform-specific methods first, then fall back to standard method + Self::build_input_stream_with_platform_config( + device, + &config, + sample_format, + data_callback, + error_callback, + timeout, + &platform_config, + ) + } + + /// Build an output stream using this configuration. + /// + /// This is a convenience method that handles platform-specific configuration + /// automatically. It detects the active audio backend at runtime and applies + /// the appropriate platform-specific settings, falling back to standard stream + /// creation if no platform-specific configuration is set. + pub fn build_output_stream( + self, + device: &crate::Device, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let sample_format = self + .sample_format + .expect("sample_format must be set before building stream"); + + self.build_output_stream_raw( + device, + sample_format, + move |data, info| { + data_callback( + data.as_slice_mut() + .expect("host supplied incorrect sample type"), + info, + ) + }, + error_callback, + timeout, + ) + } + + /// Build a raw output stream using this configuration. + /// + /// This is a convenience method that handles platform-specific configuration + /// automatically. + pub fn build_output_stream_raw( + self, + device: &crate::Device, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let (config, platform_config) = self.build(); + + // Try platform-specific methods first, then fall back to standard method + Self::build_output_stream_with_platform_config( + device, + &config, + sample_format, + data_callback, + error_callback, + timeout, + &platform_config, + ) + } + + // Helper methods for platform-specific stream creation + + fn build_input_stream_with_platform_config( + device: &crate::Device, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + _platform_config: &PlatformStreamConfig, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + // Try ALSA-specific configuration first + if let Some(_alsa_config_wrapper) = &_platform_config.alsa { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))] + { + match device.as_inner() { + crate::platform::DeviceInner::Alsa(alsa_device) => { + return alsa_device + .build_input_stream_raw_with_alsa_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_alsa_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + _ => {} // Not ALSA device, continue to standard method + } + } + } + + // Try JACK-specific configuration + if let Some(_jack_config_wrapper) = &_platform_config.jack { + #[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) + ))] + { + match device.as_inner() { + crate::platform::DeviceInner::Jack(jack_device) => { + return jack_device + .build_input_stream_raw_with_jack_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_jack_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + _ => {} // Not JACK device, continue to standard method + } + } + } + + // Try WASAPI-specific configuration + if let Some(_wasapi_config_wrapper) = &_platform_config.wasapi { + #[cfg(target_os = "windows")] + { + match device.as_inner() { + crate::platform::DeviceInner::Wasapi(wasapi_device) => { + return wasapi_device + .build_input_stream_raw_with_wasapi_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_wasapi_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + #[cfg(any(feature = "asio", feature = "jack"))] + _ => {} // Not WASAPI device, continue to standard method + } + } + } + + // Fall back to standard method + use crate::traits::DeviceTrait; + device.build_input_stream_raw( + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + fn build_output_stream_with_platform_config( + device: &crate::Device, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + _platform_config: &PlatformStreamConfig, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + // Try ALSA-specific configuration first + if let Some(_alsa_config_wrapper) = &_platform_config.alsa { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))] + { + match device.as_inner() { + crate::platform::DeviceInner::Alsa(alsa_device) => { + return alsa_device + .build_output_stream_raw_with_alsa_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_alsa_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + _ => {} // Not ALSA device, continue to standard method + } + } + } + + // Try JACK-specific configuration + if let Some(_jack_config_wrapper) = &_platform_config.jack { + #[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows" + ) + ))] + { + match device.as_inner() { + crate::platform::DeviceInner::Jack(jack_device) => { + return jack_device + .build_output_stream_raw_with_jack_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_jack_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + _ => {} // Not JACK device, continue to standard method + } + } + } + + // Try WASAPI-specific configuration + if let Some(_wasapi_config_wrapper) = &_platform_config.wasapi { + #[cfg(target_os = "windows")] + { + match device.as_inner() { + crate::platform::DeviceInner::Wasapi(wasapi_device) => { + return wasapi_device + .build_output_stream_raw_with_wasapi_config( + config, + sample_format, + data_callback, + error_callback, + timeout, + Some(&_wasapi_config_wrapper.inner), + ) + .map(crate::Stream::from); + } + #[cfg(any(feature = "asio", feature = "jack"))] + _ => {} // Not WASAPI device, continue to standard method + } + } + } + + // Fall back to standard method + use crate::traits::DeviceTrait; + device.build_output_stream_raw( + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + // If a backend does not provide an API for retrieving supported formats, we query it with a bunch // of commonly used rates. This is always the case for wasapi and is sometimes the case for alsa. // @@ -852,6 +2599,7 @@ fn test_stream_instant() { let b = StreamInstant::new(-2, 0); let min = StreamInstant::new(i64::MIN, 0); let max = StreamInstant::new(i64::MAX, 0); + assert_eq!( a.sub(Duration::from_secs(1)), Some(StreamInstant::new(1, 0)) @@ -865,6 +2613,7 @@ fn test_stream_instant() { Some(StreamInstant::new(-1, 0)) ); assert_eq!(min.sub(Duration::from_secs(1)), None); + assert_eq!( b.add(Duration::from_secs(1)), Some(StreamInstant::new(-1, 0)) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fd12eaaac..f571c25fb 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -590,6 +590,7 @@ macro_rules! impl_platform_host { default_host() } } + }; } @@ -621,7 +622,12 @@ mod platform_impl { #[cfg(any(target_os = "macos", target_os = "ios"))] mod platform_impl { pub use crate::host::coreaudio::Host as CoreAudioHost; - impl_platform_host!(CoreAudio => CoreAudioHost); + #[cfg(feature = "jack")] + pub use crate::host::jack::Host as JackHost; + impl_platform_host!( + #[cfg(feature = "jack")] Jack => JackHost, + CoreAudio => CoreAudioHost, + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -661,10 +667,13 @@ mod platform_impl { mod platform_impl { #[cfg(feature = "asio")] pub use crate::host::asio::Host as AsioHost; + #[cfg(feature = "jack")] + pub use crate::host::jack::Host as JackHost; pub use crate::host::wasapi::Host as WasapiHost; impl_platform_host!( #[cfg(feature = "asio")] Asio => AsioHost, + #[cfg(feature = "jack")] Jack => JackHost, Wasapi => WasapiHost, ); diff --git a/src/traits.rs b/src/traits.rs index 2f1bd3469..c22bcc88e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -75,7 +75,7 @@ pub trait HostTrait { /// /// Please note that `Device`s may become invalid if they get disconnected. Therefore, all the /// methods that involve a device return a `Result` allowing the user to handle this case. -pub trait DeviceTrait { +pub trait DeviceTrait: Clone { /// The iterator type yielding supported input stream formats. type SupportedInputConfigs: Iterator; /// The iterator type yielding supported output stream formats.