Skip to content

Commit f9cd814

Browse files
committed
fix(alsa): enumeration and validation of buffers and sample rates
1 parent 6bba9ec commit f9cd814

3 files changed

Lines changed: 130 additions & 102 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
- `BufferSize` now implements `Default` (returns `BufferSize::Default`).
3434
- `SupportedBufferSize` now implements `Default` (returns `SupportedBufferSize::Unknown`).
3535
- `SupportedStreamConfig` now implements `Copy`.
36+
- DSD512 sample rates added to the common rate probe list.
3637
- **AAudio**: Streams now request `PERFORMANCE_MODE_LOW_LATENCY` when the `realtime` feature is
3738
enabled; stream error callback receives `ErrorKind::RealtimeDenied` if not granted.
3839
- **AAudio**: `Device` now implements `PartialEq`, `Eq`, `Hash`, and `Debug`.
@@ -169,6 +170,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
169170
- **ALSA**: Fix silence template not being applied for DSD.
170171
- **ALSA**: Fix stream corruption on certain drivers with spurious wakeups.
171172
- **ALSA**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
173+
- **ALSA**: Fix `BufferSize::Fixed` validation opening the PCM device a second time.
174+
- **ALSA**: Fix hang when device raced to an error state without delivering POLLERR.
175+
- **ALSA**: Fix `supported_configs()` reporting buffer size instead of period size.
176+
- **ALSA**: Fix `supported_configs()` using the same buffer range for all formats and channels.
177+
- **ALSA**: Fix `supported_configs()` dropping sample rates outside of `COMMON_SAMPLE_RATES`.
178+
- **ALSA**: Fix `BufferSize::Fixed(0)` being silently accepted.
172179
- **ASIO**: Fix enumeration returning only the first device when using `collect()`.
173180
- **ASIO**: Fix device enumeration and stream creation failing when called from spawned threads.
174181
- **ASIO**: Fix buffer size not resizing when the driver reports `kAsioBufferSizeChange`.

src/host/alsa/mod.rs

Lines changed: 120 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ extern crate alsa_sys;
88
extern crate libc;
99

