Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:
env:
# MSRV varies by backend due to platform-specific dependencies
MSRV_AAUDIO: "1.82"
MSRV_ALSA: "1.77"
MSRV_ALSA: "1.82"
MSRV_COREAUDIO: "1.80"
MSRV_JACK: "1.82"
MSRV_WASIP1: "1.78"
Expand Down
29 changes: 25 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `DeviceBusy` error variant for retriable device access errors (EBUSY, EAGAIN).
- **ALSA**: `Debug` implementations for `Host`, `Device`, `Stream`, and internal types.
- **ALSA**: Example demonstrating ALSA error suppression during enumeration.

### Changed

- Overall MSRV increased to 1.78.
- **ALSA**: Update `alsa` dependency from 0.10 to 0.11.
- **ALSA**: MSRV increased from 1.77 to 1.82 (required by alsa-sys 0.4.0).

### Fixed

- **ALSA**: Enumerating input and output devices no longer interferes with each other.
- **ALSA**: Device handles are no longer exclusively held between operations.
- **ALSA**: Valgrind memory leak reports from ALSA global configuration cache.

## [0.17.1] - 2026-01-04

### Added

- **ALSA**: `Default` implementation for `Device` (returns the ALSA "default" device).
- **CI**: Checks default/no-default/all feature sets with platform-dependent MSRV for JACK.

### Changed

- **ALSA**: Devices now report direction from hint metadata and physical hardware probing.

### Fixed

- **ALSA**: Device enumeration now includes both hints and physical cards.
- **JACK**: No longer builds on iOS.
- **WASM**: WasmBindgen no longer crashes (regression from 0.17.0).

### Changed

- **ALSA**: Devices now report direction from hint metadata and physical hardware probing.

## [0.17.0] - 2025-12-20

### Added
Expand Down Expand Up @@ -1034,6 +1054,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial commit.

