Skip to content

Commit 45000c1

Browse files
authored
fix(alsa): reentrancy and partial IO handling (#1130)
1 parent 2622b29 commit 45000c1

2 files changed

Lines changed: 67 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable` instead of looping.
3131
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
3232
- **ALSA**: Try to resume from hardware after a system suspend.
33+
- **ALSA**: Loop partial reads and writes to completion.
34+
- **ALSA**: Prevent reentrancy issues with non-reentrant plugins and devices.
3335
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
3436
- **ASIO**: Timestamps now include driver-reported hardware latency.
3537
- **CoreAudio**: Timestamps now include device latency and safety offset.

src/host/alsa/mod.rs

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ mod enumerate;
8686

8787
const DEFAULT_DEVICE: &str = "default";
8888

89+
// Some ALSA plugins (e.g. alsaequal, certain USB drivers) are not reentrant.
90+
static ALSA_OPEN_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
91+
8992
// TODO: Not yet defined in rust-lang/libc crate
9093
const LIBC_ENOTSUPP: libc::c_int = 524;
9194

@@ -371,9 +374,11 @@ impl Device {
371374
}
372375
}
373376

374-
let handle = match alsa::pcm::PCM::new(&self.pcm_id, stream_type, true)
375-
.map_err(|e| (e, e.errno()))
376-
{
377+
let open_result = {
378+
let _guard = ALSA_OPEN_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
379+
alsa::pcm::PCM::new(&self.pcm_id, stream_type, true).map_err(|e| (e, e.errno()))
380+
};
381+
let handle = match open_result {
377382
Err((_, libc::ENOENT))
378383
| Err((_, libc::EPERM))
379384
| Err((_, libc::ENODEV))
@@ -410,7 +415,8 @@ impl Device {
410415

411416
// Pre-compute a period-sized buffer filled with silence values.
412417
let period_frames = period_samples / conf.channels as usize;
413-
let period_bytes = period_samples * sample_format.sample_size();
418+
let frame_size = sample_format.sample_size() * conf.channels as usize;
419+
let period_bytes = period_frames * frame_size;
414420
let mut silence_template = vec![0u8; period_bytes].into_boxed_slice();
415421

416422
// Only fill buffer for unsigned formats that don't have a zero value for silence.
@@ -426,6 +432,7 @@ impl Device {
426432
conf,
427433
period_samples,
428434
period_frames,
435+
frame_size,
429436
silence_template,
430437
can_pause,
431438
creation_instant,
@@ -472,21 +479,24 @@ impl Device {
472479
&self,
473480
stream_t: alsa::Direction,
474481
) -> Result<VecIntoIter<SupportedStreamConfigRange>, SupportedStreamConfigsError> {
475-
let pcm =
476-
match alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno())) {
477-
Err((_, libc::ENOENT))
478-
| Err((_, libc::EPERM))
479-
| Err((_, libc::ENODEV))
480-
| Err((_, LIBC_ENOTSUPP)) => {
481-
return Err(SupportedStreamConfigsError::DeviceNotAvailable)
482-
}
483-
Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => {
484-
return Err(SupportedStreamConfigsError::DeviceBusy)
485-
}
486-
Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument),
487-
Err((e, _)) => return Err(e.into()),
488-
Ok(pcm) => pcm,
489-
};
482+
let open_result = {
483+
let _guard = ALSA_OPEN_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
484+
alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno()))
485+
};
486+
let pcm = match open_result {
487+
Err((_, libc::ENOENT))
488+
| Err((_, libc::EPERM))
489+
| Err((_, libc::ENODEV))
490+
| Err((_, LIBC_ENOTSUPP)) => {
491+
return Err(SupportedStreamConfigsError::DeviceNotAvailable)
492+
}
493+
Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => {
494+
return Err(SupportedStreamConfigsError::DeviceBusy)
495+
}
496+
Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument),
497+
Err((e, _)) => return Err(e.into()),
498+
Ok(pcm) => pcm,
499+
};
490500

491501
let hw_params = alsa::pcm::HwParams::any(&pcm)?;
492502

@@ -711,6 +721,7 @@ struct StreamInner {
711721
// Cached values for performance in audio callback hot path
712722
period_samples: usize,
713723
period_frames: usize,
724+
frame_size: usize,
714725
silence_template: Box<[u8]>,
715726

716727
#[allow(dead_code)]
@@ -939,33 +950,6 @@ fn try_resume(channel: &alsa::PCM) -> Result<Poll, StreamError> {
939950
}
940951
}
941952

942-
/// Validate the result of a `writei` or `readi` call and map ALSA errors to [`StreamError`].
943-
#[inline]
944-
fn check_io_result(
945-
result: Result<usize, alsa::Error>,
946-
period_frames: usize,
947-
channel: &alsa::PCM,
948-
direction: alsa::Direction,
949-
) -> Result<(), StreamError> {
950-
match result {
951-
Ok(n) if n != period_frames => Err(BackendSpecificError {
952-
description: format!(
953-
"partial {}: expected {period_frames} frames, transferred {n}",
954-
match direction {
955-
alsa::Direction::Capture => "read",
956-
alsa::Direction::Playback => "write",
957-
}
958-
),
959-
}
960-
.into()),
961-
Ok(_) => Ok(()),
962-
Err(err) if err.errno() == libc::EPIPE => Err(StreamError::BufferUnderrun),
963-
Err(err) if err.errno() == libc::ESTRPIPE => try_resume(channel).map(|_| ()),
964-
Err(err) if err.errno() == libc::ENODEV => Err(StreamError::DeviceNotAvailable),
965-
Err(err) => Err(err.into()),
966-
}
967-
}
968-
969953
enum Poll {
970954
Pending,
971955
Ready {
@@ -1047,13 +1031,23 @@ fn process_input(
10471031
delay_frames: usize,
10481032
data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static),
10491033
) -> Result<(), StreamError> {
1050-
let result = stream.channel.io_bytes().readi(buffer);
1051-
check_io_result(
1052-
result,
1053-
stream.period_frames,
1054-
&stream.channel,
1055-
alsa::Direction::Capture,
1056-
)?;
1034+
let mut frames_read = 0;
1035+
while frames_read < stream.period_frames {
1036+
match stream
1037+
.channel
1038+
.io_bytes()
1039+
.readi(&mut buffer[frames_read * stream.frame_size..])
1040+
{
1041+
Ok(n) => frames_read += n,
1042+
Err(err) if err.errno() == libc::EPIPE || err.errno() == libc::ESTRPIPE => {
1043+
// EPIPE = xrun, ESTRPIPE = hardware suspend. Both require prepare()+restart;
1044+
// attempting resume mid-loop with a partial transfer in the ring buffer is unsafe.
1045+
return Err(StreamError::BufferUnderrun);
1046+
}
1047+
Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable),
1048+
Err(err) => return Err(err.into()),
1049+
}
1050+
}
10571051
let data = buffer.as_mut_ptr() as *mut ();
10581052
let data = unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) };
10591053
let callback = if stream.use_hw_timestamps {
@@ -1108,13 +1102,24 @@ fn process_output(
11081102
data_callback(&mut data, &info);
11091103
}
11101104

1111-
let result = stream.channel.io_bytes().writei(buffer);
1112-
check_io_result(
1113-
result,
1114-
stream.period_frames,
1115-
&stream.channel,
1116-
alsa::Direction::Playback,
1117-
)
1105+
let mut frames_written = 0;
1106+
while frames_written < stream.period_frames {
1107+
match stream
1108+
.channel
1109+
.io_bytes()
1110+
.writei(&buffer[frames_written * stream.frame_size..])
1111+
{
1112+
Ok(n) => frames_written += n,
1113+
Err(err) if err.errno() == libc::EPIPE || err.errno() == libc::ESTRPIPE => {
1114+
// EPIPE = xrun, ESTRPIPE = hardware suspend. Both require prepare()+restart;
1115+
// attempting resume mid-loop with a partial transfer in the ring buffer is unsafe.
1116+
return Err(StreamError::BufferUnderrun);
1117+
}
1118+
Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable),
1119+
Err(err) => return Err(err.into()),
1120+
}
1121+
}
1122+
Ok(())
11181123
}
11191124

11201125
// Use hardware timestamps from ALSA.

0 commit comments

Comments
 (0)