Skip to content

Commit d2bdd53

Browse files
authored
alsa: update to alsa-rs 0.11 and fix device access issues (#1085)
* chore(alsa): bump alsa-rs to 0.11 * feat(alsa): add Debug derives * feat(alsa): improve error handling * fix(alsa): suppress raw ALSA errors during enumeration on Linux * fix(alsa): remove device handle caching to fix duplex config queries Remove handle caching introduced in #506. The caching held devices open during enumeration, which prevented querying both input and output configs on duplex devices (EBUSY errors) and blocked other applications from accessing the devices. For the rare hardware where rapid open/close is problematic (like some NVIDIA HDMI cards), applications can now implement retry logic using the new DeviceBusy error variant, which separates retriable errors (EBUSY, EAGAIN) from permanent failures (ENOENT, EPERM, etc). * feat(alsa): properly check and free ALSA global config When the last Host is dropped, free the global ALSA config cache via alsa::config::update_free_global. This reduces Valgrind errors. * chore(alsa): raise MSRV to 1.82 * chore: bump overall MSRV to 1.78 and update CI Fixes: - #384 - #615 - #634
1 parent ddad8bc commit d2bdd53

File tree

8 files changed

+181
-155
lines changed

8 files changed

+181
-155
lines changed

.github/workflows/platforms.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ on:
2525
env:
2626
# MSRV varies by backend due to platform-specific dependencies
2727
MSRV_AAUDIO: "1.82"
28-
MSRV_ALSA: "1.77"
28+
MSRV_ALSA: "1.82"
2929
MSRV_COREAUDIO: "1.80"
3030
MSRV_JACK: "1.82"
3131
MSRV_WASIP1: "1.78"

CHANGELOG.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,43 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `DeviceBusy` error variant for retriable device access errors (EBUSY, EAGAIN).
13+
- **ALSA**: `Debug` implementations for `Host`, `Device`, `Stream`, and internal types.
14+
- **ALSA**: Example demonstrating ALSA error suppression during enumeration.
15+
16+
### Changed
17+
18+
- Overall MSRV increased to 1.78.
19+
- **ALSA**: Update `alsa` dependency from 0.10 to 0.11.
20+
- **ALSA**: MSRV increased from 1.77 to 1.82 (required by alsa-sys 0.4.0).
21+
22+
### Fixed
23+
24+
- **ALSA**: Enumerating input and output devices no longer interferes with each other.
25+
- **ALSA**: Device handles are no longer exclusively held between operations.
26+
- **ALSA**: Valgrind memory leak reports from ALSA global configuration cache.
27+
828
## [0.17.1] - 2026-01-04
929

1030
### Added
1131

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

35+
### Changed
36+
37+
- **ALSA**: Devices now report direction from hint metadata and physical hardware probing.
38+
1539
### Fixed
1640

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

21-
### Changed
22-
23-
- **ALSA**: Devices now report direction from hint metadata and physical hardware probing.
24-
2545
## [0.17.0] - 2025-12-20
2646

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

10351055
- Initial commit.
10361056

1057+
[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.1...HEAD
10371058
[0.17.1]: https://github.com/RustAudio/cpal/compare/v0.17.0...v0.17.1
10381059
[0.17.0]: https://github.com/RustAudio/cpal/compare/v0.16.0...v0.17.0
10391060
[0.16.0]: https://github.com/RustAudio/cpal/compare/v0.15.3...v0.16.0

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ documentation = "https://docs.rs/cpal"
77
license = "Apache-2.0"
88
keywords = ["audio", "sound"]
99
edition = "2021"
10-
rust-version = "1.77"
10+
rust-version = "1.78"
1111

1212
[features]
1313
# ASIO backend for Windows
@@ -85,7 +85,7 @@ num-traits = { version = "0.2", optional = true }
8585
jack = { version = "0.13", optional = true }
8686

8787
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies]
88-
alsa = "0.10"
88+
alsa = "0.11"
8989
libc = "0.2"
9090
audio_thread_priority = { version = "0.34", optional = true }
9191
jack = { version = "0.13", optional = true }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Low-level library for audio input and output in pure Rust.
1010
The minimum Rust version required depends on which audio backend and features you're using, as each platform has different dependencies:
1111

1212
- **AAudio (Android):** Rust **1.82** (due to `ndk` crate requirements)
13-
- **ALSA (Linux/BSD):** Rust **1.77** (due to `alsa-sys` crate requirements)
13+
- **ALSA (Linux/BSD):** Rust **1.82** (due to `alsa-sys` crate requirements)
1414
- **CoreAudio (macOS/iOS):** Rust **1.80** (due to `coreaudio-rs` crate requirements)
1515
- **JACK (Linux/BSD/macOS/Windows):** Rust **1.82** (due to `jack` crate requirements)
1616
- **WASAPI/ASIO (Windows):** Rust **1.82** (due to `windows` crate requirements)

examples/enumerate.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ extern crate cpal;
1515
use cpal::traits::{DeviceTrait, HostTrait};
1616

1717
fn main() -> Result<(), anyhow::Error> {
18+
// To print raw ALSA errors to stderr during enumeration, comment out the line below:
19+
#[cfg(target_os = "linux")]
20+
let _silence_alsa_errors = alsa::Output::local_error_handler()?;
21+
1822
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
1923
let available_hosts = cpal::available_hosts();
2024
println!("Available hosts:\n {available_hosts:?}");

src/error.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ pub enum SupportedStreamConfigsError {
122122
/// The device no longer exists. This can happen if the device is disconnected while the
123123
/// program is running.
124124
DeviceNotAvailable,
125+
/// The device is temporarily busy. This can happen when another application or stream
126+
/// is using the device. Retrying after a short delay may succeed.
127+
DeviceBusy,
125128
/// We called something the C-Layer did not understand
126129
InvalidArgument,
127130
/// See the [`BackendSpecificError`] docs for more information about this error variant.
@@ -133,6 +136,7 @@ impl Display for SupportedStreamConfigsError {
133136
match self {
134137
Self::BackendSpecific { err } => err.fmt(f),
135138
Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."),
139+
Self::DeviceBusy => f.write_str("The requested device is temporarily busy. Another application or stream may be using it."),
136140
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.")
137141
}
138142
}
@@ -152,6 +156,9 @@ pub enum DefaultStreamConfigError {
152156
/// The device no longer exists. This can happen if the device is disconnected while the
153157
/// program is running.
154158
DeviceNotAvailable,
159+
/// The device is temporarily busy. This can happen when another application or stream
160+
/// is using the device. Retrying after a short delay may succeed.
161+
DeviceBusy,
155162
/// Returned if e.g. the default input format was requested on an output-only audio device.
156163
StreamTypeNotSupported,
157164
/// See the [`BackendSpecificError`] docs for more information about this error variant.
@@ -165,6 +172,9 @@ impl Display for DefaultStreamConfigError {
165172
Self::DeviceNotAvailable => f.write_str(
166173
"The requested device is no longer available. For example, it has been unplugged.",
167174
),
175+
Self::DeviceBusy => f.write_str(
176+
"The requested device is temporarily busy. Another application or stream may be using it.",
177+
),
168178
Self::StreamTypeNotSupported => {
169179
f.write_str("The requested stream type is not supported by the device.")
170180
}
@@ -185,6 +195,9 @@ pub enum BuildStreamError {
185195
/// The device no longer exists. This can happen if the device is disconnected while the
186196
/// program is running.
187197
DeviceNotAvailable,
198+
/// The device is temporarily busy. This can happen when another application or stream
199+
/// is using the device. Retrying after a short delay may succeed.
200+
DeviceBusy,
188201
/// The specified stream configuration is not supported.
189202
StreamConfigNotSupported,
190203
/// We called something the C-Layer did not understand
@@ -205,6 +218,9 @@ impl Display for BuildStreamError {
205218
Self::DeviceNotAvailable => f.write_str(
206219
"The requested device is no longer available. For example, it has been unplugged.",
207220
),
221+
Self::DeviceBusy => f.write_str(
222+
"The requested device is temporarily busy. Another application or stream may be using it.",
223+
),
208224
Self::StreamConfigNotSupported => {
209225
f.write_str("The requested stream configuration is not supported by the device.")
210226
}

src/host/alsa/enumerate.rs

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use std::{
2-
collections::HashSet,
3-
sync::{Arc, Mutex},
4-
};
1+
use std::collections::HashSet;
52

6-
use super::{alsa, Device};
3+
use super::{alsa, Device, Host};
74
use crate::{BackendSpecificError, DeviceDirection, DevicesError};
85

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

24-
/// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices).
25-
///
26-
/// We enumerate both ALSA hints and physical devices because:
27-
/// - Hints provide virtual devices, user configurations, and card-specific devices with metadata
28-
/// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility
29-
pub fn devices() -> Result<Devices, DevicesError> {
30-
let mut devices = Vec::new();
31-
let mut seen_pcm_ids = HashSet::new();
32-
33-
let physical_devices = physical_devices();
34-
35-
// Add all hint devices, including virtual devices
36-
if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") {
37-
for hint in hints {
38-
if let Ok(device) = Device::try_from(hint) {
39-
seen_pcm_ids.insert(device.pcm_id.clone());
40-
devices.push(device);
21+
impl Host {
22+
/// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices).
23+
///
24+
/// We enumerate both ALSA hints and physical devices because:
25+
/// - Hints provide virtual devices, user configs, and card-specific devices with metadata
26+
/// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility
27+
pub(super) fn enumerate_devices(&self) -> Result<Devices, DevicesError> {
28+
let mut devices = Vec::new();
29+
let mut seen_pcm_ids = HashSet::new();
30+
31+
let physical_devices = physical_devices();
32+
33+
// Add all hint devices, including virtual devices
34+
if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") {
35+
for hint in hints {
36+
if let Some(pcm_id) = hint.name {
37+
// Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html),
38+
// NULL IOID means both Input/Output. Whether a stream can actually open in a
39+
// given direction can only be determined by attempting to open it.
40+
let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);
41+
let device = Device {
42+
pcm_id,
43+
desc: hint.desc,
44+
direction,
45+
_context: self.inner.clone(),
46+
};
47+
48+
seen_pcm_ids.insert(device.pcm_id.clone());
49+
devices.push(device);
50+
}
4151
}
4252
}
43-
}
4453

45-
// Add hw:/plughw: for all physical devices with numeric index (traditional naming)
46-
for phys_dev in physical_devices {
47-
for prefix in [HW_PREFIX, PLUGHW_PREFIX] {
48-
let pcm_id = format!(
49-
"{}:CARD={},DEV={}",
50-
prefix, phys_dev.card_index, phys_dev.device_index
51-
);
52-
53-
if seen_pcm_ids.insert(pcm_id.clone()) {
54-
devices.push(Device {
55-
pcm_id,
56-
desc: Some(format_device_description(&phys_dev, prefix)),
57-
direction: phys_dev.direction,
58-
handles: Arc::new(Mutex::new(Default::default())),
59-
});
54+
// Add hw:/plughw: for all physical devices with numeric index (traditional naming)
55+
for phys_dev in physical_devices {
56+
for prefix in [HW_PREFIX, PLUGHW_PREFIX] {
57+
let pcm_id = format!(
58+
"{}:CARD={},DEV={}",
59+
prefix, phys_dev.card_index, phys_dev.device_index
60+
);
61+
62+
if seen_pcm_ids.insert(pcm_id.clone()) {
63+
devices.push(Device {
64+
pcm_id,
65+
desc: Some(format_device_description(&phys_dev, prefix)),
66+
direction: phys_dev.direction,
67+
_context: self.inner.clone(),
68+
});
69+
}
6070
}
6171
}
62-
}
6372

64-
Ok(devices.into_iter())
73+
Ok(devices.into_iter())
74+
}
6575
}
6676

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

147-
impl TryFrom<alsa::device_name::Hint> for Device {
148-
type Error = BackendSpecificError;
149-
150-
fn try_from(hint: alsa::device_name::Hint) -> Result<Self, Self::Error> {
151-
let pcm_id = hint.name.ok_or_else(|| Self::Error {
152-
description: "ALSA hint missing PCM ID".to_string(),
153-
})?;
154-
155-
// Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html),
156-
// NULL IOID means both Input/Output. Whether a stream can actually open in a given
157-
// direction can only be determined by attempting to open it.
158-
let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);
159-
160-
Ok(Self {
161-
pcm_id: pcm_id.to_owned(),
162-
desc: hint.desc,
163-
direction,
164-
handles: Arc::new(Mutex::new(Default::default())),
165-
})
166-
}
167-
}
168-
169157
impl From<alsa::Direction> for DeviceDirection {
170158
fn from(direction: alsa::Direction) -> Self {
171159
match direction {

0 commit comments

Comments
 (0)