Skip to content
Open
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- **ALSA**: Fix a remaining timestamp segfault on 32-bit platforms with a 64-bit kernel `time_t`.
- **ALSA**: Improved enumeration accuracy for supported format, channel and rate combinations.
- **ASIO**: Fix a deadlock when dropping a `Stream` that owns another ASIO `Stream`.
- **ASIO**: Fix loading a driver while a previous driver was still unloading.
- **ASIO**: `Stream` no longer risks blocking or panicking in the driver callback while another stream is being created or destroyed.
- **AudioWorklet**: Fix processor construction failures not being reported to `error_callback`.
- **AudioWorklet**: Fix dropouts in output streams when the callback buffer grows.
- **CoreAudio**: Fix the device running at a different sample rate from the stream on hardware that reports a continuous rate range.
- **CoreAudio**: Fix `supported_configs()` only reporting `F32`, even on hardware that also supports other sample formats.
- **JACK**: Channel enumeration is capped at the physical system port count again.
- **JACK**: Streams no longer panic when the server delivers a larger period than the negotiated buffer size.
- **PipeWire**: Fix an empty chunk being emitted when a cycle requests no frames.
- **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices.
- **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize.
- **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer.
- **WASAPI**: Fix `I64` and `F64` incorrectly reported as supported output formats.
Comment thread
roderickvd marked this conversation as resolved.

## [0.18.2] - 2026-08-16

Expand Down
98 changes: 54 additions & 44 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,71 +535,81 @@ impl Device {
//SND_PCM_FORMAT_U18_3BE,
];

let min_rate = hw_params.get_rate_min()?;
let max_rate = hw_params.get_rate_max()?;

let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() {
// Fixed rate or continuous range.
vec![(min_rate, max_rate)]
} else {
// Discrete rates: probe the standard list plus the hardware's own min and max so
// that rates outside `COMMON_SAMPLE_RATES` are not missed.
let mut probe: Vec<SampleRate> = COMMON_SAMPLE_RATES.to_vec();
probe.push(min_rate);
probe.push(max_rate);
probe.sort_unstable();
probe.dedup();
probe
.into_iter()
.filter(|&r| (min_rate..=max_rate).contains(&r) && hw_params.test_rate(r).is_ok())
.map(|r| (r, r))
.collect()
};

let min_channels = hw_params.get_channels_min()?;
// 64 = AES10 (MADI) maximum; also prevents spinning on plugins like plughw that report u32::MAX.
const CHANNEL_ENUM_CAP: u32 = 64;
let max_channels = hw_params
.get_channels_max()?
.min(CHANNEL_ENUM_CAP)
.min(ChannelCount::MAX as u32);

let supported_channels: Vec<ChannelCount> =
if min_channels == max_channels || hw_params.test_channels(min_channels + 1).is_ok() {
(min_channels..=max_channels)
.map(|c| c as ChannelCount)
.collect()
} else {
(min_channels..=max_channels)
.filter(|&c| hw_params.test_channels(c).is_ok())
.map(|c| c as ChannelCount)
.collect()
};

let mut output =
Vec::with_capacity(FORMATS.len() * supported_channels.len() * sample_rates.len());
let mut output = Vec::new();
let mut seen_formats: Vec<SampleFormat> = Vec::with_capacity(FORMATS.len());

// Key: (channels, physical width in bits) with 4 physical widths (8/16/32/64 bits)
let mut buffer_size_cache: HashMap<(ChannelCount, u32), SupportedBufferSize> =
HashMap::with_capacity(supported_channels.len() * 4);
HashMap::new();

