From 730458887ce17fe0f958511b3a6600d6a9b7c441 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Jul 2026 17:03:41 +0200 Subject: [PATCH 1/4] fix(asio): share duplex buffer state across Device handles for the same driver default_input_device() and default_output_device() each enumerate a fresh Device with its own empty AsioStreams, even when they resolve to the same loaded driver. This broke the reuse path in get_or_create_input_stream / get_or_create_output_stream: building both directions triggered a second, destructive ASIOCreateBuffers call that silently discarded the first direction's buffers instead of combining them into one call as designed. Move the AsioStreams bookkeeping onto DriverInner, alongside DriverState, so every Driver handle for the same loaded driver shares it. Fixes #1287 --- CHANGELOG.md | 1 + asio-sys/src/bindings/mod.rs | 12 ++++++++++++ src/host/asio/device.rs | 6 +----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5062d7a88..11cdc2a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Timestamps now stay monotonic across device and graph changes. - **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll. - **ALSA**: Fix compilation on FreeBSD, DragonFly, and NetBSD. +- **ASIO**: Fix duplex streams silently dropping audio when built from separate `Device` handles. - **AudioWorklet**: Fix `Stream` operations to work when called from any thread. - **AudioWorklet**: Fix stale audio output when a data callback wrote a partial buffer. - **CoreAudio**: Default-output streams now report xrun status. diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 36be4e102..1a2755eff 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -63,6 +63,8 @@ pub struct Driver { #[derive(Debug)] struct DriverInner { state: Mutex, + // Input/output buffer state, shared across every `Driver` handle for this driver. + streams: Arc>, // The unique name associated with this driver. name: String, // Track whether or not the driver has been destroyed. @@ -471,11 +473,16 @@ impl Asio { CURRENT_SAMPLE_RATE.store(rate.to_bits(), Ordering::Release); } let state = Mutex::new(DriverState::Initialized); + let streams = Arc::new(Mutex::new(AsioStreams { + input: None, + output: None, + })); let name = driver_name.to_string(); let destroyed = false; let inner = Arc::new(DriverInner { name, state, + streams, destroyed, }); *loaded = Arc::downgrade(&inner); @@ -501,6 +508,11 @@ impl Driver { &self.inner.name } + /// The shared input/output buffer state for this driver. + pub fn streams(&self) -> Arc> { + self.inner.streams.clone() + } + /// Returns the number of input and output channels available on the driver. pub fn channels(&self) -> Result { let _guard = self.inner.lock_state(); diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 9652146f2..a78b504e1 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -213,13 +213,9 @@ impl Iterator for Devices { .filter(|&r| driver.can_sample_rate(r.into()).unwrap_or(false)) .collect(); + let asio_streams = driver.streams(); self.current_driver = Some(driver); - let asio_streams = Arc::new(Mutex::new(sys::AsioStreams { - input: None, - output: None, - })); - return Some(Device { name, channels_in: channels.ins as ChannelCount, From b5133d177fd00d735157c67bfd55c4c7f78e7e6b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Jul 2026 17:22:31 +0200 Subject: [PATCH 2/4] fix(asio): derive Debug on AsioStreams/AsioStream --- asio-sys/src/bindings/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 1a2755eff..24c1617da 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -133,6 +133,7 @@ struct BufferCallback(Box); /// There is only ever max one input and one output. /// /// Only one is required. +#[derive(Debug)] pub struct AsioStreams { pub input: Option, pub output: Option, @@ -141,6 +142,7 @@ pub struct AsioStreams { /// A stream to ASIO. /// /// Contains the buffers. +#[derive(Debug)] pub struct AsioStream { /// A Double buffer per channel pub buffer_infos: Vec, From 1ad37cca7baf56fc879baea0ddf2accaa25de2e2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Jul 2026 17:25:59 +0200 Subject: [PATCH 3/4] feat(examples): add --asio flag to feedback example --- examples/feedback.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/examples/feedback.rs b/examples/feedback.rs index e8d5b02cb..44c8cb688 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -38,17 +38,23 @@ struct Opt { /// Use the PulseAudio host. Requires `--features pulseaudio`. #[arg(long, default_value_t = false)] pulseaudio: bool, + + /// Use the ASIO host. Requires `--features asio`. + #[arg(long, default_value_t = false)] + asio: bool, } fn main() -> anyhow::Result<()> { let opt = Opt::parse(); - // JACK/PulseAudio support must be enabled at compile time, and is + // JACK/PulseAudio/ASIO support must be enabled at compile time, and is // only available on some platforms. #[allow(unused_mut, unused_assignments)] let mut jack_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[allow(unused_mut, unused_assignments)] let mut pulseaudio_host_id: Result = Err(ErrorKind::HostUnavailable.into()); + #[allow(unused_mut, unused_assignments)] + let mut asio_host_id: Result = Err(ErrorKind::HostUnavailable.into()); #[cfg(any( target_os = "linux", @@ -68,6 +74,14 @@ fn main() -> anyhow::Result<()> { } } + #[cfg(target_os = "windows")] + { + #[cfg(feature = "asio")] + { + asio_host_id = Ok(HostId::Asio); + } + } + // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example beep --features jack -- --jack let host = if opt.jack { @@ -78,6 +92,10 @@ fn main() -> anyhow::Result<()> { pulseaudio_host_id .and_then(cpal::host_from_id) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") + } else if opt.asio { + asio_host_id + .and_then(cpal::host_from_id) + .expect("make sure `--features asio` is specified, and the platform is supported") } else { cpal::default_host() }; From e588f91f71506bac82e1503d49755af49008816e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Jul 2026 18:09:15 +0200 Subject: [PATCH 4/4] fix(examples): dispatch feedback example generically on sample format --- examples/feedback.rs | 111 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/examples/feedback.rs b/examples/feedback.rs index 44c8cb688..ad6301f50 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -1,7 +1,7 @@ //! Feeds back the input stream directly into the output stream. //! -//! Assumes that the input and output devices can use the same stream configuration and that they -//! support the f32 sample format. +//! Assumes that the input and output devices can use the same stream configuration and share a +//! sample format. //! //! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not //! precisely synchronised. @@ -9,7 +9,8 @@ use clap::Parser; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - Error, ErrorKind, HostId, InputCallbackInfo, OutputCallbackInfo, Sample, StreamConfig, + Device, Error, ErrorKind, HostId, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + SizedSample, StreamConfig, }; use ringbuf::{ traits::{Consumer, Producer, Split}, @@ -121,48 +122,128 @@ fn main() -> anyhow::Result<()> { println!("Using output device: \"{}\"", output_device.id()?); // We'll try and use the same configuration between streams to keep it simple. - let config: StreamConfig = input_device.default_input_config()?.into(); + let input_config = input_device.default_input_config()?; + let output_config = output_device.default_output_config()?; + assert_eq!( + input_config.sample_format(), + output_config.sample_format(), + "input and output devices must share a sample format for this example" + ); + + match input_config.sample_format() { + SampleFormat::I8 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::I16 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::I32 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::I64 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::U8 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::U16 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::U32 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::U64 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::F32 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + SampleFormat::F64 => run::( + &input_device, + &output_device, + input_config.into(), + opt.latency, + ), + sample_format => panic!("Unsupported sample format '{sample_format}'"), + } +} +fn run( + input_device: &Device, + output_device: &Device, + config: StreamConfig, + latency_ms: f32, +) -> anyhow::Result<()> +where + T: SizedSample + std::fmt::Debug + Send + 'static, +{ // Create a delay in case the input and output devices aren't synced. - let latency_frames = (opt.latency / 1_000.0) * config.sample_rate as f32; + let latency_frames = (latency_ms / 1_000.0) * config.sample_rate as f32; let latency_samples = latency_frames as usize * config.channels as usize; // The buffer to share samples - let ring = HeapRb::::new(latency_samples * 2); + let ring = HeapRb::::new(latency_samples * 2); let (mut producer, mut consumer) = ring.split(); // Pre-fill with silence equal to the length of the delay. for _ in 0..latency_samples { // The ring buffer has twice as much space as necessary to add latency here, // so this should never fail - producer.try_push(f32::EQUILIBRIUM).unwrap(); + producer.try_push(T::EQUILIBRIUM).unwrap(); } - let input_data_fn = move |data: &[f32], _: &InputCallbackInfo| { + let input_data_fn = move |data: &[T], _: &InputCallbackInfo| { if producer.push_slice(data) < data.len() { eprintln!("output stream fell behind: try increasing latency"); } }; - let output_data_fn = move |data: &mut [f32], _: &OutputCallbackInfo| { + let output_data_fn = move |data: &mut [T], _: &OutputCallbackInfo| { let read = consumer.pop_slice(data); if read < data.len() { - data[read..].fill(f32::EQUILIBRIUM); + data[read..].fill(T::EQUILIBRIUM); eprintln!("input stream fell behind: try increasing latency"); } }; // Build streams. - println!("Attempting to build both streams with f32 samples and `{config:?}`."); + println!( + "Attempting to build both streams with {} samples and `{config:?}`.", + T::FORMAT + ); let input_stream = input_device.build_input_stream(config, input_data_fn, err_fn, None)?; let output_stream = output_device.build_output_stream(config, output_data_fn, err_fn, None)?; println!("Successfully built streams."); // Play the streams. - println!( - "Starting the input and output streams with `{}` milliseconds of latency.", - opt.latency - ); + println!("Starting the input and output streams with `{latency_ms}` milliseconds of latency."); input_stream.play()?; output_stream.play()?;