Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable` instead of looping.
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
- **ALSA**: Try to resume from hardware after a system suspend.
- **ALSA**: Loop partial reads and writes to completion.
- **ALSA**: Prevent reentrancy issues with non-reentrant plugins and devices.
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
- **ASIO**: Timestamps now include driver-reported hardware latency.
- **CoreAudio**: Timestamps now include device latency and safety offset.
Expand Down
119 changes: 59 additions & 60 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ mod enumerate;

const DEFAULT_DEVICE: &str = "default";

// Some ALSA plugins (e.g. alsaequal, certain USB drivers) are not reentrant.
static ALSA_OPEN_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

// TODO: Not yet defined in rust-lang/libc crate
const LIBC_ENOTSUPP: libc::c_int = 524;

Expand Down Expand Up @@ -371,9 +374,11 @@ impl Device {
}
}

let handle = match alsa::pcm::PCM::new(&self.pcm_id, stream_type, true)
.map_err(|e| (e, e.errno()))
{
let open_result = {
let _guard = ALSA_OPEN_MUTEX.lock().unwrap();
Comment thread
roderickvd marked this conversation as resolved.
Outdated
alsa::pcm::PCM::new(&self.pcm_id, stream_type, true).map_err(|e| (e, e.errno()))
};
let handle = match open_result {
Err((_, libc::ENOENT))
| Err((_, libc::EPERM))
| Err((_, libc::ENODEV))
Expand Down Expand Up @@ -410,7 +415,8 @@ impl Device {

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

// Only fill buffer for unsigned formats that don't have a zero value for silence.
Expand All @@ -426,6 +432,7 @@ impl Device {
conf,
period_samples,
period_frames,
frame_size,
silence_template,
can_pause,
creation_instant,
Expand Down Expand Up @@ -472,21 +479,24 @@ impl Device {
&self,
stream_t: alsa::Direction,
) -> Result<VecIntoIter<SupportedStreamConfigRange>, SupportedStreamConfigsError> {
let pcm =
match alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno())) {
Err((_, libc::ENOENT))
| Err((_, libc::EPERM))
| Err((_, libc::ENODEV))
| Err((_, LIBC_ENOTSUPP)) => {
return Err(SupportedStreamConfigsError::DeviceNotAvailable)
}
Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => {
return Err(SupportedStreamConfigsError::DeviceBusy)
}
Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument),
Err((e, _)) => return Err(e.into()),
Ok(pcm) => pcm,
};
let open_result = {
let _guard = ALSA_OPEN_MUTEX.lock().unwrap();
Comment thread
roderickvd marked this conversation as resolved.
Outdated
alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno()))
};
let pcm = match open_result {
Err((_, libc::ENOENT))
| Err((_, libc::EPERM))
| Err((_, libc::ENODEV))
| Err((_, LIBC_ENOTSUPP)) => {
return Err(SupportedStreamConfigsError::DeviceNotAvailable)
}
Err((_, libc::EBUSY)) | Err((_, libc::EAGAIN)) => {
return Err(SupportedStreamConfigsError::DeviceBusy)
}
Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument),
Err((e, _)) => return Err(e.into()),
Ok(pcm) => pcm,
};

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

Expand Down Expand Up @@ -711,6 +721,7 @@ struct StreamInner {
// Cached values for performance in audio callback hot path
period_samples: usize,
period_frames: usize,
frame_size: usize,
silence_template: Box<[u8]>,

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

/// Validate the result of a `writei` or `readi` call and map ALSA errors to [`StreamError`].
#[inline]
fn check_io_result(
result: Result<usize, alsa::Error>,
period_frames: usize,
channel: &alsa::PCM,
direction: alsa::Direction,
) -> Result<(), StreamError> {
match result {
Ok(n) if n != period_frames => Err(BackendSpecificError {
description: format!(
"partial {}: expected {period_frames} frames, transferred {n}",
match direction {
alsa::Direction::Capture => "read",
alsa::Direction::Playback => "write",
}
),
}
.into()),
Ok(_) => Ok(()),
Err(err) if err.errno() == libc::EPIPE => Err(StreamError::BufferUnderrun),
Err(err) if err.errno() == libc::ESTRPIPE => try_resume(channel).map(|_| ()),
Err(err) if err.errno() == libc::ENODEV => Err(StreamError::DeviceNotAvailable),
Err(err) => Err(err.into()),
}
}

enum Poll {
Pending,
Ready {
Expand Down Expand Up @@ -1047,13 +1031,20 @@ fn process_input(
delay_frames: usize,
data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static),
) -> Result<(), StreamError> {
let result = stream.channel.io_bytes().readi(buffer);
check_io_result(
result,
stream.period_frames,
&stream.channel,
alsa::Direction::Capture,
)?;
let mut frames_read = 0;
while frames_read < stream.period_frames {
match stream
.channel
.io_bytes()
.readi(&mut buffer[frames_read * stream.frame_size..])
{
Ok(n) => frames_read += n,
Err(err) if err.errno() == libc::EPIPE => return Err(StreamError::BufferUnderrun),
Err(err) if err.errno() == libc::ESTRPIPE => return Err(StreamError::BufferUnderrun),
Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable),
Err(err) => return Err(err.into()),
Comment thread
roderickvd marked this conversation as resolved.
}
}
let data = buffer.as_mut_ptr() as *mut ();
let data = unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) };
let callback = if stream.use_hw_timestamps {
Expand Down Expand Up @@ -1108,13 +1099,21 @@ fn process_output(
data_callback(&mut data, &info);
}

let result = stream.channel.io_bytes().writei(buffer);
check_io_result(
result,
stream.period_frames,
&stream.channel,
alsa::Direction::Playback,
)
let mut frames_written = 0;
while frames_written < stream.period_frames {
match stream
.channel
.io_bytes()
.writei(&buffer[frames_written * stream.frame_size..])
{
Ok(n) => frames_written += n,
Err(err) if err.errno() == libc::EPIPE => return Err(StreamError::BufferUnderrun),
Err(err) if err.errno() == libc::ESTRPIPE => return Err(StreamError::BufferUnderrun),
Err(err) if err.errno() == libc::ENODEV => return Err(StreamError::DeviceNotAvailable),
Err(err) => return Err(err.into()),
Comment thread
roderickvd marked this conversation as resolved.
}
}
Ok(())
}

// Use hardware timestamps from ALSA.
Expand Down
Loading