// `test_*` checks a value, it doesn't apply it, so format/channels/rate are each set on a
// clone in sequence rather than tested independently against the same unconstrained params.
for &(sample_format, alsa_format) in FORMATS.iter() {
if seen_formats.contains(&sample_format) || hw_params.test_format(alsa_format).is_err()
{
if seen_formats.contains(&sample_format) {
continue;
}
let format_params = hw_params.clone();
if format_params.set_format(alsa_format).is_err() {
continue;
}
seen_formats.push(sample_format);
let width = alsa_format.physical_width().unwrap_or(0) as u32;

for &channels in &supported_channels {
let (Ok(min_channels), Ok(max_channels)) = (
format_params.get_channels_min(),
format_params.get_channels_max(),
) else {
continue;
};
let max_channels = max_channels
.min(CHANNEL_ENUM_CAP)
.min(ChannelCount::MAX as u32);

for raw_channels in min_channels..=max_channels {
let channel_params = format_params.clone();
if channel_params.set_channels(raw_channels).is_err() {
continue;
}
let channels = raw_channels as ChannelCount;

let buffer_size =
*buffer_size_cache
.entry((channels, width))
.or_insert_with(|| {
supported_period_size_range(&hw_params, alsa_format, channels)
});

let (Ok(min_rate), Ok(max_rate)) =
(channel_params.get_rate_min(), channel_params.get_rate_max())
else {
continue;
};

let sample_rates =
if min_rate == max_rate || channel_params.test_rate(min_rate + 1).is_ok() {
// Fixed rate or continuous range.
vec![(min_rate, max_rate)]
} else {
// Discrete rates: probe the standard list plus the hardware's own min and max
// so that rates outside `COMMON_SAMPLE_RATES` are not missed.
let mut probe: Vec<SampleRate> = COMMON_SAMPLE_RATES.to_vec();
probe.push(min_rate);
probe.push(max_rate);
probe.sort_unstable();
probe.dedup();
probe
.into_iter()
.filter(|&r| {
(min_rate..=max_rate).contains(&r)
&& channel_params.test_rate(r).is_ok()
})
.map(|r| (r, r))
.collect()
};

for &(min_rate, max_rate) in sample_rates.iter() {
output.push(SupportedStreamConfigRange {
channels,
Expand Down
10 changes: 6 additions & 4 deletions src/host/asio/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ impl Device {
}
last_buffer_index = callback_info.buffer_index;

// There is 0% chance of lock contention the host only locks when recreating streams.
let stream_lock = asio_streams.lock().unwrap();
let Ok(stream_lock) = asio_streams.lock() else {
return;
};
Comment thread
roderickvd marked this conversation as resolved.
let asio_stream = match stream_lock.input {
Some(ref asio_stream) => asio_stream,
None => return,
Expand Down Expand Up @@ -577,8 +578,9 @@ impl Device {
}
last_buffer_index = callback_info.buffer_index;

// There is 0% chance of lock contention the host only locks when recreating streams.
let mut stream_lock = asio_streams.lock().unwrap();
let Ok(mut stream_lock) = asio_streams.lock() else {
return;
};
let asio_stream = match stream_lock.output {
Some(ref mut asio_stream) => asio_stream,
None => return,
Expand Down
89 changes: 58 additions & 31 deletions src/host/coreaudio/macos/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use coreaudio::audio_unit::{
audio_format::LinearPcmFlags,
macos_helpers::{
RateListener, audio_unit_from_device_id_uninitialized, find_matching_physical_format,
get_device_name, set_device_physical_stream_format,
get_device_name, get_supported_physical_stream_formats, set_device_physical_stream_format,
},
render_callback::{self, data},
};
Expand Down Expand Up @@ -526,18 +526,7 @@ impl Device {
n_channels += buf.mNumberChannels as usize;
}

// TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using
// I16 but just use F32 for now as it's the default anyway.
let sample_format = SampleFormat::F32;

// Get available sample rate ranges.
// The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of
// minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear.
// This may cause issues when, for example, sorting the configs by the sample rates.
// We follows the implementation of RtAudio, which returns single element of config
// when all the pairs have the same values and returns multiple elements otherwise.
// See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39

property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
let mut data_size = 0u32;
let status = AudioObjectGetPropertyDataSize(
Expand Down Expand Up @@ -575,19 +564,55 @@ impl Device {
}
let buffer_size = get_io_buffer_frame_size_range(self.audio_device_id)?;

// Most hardware reports discrete rates (mMinimum == mMaximum); some aggregate or
// virtual devices report continuous ranges.
let fmts: Vec<_> = ranges
.iter()
.map(|range| SupportedStreamConfigRange {
channels: n_channels as ChannelCount,
min_sample_rate: range.mMinimum as u32,
max_sample_rate: range.mMaximum as u32,
buffer_size,
sample_format,
})
.collect();
Ok(fmts.into_iter())
// AUHAL always converts to and from F32 regardless of the physical format, so
// advertise it at every nominal rate (most hardware reports discrete rates, i.e.
// mMinimum == mMaximum; some aggregate or virtual devices report continuous ranges).
let f32_fmts = ranges.iter().map(|range| SupportedStreamConfigRange {
channels: n_channels as ChannelCount,
min_sample_rate: range.mMinimum as u32,
max_sample_rate: range.mMaximum as u32,
buffer_size,
sample_format: SampleFormat::F32,
});

// The hardware's own physical formats, so integer-only devices advertise their
// bit-perfect paths instead of only the AUHAL-converted F32 one.
let physical_fmts = get_supported_physical_stream_formats(self.audio_device_id)
.unwrap_or_default()
.into_iter()
.filter_map(|fmt| {
let Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) =
coreaudio::audio_unit::AudioFormat::from_format_and_flag(
fmt.mFormat.mFormatID,
Some(fmt.mFormat.mFormatFlags),
)
else {
return None;
};
let sample_format = match CoreAudioSampleFormat::from_flags_and_bits_per_sample(
flags,
fmt.mFormat.mBitsPerChannel,
)? {
CoreAudioSampleFormat::I8 => SampleFormat::I8,
CoreAudioSampleFormat::I16 => SampleFormat::I16,
CoreAudioSampleFormat::I24 => SampleFormat::I24,
CoreAudioSampleFormat::I32 => SampleFormat::I32,
// Already covered by f32_fmts at every rate, not just this row's range.
CoreAudioSampleFormat::F32 => return None,
};
Some(SupportedStreamConfigRange {
channels: fmt.mFormat.mChannelsPerFrame as ChannelCount,
min_sample_rate: fmt.mSampleRateRange.mMinimum as u32,
max_sample_rate: fmt.mSampleRateRange.mMaximum as u32,
buffer_size,
sample_format,
})
});

Ok(f32_fmts
.chain(physical_fmts)
.collect::<Vec<_>>()
.into_iter())
}
}

Expand Down Expand Up @@ -711,14 +736,15 @@ impl Device {

// Set the physical stream format (bit depth + sample rate) on the hardware device.
// This avoids unnecessary format conversions, which is especially important on aggregate
// devices. Falls back to sample-rate-only if no matching physical format is available.
if set_physical_format(
// devices. Falls back to sample-rate-only if no matching physical format is available, or
// if the closest match found doesn't actually run at the requested rate.
if !set_physical_format(
self.audio_device_id,
config.sample_rate,
config.channels,
sample_format,
)
.is_err()
.is_ok_and(|asbd| (asbd.mSampleRate - config.sample_rate as f64).abs() < 1.0)
{
set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?;
}
Expand Down Expand Up @@ -845,14 +871,15 @@ impl Device {

// Best-effort: set the physical stream format (bit depth + sample rate) on the hardware.
// This avoids unnecessary conversions, especially on aggregate devices. Not an error if
// it fails — the AudioUnit will handle format conversion as before.
if set_physical_format(
// it fails: the AudioUnit will handle format conversion as before. Also falls back if the
// closest match found doesn't actually run at the requested rate.
if !set_physical_format(
self.audio_device_id,
config.sample_rate,
config.channels,
sample_format,
)
.is_err()
.is_ok_and(|asbd| (asbd.mSampleRate - config.sample_rate as f64).abs() < 1.0)
{
set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?;
}
Expand Down
Loading
Loading