From e288a72ca024e27cd8eda848b9d5940e98a72809 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 18 Feb 2026 21:36:26 +0100 Subject: [PATCH] fix(asio): cache device metadata during enumeration Enumerate by briefly loading each driver to capture metadata and skip unusable drivers; stop cleanly if a driver is already loaded. --- CHANGELOG.md | 11 ++ src/host/asio/device.rs | 252 +++++++++++++++++++++------------------- src/host/asio/stream.rs | 93 ++++++++++----- 3 files changed, 203 insertions(+), 153 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 276810d43..8a1fedcba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`. + +### Fixed + +- **ASIO**: Fix enumeration returning only the first device when using `collect`. + ## [0.17.3] - 2026-02-18 ### Changed @@ -1069,6 +1079,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial commit. +[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.3...HEAD [0.17.3]: https://github.com/RustAudio/cpal/compare/v0.17.2...v0.17.3 [0.17.2]: https://github.com/RustAudio/cpal/compare/v0.17.1...v0.17.2 [0.17.1]: https://github.com/RustAudio/cpal/compare/v0.17.0...v0.17.1 diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 69f7fd133..f72531133 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -1,7 +1,6 @@ pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use super::sys; -use crate::BackendSpecificError; use crate::ChannelCount; use crate::DefaultStreamConfigError; use crate::DeviceDescription; @@ -10,7 +9,9 @@ use crate::DeviceId; use crate::DeviceIdError; use crate::DeviceNameError; use crate::DevicesError; +use crate::FrameCount; use crate::SampleFormat; +use crate::SampleRate; use crate::SupportedBufferSize; use crate::SupportedStreamConfig; use crate::SupportedStreamConfigRange; @@ -23,25 +24,35 @@ use std::sync::{Arc, Mutex}; /// A ASIO Device #[derive(Clone)] pub struct Device { - /// The driver represented by this device. - pub driver: Arc, + name: String, + + // Metadata cached during enumeration + channels_in: ChannelCount, + channels_out: ChannelCount, + sample_rate: SampleRate, + buffer_size_min: FrameCount, + buffer_size_max: FrameCount, + input_sample_format: Option, + output_sample_format: Option, + supported_sample_rates: Vec, // Input and/or Output stream. // A driver can only have one of each. // They need to be created at the same time. - pub asio_streams: Arc>, - pub current_callback_flag: Arc, + pub(super) asio_streams: Arc>, + pub(super) current_callback_flag: Arc, } /// All available devices. pub struct Devices { asio: Arc, drivers: std::vec::IntoIter, + current_driver: Option, } impl PartialEq for Device { fn eq(&self, other: &Self) -> bool { - self.driver.name() == other.driver.name() + self.name == other.name } } @@ -49,30 +60,25 @@ impl Eq for Device {} impl Hash for Device { fn hash(&self, state: &mut H) { - self.driver.name().hash(state); + self.name.hash(state); } } impl Device { pub fn description(&self) -> Result { - let driver_name = self.driver.name().to_string(); - let direction = crate::device_description::direction_from_counts( - self.driver.channels().ok().map(|c| c.ins as ChannelCount), - self.driver.channels().ok().map(|c| c.outs as ChannelCount), + Some(self.channels_in), + Some(self.channels_out), ); - Ok(DeviceDescriptionBuilder::new(driver_name.clone()) - .driver(driver_name) + Ok(DeviceDescriptionBuilder::new(self.name.clone()) + .driver(self.name.clone()) .direction(direction) .build()) } pub fn id(&self) -> Result { - Ok(DeviceId( - crate::platform::HostId::Asio, - self.driver.name().to_string(), - )) + Ok(DeviceId(crate::platform::HostId::Asio, self.name.clone())) } /// Gets the supported input configs. @@ -81,35 +87,10 @@ impl Device { pub fn supported_input_configs( &self, ) -> Result { - // Retrieve the default config for the total supported channels and supported sample - // format. - let f = match self.default_input_config() { - Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), - Ok(f) => f, - }; - - // Collect a config for every combination of supported sample rate and number of channels. - let mut supported_configs = vec![]; - for &rate in crate::COMMON_SAMPLE_RATES { - if !self - .driver - .can_sample_rate(rate.into()) - .ok() - .unwrap_or(false) - { - continue; - } - for channels in 1..f.channels + 1 { - supported_configs.push(SupportedStreamConfigRange { - channels, - min_sample_rate: rate, - max_sample_rate: rate, - buffer_size: f.buffer_size, - sample_format: f.sample_format, - }) - } - } - Ok(supported_configs.into_iter()) + let default = self + .default_input_config() + .map_err(|_| SupportedStreamConfigsError::DeviceNotAvailable)?; + Ok(self.configs_for(default).into_iter()) } /// Gets the supported output configs. @@ -118,107 +99,149 @@ impl Device { pub fn supported_output_configs( &self, ) -> Result { - // Retrieve the default config for the total supported channels and supported sample - // format. - let f = match self.default_output_config() { - Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), - Ok(f) => f, - }; - - // Collect a config for every combination of supported sample rate and number of channels. - let mut supported_configs = vec![]; - for &rate in crate::COMMON_SAMPLE_RATES { - if !self - .driver - .can_sample_rate(rate.into()) - .ok() - .unwrap_or(false) - { - continue; - } - for channels in 1..f.channels + 1 { - supported_configs.push(SupportedStreamConfigRange { - channels, - min_sample_rate: rate, - max_sample_rate: rate, - buffer_size: f.buffer_size, - sample_format: f.sample_format, - }) - } - } - Ok(supported_configs.into_iter()) + let default = self + .default_output_config() + .map_err(|_| SupportedStreamConfigsError::DeviceNotAvailable)?; + Ok(self.configs_for(default).into_iter()) } /// Returns the default input config pub fn default_input_config(&self) -> Result { - let channels = self.driver.channels().map_err(default_config_err)?.ins as u16; - let sample_rate = self.driver.sample_rate().map_err(default_config_err)? as u32; - let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; - let buffer_size = SupportedBufferSize::Range { - min: min as u32, - max: max as u32, - }; - // Map th ASIO sample type to a CPAL sample type - let data_type = self.driver.input_data_type().map_err(default_config_err)?; - let sample_format = convert_data_type(&data_type) - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; - Ok(SupportedStreamConfig { - channels, - sample_rate, - buffer_size, - sample_format, - }) + self.default_config(self.channels_in, self.input_sample_format) } /// Returns the default output config pub fn default_output_config(&self) -> Result { - let channels = self.driver.channels().map_err(default_config_err)?.outs as u16; - let sample_rate = self.driver.sample_rate().map_err(default_config_err)? as u32; - let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; - let buffer_size = SupportedBufferSize::Range { - min: min as u32, - max: max as u32, - }; - let data_type = self.driver.output_data_type().map_err(default_config_err)?; - let sample_format = convert_data_type(&data_type) - .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; + self.default_config(self.channels_out, self.output_sample_format) + } + + fn default_config( + &self, + channels: ChannelCount, + sample_format: Option, + ) -> Result { + if channels == 0 { + return Err(DefaultStreamConfigError::StreamTypeNotSupported); + } + let sample_format = + sample_format.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; Ok(SupportedStreamConfig { channels, - sample_rate, - buffer_size, + sample_rate: self.sample_rate, + buffer_size: SupportedBufferSize::Range { + min: self.buffer_size_min, + max: self.buffer_size_max, + }, sample_format, }) } + + fn configs_for(&self, default: SupportedStreamConfig) -> Vec { + let mut configs = Vec::with_capacity(default.channels as usize); + for &rate in &self.supported_sample_rates { + for channels in 1..=default.channels { + configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: default.buffer_size, + sample_format: default.sample_format, + }); + } + } + configs + } } impl Devices { pub fn new(asio: Arc) -> Result { let drivers = asio.driver_names().into_iter(); - Ok(Devices { asio, drivers }) + Ok(Self { + asio, + drivers, + current_driver: None, + }) } } impl Iterator for Devices { type Item = Device; - /// Load drivers and return device + /// Enumerate devices by briefly loading each driver to capture its metadata. fn next(&mut self) -> Option { + // Drop the previously loaded driver before attempting to load the next one. + self.current_driver = None; + loop { match self.drivers.next() { Some(name) => match self.asio.load_driver(&name) { Ok(driver) => { - let driver = Arc::new(driver); + let Ok(channels) = driver.channels() else { + continue; + }; + if channels.ins == 0 && channels.outs == 0 { + continue; + } + + let Ok(sample_rate) = driver.sample_rate() else { + continue; + }; + if sample_rate == 0.0 { + continue; + } + + let Ok((buffer_size_min, buffer_size_max)) = driver.buffersize_range() + else { + continue; + }; + + let input_sample_format = driver + .input_data_type() + .ok() + .and_then(|t| convert_data_type(&t)); + let output_sample_format = driver + .output_data_type() + .ok() + .and_then(|t| convert_data_type(&t)); + if input_sample_format.is_none() && output_sample_format.is_none() { + continue; + } + + let supported_sample_rates: Vec = crate::COMMON_SAMPLE_RATES + .iter() + .copied() + .filter(|&r| driver.can_sample_rate(r.into()).unwrap_or(false)) + .collect(); + if supported_sample_rates.is_empty() { + continue; + } + + self.current_driver = Some(driver); + let asio_streams = Arc::new(Mutex::new(sys::AsioStreams { input: None, output: None, })); + return Some(Device { - driver, + name, + channels_in: channels.ins as ChannelCount, + channels_out: channels.outs as ChannelCount, + sample_rate: sample_rate as SampleRate, + buffer_size_min: buffer_size_min as FrameCount, + buffer_size_max: buffer_size_max as FrameCount, + input_sample_format, + output_sample_format, + supported_sample_rates, asio_streams, // Initialize with sentinel value so it never matches global flag state (0 or 1). current_callback_flag: Arc::new(AtomicU32::new(u32::MAX)), }); } + // A different driver is already loaded (e.g. an active Stream holds it). Stop + // cleanly rather than spinning through the rest of the list. + Err(sys::LoadDriverError::DriverAlreadyExists) => return None, + // Driver failed to load for its own reasons; skip and try the next. Err(_) => continue, }, None => return None, @@ -243,16 +266,3 @@ pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option DefaultStreamConfigError { - match e { - sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { - DefaultStreamConfigError::DeviceNotAvailable - } - sys::AsioError::NoRate => DefaultStreamConfigError::StreamTypeNotSupported, - err => { - let description = format!("{}", err); - BackendSpecificError { description }.into() - } - } -} diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 958a52bc3..5999f8d52 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -52,7 +52,16 @@ impl Device { D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - let stream_type = self.driver.input_data_type().map_err(build_stream_err)?; + let description = self + .description() + .map_err(|_| BuildStreamError::DeviceNotAvailable)?; + let driver = super::GLOBAL_ASIO + .get() + .ok_or(BuildStreamError::DeviceNotAvailable)? + .load_driver(description.name()) + .map_err(load_driver_err)?; + + let stream_type = driver.input_data_type().map_err(build_stream_err)?; // Ensure that the desired sample type is supported. let expected_sample_format = super::device::convert_data_type(&stream_type) @@ -62,10 +71,10 @@ impl Device { } // Register the message callback with the driver - let message_callback_id = self.add_message_callback(error_callback); + let message_callback_id = self.add_message_callback(&driver, error_callback); let num_channels = config.channels; - let buffer_size = self.get_or_create_input_stream(config, sample_format)?; + let buffer_size = self.get_or_create_input_stream(&driver, config, sample_format)?; let cpal_num_samples = buffer_size * num_channels as usize; // Create the buffer depending on the size of the data type. @@ -79,7 +88,7 @@ impl Device { // Set the input callback. // This is most performance critical part of the ASIO bindings. let config = config.clone(); - let callback_id = self.driver.add_callback(move |callback_info| unsafe { + let callback_id = driver.add_callback(move |callback_info| unsafe { // If not playing return early. if !playing.load(Ordering::SeqCst) { return; @@ -253,11 +262,11 @@ impl Device { } }); - let driver = self.driver.clone(); + let driver = Arc::new(driver); let asio_streams = self.asio_streams.clone(); // Immediately start the device? - self.driver.start().map_err(build_stream_err)?; + driver.start().map_err(build_stream_err)?; Ok(Stream { playing: stream_playing, @@ -280,7 +289,16 @@ impl Device { D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - let stream_type = self.driver.output_data_type().map_err(build_stream_err)?; + let description = self + .description() + .map_err(|_| BuildStreamError::DeviceNotAvailable)?; + let driver = super::GLOBAL_ASIO + .get() + .ok_or(BuildStreamError::DeviceNotAvailable)? + .load_driver(description.name()) + .map_err(load_driver_err)?; + + let stream_type = driver.output_data_type().map_err(build_stream_err)?; // Ensure that the desired sample type is supported. let expected_sample_format = super::device::convert_data_type(&stream_type) @@ -290,10 +308,10 @@ impl Device { } // Register the message callback with the driver - let message_callback_id = self.add_message_callback(error_callback); + let message_callback_id = self.add_message_callback(&driver, error_callback); let num_channels = config.channels; - let buffer_size = self.get_or_create_output_stream(config, sample_format)?; + let buffer_size = self.get_or_create_output_stream(&driver, config, sample_format)?; let cpal_num_samples = buffer_size * num_channels as usize; // Create buffers depending on data type. @@ -306,7 +324,7 @@ impl Device { let asio_streams = self.asio_streams.clone(); let config = config.clone(); - let callback_id = self.driver.add_callback(move |callback_info| unsafe { + let callback_id = driver.add_callback(move |callback_info| unsafe { // If not playing, return early. if !playing.load(Ordering::SeqCst) { return; @@ -532,11 +550,11 @@ impl Device { } }); - let driver = self.driver.clone(); + let driver = Arc::new(driver); let asio_streams = self.asio_streams.clone(); // Immediately start the device? - self.driver.start().map_err(build_stream_err)?; + driver.start().map_err(build_stream_err)?; Ok(Stream { playing: stream_playing, @@ -554,16 +572,15 @@ impl Device { /// On success, the buffer size of the stream is returned. fn get_or_create_input_stream( &self, + driver: &sys::Driver, config: &StreamConfig, sample_format: SampleFormat, ) -> Result { - match self.default_input_config() { - Ok(f) => { - let num_asio_channels = f.channels; - check_config(&self.driver, config, sample_format, num_asio_channels) - } - Err(_) => Err(BuildStreamError::StreamConfigNotSupported), - }?; + let num_asio_channels = self + .default_input_config() + .map_err(|_| BuildStreamError::StreamConfigNotSupported)? + .channels; + check_config(driver, config, sample_format, num_asio_channels)?; let num_channels = config.channels as usize; let mut streams = self.asio_streams.lock().unwrap(); @@ -578,7 +595,7 @@ impl Device { Some(ref input) => Ok(input.buffer_size as usize), None => { let output = streams.output.take(); - self.driver + driver .prepare_input_stream(output, num_channels, buffer_size) .map(|new_streams| { let bs = match new_streams.input { @@ -601,16 +618,15 @@ impl Device { /// If there is no existing ASIO Output Stream it will be created. fn get_or_create_output_stream( &self, + driver: &sys::Driver, config: &StreamConfig, sample_format: SampleFormat, ) -> Result { - match self.default_output_config() { - Ok(f) => { - let num_asio_channels = f.channels; - check_config(&self.driver, config, sample_format, num_asio_channels) - } - Err(_) => Err(BuildStreamError::StreamConfigNotSupported), - }?; + let num_asio_channels = self + .default_output_config() + .map_err(|_| BuildStreamError::StreamConfigNotSupported)? + .channels; + check_config(driver, config, sample_format, num_asio_channels)?; let num_channels = config.channels as usize; let mut streams = self.asio_streams.lock().unwrap(); @@ -625,7 +641,7 @@ impl Device { Some(ref output) => Ok(output.buffer_size as usize), None => { let input = streams.input.take(); - self.driver + driver .prepare_output_stream(input, num_channels, buffer_size) .map(|new_streams| { let bs = match new_streams.output { @@ -643,13 +659,17 @@ impl Device { } } - fn add_message_callback(&self, error_callback: E) -> sys::MessageCallbackId + fn add_message_callback( + &self, + driver: &sys::Driver, + error_callback: E, + ) -> sys::MessageCallbackId where E: FnMut(StreamError) + Send + 'static, { let error_callback_shared = Arc::new(Mutex::new(error_callback)); - self.driver.add_message_callback(move |msg| { + driver.add_message_callback(move |msg| { // Check specifically for ResetRequest if let sys::AsioMessageSelectors::kAsioResetRequest = msg { if let Ok(mut cb) = error_callback_shared.lock() { @@ -669,8 +689,8 @@ impl Drop for Stream { } fn asio_ns_to_double(val: sys::bindings::asio_import::ASIOTimeStamp) -> f64 { - let two_raised_to_32 = 4294967296.0; - val.lo as f64 + val.hi as f64 * two_raised_to_32 + const TWO_RAISED_TO_32: f64 = 4294967296.0; + val.lo as f64 + val.hi as f64 * TWO_RAISED_TO_32 } /// Asio retrieves system time via `timeGetTime` which returns the time in milliseconds. @@ -793,6 +813,15 @@ unsafe fn asio_channel_slice_mut( std::slice::from_raw_parts_mut(buff_ptr, channel_length) } +fn load_driver_err(e: sys::LoadDriverError) -> BuildStreamError { + match e { + sys::LoadDriverError::LoadDriverFailed | sys::LoadDriverError::DriverAlreadyExists => { + BuildStreamError::DeviceNotAvailable + } + sys::LoadDriverError::InitializationFailed(asio_err) => build_stream_err(asio_err), + } +} + fn build_stream_err(e: sys::AsioError) -> BuildStreamError { match e { sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => {