feat!: add StreamConfigBuilder with cross-platform configuration support
This PR introduces an StreamConfigBuilder with support for platform-specific audio configuration options. This addresses recurring requests for fine-grained control over backend-specific settings while maintaining cross-platform compatibility. At the same time, it improves ergonomics of stream creation.
Motivation
There is a recurring interest in exposing platform-specific audio configuration options:
- #990 - ALSA buffer size configuration issues requiring
set_buffer_size_near - #995 - Android
input_presetconfiguration for AEC support - #987 - WASAPI raw audio stream configuration needs
- #917 - ALSA period/buffer size configuration problems
Users have consistently needed platform-specific control over:
- ALSA: Period counts, access types (RW vs MMap; interleaved vs. non-interleaved)
- JACK: Client names, automatic port connections, server startup
- WASAPI: Exclusive vs shared mode
Not yet added:
- Android: Input presets for AEC and noise cancellation
Currently, these options are either impossible to configure or would require hard-coded defaults or introduction of environment variables.
Solution
All existing APIs remain unchanged. The new builder and platform configuration are purely additive:
use cpal::{StreamConfigBuilder, SampleRate, SampleFormat, BufferSize};
// - builds on all platforms
// - offers more ergonomic stream creation
// - allows platform-specific options
let stream = device.default_output_config()?
.with_buffer_size(BufferSize::Fixed(256))
.on_alsa(|alsa| alsa.periods(2)) // No-op on non-Linux
.on_wasapi(|wasapi| wasapi.exclusive_mode(true)) // No-op on non-Windows
.on_jack(|jack| jack.client_name("app")) // No-op without JACK
.build_output_stream(/* callbacks */)?;
As opportunistic refactoring, the following feature flags were re-added:
jack- Enable JACK audio backend supportaudio_thread_priority- Enable real-time thread priority for ALSA/WASAPI
And documentation was updated for readability and correctness.
Note to self: consider making configurable whether CoreAudio follows default audio device changes or not (#1012).
Instead of this we could use extension traits. These would add backend-specific methods without polluting the core trait:
// Core trait - stays platform-agnostic
pub trait DeviceTrait {
fn name(&self) -> Result<String, DeviceNameError>;
// ... other core methods
}
// Backend-specific extension trait
#[cfg(target_os = "windows")]
pub trait WasapiDeviceExt {
/// Access the underlying Windows IMMDevice COM interface.
fn immdevice(&self) -> &IMMDevice;
}
// Implement the extension only for the WASAPI Device
#[cfg(target_os = "windows")]
impl WasapiDeviceExt for crate::platform::wasapi::Device {
fn immdevice(&self) -> &IMMDevice {
&self.device
}
}
Then how to use:
use cpal::{Device, DeviceTrait}; // Core API
// Works on all platforms
fn portable_code(device: &Device) {
let name = device.name().unwrap();
println!("Device: {}", name);
}
// Platform-specific code
#[cfg(target_os = "windows")]
fn windows_specific(device: &Device) {
use cpal::platform::WasapiDeviceExt;
let imm_device = device.immdevice();
// ... do Windows-specific stuff
}
#[cfg(not(target_os = "windows"))]
fn windows_specific(_device: &Device) {
// ... no-op
}
Or similarly one could create fn platform_specific_setup() for various targets.