From fb1e2eba4b30b50b64f39dcd2c9520ae95c18285 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 19 Aug 2026 20:08:59 +0200 Subject: [PATCH 1/2] implement `azo` based ASIO backend --- Cargo.toml | 13 +- README.md | 1 + src/host/asio_new/callbacks.rs | 268 +++++++++++++++++++++++ src/host/asio_new/capabilities.rs | 76 +++++++ src/host/asio_new/mod.rs | 307 +++++++++++++++++++++++++++ src/host/asio_new/session.rs | 342 ++++++++++++++++++++++++++++++ src/host/asio_new/simplex.rs | 177 ++++++++++++++++ src/host/asio_new/utils.rs | 88 ++++++++ src/host/com.rs | 3 + src/host/com/worker.rs | 49 +++++ src/host/mod.rs | 3 + src/platform/mod.rs | 3 + 12 files changed, 1329 insertions(+), 1 deletion(-) create mode 100644 src/host/asio_new/callbacks.rs create mode 100644 src/host/asio_new/capabilities.rs create mode 100644 src/host/asio_new/mod.rs create mode 100644 src/host/asio_new/session.rs create mode 100644 src/host/asio_new/simplex.rs create mode 100644 src/host/asio_new/utils.rs create mode 100644 src/host/com/worker.rs diff --git a/Cargo.toml b/Cargo.toml index 06d86479f..b3887c6ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ rust-version = "1.85" maintenance = { status = "actively-developed" } [features] -default = [] +default = ["custom", "asio-new"] # Real-time audio thread scheduling # Applies platform-specific real-time scheduling and performance modes to audio threads. @@ -30,9 +30,15 @@ realtime-dbus = ["realtime", "audio_thread_priority/with_dbus"] # ASIO backend for Windows # Provides low-latency audio I/O by bypassing the Windows audio stack # Requires: ASIO drivers and LLVM/Clang for build-time bindings +# Platform: Windows # See README for detailed setup instructions asio = ["dep:asio-sys", "dep:num-traits"] +# Experimental ASIO implementation with multi-driver support and no external build requirements. +# Requires: ASIO drivers +# Platform: Windows +asio-new = ["dep:azo", "dep:closure-ffi", "dep:tap", "dep:oneshot", "dep:parking_lot"] + # Audio Worklet backend for WebAssembly # Provides lower-latency web audio processing compared to default Web Audio API # Requires: Build with atomics support and Cross-Origin headers for SharedArrayBuffer @@ -131,7 +137,12 @@ windows = { version = "0.62", features = [ ] } audio_thread_priority = { version = "0.36", optional = true, default-features = false } asio-sys = { version = "0.5.0", path = "asio-sys", optional = true } +azo = { version = "0.1.0", optional = true } +closure-ffi = { version = "5.1.2", optional = true } num-traits = { version = "0.2", optional = true } +oneshot = { version = "0.2.1", features = ["std"], optional = true } +parking_lot = { version = "0.12.5", optional = true } +tap = { version = "1.0.1", optional = true } jack = { version = "0.13.5", optional = true } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] diff --git a/README.md b/README.md index e7bf06599..098b91c43 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The `audioworklet` backend additionally requires `-Zbuild-std` with atomics supp | Feature | Platform | Description | | ------- | -------- | ----------- | | `asio` | Windows | ASIO backend for low-latency audio, bypassing the Windows audio stack. Requires ASIO drivers and LLVM/Clang. See the [ASIO setup guide](#compiling-for-asio). | +| `asio-new` | Windows | Experimental ASIO implementation with multi-driver support and no external build requirements. | | `audioworklet` | WebAssembly (`wasm32-unknown-unknown`) | Audio Worklet backend for lower-latency web audio than the default Web Audio API, running audio on a dedicated thread. Requires atomics support (`RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"`) and `Cross-Origin` headers for `SharedArrayBuffer`. See the `audioworklet` example. | | `custom` | All | User-defined backend implementations for audio systems not natively supported by CPAL. See `examples/custom.rs`. | | `jack` | Linux, BSD, macOS, Windows | JACK Audio Connection Kit backend for pro-audio routing and inter-application connectivity. Requires `libjack-jackd2-dev` (Debian/Ubuntu) or `jack-devel` (Fedora). | diff --git a/src/host/asio_new/callbacks.rs b/src/host/asio_new/callbacks.rs new file mode 100644 index 000000000..ddc8ed1d7 --- /dev/null +++ b/src/host/asio_new/callbacks.rs @@ -0,0 +1,268 @@ +use super::session::Session; +use super::simplex::{In, Out, Simplex}; +use crate::ErrorKind::*; +use crate::*; +use azo::sys::{ + AsioMessage, Bool, BufferSwitch, BufferSwitchTimeInfo, Callbacks as Pointers, MessageSelector, + SampleRateDidChange, Time, +}; +use closure_ffi::BareFnMutSync; +use parking_lot::Mutex; +use std::borrow::Cow; +use std::ffi::c_long; +use std::fmt::{self, Debug}; +use std::marker::PhantomPinned; +use std::pin::Pin; +use std::sync::Arc; +use tap::{Conv, Pipe}; + +const ASIO_VERSION_MAJOR: c_long = 2; // = 2.x + +const SUPPORTED_MESSAGE_SELECTORS: &[MessageSelector] = &[ + MessageSelector::SELECTOR_SUPPORTED, + MessageSelector::ENGINE_VERSION, + MessageSelector::RESET_REQUEST, + MessageSelector::BUFFER_SIZE_CHANGE, + MessageSelector::RESYNC_REQUEST, + MessageSelector::LATENCIES_CHANGED, + MessageSelector::SUPPORTS_TIME_INFO, + MessageSelector::SUPPORTS_TIME_CODE, + MessageSelector::OVERLOAD +]; + +type Bare = BareFnMutSync<'static, T>; + +#[derive(Debug)] +pub struct Callbacks { + pointers: Pointers, + closures: Option, + _marker : PhantomPinned +} + +impl Callbacks { + pub const fn pointers(&self) -> &Pointers { + &self.pointers + } + + pub fn populate(self: Pin<&mut Self>, context: context_type!()) { + // SAFETY: + // The ffi closures relying on this pin are (re-)created here + let mutable = unsafe { Pin::get_unchecked_mut(self) }; + + // If `self` has been populated before, then replacing the closures + // would cause the fn pointers to dangle, which would be UB because + // unlike regular raw pointers, fn pointers are implicitly non-nullable + mutable.pointers = Pointers::noop(); + + // it is now safe to overwrite the closures + mutable.closures = context + .pipe(Mutex::new) + .pipe(Closures::new) + .pipe(Some); + + // This makes `self` self-referential, which is why it needs to be pinned + mutable.pointers = mutable + .closures + .as_ref() + .unwrap() // infallible, as it was just assigned + .to_pointers(); + } +} + +impl Default for Callbacks { + fn default() -> Self { + Self { + pointers: Pointers::noop(), + closures: None, + _marker : PhantomPinned + } + } +} + +struct Closures { + buffer_switch : Bare, + sample_rate_did_change : Bare, + asio_message : Bare, + buffer_switch_time_info: Bare +} + +impl Closures { + fn new(context: Mutex) -> Self { + let arc1 = Arc::new(context); + let arc2 = Arc::clone(&arc1); + let arc3 = Arc::clone(&arc1); + let arc4 = Arc::clone(&arc1); + + Self { + sample_rate_did_change : create_sample_rate_did_change (arc1), + asio_message : create_asio_message (arc2), + buffer_switch_time_info: create_buffer_switch_time_info(arc3), + buffer_switch : create_buffer_switch (arc4), + } + } + + fn to_pointers(&self) -> Pointers { + Pointers { + buffer_switch : self.buffer_switch .bare(), + buffer_switch_time_info: self.buffer_switch_time_info.bare(), + sample_rate_did_change : self.sample_rate_did_change .bare(), + asio_message : self.asio_message .bare(), + } + } +} + +impl Debug for Closures { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(stringify!(Closures)) + .field("buffer_switch" , &self.buffer_switch .bare()) + .field("sample_rate_did_change" , &self.sample_rate_did_change .bare()) + .field("asio_message" , &self.asio_message .bare()) + .field("buffer_switch_time_info", &self.buffer_switch_time_info.bare()) + .finish() + } +} + +fn create_buffer_switch(context_handle: context_handle_type!()) -> Bare { + let closure = move |buffer_side: c_long, direct_process: Bool| { + let mut context = context_handle.lock(); + + let instant = context.session.now(); + context.process_buffers(direct_process, buffer_side as _, instant); + }; + + Bare::new_system(closure) +} + +fn create_sample_rate_did_change(context_handle: context_handle_type!()) -> Bare { + let closure = move |new_rate| { + let mut context = context_handle.lock(); + // `ErrorKind::Other` because this isn't fatal + context.throw(Other, format!("ASIO driver changed the sample rate (to {new_rate})")); + }; + + Bare::new_system(closure) +} + +fn create_asio_message(context_handle: context_handle_type!()) -> Bare { + let closure = move |selector, value, _message, _opt| { + let mut context = context_handle.lock(); + + match selector { + MessageSelector::SELECTOR_SUPPORTED => { + SUPPORTED_MESSAGE_SELECTORS + .contains(&MessageSelector(value)) + .conv::() + .0 + } + + MessageSelector::ENGINE_VERSION => { + ASIO_VERSION_MAJOR + } + + MessageSelector::RESET_REQUEST => { + context.throw(StreamInvalidated, "ASIO driver requested a reset"); + Bool::TRUE.0 + } + + MessageSelector::BUFFER_SIZE_CHANGE => { + if value.is_negative() { + context.throw(BackendError, format!("ASIO driver reported invalid buffer size: {value}")); + Bool::FALSE.0 + } else { + context.throw(StreamInvalidated, format!("ASIO driver changed its buffer size (to {value})")); + Bool::TRUE.0 + } + } + + MessageSelector::RESYNC_REQUEST => { + context.throw(StreamInvalidated, "ASIO driver requested a resync"); + Bool::TRUE.0 + }, + + MessageSelector::LATENCIES_CHANGED => { + context.update_latencies(); + Bool::TRUE.0 + } + + MessageSelector::SUPPORTS_TIME_INFO => { + Bool::TRUE.0 + } + + _ => Bool::FALSE.0 + } + }; + + Bare::new_system(closure) +} + +fn create_buffer_switch_time_info(context_handle: context_handle_type!()) -> Bare { + let closure = move |time_ptr: *mut Time, buffer_side: c_long, direct_process: Bool| { + let mut context = context_handle.lock(); + + match unsafe { time_ptr.as_ref() } { + Some(time) => context.process_buffers(direct_process, buffer_side as _, StreamInstant::from_millis(time.time_info.system_time as _)), + None => context.throw(BackendError, "ASIO driver produced invalid time pointer") + } + + time_ptr + }; + + Bare::new_system(closure) +} + +pub struct Context { + pub session : Arc, + pub data_cb : DataCb, + pub error_cb : ErrorCb, + pub sample_rate: SampleRate, + pub simplex_in : Simplex, + pub simplex_out: Simplex, +} + +impl Context +where + DataCb : FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, + ErrorCb: FnMut(Error) + Send + 'static +{ + fn process_buffers(&mut self, direct_process: Bool, buffer_side: usize, cb_time: StreamInstant) { + // The ASIO spec contexts `direct_process` to always be true on Windows, + // and dropped support for other platforms. But just in case: + if direct_process == Bool::FALSE { + self.throw(RealtimeDenied, "ASIO driver prohibits processing within the buffer switch callback"); + return; + } + + let data_in = self.simplex_in.data(buffer_side); + let mut data_out = self.simplex_out.data(buffer_side); + let callback_info = self.create_cb_info(cb_time); + + self.simplex_in.interleave(buffer_side); + (self.data_cb)(&data_in, &mut data_out, &callback_info); + self.simplex_out.deinterleave(buffer_side); + } + + fn create_cb_info(&self, cb_time: StreamInstant) -> DuplexCallbackInfo { + let time_in = cb_time - self.simplex_in .latency; + let time_out = cb_time + self.simplex_out.latency; + + [time_in, time_out] + .map (| dev_time | StreamTimestamp { callback: cb_time, device: dev_time }) + .map (| timestamp| CallbackInfo::new(timestamp, false)) + .pipe(|[in_, out]| DuplexCallbackInfo::new(in_, out)) + } + + fn update_latencies(&mut self) { + match self.session.latencies(self.sample_rate) { + Ok([latency_in, latency_out]) => { + self.simplex_in .latency = latency_in; + self.simplex_out.latency = latency_out; + } + Err(error) => { + (self.error_cb)(error); + }, + } + } + fn throw(&mut self, kind: ErrorKind, message: impl Into>) { + (self.error_cb)(Error::with_message(kind, message)); + } +} diff --git a/src/host/asio_new/capabilities.rs b/src/host/asio_new/capabilities.rs new file mode 100644 index 000000000..ada727886 --- /dev/null +++ b/src/host/asio_new/capabilities.rs @@ -0,0 +1,76 @@ +use super::utils::create_report; +use crate::ErrorKind::*; +use crate::*; +use azo::Driver; +use azo::dto::{ChannelCounts, ChannelId}; +use std::collections::HashSet; +use tap::Pipe; + +use super::{CpalResult, err, sample_format_asio2cpal}; + +pub fn channel_count(driver: &Driver) -> CpalResult { + channel_counts(driver) + .map(|counts| + if INPUT { counts.in_ } + else { counts.out } + ) +} + +pub fn channel_counts(driver: &Driver) -> CpalResult { + driver + .channel_counts() + .map_err(|error| Error::with_message(BackendError, format!("failed to retrieve channel coounts: {error}"))) +} + +pub fn sample_rates(driver: &Driver) -> CpalResult<(SampleRate, SampleRate)> { + let mut rates_iter = COMMON_SAMPLE_RATES + .iter() + .copied() + .filter(|rate| + driver + .can_sample_rate(*rate as _) + .is_ok() + ); + + let min = rates_iter.next().ok_or(Error::with_message(DeviceNotAvailable, "no supported sample rate found"))?; + let max = rates_iter.next_back().unwrap_or(min); + + Ok((min, max)) +} + +pub fn supported_buffer_size(driver: &Driver) -> SupportedBufferSize { + use crate::SupportedBufferSize::*; + + driver + .buffer_size() + .map_or( + Unknown, + |bs| Range { min: bs.min as _, max: bs.max as _ } + ) +} + +pub fn preferred_buffer_size(driver: &Driver) -> CpalResult { + let value = driver + .buffer_size() + .map_err(|error| create_report(driver, error, stringify!(Driver::buffer_size)))? + .preferred; + + if value.is_negative() { + return err(BackendError, format!("ASIO driver reported invalid buffer size {value}")); + } + + Ok(value) +} + +pub fn sample_formats(driver: &Driver, ch_count: i32) -> CpalResult> { + (0..ch_count) + .map(move |index| driver + .channel_info(ChannelId { index, input: INPUT }) + .map(|ch_info| ch_info.sample_type) + .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info))) + ) + .collect::>>()? // aggregates errors and deduplicates the values + .into_iter() + .filter_map(sample_format_asio2cpal) + .pipe(Ok) +} diff --git a/src/host/asio_new/mod.rs b/src/host/asio_new/mod.rs new file mode 100644 index 000000000..7c3c9adb2 --- /dev/null +++ b/src/host/asio_new/mod.rs @@ -0,0 +1,307 @@ +//! Experimental ASIO backend implementation. +//! +//! Available on Windows with the `asio-new` feature. + +use crate::ErrorKind::*; +use crate::traits::*; +use crate::*; +use std::fmt; +use std::fmt::Debug; +use std::hash::Hash; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use std::vec; +use tap::prelude::*; + +#[macro_use] +mod utils; +mod callbacks; +mod capabilities; +mod session; +mod simplex; + +use self::callbacks::Callbacks; +use self::session::Session; +use self::utils::*; + +#[derive(Debug, Clone)] +pub struct Host(Arc); + +impl Host { + /// Required by the `impl_platform_host!` macro + pub fn new() -> CpalResult { + session::Factory + ::new() + .pipe(Arc::new) + .pipe(Self) + .pipe(Ok) + } +} + +impl HostTrait for Host { + type Device = Device; + type Devices = Devices; + + fn is_available() -> bool { + // this will return false if the ASIO registry keys are either + // * missing - meaning no ASIO driver has ever been installed on the system + // * corrupted - in which case ASIO is unusable + azo::get_drivers().is_ok() + } + + fn devices(&self) -> CpalResult { + self.0 + .pipe_ref(Arc::clone) + .pipe(Devices::new) + .map_err(|win_error| Error::with_message(HostUnavailable, win_error.message())) + } + + fn default_input_device(&self) -> Option { + self.devices() + .ok()? + .into_iter() + .find(Device::supports_input) + } + + fn default_output_device(&self) -> Option { + self.devices() + .ok()? + .into_iter() + .find(Device::supports_output) + } + + fn device_by_id(&self, id: &DeviceId) -> Option { + if id.host() != HostId::AsioNew { + return None; + } + + let clsid = id.id().try_into().ok()?; + + self.0 + .get_session(&clsid) + .ok() + .map(Device) + } +} + +#[derive(Debug, Clone)] +pub struct Devices(Arc, vec::IntoIter); + +impl Devices { + pub fn new(factory: Arc) -> azo::WinResult { + let metas = azo::get_drivers()?.into_iter(); + + Ok(Self(factory, metas)) + } +} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + self.1 + .find_map(|metadata| + self.0 + .get_session(&metadata.clsid) + .ok() + ) + .map(Device) + } +} + +pub type SupportedConfigs = vec::IntoIter; + +#[expect( + clippy::derived_hash_with_manual_eq, + reason = "manual eq is more strict" +)] +#[derive(Debug, Hash)] +pub struct Device(Arc); + +impl Device { + fn new(session: Session) -> Self { + session + .pipe(Arc::new) + .pipe(Self) + } +} + +impl Clone for Device { + fn clone(&self) -> Self { + self.0 + .pipe_ref(Arc::clone) + .pipe(Self) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0.name()) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedConfigs; + type SupportedOutputConfigs = SupportedConfigs; + type Stream = Stream; + + fn description(&self) -> CpalResult { + self.0.description() + } + + fn id(&self) -> CpalResult { + self.0.id() + } + + fn supported_input_configs(&self) -> CpalResult { + self.0.supported_configs::() + } + + fn supported_output_configs(&self) -> CpalResult { + self.0.supported_configs::() + } + + fn default_input_config(&self) -> CpalResult { + self.0.default_config::() + } + + fn default_output_config(&self) -> CpalResult { + self.0.default_config::() + } + + fn supports_input(&self) -> bool { + self.0.supports_direction::() + } + + fn supports_output(&self) -> bool { + self.0.supports_direction::() + } + + fn supports_duplex(&self) -> bool { + self.supports_input() && self.supports_output() + } + + fn build_input_stream_raw( + &self, + config : StreamConfig, + format : SampleFormat, + mut data_cb: DataCb, + error_cb : ErrorCb, + timeout : Option, + ) -> CpalResult + where + DataCb: FnMut(&Data, &CallbackInfo) + Send + 'static, + ErrorCb: FnMut(Error) + Send + 'static, + { + let duplex_cfg = DuplexStreamConfig { + input_channels : config.channels, + output_channels: 0, + sample_rate : config.sample_rate, + buffer_size : config.buffer_size + }; + + self.build_duplex_stream_raw( + duplex_cfg, + format, + format, + move |data, _, cbi| data_cb(data, &cbi.input()), + error_cb, + timeout + ) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + format: SampleFormat, + mut data_cb: DataCb, + error_cb: ErrorCb, + timeout: Option, + ) -> CpalResult + where + DataCb: FnMut(&mut Data, &CallbackInfo) + Send + 'static, + ErrorCb: FnMut(Error) + Send + 'static, + { + let duplex_cfg = DuplexStreamConfig { + input_channels : 0, + output_channels: config.channels, + sample_rate : config.sample_rate, + buffer_size : config.buffer_size + }; + + self.build_duplex_stream_raw( + duplex_cfg, + format, + format, + move |_, data, cbi| data_cb(data, &cbi.output()), + error_cb, + timeout + ) + } + + fn build_duplex_stream_raw( + &self, + DuplexStreamConfig { input_channels, output_channels, sample_rate, buffer_size }: DuplexStreamConfig, + format_in : SampleFormat, + format_out: SampleFormat, + data_cb : DataCb, + error_cb : ErrorCb, + _timeout : Option, + ) -> CpalResult + where + DataCb: FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, + ErrorCb: FnMut(Error) + Send + 'static, + { + let cfg_in = simplex::Config { format: format_in , channels: input_channels , input: true }; + let cfg_out = simplex::Config { format: format_out, channels: output_channels, input: false }; + + Session::build_stream(&self.0, cfg_in, cfg_out, sample_rate, buffer_size, data_cb, error_cb) + } +} + +#[derive(Debug)] +pub struct Stream { + session : Arc, + frame_count: FrameCount, + _callbacks : Pin> +} + +unsafe impl Send for Stream {} +unsafe impl Sync for Stream {} + +impl StreamTrait for Stream { + fn start(&self) -> CpalResult<()> { + self.session.start() + } + + fn pause(&self) -> CpalResult<()> { + self.session.pause() + } + + fn stop(&self, _timeout: Option) -> CpalResult<()> { + self.session.stop() + } + + fn now(&self) -> StreamInstant { + self.session.now() + } + + fn buffer_size(&self) -> CpalResult { + Ok(self.frame_count) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + self.session.reset(); + } +} diff --git a/src/host/asio_new/session.rs b/src/host/asio_new/session.rs new file mode 100644 index 000000000..ed573d1af --- /dev/null +++ b/src/host/asio_new/session.rs @@ -0,0 +1,342 @@ +use crate::ErrorKind::*; +use crate::host::com; +use crate::*; +use azo::dto::ChannelCounts; +use azo::{Driver, WinResult}; +use parking_lot::{Mutex, RwLock}; +use std::collections::HashMap; +use std::ffi::CStr; +use std::fmt::Debug; +use std::hash::Hash; +use std::hash::Hasher; +use std::pin::Pin; +use std::sync::{Arc, Weak}; +use std::time::Duration; +use std::vec; +use tap::prelude::*; +use windows_core::GUID; + +use super::callbacks::{Callbacks, Context}; +use super::simplex::{In, Out, Simplex}; +use super::utils::{CpalResult, DoubleBuffer, create_report, err}; +use super::{SupportedConfigs, capabilities, simplex}; + +#[derive(Debug)] +pub struct Factory { + com_worker: com::worker::Handle, + cache: Mutex>>, +} + +impl Factory { + pub fn new() -> Self { + Self { + com_worker: com::worker::Handle::new(), + cache: Mutex::default(), + } + } + + pub fn get_session(&self, clsid: &GUID) -> WinResult> { + let mut guard = self.cache.lock(); + + if let Some(existing) = guard.get(clsid).and_then(Weak::upgrade) { + return Ok(existing); + } + + let new = Session + ::new(*clsid, &self.com_worker)? + .pipe(Arc::new); + + guard.insert(*clsid, Arc::downgrade(&new)); + + Ok(new) + } +} + +#[derive(Debug)] +pub struct Session { + state : RwLock, + clsid_string: String, + _com_worker : com::worker::Handle +} + +#[derive(Debug)] +struct State { + driver : Driver, + stage : AsioStage, +} + +impl Hash for Session { + fn hash(&self, state: &mut H) { + self.clsid_string.hash(state); + } +} + +impl Session { + pub fn new(clsid: GUID, com_worker: &com::worker::Handle) -> WinResult { + let driver = com_worker.create_driver(clsid)?; + let init_success= driver.init(None); + + Self { + state: State { + driver, + stage: if init_success { AsioStage::Initialized } else { AsioStage::Loaded }, + }.pipe(RwLock::new), + clsid_string: format!("{clsid:?}"), + _com_worker: com_worker.clone(), // hold on to this to keep the thread alive that initialized the COM apartment in which the driver was created + } + .pipe(Ok) + } + + pub fn name(&self) -> String { + self.state + .read() + .driver + .name() + .pipe_as_ref(CStr::to_string_lossy) + .into_owned() + } + + pub fn id(&self) -> CpalResult { + DeviceId::new( + HostId::AsioNew, + self.clsid_string.clone() + ) + .pipe(Ok) + } + + pub fn description(&self) -> CpalResult { + let state = self.state.read(); + + let name_c = state.driver.name(); + let name = name_c.to_string_lossy(); + + let direction = match capabilities::channel_counts(&state.driver)? { + ChannelCounts { in_: 1.., out: 1.. } => DeviceDirection::Duplex, + ChannelCounts { in_: 1.., out: 0 } => DeviceDirection::Input, + ChannelCounts { in_: 0 , out: 1.. } => DeviceDirection::Output, + _ => DeviceDirection::Unknown, + }; + + let mut extended = vec![format!("driver version: {}", state.driver.version())]; + + if state.stage < AsioStage::Initialized { + extended.push("ASIO driver failed to initialize".to_owned()); // ASIO drivers can often still do *something* when they fail to initialize + extended.push(format!("last error: {}", state.driver.last_error().to_string_lossy())); + } + + DeviceDescriptionBuilder + ::new(&name) + .driver(name) + .direction(direction) + .extended(extended) + .build() + .pipe(Ok) + } + + #[must_use] + pub fn supports_direction(&self) -> bool { + let Ok(counts) = self.state.read().driver.channel_counts() + else { return false; }; // can't do anything if it can't even count the channels + + if IN && counts.in_ == 0 { + return false; + } + + if OUT && counts.out == 0 { + return false; + } + + true + } + + pub fn supported_configs(&self) -> CpalResult { + let state = self.state.read(); + + let ch_count = capabilities::channel_count::(&state.driver)?; + let (min_rate, max_rate) = capabilities::sample_rates(&state.driver)?; + let buf_size = capabilities::supported_buffer_size(&state.driver); + let sample_formats = capabilities::sample_formats::(&state.driver, ch_count)?; + + sample_formats + .map(move |format| SupportedStreamConfigRange::new(ch_count as _, min_rate, max_rate, buf_size, format)) + .collect::>() + .into_iter() + .pipe(Ok) + } + + pub fn default_config(&self) -> CpalResult { + self.supported_configs::()? + .next() + .ok_or(Error::with_message(UnsupportedOperation, "the device has no channels in this direction"))? + .pipe(|range| + SupportedStreamConfig::new( + range.channels(), + range.min_sample_rate(), + *range.buffer_size(), + range.sample_format() + ) + ) + .pipe(Ok) + } + + pub fn build_stream( + self : &Arc, + cfg_in : simplex::Config, + cfg_out : simplex::Config, + sample_rate: SampleRate, + buffer_size: BufferSize, + data_cb : data_cb_type!(), + error_cb : error_cb_type!() + ) -> CpalResult { + let mut state = self.state.write(); + + if state.stage < AsioStage::Initialized { + return err(DeviceNotAvailable, "ASIO driver failed to initialize"); + } + if state.stage > AsioStage::Initialized { + return err(UnsupportedOperation, "ASIO only supports 1 stream per device"); + } + + state.set_sample_rate(sample_rate)?; + let frame_count = state.determine_buffer_size(buffer_size)?; + let callbacks = state.prepare(Arc::clone(self), sample_rate, frame_count, cfg_in, cfg_out, data_cb, error_cb)?; + + state.stage = AsioStage::Prepared; + + super::Stream { + session: Arc::clone(self), + frame_count, + _callbacks: callbacks // keep this alive until the stream is dropped + }.pipe(Ok) + } + + pub fn latencies(&self, sample_rate: SampleRate) -> CpalResult<[Duration; 2]> { + self.state + .read() + .temporal_latencies(sample_rate) + } + + pub fn start(&self) -> CpalResult<()> { + let mut state = self.state.write(); + + state.driver + .start() + .map_err(|error| create_report(&state.driver, error, stringify!(Driver::start))) + } + + pub fn pause(&self) -> CpalResult<()> { + let mut state = self.state.write(); + + state.driver + .stop() + .map_err(|error| create_report(&state.driver, error, stringify!(Driver::stop))) + } + + pub fn stop(&self) -> CpalResult<()> { + todo!() + } + + pub fn now(&self) -> StreamInstant { + self.state + .write() + .driver + .sample_position() + .map_or(0, |pos| pos.time_stamp as u64) // `StreamTrait` requires this functio to be infallible + .pipe(StreamInstant::from_millis) + } + + pub fn reset(&self) { + _ = self.pause(); // may fail if the stream is already halted + + let mut state = self.state.write(); + _ = state.driver.dispose_all_buffers(); // if something important goes wrong here, the driver will keep complaining in subsequent interactions + state.stage = AsioStage::Initialized; + } +} + +impl State { + fn set_sample_rate(&self, sample_rate: SampleRate) -> CpalResult<()> { + self.driver + .can_sample_rate(sample_rate as _) + .map_err(|_| Error::with_message(InvalidInput, "sample rate not supported"))?; + + self.driver + .set_sample_rate(sample_rate as _) + .map_err(|asio_error| create_report(&self.driver, asio_error, stringify!(Driver::set_sample_rate)))?; + + Ok(()) + } + + fn determine_buffer_size(&self, requested: BufferSize) -> CpalResult { + match requested { + BufferSize::Fixed(n) => n, + BufferSize::Default => capabilities::preferred_buffer_size(&self.driver)? as FrameCount, + } + .pipe(Ok) + } + + fn temporal_latencies(&self, sample_rate: SampleRate) -> CpalResult<[Duration; 2]> { + self.driver + .latencies() + .map_err(|error| create_report(&self.driver, error, stringify!(Driver::latencies)))? + .pipe(|latencies| [latencies.in_, latencies.out]) + .map(|latency| latency as f64 / sample_rate as f64) + .map(Duration::from_secs_f64) + .pipe(Ok) + } + + fn prepare( + &self, + session : Arc, + sample_rate: SampleRate, + frame_count: FrameCount, + cfg_in : simplex::Config, + cfg_out : simplex::Config, + data_cb : data_cb_type!(), + error_cb : error_cb_type!() + ) -> CpalResult>> { + let channel_ids: Vec<_> = [cfg_in, cfg_out] + .into_iter() + .flat_map(|cfg| cfg.validate(&self.driver)) + .collect::>()?; + + let [latency_in, latency_out] = self.temporal_latencies(sample_rate)?; + + // FIXME: consider using `Pin::defaul()` once MSRV has risen to 1.91+ + let mut callbacks = Callbacks::default().pipe(Box::pin); + + // SAFETY: + // `Callbacks` is pinned, and kept alive until after the buffers are disposed (see `Drop` implementation of `Stream`) + let mut double_buffers = + unsafe { self.driver.create_buffers(channel_ids, frame_count as _, callbacks.pointers()) } + .map_err(|error| create_report(&self.driver, error, stringify!(Driver::create_buffers)))? + .map(DoubleBuffer); + + let buffers_in = double_buffers.by_ref().take(cfg_in.channels as _).collect(); + let buffers_out = double_buffers.collect(); + + let state = Context { + data_cb , + error_cb , + sample_rate, + session , + simplex_in : Simplex::::new(cfg_in .format, frame_count, buffers_in , latency_in ), + simplex_out: Simplex::::new(cfg_out.format, frame_count, buffers_out, latency_out) + }; + + callbacks.as_mut().populate(state); + + Ok(callbacks) + } +} + +/// ASIO lifecycle stages (see ASIO specification section II.2) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AsioStage { + Unloaded, + Loaded, + Initialized, + Prepared, + Running, + Draining +} diff --git a/src/host/asio_new/simplex.rs b/src/host/asio_new/simplex.rs new file mode 100644 index 000000000..fa099b743 --- /dev/null +++ b/src/host/asio_new/simplex.rs @@ -0,0 +1,177 @@ +use super::*; +use azo::Driver; +use azo::dto::ChannelId; +use std::{ptr, slice}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Config { + pub format : SampleFormat, + pub channels: u16, + pub input : bool, +} + +impl Config { + pub fn validate(self, driver: &Driver) -> impl Iterator> { + (0..self.channels) + .map(move |i| { + let id = ChannelId { input: self.input, index: i as _ }; + let actual_format = driver + .channel_info(id) + .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info)))? + .sample_type + .pipe(sample_format_asio2cpal); + if actual_format != Some(self.format) { + return err(UnsupportedConfig, "Sample format mismatch"); + } + Ok(id) + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Head { + pub format : SampleFormat, + pub frame_count: FrameCount, + pub buf_ptrs : Vec +} + +impl Head { + const fn frame_count(&self) -> usize { + self.frame_count as usize + } + + fn channel_count(&self) -> usize { + self.buf_ptrs.len() + } + + fn sample_size(&self) -> usize { + self.format.sample_size() + } + + fn sample_count(&self) -> usize { + self.frame_count() * self.channel_count() + } + + fn bytes_per_channel(&self) -> usize { + self.frame_count() * self.sample_size() + } + + fn _frame_size(&self) -> usize { + self.channel_count() * self.sample_size() + } + + fn total_buffer_space(&self) -> usize { + self.frame_count() * self.channel_count() * self.sample_size() + } + + fn get_buf_ptr(&self, channel: usize, side: usize) -> *mut u8 { + self.buf_ptrs[channel] + .0[side] + .cast() + } + + fn get_buf<'buf>(&self, channel: usize, side: usize) -> &'buf [u8] { + let ptr = self.get_buf_ptr(channel, side); + let len = self.bytes_per_channel(); + unsafe { slice::from_raw_parts(ptr, len) } + } + + #[expect(clippy::needless_pass_by_ref_mut, reason = "match mutability of the returned slice")] + fn get_buf_mut<'buf>(&mut self, channel: usize, side: usize) -> &'buf mut [u8] { + let ptr = self.get_buf_ptr(channel, side); + let len = self.bytes_per_channel(); + unsafe { slice::from_raw_parts_mut(ptr, len) } + } +} + +pub struct Simplex { + head : Head, + scratch : Box<[u8]>, + pub latency: Duration, + _marker : Marker +} + +impl Simplex { + pub fn new(format: SampleFormat, frame_count: FrameCount, buf_ptrs: Vec, latency: Duration) -> Self { + let head = Head { format, frame_count, buf_ptrs }; + + // when the stream is mono, the ASIO buffer can be exposed to the user callback directly + let scratch_len = if head.channel_count() == 1 { 0 } else { head.total_buffer_space() }; + let scratch = vec![0; scratch_len].into_boxed_slice(); + Self { head, scratch, latency, _marker: Marker::default() } + } + + pub fn data(&mut self, side: usize) -> Data { + let ptr = match self.head.channel_count() { + 0 => ptr::null_mut(), + 1 => self.head.get_buf_ptr(0, side).cast(), + 2.. => self.scratch.as_mut_ptr().cast() + }; + unsafe { + Data::from_parts( + ptr, + self.head.sample_count(), + self.head.format + ) + } + } +} + +impl Simplex { + /// copies channel data to the scratch buffer, interleaving it in the process + pub fn interleave(&mut self, side: usize) { + if self.head.channel_count() < 2 { + // When the simplex is mono, the ASIO buffers are exposed directly + return; + } + + let stride = self.head.sample_size(); + let scratch_frames = self + .scratch + .chunks_exact_mut(self.head.channel_count() * stride); + + for (i_frame, scratch_frame) in scratch_frames.enumerate() { + for (i_channel, scratch_sample) in scratch_frame.chunks_exact_mut(stride).enumerate() { + let pos = i_frame * stride; + self.head + .get_buf(i_channel, side) + [pos..][..stride] + .pipe_ref(|slice| scratch_sample.copy_from_slice(slice)); + } + } + } +} + +impl Simplex { + /// copies scratch data to the channels, deinterleaving it in the process + pub fn deinterleave(&mut self, side: usize) { + if self.head.channel_count() < 2 { + // When the simplex is mono, the ASIO buffers are exposed directly + return; + } + + let stride = self.head.sample_size(); + let scratch_frames = self + .scratch + .chunks_exact(self.head.channel_count() * stride); + + for (i_frame, scratch_frame) in scratch_frames.enumerate() { + for (i_channel, scratch_sample) in scratch_frame.chunks_exact(stride).enumerate() { + let pos = i_frame * stride; + self.head + .get_buf_mut(i_channel, side) + [pos..][..stride] + .copy_from_slice(scratch_sample); + } + } + } +} + +pub trait DirectionMarker: Debug + Clone + Copy + Default + PartialEq + Eq + Hash {} +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct In; +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct Out; + +impl DirectionMarker for In {} +impl DirectionMarker for Out {} diff --git a/src/host/asio_new/utils.rs b/src/host/asio_new/utils.rs new file mode 100644 index 000000000..c7bcf8113 --- /dev/null +++ b/src/host/asio_new/utils.rs @@ -0,0 +1,88 @@ +use crate::ErrorKind::*; +use crate::*; +use azo::Driver; +use azo::dto::*; +use azo::sys::*; +use std::borrow::Cow; +use std::ffi::c_void; +use std::mem; + +pub type CpalResult = Result; + +/// workaround until `#![feature(type_alias_impl_trait)]` is stabilized +#[macro_export] +macro_rules! data_cb_type { + () => { impl FnMut(&$crate::Data, &mut $crate::Data, &$crate::DuplexCallbackInfo) + Send + 'static } +} +/// workaround until `#![feature(type_alias_impl_trait)]` is stabilized +#[macro_export] +macro_rules! error_cb_type { + () => { impl FnMut($crate::Error) + Send + 'static }; +} +/// workaround until `#![feature(type_alias_impl_trait)]` is stabilized +#[macro_export] +macro_rules! context_type { + () => { Context }; +} +/// workaround until `#![feature(type_alias_impl_trait)]` is stabilized +#[macro_export] +macro_rules! context_handle_type { + () => { Arc> }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// just to make the pointers `Send` +pub struct DoubleBuffer(pub [*mut c_void; 2]); + +unsafe impl Send for DoubleBuffer {} +unsafe impl Sync for DoubleBuffer {} + +use crate::SampleFormat as CpalFormat; +use azo::sys::SampleType as AsioFormat; + +pub const fn sample_format_asio2cpal(asio_format: AsioFormat) -> Option { + // FIXME: consider using `cfg_select!` here once the MSRV has risen to 1.95+ + const BIG_ENDIAN: bool = cfg!(target_endian = "big"); + const PCM_I16: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I16_MSB } else { AsioFormat::PCM_I16_LSB }; + const PCM_I24: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I32_MSB_24 } else { AsioFormat::PCM_I32_LSB_24 }; + const PCM_I32: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I32_MSB } else { AsioFormat::PCM_I32_LSB }; + const PCM_F32: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_F32_MSB } else { AsioFormat::PCM_F32_LSB }; + const DSD_U8 : AsioFormat = if BIG_ENDIAN { AsioFormat::DSD_I8_MSB_1 } else { AsioFormat::DSD_I8_LSB_1 }; + + #[deny(nonstandard_style, reason = "prevent accidental wildcard patterns")] + match asio_format { + PCM_I16 => Some(CpalFormat::I16), + PCM_I24 => Some(CpalFormat::I24), + PCM_I32 => Some(CpalFormat::I32), + PCM_F32 => Some(CpalFormat::F32), + DSD_U8 => Some(CpalFormat::DsdU8), + + _ => None, // no matching counterpart in cpal + } +} + +/// just for convenience +pub fn err(kind: ErrorKind, message: impl Into>) -> CpalResult { + Err(Error::with_message(kind, message)) +} + +pub fn create_report(driver: &Driver, asio_error: azo::Error, origin: &str) -> Error { + let last_error = driver.last_error(); + + Error::with_message( + BackendError, + format!("[ASIO] {origin}() failed with `{asio_error}` - {last_error:?}"), + ) +} + +pub fn create_minimal_asio_time(pos: &SamplePosition) -> Time { + Time { + time_info: TimeInfo { + system_time: pos.time_stamp, + sample_position: pos.position, + flags: TimeInfoFlags::SYSTEM_TIME_VALID | TimeInfoFlags::SAMPLE_POSITION_VALID, + ..unsafe { mem::zeroed() } + }, + ..unsafe { mem::zeroed() } + } +} diff --git a/src/host/com.rs b/src/host/com.rs index 2e9781760..69a2900df 100644 --- a/src/host/com.rs +++ b/src/host/com.rs @@ -7,6 +7,9 @@ use windows::Win32::{ System::Com::{COINIT_APARTMENTTHREADED, CoInitializeEx, CoTaskMemFree, CoUninitialize}, }; +#[cfg(feature = "asio-new")] +pub mod worker; + thread_local!(static COM_INITIALIZED: ComInitialized = { unsafe { // Try to initialize COM with STA by default to avoid compatibility issues with the ASIO diff --git a/src/host/com/worker.rs b/src/host/com/worker.rs new file mode 100644 index 000000000..a8e0f5b37 --- /dev/null +++ b/src/host/com/worker.rs @@ -0,0 +1,49 @@ +use std::sync::mpsc::{self, SyncSender}; +use std::thread; + +use azo::utils::com; +use azo::*; +use windows_core::GUID; + +type Request = (GUID, oneshot::Sender); +type Response = WinResult; + +#[derive(Debug, Clone)] +pub struct Handle(SyncSender); + +impl Handle { + pub fn new() -> Self { + let (sender, receiver) = mpsc::sync_channel::(0); + + // This thread will live exactly as long as we need it to, no more and no less. + // This is because `receiver.recv()` returns an error IFF all senders got dropped, + // causing the `while` loop to end, and the thread to run out (dropping the COM init + // guard along the way) + thread::spawn(move || { + // inits COM on creation, + // and uninits it on drop + let _guard = com::InitGuard::new(COINIT_APARTMENTTHREADED) + .expect("STA COM init on a fresh thread should be infallible"); + // except for stuff like E_OUTOFMEMORY of course, but that's pretty fatal anyway + + while let Ok((guid, ret)) = receiver.recv() { + let result = unsafe { Driver::new_unguarded(&guid) }; + _ = ret.send(result); // if the recipient bailed for some reason, just drop and continue + } + }); + + Self(sender) + } + + #[expect(clippy::unwrap_in_result, reason = "infallible")] + pub fn create_driver(&self, guid: GUID) -> Response { + let (ret_sender, ret_receiver) = oneshot::channel(); + + self.0 + .send((guid, ret_sender)) + .expect("the worker thread should never die prematurely"); + ret_receiver + .recv() + .expect("the worker thread should never die prematurely") + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index b1df58e1d..7efc35fac 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -26,6 +26,9 @@ pub(crate) mod alsa; #[cfg(all(windows, feature = "asio"))] pub(crate) mod asio; +#[cfg(all(windows, feature = "asio-new"))] +pub(crate) mod asio_new; + #[cfg(all( target_arch = "wasm32", target_os = "unknown", diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5b2d5aeb6..b9f18c0e0 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -961,10 +961,13 @@ mod platform_impl { use super::JackHost; #[cfg(feature = "asio")] use crate::host::asio::Host as AsioHost; + #[cfg(feature = "asio-new")] + use crate::host::asio_new::Host as AsioNewHost; use crate::host::wasapi::Host as WasapiHost; impl_platform_host!( #[cfg(feature = "asio")] Asio "ASIO" => AsioHost, + #[cfg(feature = "asio-new")] AsioNew "ASIOnew" => AsioNewHost, Wasapi "WASAPI" => WasapiHost, #[cfg(feature = "jack")] Jack "JACK" => JackHost, #[cfg(feature = "custom")] Custom => super::CustomHost, From 02e256bf6000e6b0f6e34f8b7ad1a223d0650dac Mon Sep 17 00:00:00 2001 From: LastExceed Date: Mon, 14 Sep 2026 16:54:16 +0200 Subject: [PATCH 2/2] rustfmt --- src/host/asio_new/callbacks.rs | 137 +++++++++++++++----------- src/host/asio_new/capabilities.rs | 60 +++++++----- src/host/asio_new/mod.rs | 92 +++++++++-------- src/host/asio_new/session.rs | 157 +++++++++++++++++------------- src/host/asio_new/simplex.rs | 99 ++++++++++--------- src/host/asio_new/utils.rs | 32 ++++-- 6 files changed, 337 insertions(+), 240 deletions(-) diff --git a/src/host/asio_new/callbacks.rs b/src/host/asio_new/callbacks.rs index ddc8ed1d7..4968379d6 100644 --- a/src/host/asio_new/callbacks.rs +++ b/src/host/asio_new/callbacks.rs @@ -27,7 +27,7 @@ const SUPPORTED_MESSAGE_SELECTORS: &[MessageSelector] = &[ MessageSelector::LATENCIES_CHANGED, MessageSelector::SUPPORTS_TIME_INFO, MessageSelector::SUPPORTS_TIME_CODE, - MessageSelector::OVERLOAD + MessageSelector::OVERLOAD, ]; type Bare = BareFnMutSync<'static, T>; @@ -36,7 +36,7 @@ type Bare = BareFnMutSync<'static, T>; pub struct Callbacks { pointers: Pointers, closures: Option, - _marker : PhantomPinned + _marker: PhantomPinned, } impl Callbacks { @@ -55,10 +55,7 @@ impl Callbacks { mutable.pointers = Pointers::noop(); // it is now safe to overwrite the closures - mutable.closures = context - .pipe(Mutex::new) - .pipe(Closures::new) - .pipe(Some); + mutable.closures = context.pipe(Mutex::new).pipe(Closures::new).pipe(Some); // This makes `self` self-referential, which is why it needs to be pinned mutable.pointers = mutable @@ -74,16 +71,16 @@ impl Default for Callbacks { Self { pointers: Pointers::noop(), closures: None, - _marker : PhantomPinned + _marker: PhantomPinned, } } } struct Closures { - buffer_switch : Bare, - sample_rate_did_change : Bare, - asio_message : Bare, - buffer_switch_time_info: Bare + buffer_switch: Bare, + sample_rate_did_change: Bare, + asio_message: Bare, + buffer_switch_time_info: Bare, } impl Closures { @@ -94,19 +91,19 @@ impl Closures { let arc4 = Arc::clone(&arc1); Self { - sample_rate_did_change : create_sample_rate_did_change (arc1), - asio_message : create_asio_message (arc2), + sample_rate_did_change: create_sample_rate_did_change(arc1), + asio_message: create_asio_message(arc2), buffer_switch_time_info: create_buffer_switch_time_info(arc3), - buffer_switch : create_buffer_switch (arc4), + buffer_switch: create_buffer_switch(arc4), } } fn to_pointers(&self) -> Pointers { Pointers { - buffer_switch : self.buffer_switch .bare(), + buffer_switch: self.buffer_switch.bare(), buffer_switch_time_info: self.buffer_switch_time_info.bare(), - sample_rate_did_change : self.sample_rate_did_change .bare(), - asio_message : self.asio_message .bare(), + sample_rate_did_change: self.sample_rate_did_change.bare(), + asio_message: self.asio_message.bare(), } } } @@ -114,10 +111,16 @@ impl Closures { impl Debug for Closures { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(stringify!(Closures)) - .field("buffer_switch" , &self.buffer_switch .bare()) - .field("sample_rate_did_change" , &self.sample_rate_did_change .bare()) - .field("asio_message" , &self.asio_message .bare()) - .field("buffer_switch_time_info", &self.buffer_switch_time_info.bare()) + .field("buffer_switch", &self.buffer_switch.bare()) + .field( + "sample_rate_did_change", + &self.sample_rate_did_change.bare(), + ) + .field("asio_message", &self.asio_message.bare()) + .field( + "buffer_switch_time_info", + &self.buffer_switch_time_info.bare(), + ) .finish() } } @@ -133,11 +136,16 @@ fn create_buffer_switch(context_handle: context_handle_type!()) -> Bare Bare { +fn create_sample_rate_did_change( + context_handle: context_handle_type!(), +) -> Bare { let closure = move |new_rate| { let mut context = context_handle.lock(); // `ErrorKind::Other` because this isn't fatal - context.throw(Other, format!("ASIO driver changed the sample rate (to {new_rate})")); + context.throw( + Other, + format!("ASIO driver changed the sample rate (to {new_rate})"), + ); }; Bare::new_system(closure) @@ -150,14 +158,12 @@ fn create_asio_message(context_handle: context_handle_type!()) -> Bare { SUPPORTED_MESSAGE_SELECTORS - .contains(&MessageSelector(value)) - .conv::() - .0 + .contains(&MessageSelector(value)) + .conv::() + .0 } - MessageSelector::ENGINE_VERSION => { - ASIO_VERSION_MAJOR - } + MessageSelector::ENGINE_VERSION => ASIO_VERSION_MAJOR, MessageSelector::RESET_REQUEST => { context.throw(StreamInvalidated, "ASIO driver requested a reset"); @@ -166,10 +172,16 @@ fn create_asio_message(context_handle: context_handle_type!()) -> Bare { if value.is_negative() { - context.throw(BackendError, format!("ASIO driver reported invalid buffer size: {value}")); + context.throw( + BackendError, + format!("ASIO driver reported invalid buffer size: {value}"), + ); Bool::FALSE.0 } else { - context.throw(StreamInvalidated, format!("ASIO driver changed its buffer size (to {value})")); + context.throw( + StreamInvalidated, + format!("ASIO driver changed its buffer size (to {value})"), + ); Bool::TRUE.0 } } @@ -177,31 +189,35 @@ fn create_asio_message(context_handle: context_handle_type!()) -> Bare { context.throw(StreamInvalidated, "ASIO driver requested a resync"); Bool::TRUE.0 - }, + } MessageSelector::LATENCIES_CHANGED => { context.update_latencies(); Bool::TRUE.0 } - MessageSelector::SUPPORTS_TIME_INFO => { - Bool::TRUE.0 - } + MessageSelector::SUPPORTS_TIME_INFO => Bool::TRUE.0, - _ => Bool::FALSE.0 + _ => Bool::FALSE.0, } }; Bare::new_system(closure) } -fn create_buffer_switch_time_info(context_handle: context_handle_type!()) -> Bare { +fn create_buffer_switch_time_info( + context_handle: context_handle_type!(), +) -> Bare { let closure = move |time_ptr: *mut Time, buffer_side: c_long, direct_process: Bool| { let mut context = context_handle.lock(); match unsafe { time_ptr.as_ref() } { - Some(time) => context.process_buffers(direct_process, buffer_side as _, StreamInstant::from_millis(time.time_info.system_time as _)), - None => context.throw(BackendError, "ASIO driver produced invalid time pointer") + Some(time) => context.process_buffers( + direct_process, + buffer_side as _, + StreamInstant::from_millis(time.time_info.system_time as _), + ), + None => context.throw(BackendError, "ASIO driver produced invalid time pointer"), } time_ptr @@ -211,29 +227,37 @@ fn create_buffer_switch_time_info(context_handle: context_handle_type!()) -> Bar } pub struct Context { - pub session : Arc, - pub data_cb : DataCb, - pub error_cb : ErrorCb, + pub session: Arc, + pub data_cb: DataCb, + pub error_cb: ErrorCb, pub sample_rate: SampleRate, - pub simplex_in : Simplex, + pub simplex_in: Simplex, pub simplex_out: Simplex, } impl Context where - DataCb : FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, - ErrorCb: FnMut(Error) + Send + 'static + DataCb: FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, + ErrorCb: FnMut(Error) + Send + 'static, { - fn process_buffers(&mut self, direct_process: Bool, buffer_side: usize, cb_time: StreamInstant) { + fn process_buffers( + &mut self, + direct_process: Bool, + buffer_side: usize, + cb_time: StreamInstant, + ) { // The ASIO spec contexts `direct_process` to always be true on Windows, // and dropped support for other platforms. But just in case: if direct_process == Bool::FALSE { - self.throw(RealtimeDenied, "ASIO driver prohibits processing within the buffer switch callback"); + self.throw( + RealtimeDenied, + "ASIO driver prohibits processing within the buffer switch callback", + ); return; } - let data_in = self.simplex_in.data(buffer_side); - let mut data_out = self.simplex_out.data(buffer_side); + let data_in = self.simplex_in.data(buffer_side); + let mut data_out = self.simplex_out.data(buffer_side); let callback_info = self.create_cb_info(cb_time); self.simplex_in.interleave(buffer_side); @@ -242,24 +266,27 @@ where } fn create_cb_info(&self, cb_time: StreamInstant) -> DuplexCallbackInfo { - let time_in = cb_time - self.simplex_in .latency; + let time_in = cb_time - self.simplex_in.latency; let time_out = cb_time + self.simplex_out.latency; [time_in, time_out] - .map (| dev_time | StreamTimestamp { callback: cb_time, device: dev_time }) - .map (| timestamp| CallbackInfo::new(timestamp, false)) - .pipe(|[in_, out]| DuplexCallbackInfo::new(in_, out)) + .map(|dev_time| StreamTimestamp { + callback: cb_time, + device: dev_time, + }) + .map(|timestamp| CallbackInfo::new(timestamp, false)) + .pipe(|[in_, out]| DuplexCallbackInfo::new(in_, out)) } fn update_latencies(&mut self) { match self.session.latencies(self.sample_rate) { Ok([latency_in, latency_out]) => { - self.simplex_in .latency = latency_in; + self.simplex_in.latency = latency_in; self.simplex_out.latency = latency_out; } Err(error) => { (self.error_cb)(error); - }, + } } } fn throw(&mut self, kind: ErrorKind, message: impl Into>) { diff --git a/src/host/asio_new/capabilities.rs b/src/host/asio_new/capabilities.rs index ada727886..d5262f25d 100644 --- a/src/host/asio_new/capabilities.rs +++ b/src/host/asio_new/capabilities.rs @@ -9,30 +9,28 @@ use tap::Pipe; use super::{CpalResult, err, sample_format_asio2cpal}; pub fn channel_count(driver: &Driver) -> CpalResult { - channel_counts(driver) - .map(|counts| - if INPUT { counts.in_ } - else { counts.out } - ) + channel_counts(driver).map(|counts| if INPUT { counts.in_ } else { counts.out }) } pub fn channel_counts(driver: &Driver) -> CpalResult { - driver - .channel_counts() - .map_err(|error| Error::with_message(BackendError, format!("failed to retrieve channel coounts: {error}"))) + driver.channel_counts().map_err(|error| { + Error::with_message( + BackendError, + format!("failed to retrieve channel coounts: {error}"), + ) + }) } pub fn sample_rates(driver: &Driver) -> CpalResult<(SampleRate, SampleRate)> { let mut rates_iter = COMMON_SAMPLE_RATES .iter() .copied() - .filter(|rate| - driver - .can_sample_rate(*rate as _) - .is_ok() - ); + .filter(|rate| driver.can_sample_rate(*rate as _).is_ok()); - let min = rates_iter.next().ok_or(Error::with_message(DeviceNotAvailable, "no supported sample rate found"))?; + let min = rates_iter.next().ok_or(Error::with_message( + DeviceNotAvailable, + "no supported sample rate found", + ))?; let max = rates_iter.next_back().unwrap_or(min); Ok((min, max)) @@ -41,12 +39,10 @@ pub fn sample_rates(driver: &Driver) -> CpalResult<(SampleRate, SampleRate)> { pub fn supported_buffer_size(driver: &Driver) -> SupportedBufferSize { use crate::SupportedBufferSize::*; - driver - .buffer_size() - .map_or( - Unknown, - |bs| Range { min: bs.min as _, max: bs.max as _ } - ) + driver.buffer_size().map_or(Unknown, |bs| Range { + min: bs.min as _, + max: bs.max as _, + }) } pub fn preferred_buffer_size(driver: &Driver) -> CpalResult { @@ -56,19 +52,29 @@ pub fn preferred_buffer_size(driver: &Driver) -> CpalResult { .preferred; if value.is_negative() { - return err(BackendError, format!("ASIO driver reported invalid buffer size {value}")); + return err( + BackendError, + format!("ASIO driver reported invalid buffer size {value}"), + ); } Ok(value) } -pub fn sample_formats(driver: &Driver, ch_count: i32) -> CpalResult> { +pub fn sample_formats( + driver: &Driver, + ch_count: i32, +) -> CpalResult> { (0..ch_count) - .map(move |index| driver - .channel_info(ChannelId { index, input: INPUT }) - .map(|ch_info| ch_info.sample_type) - .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info))) - ) + .map(move |index| { + driver + .channel_info(ChannelId { + index, + input: INPUT, + }) + .map(|ch_info| ch_info.sample_type) + .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info))) + }) .collect::>>()? // aggregates errors and deduplicates the values .into_iter() .filter_map(sample_format_asio2cpal) diff --git a/src/host/asio_new/mod.rs b/src/host/asio_new/mod.rs index 7c3c9adb2..5e36e673b 100644 --- a/src/host/asio_new/mod.rs +++ b/src/host/asio_new/mod.rs @@ -31,11 +31,7 @@ pub struct Host(Arc); impl Host { /// Required by the `impl_platform_host!` macro pub fn new() -> CpalResult { - session::Factory - ::new() - .pipe(Arc::new) - .pipe(Self) - .pipe(Ok) + session::Factory::new().pipe(Arc::new).pipe(Self).pipe(Ok) } } @@ -78,10 +74,7 @@ impl HostTrait for Host { let clsid = id.id().try_into().ok()?; - self.0 - .get_session(&clsid) - .ok() - .map(Device) + self.0.get_session(&clsid).ok().map(Device) } } @@ -101,11 +94,7 @@ impl Iterator for Devices { fn next(&mut self) -> Option { self.1 - .find_map(|metadata| - self.0 - .get_session(&metadata.clsid) - .ok() - ) + .find_map(|metadata| self.0.get_session(&metadata.clsid).ok()) .map(Device) } } @@ -121,17 +110,13 @@ pub struct Device(Arc); impl Device { fn new(session: Session) -> Self { - session - .pipe(Arc::new) - .pipe(Self) + session.pipe(Arc::new).pipe(Self) } } impl Clone for Device { fn clone(&self) -> Self { - self.0 - .pipe_ref(Arc::clone) - .pipe(Self) + self.0.pipe_ref(Arc::clone).pipe(Self) } } @@ -192,21 +177,21 @@ impl DeviceTrait for Device { fn build_input_stream_raw( &self, - config : StreamConfig, - format : SampleFormat, + config: StreamConfig, + format: SampleFormat, mut data_cb: DataCb, - error_cb : ErrorCb, - timeout : Option, + error_cb: ErrorCb, + timeout: Option, ) -> CpalResult where DataCb: FnMut(&Data, &CallbackInfo) + Send + 'static, ErrorCb: FnMut(Error) + Send + 'static, { let duplex_cfg = DuplexStreamConfig { - input_channels : config.channels, + input_channels: config.channels, output_channels: 0, - sample_rate : config.sample_rate, - buffer_size : config.buffer_size + sample_rate: config.sample_rate, + buffer_size: config.buffer_size, }; self.build_duplex_stream_raw( @@ -215,7 +200,7 @@ impl DeviceTrait for Device { format, move |data, _, cbi| data_cb(data, &cbi.input()), error_cb, - timeout + timeout, ) } @@ -232,10 +217,10 @@ impl DeviceTrait for Device { ErrorCb: FnMut(Error) + Send + 'static, { let duplex_cfg = DuplexStreamConfig { - input_channels : 0, + input_channels: 0, output_channels: config.channels, - sample_rate : config.sample_rate, - buffer_size : config.buffer_size + sample_rate: config.sample_rate, + buffer_size: config.buffer_size, }; self.build_duplex_stream_raw( @@ -244,35 +229,56 @@ impl DeviceTrait for Device { format, move |_, data, cbi| data_cb(data, &cbi.output()), error_cb, - timeout + timeout, ) } fn build_duplex_stream_raw( &self, - DuplexStreamConfig { input_channels, output_channels, sample_rate, buffer_size }: DuplexStreamConfig, - format_in : SampleFormat, + DuplexStreamConfig { + input_channels, + output_channels, + sample_rate, + buffer_size, + }: DuplexStreamConfig, + format_in: SampleFormat, format_out: SampleFormat, - data_cb : DataCb, - error_cb : ErrorCb, - _timeout : Option, + data_cb: DataCb, + error_cb: ErrorCb, + _timeout: Option, ) -> CpalResult where DataCb: FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, ErrorCb: FnMut(Error) + Send + 'static, - { - let cfg_in = simplex::Config { format: format_in , channels: input_channels , input: true }; - let cfg_out = simplex::Config { format: format_out, channels: output_channels, input: false }; + { + let cfg_in = simplex::Config { + format: format_in, + channels: input_channels, + input: true, + }; + let cfg_out = simplex::Config { + format: format_out, + channels: output_channels, + input: false, + }; - Session::build_stream(&self.0, cfg_in, cfg_out, sample_rate, buffer_size, data_cb, error_cb) + Session::build_stream( + &self.0, + cfg_in, + cfg_out, + sample_rate, + buffer_size, + data_cb, + error_cb, + ) } } #[derive(Debug)] pub struct Stream { - session : Arc, + session: Arc, frame_count: FrameCount, - _callbacks : Pin> + _callbacks: Pin>, } unsafe impl Send for Stream {} diff --git a/src/host/asio_new/session.rs b/src/host/asio_new/session.rs index ed573d1af..2d063b949 100644 --- a/src/host/asio_new/session.rs +++ b/src/host/asio_new/session.rs @@ -42,9 +42,7 @@ impl Factory { return Ok(existing); } - let new = Session - ::new(*clsid, &self.com_worker)? - .pipe(Arc::new); + let new = Session::new(*clsid, &self.com_worker)?.pipe(Arc::new); guard.insert(*clsid, Arc::downgrade(&new)); @@ -54,15 +52,15 @@ impl Factory { #[derive(Debug)] pub struct Session { - state : RwLock, + state: RwLock, clsid_string: String, - _com_worker : com::worker::Handle + _com_worker: com::worker::Handle, } #[derive(Debug)] struct State { - driver : Driver, - stage : AsioStage, + driver: Driver, + stage: AsioStage, } impl Hash for Session { @@ -74,13 +72,18 @@ impl Hash for Session { impl Session { pub fn new(clsid: GUID, com_worker: &com::worker::Handle) -> WinResult { let driver = com_worker.create_driver(clsid)?; - let init_success= driver.init(None); + let init_success = driver.init(None); Self { state: State { driver, - stage: if init_success { AsioStage::Initialized } else { AsioStage::Loaded }, - }.pipe(RwLock::new), + stage: if init_success { + AsioStage::Initialized + } else { + AsioStage::Loaded + }, + } + .pipe(RwLock::new), clsid_string: format!("{clsid:?}"), _com_worker: com_worker.clone(), // hold on to this to keep the thread alive that initialized the COM apartment in which the driver was created } @@ -97,11 +100,7 @@ impl Session { } pub fn id(&self) -> CpalResult { - DeviceId::new( - HostId::AsioNew, - self.clsid_string.clone() - ) - .pipe(Ok) + DeviceId::new(HostId::AsioNew, self.clsid_string.clone()).pipe(Ok) } pub fn description(&self) -> CpalResult { @@ -112,20 +111,22 @@ impl Session { let direction = match capabilities::channel_counts(&state.driver)? { ChannelCounts { in_: 1.., out: 1.. } => DeviceDirection::Duplex, - ChannelCounts { in_: 1.., out: 0 } => DeviceDirection::Input, - ChannelCounts { in_: 0 , out: 1.. } => DeviceDirection::Output, - _ => DeviceDirection::Unknown, + ChannelCounts { in_: 1.., out: 0 } => DeviceDirection::Input, + ChannelCounts { in_: 0, out: 1.. } => DeviceDirection::Output, + _ => DeviceDirection::Unknown, }; let mut extended = vec![format!("driver version: {}", state.driver.version())]; if state.stage < AsioStage::Initialized { extended.push("ASIO driver failed to initialize".to_owned()); // ASIO drivers can often still do *something* when they fail to initialize - extended.push(format!("last error: {}", state.driver.last_error().to_string_lossy())); + extended.push(format!( + "last error: {}", + state.driver.last_error().to_string_lossy() + )); } - DeviceDescriptionBuilder - ::new(&name) + DeviceDescriptionBuilder::new(&name) .driver(name) .direction(direction) .extended(extended) @@ -135,8 +136,9 @@ impl Session { #[must_use] pub fn supports_direction(&self) -> bool { - let Ok(counts) = self.state.read().driver.channel_counts() - else { return false; }; // can't do anything if it can't even count the channels + let Ok(counts) = self.state.read().driver.channel_counts() else { + return false; + }; // can't do anything if it can't even count the channels if IN && counts.in_ == 0 { return false; @@ -152,13 +154,15 @@ impl Session { pub fn supported_configs(&self) -> CpalResult { let state = self.state.read(); - let ch_count = capabilities::channel_count::(&state.driver)?; + let ch_count = capabilities::channel_count::(&state.driver)?; let (min_rate, max_rate) = capabilities::sample_rates(&state.driver)?; - let buf_size = capabilities::supported_buffer_size(&state.driver); - let sample_formats = capabilities::sample_formats::(&state.driver, ch_count)?; + let buf_size = capabilities::supported_buffer_size(&state.driver); + let sample_formats = capabilities::sample_formats::(&state.driver, ch_count)?; sample_formats - .map(move |format| SupportedStreamConfigRange::new(ch_count as _, min_rate, max_rate, buf_size, format)) + .map(move |format| { + SupportedStreamConfigRange::new(ch_count as _, min_rate, max_rate, buf_size, format) + }) .collect::>() .into_iter() .pipe(Ok) @@ -167,26 +171,29 @@ impl Session { pub fn default_config(&self) -> CpalResult { self.supported_configs::()? .next() - .ok_or(Error::with_message(UnsupportedOperation, "the device has no channels in this direction"))? - .pipe(|range| + .ok_or(Error::with_message( + UnsupportedOperation, + "the device has no channels in this direction", + ))? + .pipe(|range| { SupportedStreamConfig::new( range.channels(), range.min_sample_rate(), *range.buffer_size(), - range.sample_format() + range.sample_format(), ) - ) + }) .pipe(Ok) } - - pub fn build_stream( - self : &Arc, - cfg_in : simplex::Config, - cfg_out : simplex::Config, + + pub fn build_stream( + self: &Arc, + cfg_in: simplex::Config, + cfg_out: simplex::Config, sample_rate: SampleRate, buffer_size: BufferSize, - data_cb : data_cb_type!(), - error_cb : error_cb_type!() + data_cb: data_cb_type!(), + error_cb: error_cb_type!(), ) -> CpalResult { let mut state = self.state.write(); @@ -194,32 +201,43 @@ impl Session { return err(DeviceNotAvailable, "ASIO driver failed to initialize"); } if state.stage > AsioStage::Initialized { - return err(UnsupportedOperation, "ASIO only supports 1 stream per device"); + return err( + UnsupportedOperation, + "ASIO only supports 1 stream per device", + ); } state.set_sample_rate(sample_rate)?; let frame_count = state.determine_buffer_size(buffer_size)?; - let callbacks = state.prepare(Arc::clone(self), sample_rate, frame_count, cfg_in, cfg_out, data_cb, error_cb)?; + let callbacks = state.prepare( + Arc::clone(self), + sample_rate, + frame_count, + cfg_in, + cfg_out, + data_cb, + error_cb, + )?; state.stage = AsioStage::Prepared; super::Stream { session: Arc::clone(self), frame_count, - _callbacks: callbacks // keep this alive until the stream is dropped - }.pipe(Ok) + _callbacks: callbacks, // keep this alive until the stream is dropped + } + .pipe(Ok) } pub fn latencies(&self, sample_rate: SampleRate) -> CpalResult<[Duration; 2]> { - self.state - .read() - .temporal_latencies(sample_rate) + self.state.read().temporal_latencies(sample_rate) } pub fn start(&self) -> CpalResult<()> { let mut state = self.state.write(); - state.driver + state + .driver .start() .map_err(|error| create_report(&state.driver, error, stringify!(Driver::start))) } @@ -227,7 +245,8 @@ impl Session { pub fn pause(&self) -> CpalResult<()> { let mut state = self.state.write(); - state.driver + state + .driver .stop() .map_err(|error| create_report(&state.driver, error, stringify!(Driver::stop))) } @@ -262,7 +281,13 @@ impl State { self.driver .set_sample_rate(sample_rate as _) - .map_err(|asio_error| create_report(&self.driver, asio_error, stringify!(Driver::set_sample_rate)))?; + .map_err(|asio_error| { + create_report( + &self.driver, + asio_error, + stringify!(Driver::set_sample_rate), + ) + })?; Ok(()) } @@ -270,7 +295,7 @@ impl State { fn determine_buffer_size(&self, requested: BufferSize) -> CpalResult { match requested { BufferSize::Fixed(n) => n, - BufferSize::Default => capabilities::preferred_buffer_size(&self.driver)? as FrameCount, + BufferSize::Default => capabilities::preferred_buffer_size(&self.driver)? as FrameCount, } .pipe(Ok) } @@ -287,13 +312,13 @@ impl State { fn prepare( &self, - session : Arc, + session: Arc, sample_rate: SampleRate, frame_count: FrameCount, - cfg_in : simplex::Config, - cfg_out : simplex::Config, - data_cb : data_cb_type!(), - error_cb : error_cb_type!() + cfg_in: simplex::Config, + cfg_out: simplex::Config, + data_cb: data_cb_type!(), + error_cb: error_cb_type!(), ) -> CpalResult>> { let channel_ids: Vec<_> = [cfg_in, cfg_out] .into_iter() @@ -307,21 +332,23 @@ impl State { // SAFETY: // `Callbacks` is pinned, and kept alive until after the buffers are disposed (see `Drop` implementation of `Stream`) - let mut double_buffers = - unsafe { self.driver.create_buffers(channel_ids, frame_count as _, callbacks.pointers()) } - .map_err(|error| create_report(&self.driver, error, stringify!(Driver::create_buffers)))? - .map(DoubleBuffer); + let mut double_buffers = unsafe { + self.driver + .create_buffers(channel_ids, frame_count as _, callbacks.pointers()) + } + .map_err(|error| create_report(&self.driver, error, stringify!(Driver::create_buffers)))? + .map(DoubleBuffer); - let buffers_in = double_buffers.by_ref().take(cfg_in.channels as _).collect(); + let buffers_in = double_buffers.by_ref().take(cfg_in.channels as _).collect(); let buffers_out = double_buffers.collect(); let state = Context { - data_cb , - error_cb , + data_cb, + error_cb, sample_rate, - session , - simplex_in : Simplex::::new(cfg_in .format, frame_count, buffers_in , latency_in ), - simplex_out: Simplex::::new(cfg_out.format, frame_count, buffers_out, latency_out) + session, + simplex_in: Simplex::::new(cfg_in.format, frame_count, buffers_in, latency_in), + simplex_out: Simplex::::new(cfg_out.format, frame_count, buffers_out, latency_out), }; callbacks.as_mut().populate(state); @@ -338,5 +365,5 @@ pub enum AsioStage { Initialized, Prepared, Running, - Draining + Draining, } diff --git a/src/host/asio_new/simplex.rs b/src/host/asio_new/simplex.rs index fa099b743..2992a3bc3 100644 --- a/src/host/asio_new/simplex.rs +++ b/src/host/asio_new/simplex.rs @@ -5,34 +5,36 @@ use std::{ptr, slice}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Config { - pub format : SampleFormat, + pub format: SampleFormat, pub channels: u16, - pub input : bool, + pub input: bool, } impl Config { pub fn validate(self, driver: &Driver) -> impl Iterator> { - (0..self.channels) - .map(move |i| { - let id = ChannelId { input: self.input, index: i as _ }; - let actual_format = driver - .channel_info(id) - .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info)))? - .sample_type - .pipe(sample_format_asio2cpal); - if actual_format != Some(self.format) { - return err(UnsupportedConfig, "Sample format mismatch"); - } - Ok(id) - }) + (0..self.channels).map(move |i| { + let id = ChannelId { + input: self.input, + index: i as _, + }; + let actual_format = driver + .channel_info(id) + .map_err(|error| create_report(driver, error, stringify!(Driver::channel_info)))? + .sample_type + .pipe(sample_format_asio2cpal); + if actual_format != Some(self.format) { + return err(UnsupportedConfig, "Sample format mismatch"); + } + Ok(id) + }) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Head { - pub format : SampleFormat, + pub format: SampleFormat, pub frame_count: FrameCount, - pub buf_ptrs : Vec + pub buf_ptrs: Vec, } impl Head { @@ -65,9 +67,7 @@ impl Head { } fn get_buf_ptr(&self, channel: usize, side: usize) -> *mut u8 { - self.buf_ptrs[channel] - .0[side] - .cast() + self.buf_ptrs[channel].0[side].cast() } fn get_buf<'buf>(&self, channel: usize, side: usize) -> &'buf [u8] { @@ -76,7 +76,10 @@ impl Head { unsafe { slice::from_raw_parts(ptr, len) } } - #[expect(clippy::needless_pass_by_ref_mut, reason = "match mutability of the returned slice")] + #[expect( + clippy::needless_pass_by_ref_mut, + reason = "match mutability of the returned slice" + )] fn get_buf_mut<'buf>(&mut self, channel: usize, side: usize) -> &'buf mut [u8] { let ptr = self.get_buf_ptr(channel, side); let len = self.bytes_per_channel(); @@ -85,35 +88,47 @@ impl Head { } pub struct Simplex { - head : Head, - scratch : Box<[u8]>, + head: Head, + scratch: Box<[u8]>, pub latency: Duration, - _marker : Marker + _marker: Marker, } impl Simplex { - pub fn new(format: SampleFormat, frame_count: FrameCount, buf_ptrs: Vec, latency: Duration) -> Self { - let head = Head { format, frame_count, buf_ptrs }; + pub fn new( + format: SampleFormat, + frame_count: FrameCount, + buf_ptrs: Vec, + latency: Duration, + ) -> Self { + let head = Head { + format, + frame_count, + buf_ptrs, + }; // when the stream is mono, the ASIO buffer can be exposed to the user callback directly - let scratch_len = if head.channel_count() == 1 { 0 } else { head.total_buffer_space() }; + let scratch_len = if head.channel_count() == 1 { + 0 + } else { + head.total_buffer_space() + }; let scratch = vec![0; scratch_len].into_boxed_slice(); - Self { head, scratch, latency, _marker: Marker::default() } + Self { + head, + scratch, + latency, + _marker: Marker::default(), + } } pub fn data(&mut self, side: usize) -> Data { let ptr = match self.head.channel_count() { - 0 => ptr::null_mut(), - 1 => self.head.get_buf_ptr(0, side).cast(), - 2.. => self.scratch.as_mut_ptr().cast() + 0 => ptr::null_mut(), + 1 => self.head.get_buf_ptr(0, side).cast(), + 2.. => self.scratch.as_mut_ptr().cast(), }; - unsafe { - Data::from_parts( - ptr, - self.head.sample_count(), - self.head.format - ) - } + unsafe { Data::from_parts(ptr, self.head.sample_count(), self.head.format) } } } @@ -133,9 +148,7 @@ impl Simplex { for (i_frame, scratch_frame) in scratch_frames.enumerate() { for (i_channel, scratch_sample) in scratch_frame.chunks_exact_mut(stride).enumerate() { let pos = i_frame * stride; - self.head - .get_buf(i_channel, side) - [pos..][..stride] + self.head.get_buf(i_channel, side)[pos..][..stride] .pipe_ref(|slice| scratch_sample.copy_from_slice(slice)); } } @@ -158,9 +171,7 @@ impl Simplex { for (i_frame, scratch_frame) in scratch_frames.enumerate() { for (i_channel, scratch_sample) in scratch_frame.chunks_exact(stride).enumerate() { let pos = i_frame * stride; - self.head - .get_buf_mut(i_channel, side) - [pos..][..stride] + self.head.get_buf_mut(i_channel, side)[pos..][..stride] .copy_from_slice(scratch_sample); } } diff --git a/src/host/asio_new/utils.rs b/src/host/asio_new/utils.rs index c7bcf8113..f7403bb3f 100644 --- a/src/host/asio_new/utils.rs +++ b/src/host/asio_new/utils.rs @@ -43,11 +43,31 @@ use azo::sys::SampleType as AsioFormat; pub const fn sample_format_asio2cpal(asio_format: AsioFormat) -> Option { // FIXME: consider using `cfg_select!` here once the MSRV has risen to 1.95+ const BIG_ENDIAN: bool = cfg!(target_endian = "big"); - const PCM_I16: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I16_MSB } else { AsioFormat::PCM_I16_LSB }; - const PCM_I24: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I32_MSB_24 } else { AsioFormat::PCM_I32_LSB_24 }; - const PCM_I32: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_I32_MSB } else { AsioFormat::PCM_I32_LSB }; - const PCM_F32: AsioFormat = if BIG_ENDIAN { AsioFormat::PCM_F32_MSB } else { AsioFormat::PCM_F32_LSB }; - const DSD_U8 : AsioFormat = if BIG_ENDIAN { AsioFormat::DSD_I8_MSB_1 } else { AsioFormat::DSD_I8_LSB_1 }; + const PCM_I16: AsioFormat = if BIG_ENDIAN { + AsioFormat::PCM_I16_MSB + } else { + AsioFormat::PCM_I16_LSB + }; + const PCM_I24: AsioFormat = if BIG_ENDIAN { + AsioFormat::PCM_I32_MSB_24 + } else { + AsioFormat::PCM_I32_LSB_24 + }; + const PCM_I32: AsioFormat = if BIG_ENDIAN { + AsioFormat::PCM_I32_MSB + } else { + AsioFormat::PCM_I32_LSB + }; + const PCM_F32: AsioFormat = if BIG_ENDIAN { + AsioFormat::PCM_F32_MSB + } else { + AsioFormat::PCM_F32_LSB + }; + const DSD_U8: AsioFormat = if BIG_ENDIAN { + AsioFormat::DSD_I8_MSB_1 + } else { + AsioFormat::DSD_I8_LSB_1 + }; #[deny(nonstandard_style, reason = "prevent accidental wildcard patterns")] match asio_format { @@ -55,7 +75,7 @@ pub const fn sample_format_asio2cpal(asio_format: AsioFormat) -> Option Some(CpalFormat::I24), PCM_I32 => Some(CpalFormat::I32), PCM_F32 => Some(CpalFormat::F32), - DSD_U8 => Some(CpalFormat::DsdU8), + DSD_U8 => Some(CpalFormat::DsdU8), _ => None, // no matching counterpart in cpal }