1010
use std::{
11-
cmp, fmt,
11+
fmt,
1212
sync::{
1313
atomic::{AtomicBool, Ordering},
1414
Arc, Mutex,
@@ -368,7 +368,7 @@ impl Device {
368368

369369
let hw_params = set_hw_params_from_format(&handle, conf, sample_format)?;
370370
let (buffer_size, period_size) = set_sw_params_from_format(&handle, stream_type)?;
371-
if buffer_size == 0 {
371+
if buffer_size == 0 || period_size == 0 {
372372
return Err(ErrorKind::DeviceNotAvailable.into());
373373
}
374374

@@ -494,68 +494,52 @@ impl Device {
494494
//SND_PCM_FORMAT_U18_3BE,
495495
];
496496

497-
// Collect supported formats, deduplicating since we test both LE and BE variants.
498-
// If hardware supports both endiannesses (rare), we only report the format once.
499-
let mut supported_formats = Vec::new();
500-
for &(sample_format, alsa_format) in FORMATS.iter() {
501-
if hw_params.test_format(alsa_format).is_ok()
502-
&& !supported_formats.contains(&sample_format)
503-
{
504-
supported_formats.push(sample_format);
505-
}
506-
}
507-
508497
let min_rate = hw_params.get_rate_min()?;
509498
let max_rate = hw_params.get_rate_max()?;
510499

511500
let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() {
501+
// Fixed rate or continuous range.
512502
vec![(min_rate, max_rate)]
513503
} else {
514-
let mut rates = Vec::new();
515-
for &sample_rate in COMMON_SAMPLE_RATES.iter() {
516-
if hw_params.test_rate(sample_rate).is_ok() {
517-
rates.push((sample_rate, sample_rate));
518-
}
519-
}
520-
521-
if rates.is_empty() {
522-
vec![(min_rate, max_rate)]
523-
} else {
524-
rates
525-
}
504+
// Discrete rates: probe the standard list plus the hardware's own min and max so
505+
// that rates outside `COMMON_SAMPLE_RATES` are not missed.
506+
let mut probe: Vec<SampleRate> = COMMON_SAMPLE_RATES.to_vec();
507+
probe.push(min_rate);
508+
probe.push(max_rate);
509+
probe.sort_unstable();
510+
probe.dedup();
511+
probe
512+
.into_iter()
513+
.filter(|&r| (min_rate..=max_rate).contains(&r) && hw_params.test_rate(r).is_ok())
514+
.map(|r| (r, r))
515+
.collect()
526516
};
527517

528518
let min_channels = hw_params.get_channels_min()?;
529-
let max_channels = hw_params.get_channels_max()?;
519+
let max_channels = hw_params.get_channels_max()?.min(32); // TODO: cap at 32 or too many configs
530520

531-
let max_channels = cmp::min(max_channels, 32); // TODO: limiting to 32 channels or too much stuff is returned
532-
let supported_channels = (min_channels..=max_channels)
533-
.filter_map(|num| {
534-
if hw_params.test_channels(num).is_ok() {
535-
Some(num as ChannelCount)
536-
} else {
537-
None
538-
}
539-
})
540-
.collect::<Vec<_>>();
521+
let mut output = Vec::new();
522+
let mut seen_formats: Vec<SampleFormat> = Vec::new();
523+
for &(sample_format, alsa_format) in FORMATS.iter() {
524+
if seen_formats.contains(&sample_format) || hw_params.test_format(alsa_format).is_err()
525+
{
526+
continue;
527+
}
528+
seen_formats.push(sample_format);
541529

542-
let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(&hw_params);
543-
let buffer_size_range = SupportedBufferSize::Range {
544-
min: min_buffer_size,
545-
max: max_buffer_size,
546-
};
530+
for channels in min_channels..=max_channels {
531+
if hw_params.test_channels(channels).is_err() {
532+
continue;
533+
}
534+
let channels = channels as ChannelCount;
535+
let buffer_size = supported_period_size_range(&pcm, alsa_format, channels);
547536

548-
let mut output = Vec::with_capacity(
549-
supported_formats.len() * supported_channels.len() * sample_rates.len(),
550-
);
551-
for &sample_format in supported_formats.iter() {
552-
for &channels in supported_channels.iter() {
553537
for &(min_rate, max_rate) in sample_rates.iter() {
554538
output.push(SupportedStreamConfigRange {
555539
channels,
556540
min_sample_rate: min_rate,
557541
max_sample_rate: max_rate,
558-
buffer_size: buffer_size_range,
542+
buffer_size,
559543
sample_format,
560544
});
561545
}
@@ -579,7 +563,7 @@ impl Device {
579563
let mut formats: Vec<_> = {
580564
match self.supported_configs(stream_t) {
581565
// EINVAL when querying direction the device does not support (input-only or output-only)
582-
Err(err) if err.kind() == ErrorKind::InvalidInput => {
566+
Err(err) if err.kind() == ErrorKind::UnsupportedConfig => {
583567
let dir = match stream_t {
584568
alsa::Direction::Capture => "input",
585569
alsa::Direction::Playback => "output",
@@ -1036,9 +1020,7 @@ fn try_resume(handle: &alsa::PCM) -> Result<Poll, Error> {
10361020
// device is still resuming; poll again until it is ready.
10371021
Err(e) if e.errno() == libc::EAGAIN => Ok(Poll::Pending),
10381022
// hardware does not support soft resume; treat as xrun so the worker calls prepare()
1039-
Err(e) if e.errno() == libc::ENOSYS => {
1040-
Err(Error::with_message(ErrorKind::Xrun, e.to_string()))
1041-
}
1023+
Err(e) if e.errno() == libc::ENOSYS => Err(ErrorKind::Xrun.into()),
10421024
Err(e) => Err(e.into()),
10431025
}
10441026
}
@@ -1110,9 +1092,7 @@ fn poll_for_period(
11101092
// POLLIN/POLLOUT: data is ready, fall through to process it.
11111093
let (avail_frames, delay_frames) = match stream.handle.avail_delay() {
11121094
// Xrun: recover via prepare() (+ start() for capture, handled by the worker).
1113-
Err(err) if err.errno() == libc::EPIPE => {
1114-
return Err(Error::with_message(ErrorKind::Xrun, err.to_string()))
1115-
}
1095+
Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()),
11161096
// Suspend: try hardware resume first; fall back to prepare() if unsupported.
11171097
Err(err) if err.errno() == libc::ESTRPIPE => return try_resume(&stream.handle),
11181098
res => res,
@@ -1168,13 +1148,11 @@ fn process_input(
11681148
if frames_read == 0 {
11691149
return Ok(());
11701150
} else {
1171-
return Err(Error::with_message(ErrorKind::Xrun, err.to_string()));
1151+
return Err(ErrorKind::Xrun.into());
11721152
}
11731153
}
11741154
// EPIPE = xrun: full underrun recovery (prepare + start) required.
1175-
Err(err) if err.errno() == libc::EPIPE => {
1176-
return Err(Error::with_message(ErrorKind::Xrun, err.to_string()))
1177-
}
1155+
Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()),
11781156
// ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun
11791157
// recovery if the hardware doesn't support it.
11801158
Err(err) if err.errno() == libc::ESTRPIPE => {
@@ -1238,13 +1216,11 @@ fn process_output(
12381216
if frames_written == 0 {
12391217
return Ok(());
12401218
} else {
1241-
return Err(Error::with_message(ErrorKind::Xrun, err.to_string()));
1219+
return Err(ErrorKind::Xrun.into());
12421220
}
12431221
}
12441222
// EPIPE = xrun: full underrun recovery (prepare) required.
1245-
Err(err) if err.errno() == libc::EPIPE => {
1246-
return Err(Error::with_message(ErrorKind::Xrun, err.to_string()))
1247-
}
1223+
Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()),
12481224
// ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun
12491225
// recovery if the hardware doesn't support it.
12501226
Err(err) if err.errno() == libc::ESTRPIPE => {
@@ -1439,22 +1415,52 @@ impl StreamTrait for Stream {
14391415
}
14401416
}
14411417

1442-
// Convert ALSA frames to FrameCount, clamping to valid range.
1443-
// ALSA Frames are i64 (64-bit) or i32 (32-bit).
1444-
fn clamp_frame_count(buffer_size: alsa::pcm::Frames) -> FrameCount {
1445-
buffer_size.max(1).try_into().unwrap_or(FrameCount::MAX)
1418+
fn supported_period_size_range(
1419+
pcm: &alsa::pcm::PCM,
1420+
alsa_format: alsa::pcm::Format,
1421+
channels: ChannelCount,
1422+
) -> SupportedBufferSize {
1423+
let Ok(p) = alsa::pcm::HwParams::any(pcm) else {
1424+
return SupportedBufferSize::Unknown;
1425+
};
1426+
if p.set_access(alsa::pcm::Access::RWInterleaved).is_err()
1427+
|| p.set_channels(channels as u32).is_err()
1428+
|| p.set_format(alsa_format).is_err()
1429+
{
1430+
return SupportedBufferSize::Unknown;
1431+
}
1432+
let Some((min, max)) = hw_params_period_size_min_max(&p) else {
1433+
return SupportedBufferSize::Unknown;
1434+
};
1435+
let min_frames = min.max(1);
1436+
// cpal double-buffers (ring = DEFAULT_PERIODS × period), so the achievable
1437+
// period maximum is also bounded by max_buffer / DEFAULT_PERIODS.
1438+
let effective_max = match p.get_buffer_size_max() {
1439+
Ok(max_buf) if max_buf > 0 => max.min(max_buf / DEFAULT_PERIODS),
1440+
_ => max,
1441+
};
1442+
if effective_max >= min_frames {
1443+
let Ok(min) = min_frames.try_into() else {
1444+
return SupportedBufferSize::Unknown;
1445+
};
1446+
SupportedBufferSize::Range {
1447+
min,
1448+
max: effective_max.try_into().unwrap_or(FrameCount::MAX),
1449+
}
1450+
} else {
1451+
SupportedBufferSize::Unknown
1452+
}
14461453
}
14471454

1448-
fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) {
1449-
let min_buf = hw_params
1450-
.get_buffer_size_min()
1451-
.map(clamp_frame_count)
1452-
.unwrap_or(1);
1453-
let max_buf = hw_params
1454-
.get_buffer_size_max()
1455-
.map(clamp_frame_count)
1456-
.unwrap_or(FrameCount::MAX);
1457-
(min_buf, max_buf)
1455+
fn hw_params_period_size_min_max(
1456+
hw_params: &alsa::pcm::HwParams,
1457+
) -> Option<(alsa::pcm::Frames, alsa::pcm::Frames)> {
1458+
let min = hw_params.get_period_size_min().ok()?;
1459+
let max = hw_params.get_period_size_max().ok()?;
1460+
// min=0 means no hardware lower bound (PipeWire reports this on unconstrained params);
1461+
// it is handled in the caller by clamping to 1. max <= 0 is degenerate (or ULONG_MAX
1462+
// wrapping negative), so we return None in that case rather than a misleading range.
1463+
(max > 0 && max >= min).then_some((min, max))
14581464
}
14591465

14601466
fn init_hw_params<'a>(
@@ -1563,21 +1569,42 @@ fn set_hw_params_from_format(
15631569
// When BufferSize::Fixed(x) is specified, we configure double-buffering with
15641570
// buffer_size = 2x and period_size = x. This provides consistent low-latency
15651571
// behavior across different ALSA implementations and hardware.
1566-
if let BufferSize::Fixed(buffer_frames) = config.buffer_size {
1567-
// Validate the requested size against the device's supported range using the same PCM
1572+
if let BufferSize::Fixed(period_size) = config.buffer_size {
1573+
if period_size == 0 {
1574+
return Err(Error::with_message(
1575+
ErrorKind::InvalidInput,
1576+
"Buffer size must be greater than 0",
1577+
));
1578+
}
1579+
1580+
let period_size = period_size as alsa::pcm::Frames;
1581+
1582+
// Validate the requested size against the device's supported ranges using the same PCM
15681583
// handle we'll use for streaming. This avoids a second PCM open (which can disturb
15691584
// hardware clock state on some drivers) while still catching wildly out-of-range
15701585
// requests before set_period_size_near silently rounds them.
1571-
let (min_buffer, max_buffer) = hw_params_buffer_size_min_max(&hw_params);
1572-
if !(min_buffer..=max_buffer).contains(&buffer_frames) {
1573-
return Err(Error::with_message(
1574-
ErrorKind::UnsupportedConfig,
1575-
format!("Buffer size {buffer_frames} is not in the supported range {min_buffer}..={max_buffer}"),
1576-
));
1586+
if let Some((min_period, max_period)) = hw_params_period_size_min_max(&hw_params) {
1587+
if !(min_period..=max_period).contains(&period_size) {
1588+
return Err(Error::with_message(
1589+
ErrorKind::UnsupportedConfig,
1590+
format!("Buffer size {period_size} is not in the supported range {min_period}..={max_period}"),
1591+
));
1592+
}
1593+
}
1594+
1595+
let buffer_size = DEFAULT_PERIODS * period_size;
1596+
if let Ok(max_buffer) = hw_params.get_buffer_size_max() {
1597+
if max_buffer > 0 && buffer_size > max_buffer {
1598+
let effective_max = max_buffer / DEFAULT_PERIODS;
1599+
return Err(Error::with_message(
1600+
ErrorKind::UnsupportedConfig,
1601+
format!("Buffer size {period_size} exceeds the maximum supported value of {effective_max}"),
1602+
));
1603+
}
15771604
}
1578-
hw_params.set_buffer_size_near(DEFAULT_PERIODS * buffer_frames as alsa::pcm::Frames)?;
1579-
hw_params
1580-
.set_period_size_near(buffer_frames as alsa::pcm::Frames, alsa::ValueOr::Nearest)?;
1605+
1606+
hw_params.set_buffer_size_near(buffer_size)?;
1607+
hw_params.set_period_size_near(period_size, alsa::ValueOr::Nearest)?;
15811608
}
15821609

15831610
// Apply hardware parameters
@@ -1587,7 +1614,7 @@ fn set_hw_params_from_format(
15871614
// PipeWire-ALSA picks a good period size but pairs it with many periods (huge buffer).
15881615
// We need to re-initialize hw_params and set BOTH period and buffer to constrain properly.
15891616
if config.buffer_size == BufferSize::Default {
1590-
if let Ok(period_size) = hw_params.get_period_size().map(|s| s as alsa::pcm::Frames) {
1617+
if let Ok(period_size) = hw_params.get_period_size() {
15911618
// Re-initialize hw_params to clear previous constraints
15921619
let hw_params = init_hw_params(pcm_handle, config, sample_format)?;
15931620

@@ -1655,18 +1682,11 @@ fn canonical_pcm_id(pcm_id: &str) -> String {
16551682
impl From<alsa::Error> for Error {
16561683
fn from(err: alsa::Error) -> Self {
16571684
match err.errno() {
1658-
libc::ENODEV | libc::ENOENT | LIBC_ENOTSUPP => {
1659-
Error::with_message(ErrorKind::DeviceNotAvailable, err.to_string())
1660-
}
1661-
libc::EPERM | libc::EACCES => {
1662-
Error::with_message(ErrorKind::PermissionDenied, err.to_string())
1663-
}
1664-
libc::EBUSY | libc::EAGAIN => {
1665-
Error::with_message(ErrorKind::DeviceBusy, err.to_string())
1666-
}
1667-
libc::EINVAL => Error::with_message(ErrorKind::InvalidInput, err.to_string()),
1668-
libc::EPIPE => Error::with_message(ErrorKind::Xrun, err.to_string()),
1669-
libc::ENOSYS => Error::with_message(ErrorKind::UnsupportedOperation, err.to_string()),
1685+
libc::ENODEV | libc::ENOENT | LIBC_ENOTSUPP => ErrorKind::DeviceNotAvailable.into(),
1686+
libc::EPERM | libc::EACCES => ErrorKind::PermissionDenied.into(),
1687+
libc::EBUSY | libc::EAGAIN => ErrorKind::DeviceBusy.into(),
1688+
libc::EINVAL | libc::ENOSYS => ErrorKind::UnsupportedConfig.into(),
1689+
libc::EPIPE => ErrorKind::Xrun.into(),
16701690
_ => Error::with_message(ErrorKind::BackendError, err.to_string()),
16711691
}
16721692
}

src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ impl From<SupportedStreamConfig> for StreamConfig {
975975
// of commonly used rates. This is always the case for WASAPI and is sometimes the case for ALSA.
976976
#[allow(dead_code)]
977977
pub(crate) const COMMON_SAMPLE_RATES: &[SampleRate] = &[
978-
5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000,
979-
176400, 192000, 352800, 384000, 705600, 768000, 1411200, 1536000,
978+
// Standard PCM rates
979+
5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, 176400,
980+
192000, 352800, 384000, 705600, 768000, 1411200, 1536000, 2822400, 3072000,
980981
];

0 commit comments

Comments
 (0)