[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.1...HEAD
[0.17.1]: https://github.com/RustAudio/cpal/compare/v0.17.0...v0.17.1
[0.17.0]: https://github.com/RustAudio/cpal/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/RustAudio/cpal/compare/v0.15.3...v0.16.0
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ documentation = "https://docs.rs/cpal"
license = "Apache-2.0"
keywords = ["audio", "sound"]
edition = "2021"
rust-version = "1.77"
rust-version = "1.78"

[features]
# ASIO backend for Windows
Expand Down Expand Up @@ -85,7 +85,7 @@ num-traits = { version = "0.2", optional = true }
jack = { version = "0.13", optional = true }

[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies]
alsa = "0.10"
alsa = "0.11"
libc = "0.2"
audio_thread_priority = { version = "0.34", optional = true }
jack = { version = "0.13", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Low-level library for audio input and output in pure Rust.
The minimum Rust version required depends on which audio backend and features you're using, as each platform has different dependencies:

- **AAudio (Android):** Rust **1.82** (due to `ndk` crate requirements)
- **ALSA (Linux/BSD):** Rust **1.77** (due to `alsa-sys` crate requirements)
- **ALSA (Linux/BSD):** Rust **1.82** (due to `alsa-sys` crate requirements)
- **CoreAudio (macOS/iOS):** Rust **1.80** (due to `coreaudio-rs` crate requirements)
- **JACK (Linux/BSD/macOS/Windows):** Rust **1.82** (due to `jack` crate requirements)
- **WASAPI/ASIO (Windows):** Rust **1.82** (due to `windows` crate requirements)
Expand Down
4 changes: 4 additions & 0 deletions examples/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ extern crate cpal;
use cpal::traits::{DeviceTrait, HostTrait};

fn main() -> Result<(), anyhow::Error> {
// To print raw ALSA errors to stderr during enumeration, comment out the line below:
#[cfg(target_os = "linux")]
let _silence_alsa_errors = alsa::Output::local_error_handler()?;
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alsa crate is used here but not imported. You need to add extern crate alsa; at the top of the file (after the existing extern crate declarations) for this code to compile.

Copilot uses AI. Check for mistakes.

println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
let available_hosts = cpal::available_hosts();
println!("Available hosts:\n {available_hosts:?}");
Expand Down
16 changes: 16 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ pub enum SupportedStreamConfigsError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// The device is temporarily busy. This can happen when another application or stream
/// is using the device. Retrying after a short delay may succeed.
DeviceBusy,
/// We called something the C-Layer did not understand
InvalidArgument,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
Expand All @@ -133,6 +136,7 @@ impl Display for SupportedStreamConfigsError {
match self {
Self::BackendSpecific { err } => err.fmt(f),
Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."),
Self::DeviceBusy => f.write_str("The requested device is temporarily busy. Another application or stream may be using it."),
Self::InvalidArgument => f.write_str("Invalid argument passed to the backend. For example, this happens when trying to read capture capabilities when the device does not support it.")
}
}
Expand All @@ -152,6 +156,9 @@ pub enum DefaultStreamConfigError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// The device is temporarily busy. This can happen when another application or stream
/// is using the device. Retrying after a short delay may succeed.
DeviceBusy,
/// Returned if e.g. the default input format was requested on an output-only audio device.
StreamTypeNotSupported,
/// See the [`BackendSpecificError`] docs for more information about this error variant.
Expand All @@ -165,6 +172,9 @@ impl Display for DefaultStreamConfigError {
Self::DeviceNotAvailable => f.write_str(
"The requested device is no longer available. For example, it has been unplugged.",
),
Self::DeviceBusy => f.write_str(
"The requested device is temporarily busy. Another application or stream may be using it.",
),
Self::StreamTypeNotSupported => {
f.write_str("The requested stream type is not supported by the device.")
}
Expand All @@ -185,6 +195,9 @@ pub enum BuildStreamError {
/// The device no longer exists. This can happen if the device is disconnected while the
/// program is running.
DeviceNotAvailable,
/// The device is temporarily busy. This can happen when another application or stream
/// is using the device. Retrying after a short delay may succeed.
DeviceBusy,
/// The specified stream configuration is not supported.
StreamConfigNotSupported,
/// We called something the C-Layer did not understand
Expand All @@ -205,6 +218,9 @@ impl Display for BuildStreamError {
Self::DeviceNotAvailable => f.write_str(
"The requested device is no longer available. For example, it has been unplugged.",
),
Self::DeviceBusy => f.write_str(
"The requested device is temporarily busy. Another application or stream may be using it.",
),
Self::StreamConfigNotSupported => {
f.write_str("The requested stream configuration is not supported by the device.")
}
Expand Down
112 changes: 50 additions & 62 deletions src/host/alsa/enumerate.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use std::collections::HashSet;

use super::{alsa, Device};
use super::{alsa, Device, Host};
use crate::{BackendSpecificError, DeviceDirection, DevicesError};

const HW_PREFIX: &str = "hw";
Expand All @@ -21,47 +18,60 @@ struct PhysicalDevice {
/// Iterator over available ALSA PCM devices (physical hardware and virtual/plugin devices).
pub type Devices = std::vec::IntoIter<Device>;

/// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices).
///
/// We enumerate both ALSA hints and physical devices because:
/// - Hints provide virtual devices, user configurations, and card-specific devices with metadata
/// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility
pub fn devices() -> Result<Devices, DevicesError> {
let mut devices = Vec::new();
let mut seen_pcm_ids = HashSet::new();

let physical_devices = physical_devices();

// Add all hint devices, including virtual devices
if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") {
for hint in hints {
if let Ok(device) = Device::try_from(hint) {
seen_pcm_ids.insert(device.pcm_id.clone());
devices.push(device);
impl Host {
/// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices).
///
/// We enumerate both ALSA hints and physical devices because:
/// - Hints provide virtual devices, user configs, and card-specific devices with metadata
/// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility
pub(super) fn enumerate_devices(&self) -> Result<Devices, DevicesError> {
let mut devices = Vec::new();
let mut seen_pcm_ids = HashSet::new();

let physical_devices = physical_devices();

// Add all hint devices, including virtual devices
if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") {
for hint in hints {
if let Some(pcm_id) = hint.name {
// Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html),
// NULL IOID means both Input/Output. Whether a stream can actually open in a
// given direction can only be determined by attempting to open it.
let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);
let device = Device {
pcm_id,
desc: hint.desc,
direction,
_context: self.inner.clone(),
};

seen_pcm_ids.insert(device.pcm_id.clone());
devices.push(device);
}
}
}
}

// Add hw:/plughw: for all physical devices with numeric index (traditional naming)
for phys_dev in physical_devices {
for prefix in [HW_PREFIX, PLUGHW_PREFIX] {
let pcm_id = format!(
"{}:CARD={},DEV={}",
prefix, phys_dev.card_index, phys_dev.device_index
);

if seen_pcm_ids.insert(pcm_id.clone()) {
devices.push(Device {
pcm_id,
desc: Some(format_device_description(&phys_dev, prefix)),
direction: phys_dev.direction,
handles: Arc::new(Mutex::new(Default::default())),
});
// Add hw:/plughw: for all physical devices with numeric index (traditional naming)
for phys_dev in physical_devices {
for prefix in [HW_PREFIX, PLUGHW_PREFIX] {
let pcm_id = format!(
"{}:CARD={},DEV={}",
prefix, phys_dev.card_index, phys_dev.device_index
);

if seen_pcm_ids.insert(pcm_id.clone()) {
devices.push(Device {
pcm_id,
desc: Some(format_device_description(&phys_dev, prefix)),
direction: phys_dev.direction,
_context: self.inner.clone(),
});
}
}
}
}

Ok(devices.into_iter())
Ok(devices.into_iter())
}
}

/// Formats device description in ALSA style: "Card Name, Device Name\nPurpose"
Expand Down Expand Up @@ -144,28 +154,6 @@ impl From<alsa::Error> for DevicesError {
}
}

impl TryFrom<alsa::device_name::Hint> for Device {
type Error = BackendSpecificError;

fn try_from(hint: alsa::device_name::Hint) -> Result<Self, Self::Error> {
let pcm_id = hint.name.ok_or_else(|| Self::Error {
description: "ALSA hint missing PCM ID".to_string(),
})?;

// Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html),
// NULL IOID means both Input/Output. Whether a stream can actually open in a given
// direction can only be determined by attempting to open it.
let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);

Ok(Self {
pcm_id: pcm_id.to_owned(),
desc: hint.desc,
direction,
handles: Arc::new(Mutex::new(Default::default())),
})
}
}

impl From<alsa::Direction> for DeviceDirection {
fn from(direction: alsa::Direction) -> Self {
match direction {
Expand Down
Loading