From 4851ba886c7fcd65f515fe3810721b7b6df22d5f 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 | 12 +- README.md | 1 + src/host/asio_new/callbacks.rs | 219 ++++++++++++++++++++ src/host/asio_new/capabilities.rs | 73 +++++++ src/host/asio_new/mod.rs | 319 ++++++++++++++++++++++++++++++ src/host/asio_new/session.rs | 248 +++++++++++++++++++++++ src/host/asio_new/simplex.rs | 160 +++++++++++++++ src/host/asio_new/utils.rs | 78 ++++++++ src/host/com.rs | 3 + src/host/com/worker.rs | 49 +++++ src/host/mod.rs | 3 + src/platform/mod.rs | 3 + 12 files changed, 1167 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..eb3490516 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"] + # 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,11 @@ 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 } +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..81dfcf156 --- /dev/null +++ b/src/host/asio_new/callbacks.rs @@ -0,0 +1,219 @@ +use super::*; +use azo::sys::Callbacks as Pointers; +use azo::sys::{AsioMessage, Bool, BufferSwitch, BufferSwitchTimeInfo, MessageSelector, SampleRateDidChange, Time}; +use closure_ffi::BareFnMutSync; +use std::ffi::c_long; +use std::fmt::{self, Debug}; +use std::marker::PhantomPinned; + +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: Closures, + _marker : PhantomPinned +} + +impl Callbacks { + pub const fn pointers(&self) -> &Pointers { + &self.pointers + } + + pub fn prime( + self: Pin<&mut Self>, + session : Arc, + data_cb : data_cb_type!(), + error_cb : error_cb_type!(), + simplex_in : simplex::WithScratch, + simplex_out: simplex::WithScratch + ) { + // SAFETY: + // The closures relying on this pin are (re-)created here + let mutable = unsafe { Pin::get_unchecked_mut(self) }; + + let error_cb1 = error_cb + .pipe(Mutex::new) + .pipe(Arc::new); + + let error_cb2 = Arc::clone(&error_cb1); + let error_cb3 = Arc::clone(&error_cb1); + let error_cb4 = Arc::clone(&error_cb1); + + mutable.closures.sample_rate_did_change = create_sample_rate_did_change (error_cb2); + mutable.closures.asio_message = create_asio_message (error_cb3); + mutable.closures.buffer_switch_time_info = create_buffer_switch_time_info(error_cb4, data_cb, simplex_in, simplex_out); + mutable.closures.buffer_switch = create_buffer_switch (error_cb1, session, mutable.closures.buffer_switch_time_info.bare()); + + mutable.pointers.buffer_switch = mutable.closures.buffer_switch .bare(); + mutable.pointers.buffer_switch_time_info = mutable.closures.buffer_switch_time_info.bare(); + mutable.pointers.sample_rate_did_change = mutable.closures.sample_rate_did_change .bare(); + mutable.pointers.asio_message = mutable.closures.asio_message .bare(); + } +} + +impl Default for Callbacks { + fn default() -> Self { + Self { + pointers: Pointers::noop(), + closures: Closures::noop(), + _marker : PhantomPinned + } + } +} + +pub struct Closures { + buffer_switch : Bare, + sample_rate_did_change : Bare, + asio_message : Bare, + buffer_switch_time_info: Bare +} + +impl Closures { + fn noop() -> Self { + Self { + buffer_switch : Bare::new_system(|_, _| ()), + sample_rate_did_change : Bare::new_system(|_| ()), + asio_message : Bare::new_system(|_, _, _, _| 0), + buffer_switch_time_info: Bare::new_system(|time, _, _| time), + } + } +} + +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() + } +} + +/// forwards to bsti by retrieving [`Time`] the old way +pub fn create_buffer_switch( + error_cb: Arc>, + session : Arc, + bsti_ptr: BufferSwitchTimeInfo +) -> Bare { + let closure = + move |buf_idx: c_long, direct_process: Bool| match session.driver.sample_position() { + Ok(pos) => { + let mut time = create_minimal_asio_time(&pos); + unsafe { bsti_ptr(&raw mut time, buf_idx, direct_process); } + } + Err(error) => throw( + &error_cb, + create_report(&session.driver, error, "sample_position") + ) + }; + + Bare::new_system(closure) +} + +pub fn create_sample_rate_did_change(error_cb: Arc>) -> Bare { + let closure = move |new_rate| { + // `ErrorKind::Other` because this isn't fatal + throw(&error_cb, Error::with_message(Other, format!("ASIO driver changed the sample rate (to {new_rate})"))); + }; + + Bare::new_system(closure) +} + +pub fn create_asio_message(error_cb: Arc>) -> Bare { + let closure = move |selector, value, _message, _opt| { + match selector { + MessageSelector::SELECTOR_SUPPORTED => + SUPPORTED_MESSAGE_SELECTORS + .contains(&MessageSelector(value)) + .conv::() + .0, + + MessageSelector::ENGINE_VERSION => + ASIO_VERSION_MAJOR, + + MessageSelector::RESET_REQUEST => { + throw(&error_cb, Error::with_message(StreamInvalidated, "ASIO driver requested a reset")); + Bool::TRUE.0 + } + + MessageSelector::BUFFER_SIZE_CHANGE => { + if value.is_negative() { + throw(&error_cb, Error::with_message(BackendError, format!("ASIO driver reported invalid buffer size: {value}"))); + Bool::FALSE + } else { + throw(&error_cb, Error::with_message(StreamInvalidated, format!("ASIO driver changed its buffer size (to {value})"))); + Bool::TRUE + } + .0 + } + + MessageSelector::RESYNC_REQUEST => { + throw(&error_cb, Error::with_message(StreamInvalidated, "ASIO driver requested a resync")); + Bool::TRUE.0 + }, + + MessageSelector::SUPPORTS_TIME_INFO => + Bool::TRUE.0, + + _ => Bool::FALSE.0 + } + }; + + Bare::new_system(closure) +} + +pub fn create_buffer_switch_time_info( + error_callback : Arc>, + mut data_callback: data_cb_type!(), + mut simplex_in : simplex::WithScratch, + mut simplex_out : simplex::WithScratch +) -> Bare { + let closure = move |time: *mut Time, buf_idx: c_long, direct_process: Bool| { + // The ASIO spec claims `direct_process` to always be true on Windows, + // and dropped support for other platforms. But just in case: + if direct_process != Bool::TRUE { + throw(&error_callback, Error::with_message(RealtimeDenied, "ASIO driver prohibits processing within the buffer switch callback")); + return time; + } + + let callback_info = unsafe { time.read() } + .time_info + .system_time + .pipe(|time| StreamInstant::from_millis(time as _)) + .pipe(|instant| StreamTimestamp { callback: instant, device: instant }) + .pipe(|stamp| CallbackInfo::new(stamp, false)) + .pipe(|cbi| DuplexCallbackInfo::new(cbi, cbi)); + + simplex_in.interleave(buf_idx as _); + data_callback( + &simplex_in.data(buf_idx as _), + &mut simplex_out.data(buf_idx as _), + &callback_info, + ); + simplex_out.deinterleave(buf_idx as _); + + time + }; + + Bare::new_system(closure) +} + +fn throw(error_cb: &Mutex, error: Error) { + error_cb.lock().expect("mutex poisoned")(error); +} diff --git a/src/host/asio_new/capabilities.rs b/src/host/asio_new/capabilities.rs new file mode 100644 index 000000000..0051248cb --- /dev/null +++ b/src/host/asio_new/capabilities.rs @@ -0,0 +1,73 @@ +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 buffer_size_supported(driver: &Driver) -> SupportedBufferSize { + use crate::SupportedBufferSize::*; + + driver + .buffer_size() + .map_or(Unknown, |bs| Range { min: bs.min as _, max: bs.max as _ }) +} + +pub fn buffer_size_preferred(driver: &Driver) -> CpalResult { + let value = driver + .buffer_size() + .map_err(|error| Error::with_message(BackendError, format!("buffer size lookup failed: {error}")))? + .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, "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..a0dc523ae --- /dev/null +++ b/src/host/asio_new/mod.rs @@ -0,0 +1,319 @@ +//! 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, Mutex}; +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.driver.name().to_string_lossy()) + } +} + +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.0.supports_direction::() + } + + 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 }; + + self.0.build_stream(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 + .driver + .start() + .map_err(|error| create_report(&self.session.driver, error, "start")) + } + + fn pause(&self) -> CpalResult<()> { + self.session + .driver + .stop() + .map_err(|error| create_report(&self.session.driver, error, "stop")) + } + + fn stop(&self, _timeout: Option) -> Result<(), Error> { + self.pause() + } + + fn now(&self) -> StreamInstant { + self.session + .driver + .sample_position() + .map_or(0, |pos| pos.time_stamp as u64) + .pipe(StreamInstant::from_millis) + } + + fn buffer_size(&self) -> CpalResult { + Ok(self.frame_count) // ASIO channels are always mono + } +} + +impl Drop for Stream { + fn drop(&mut self) { + _ = self.pause(); // might fail if the stream is already halted + _ = self.session.driver.dispose_all_buffers(); // if something important goes wrong here, the driver will keep complaining in subsequent interactions + *self.session.stream_exists.lock().expect("mutex poisoned") = false; + } +} diff --git a/src/host/asio_new/session.rs b/src/host/asio_new/session.rs new file mode 100644 index 000000000..946e0856c --- /dev/null +++ b/src/host/asio_new/session.rs @@ -0,0 +1,248 @@ +use crate::ErrorKind::*; +use crate::host::com; +use crate::*; +use azo::dto::ChannelCounts; +use azo::{Driver, WinResult}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::hash::Hasher; +use std::pin::Pin; +use std::sync::{Arc, Mutex, Weak}; +use std::vec; +use tap::prelude::*; +use windows_core::{GUID, Interface}; + +use super::callbacks::Callbacks; +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(), + } + } + + #[expect(clippy::unwrap_in_result, reason = "should be infallible")] + pub fn get_session(&self, clsid: &GUID) -> WinResult> { + let mut guard = self.cache.lock().expect("Mutex poisoned"); + + 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 { + pub driver : Driver, + init_success : bool, + clsid_string : String, + pub stream_exists: Mutex, + _com_worker : com::worker::Handle +} + +impl Session { + pub fn new(clsid: GUID, com_worker: &com::worker::Handle) -> WinResult { + let driver = com_worker.create_driver(clsid)?; + + Self { + init_success: driver.init(None), + driver, + clsid_string: format!("{clsid:?}"), + stream_exists: Mutex::default(), + _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 id(&self) -> CpalResult { + DeviceId::new( + HostId::AsioNew, + self.clsid_string.clone() + ) + .pipe(Ok) + } + + pub fn description(&self) -> CpalResult { + let name_c = self.driver.name(); + let name = name_c.to_string_lossy(); + + let direction = match capabilities::channel_counts(&self.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: {}", self.driver.version())]; + + if !self.init_success { + 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: {}", self.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 { + if !self.init_success { + return false; + } + + let Ok(counts) = self.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 ch_count = capabilities::channel_count::(&self.driver)?; + if ch_count == 0 { + return err(UnsupportedOperation, "the device has no channels in this direction"); + } + + let (min_rate, max_rate) = capabilities::sample_rates(&self.driver)?; + let buf_size = capabilities::buffer_size_supported(&self.driver); + let sample_formats = capabilities::sample_formats::(&self.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() + .expect("infallible") + .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 guard = self.stream_exists.lock().or(err(DeviceNotAvailable, "Mutex poisoned. This device is most likely dead"))?; + if *guard { + return err(UnsupportedOperation, "ASIO only supports 1 stream per device"); + } + + self.set_sample_rate(sample_rate)?; + let frame_count = self.get_frame_count(buffer_size)?; + let callbacks = self.prepare(cfg_in, cfg_out, frame_count, data_cb, error_cb)?; + + *guard = true; + + super::Stream { + session: Arc::clone(self), + frame_count, + _callbacks: callbacks // keep this alive until the stream is dropped + }.pipe(Ok) + } + + 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, "set_sample_rate"))?; + + Ok(()) + } + + fn get_frame_count(&self, requested: BufferSize) -> CpalResult { + match requested { + BufferSize::Fixed(n) => n, + BufferSize::Default => capabilities::buffer_size_preferred(&self.driver)? as FrameCount, + } + .pipe(Ok) + } + + /// ASIO lifecycle stage 2 ("initialized") -> stage 3 ("prepared") + /// - See ASIO specification section II.2 + fn prepare( + self : &Arc, + cfg_in : simplex::Config, + cfg_out : simplex::Config, + frame_count: FrameCount, + 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::>()?; + + // 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, "create_buffers"))? + .map(DoubleBuffer); + + let buffers_in = double_buffers.by_ref().take(cfg_in.channels as _).collect(); + let buffers_out = double_buffers.collect(); + let simplex_in = simplex::WithScratch::new(cfg_in .format, frame_count, buffers_in ); + let simplex_out = simplex::WithScratch::new(cfg_out.format, frame_count, buffers_out); + + callbacks.as_mut().prime(Arc::clone(self), data_cb, error_cb, simplex_in, simplex_out); + + Ok(callbacks) + } +} + +impl Hash for Session { + fn hash(&self, state: &mut H) { + self.driver.as_raw().as_raw().hash(state); + self.init_success.hash(state); + self.clsid_string.hash(state); + } +} diff --git a/src/host/asio_new/simplex.rs b/src/host/asio_new/simplex.rs new file mode 100644 index 000000000..b39bc201a --- /dev/null +++ b/src/host/asio_new/simplex.rs @@ -0,0 +1,160 @@ +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, "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)] +pub 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, dbuf_side: usize) -> *mut u8 { + self.buf_ptrs[channel] + .0[dbuf_side] + .cast() + } + + fn get_buf<'buf>(&self, channel: usize, dbuf_side: usize) -> &'buf [u8] { + let ptr = self.get_buf_ptr(channel, dbuf_side); + let len = self.bytes_per_channel(); + unsafe { slice::from_raw_parts(ptr, len) } + } + + fn get_buf_mut<'buf>(&self, channel: usize, dbuf_side: usize) -> &'buf mut [u8] { + let ptr = self.get_buf_ptr(channel, dbuf_side); + let len = self.bytes_per_channel(); + unsafe { slice::from_raw_parts_mut(ptr, len) } + } +} + +pub struct WithScratch { + head: Head, + scratch: Box<[u8]> +} + +impl WithScratch { + pub fn new(format: SampleFormat, frame_count: FrameCount, buf_ptrs: Vec) -> 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 } + } + + pub fn data(&mut self, dbuf_side: usize) -> Data { + let ptr = match self.head.channel_count() { + 0 => ptr::null_mut(), + 1 => self.head.get_buf_ptr(0, dbuf_side).cast(), + 2.. => self.scratch.as_mut_ptr().cast() + }; + unsafe { + Data::from_parts( + ptr, + self.head.sample_count(), + self.head.format + ) + } + } + + /// copies channel data to the scratch buffer, interleaving it in the process + pub fn interleave(&mut self, dbuf_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, dbuf_side) + [pos..][..stride] + .pipe_ref(|slice| scratch_sample.copy_from_slice(slice)); + } + } + } + /// copies scratch data to the channels, deinterleaving it in the process + pub fn deinterleave(&self, dbuf_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, dbuf_side) + [pos..][..stride] + .copy_from_slice(scratch_sample); + } + } + } +} diff --git a/src/host/asio_new/utils.rs b/src/host/asio_new/utils.rs new file mode 100644 index 000000000..e6ae68620 --- /dev/null +++ b/src/host/asio_new/utils.rs @@ -0,0 +1,78 @@ +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 }; +} + +#[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!(".{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 7f16e5f676e1497c713330fda149ee1be53e90f9 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 9 Sep 2026 08:37:47 +0200 Subject: [PATCH 2/2] rustfmt --- src/host/asio_new/callbacks.rs | 182 +++++++++++++++++++----------- src/host/asio_new/capabilities.rs | 61 ++++++---- src/host/asio_new/mod.rs | 87 +++++++------- src/host/asio_new/session.rs | 117 ++++++++++--------- src/host/asio_new/simplex.rs | 82 +++++++------- src/host/asio_new/utils.rs | 32 +++++- 6 files changed, 325 insertions(+), 236 deletions(-) diff --git a/src/host/asio_new/callbacks.rs b/src/host/asio_new/callbacks.rs index 81dfcf156..ae68a0934 100644 --- a/src/host/asio_new/callbacks.rs +++ b/src/host/asio_new/callbacks.rs @@ -1,6 +1,9 @@ use super::*; use azo::sys::Callbacks as Pointers; -use azo::sys::{AsioMessage, Bool, BufferSwitch, BufferSwitchTimeInfo, MessageSelector, SampleRateDidChange, Time}; +use azo::sys::{ + AsioMessage, Bool, BufferSwitch, BufferSwitchTimeInfo, MessageSelector, SampleRateDidChange, + Time, +}; use closure_ffi::BareFnMutSync; use std::ffi::c_long; use std::fmt::{self, Debug}; @@ -17,7 +20,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>; @@ -26,7 +29,7 @@ type Bare = BareFnMutSync<'static, T>; pub struct Callbacks { pointers: Pointers, closures: Closures, - _marker : PhantomPinned + _marker: PhantomPinned, } impl Callbacks { @@ -36,33 +39,36 @@ impl Callbacks { pub fn prime( self: Pin<&mut Self>, - session : Arc, - data_cb : data_cb_type!(), - error_cb : error_cb_type!(), - simplex_in : simplex::WithScratch, - simplex_out: simplex::WithScratch + session: Arc, + data_cb: data_cb_type!(), + error_cb: error_cb_type!(), + simplex_in: simplex::WithScratch, + simplex_out: simplex::WithScratch, ) { // SAFETY: // The closures relying on this pin are (re-)created here let mutable = unsafe { Pin::get_unchecked_mut(self) }; - let error_cb1 = error_cb - .pipe(Mutex::new) - .pipe(Arc::new); + let error_cb1 = error_cb.pipe(Mutex::new).pipe(Arc::new); let error_cb2 = Arc::clone(&error_cb1); let error_cb3 = Arc::clone(&error_cb1); let error_cb4 = Arc::clone(&error_cb1); - mutable.closures.sample_rate_did_change = create_sample_rate_did_change (error_cb2); - mutable.closures.asio_message = create_asio_message (error_cb3); - mutable.closures.buffer_switch_time_info = create_buffer_switch_time_info(error_cb4, data_cb, simplex_in, simplex_out); - mutable.closures.buffer_switch = create_buffer_switch (error_cb1, session, mutable.closures.buffer_switch_time_info.bare()); + mutable.closures.sample_rate_did_change = create_sample_rate_did_change(error_cb2); + mutable.closures.asio_message = create_asio_message(error_cb3); + mutable.closures.buffer_switch_time_info = + create_buffer_switch_time_info(error_cb4, data_cb, simplex_in, simplex_out); + mutable.closures.buffer_switch = create_buffer_switch( + error_cb1, + session, + mutable.closures.buffer_switch_time_info.bare(), + ); - mutable.pointers.buffer_switch = mutable.closures.buffer_switch .bare(); + mutable.pointers.buffer_switch = mutable.closures.buffer_switch.bare(); mutable.pointers.buffer_switch_time_info = mutable.closures.buffer_switch_time_info.bare(); - mutable.pointers.sample_rate_did_change = mutable.closures.sample_rate_did_change .bare(); - mutable.pointers.asio_message = mutable.closures.asio_message .bare(); + mutable.pointers.sample_rate_did_change = mutable.closures.sample_rate_did_change.bare(); + mutable.pointers.asio_message = mutable.closures.asio_message.bare(); } } @@ -71,24 +77,24 @@ impl Default for Callbacks { Self { pointers: Pointers::noop(), closures: Closures::noop(), - _marker : PhantomPinned + _marker: PhantomPinned, } } } pub 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 { fn noop() -> Self { Self { - buffer_switch : Bare::new_system(|_, _| ()), - sample_rate_did_change : Bare::new_system(|_| ()), - asio_message : Bare::new_system(|_, _, _, _| 0), + buffer_switch: Bare::new_system(|_, _| ()), + sample_rate_did_change: Bare::new_system(|_| ()), + asio_message: Bare::new_system(|_, _, _, _| 0), buffer_switch_time_info: Bare::new_system(|time, _, _| time), } } @@ -98,9 +104,15 @@ 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( + "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_time_info", + &self.buffer_switch_time_info.bare(), + ) .finish() } } @@ -108,87 +120,118 @@ impl Debug for Closures { /// forwards to bsti by retrieving [`Time`] the old way pub fn create_buffer_switch( error_cb: Arc>, - session : Arc, - bsti_ptr: BufferSwitchTimeInfo + session: Arc, + bsti_ptr: BufferSwitchTimeInfo, ) -> Bare { let closure = move |buf_idx: c_long, direct_process: Bool| match session.driver.sample_position() { Ok(pos) => { let mut time = create_minimal_asio_time(&pos); - unsafe { bsti_ptr(&raw mut time, buf_idx, direct_process); } + unsafe { + bsti_ptr(&raw mut time, buf_idx, direct_process); + } } Err(error) => throw( &error_cb, - create_report(&session.driver, error, "sample_position") - ) + create_report(&session.driver, error, "sample_position"), + ), }; Bare::new_system(closure) } -pub fn create_sample_rate_did_change(error_cb: Arc>) -> Bare { +pub fn create_sample_rate_did_change( + error_cb: Arc>, +) -> Bare { let closure = move |new_rate| { // `ErrorKind::Other` because this isn't fatal - throw(&error_cb, Error::with_message(Other, format!("ASIO driver changed the sample rate (to {new_rate})"))); + throw( + &error_cb, + Error::with_message( + Other, + format!("ASIO driver changed the sample rate (to {new_rate})"), + ), + ); }; Bare::new_system(closure) } pub fn create_asio_message(error_cb: Arc>) -> Bare { - let closure = move |selector, value, _message, _opt| { - match selector { - MessageSelector::SELECTOR_SUPPORTED => - SUPPORTED_MESSAGE_SELECTORS + let closure = move |selector, value, _message, _opt| match selector { + MessageSelector::SELECTOR_SUPPORTED => { + SUPPORTED_MESSAGE_SELECTORS .contains(&MessageSelector(value)) .conv::() - .0, + .0 + } - MessageSelector::ENGINE_VERSION => - ASIO_VERSION_MAJOR, + MessageSelector::ENGINE_VERSION => ASIO_VERSION_MAJOR, - MessageSelector::RESET_REQUEST => { - throw(&error_cb, Error::with_message(StreamInvalidated, "ASIO driver requested a reset")); - Bool::TRUE.0 - } + MessageSelector::RESET_REQUEST => { + throw( + &error_cb, + Error::with_message(StreamInvalidated, "ASIO driver requested a reset"), + ); + Bool::TRUE.0 + } - MessageSelector::BUFFER_SIZE_CHANGE => { - if value.is_negative() { - throw(&error_cb, Error::with_message(BackendError, format!("ASIO driver reported invalid buffer size: {value}"))); - Bool::FALSE - } else { - throw(&error_cb, Error::with_message(StreamInvalidated, format!("ASIO driver changed its buffer size (to {value})"))); - Bool::TRUE - } - .0 + MessageSelector::BUFFER_SIZE_CHANGE => { + if value.is_negative() { + throw( + &error_cb, + Error::with_message( + BackendError, + format!("ASIO driver reported invalid buffer size: {value}"), + ), + ); + Bool::FALSE + } else { + throw( + &error_cb, + Error::with_message( + StreamInvalidated, + format!("ASIO driver changed its buffer size (to {value})"), + ), + ); + Bool::TRUE } + .0 + } - MessageSelector::RESYNC_REQUEST => { - throw(&error_cb, Error::with_message(StreamInvalidated, "ASIO driver requested a resync")); - Bool::TRUE.0 - }, + MessageSelector::RESYNC_REQUEST => { + throw( + &error_cb, + Error::with_message(StreamInvalidated, "ASIO driver requested a resync"), + ); + 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) } pub fn create_buffer_switch_time_info( - error_callback : Arc>, + error_callback: Arc>, mut data_callback: data_cb_type!(), - mut simplex_in : simplex::WithScratch, - mut simplex_out : simplex::WithScratch + mut simplex_in: simplex::WithScratch, + mut simplex_out: simplex::WithScratch, ) -> Bare { let closure = move |time: *mut Time, buf_idx: c_long, direct_process: Bool| { // The ASIO spec claims `direct_process` to always be true on Windows, // and dropped support for other platforms. But just in case: if direct_process != Bool::TRUE { - throw(&error_callback, Error::with_message(RealtimeDenied, "ASIO driver prohibits processing within the buffer switch callback")); + throw( + &error_callback, + Error::with_message( + RealtimeDenied, + "ASIO driver prohibits processing within the buffer switch callback", + ), + ); return time; } @@ -196,7 +239,10 @@ pub fn create_buffer_switch_time_info( .time_info .system_time .pipe(|time| StreamInstant::from_millis(time as _)) - .pipe(|instant| StreamTimestamp { callback: instant, device: instant }) + .pipe(|instant| StreamTimestamp { + callback: instant, + device: instant, + }) .pipe(|stamp| CallbackInfo::new(stamp, false)) .pipe(|cbi| DuplexCallbackInfo::new(cbi, cbi)); diff --git a/src/host/asio_new/capabilities.rs b/src/host/asio_new/capabilities.rs index 0051248cb..ea7e9700f 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,31 +39,44 @@ pub fn sample_rates(driver: &Driver) -> CpalResult<(SampleRate, SampleRate)> { pub fn buffer_size_supported(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 buffer_size_preferred(driver: &Driver) -> CpalResult { let value = driver .buffer_size() - .map_err(|error| Error::with_message(BackendError, format!("buffer size lookup failed: {error}")))? + .map_err(|error| { + Error::with_message(BackendError, format!("buffer size lookup failed: {error}")) + })? .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, "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, "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 a0dc523ae..db5611a01 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,49 @@ 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 }; - - self.0.build_stream(cfg_in, cfg_out, sample_rate, buffer_size, data_cb, error_cb) + { + 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, + }; + + self.0 + .build_stream(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 946e0856c..40bd2b140 100644 --- a/src/host/asio_new/session.rs +++ b/src/host/asio_new/session.rs @@ -48,11 +48,11 @@ impl Factory { #[derive(Debug)] pub struct Session { - pub driver : Driver, - init_success : bool, - clsid_string : String, + pub driver: Driver, + init_success: bool, + clsid_string: String, pub stream_exists: Mutex, - _com_worker : com::worker::Handle + _com_worker: com::worker::Handle, } impl Session { @@ -70,11 +70,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 { @@ -83,20 +79,22 @@ impl Session { let direction = match capabilities::channel_counts(&self.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: {}", self.driver.version())]; if !self.init_success { 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: {}", self.driver.last_error().to_string_lossy())); + extended.push(format!( + "last error: {}", + self.driver.last_error().to_string_lossy() + )); } - DeviceDescriptionBuilder - ::new(&name) + DeviceDescriptionBuilder::new(&name) .driver(name) .direction(direction) .extended(extended) @@ -110,8 +108,9 @@ impl Session { return false; } - let Ok(counts) = self.driver.channel_counts() - else { return false; }; // can't do anything if it can't even count the channels + let Ok(counts) = self.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; @@ -127,15 +126,20 @@ impl Session { pub fn supported_configs(&self) -> CpalResult { let ch_count = capabilities::channel_count::(&self.driver)?; if ch_count == 0 { - return err(UnsupportedOperation, "the device has no channels in this direction"); + return err( + UnsupportedOperation, + "the device has no channels in this direction", + ); } let (min_rate, max_rate) = capabilities::sample_rates(&self.driver)?; - let buf_size = capabilities::buffer_size_supported(&self.driver); - let sample_formats = capabilities::sample_formats::(&self.driver, ch_count)?; + let buf_size = capabilities::buffer_size_supported(&self.driver); + let sample_formats = capabilities::sample_formats::(&self.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) @@ -145,42 +149,49 @@ impl Session { self.supported_configs::()? .next() .expect("infallible") - .pipe(|range| + .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 guard = self.stream_exists.lock().or(err(DeviceNotAvailable, "Mutex poisoned. This device is most likely dead"))?; + let mut guard = self.stream_exists.lock().or(err( + DeviceNotAvailable, + "Mutex poisoned. This device is most likely dead", + ))?; if *guard { - return err(UnsupportedOperation, "ASIO only supports 1 stream per device"); + return err( + UnsupportedOperation, + "ASIO only supports 1 stream per device", + ); } - + self.set_sample_rate(sample_rate)?; let frame_count = self.get_frame_count(buffer_size)?; let callbacks = self.prepare(cfg_in, cfg_out, frame_count, data_cb, error_cb)?; - + *guard = true; - + 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) } fn set_sample_rate(&self, sample_rate: SampleRate) -> CpalResult<()> { @@ -198,7 +209,7 @@ impl Session { fn get_frame_count(&self, requested: BufferSize) -> CpalResult { match requested { BufferSize::Fixed(n) => n, - BufferSize::Default => capabilities::buffer_size_preferred(&self.driver)? as FrameCount, + BufferSize::Default => capabilities::buffer_size_preferred(&self.driver)? as FrameCount, } .pipe(Ok) } @@ -206,12 +217,12 @@ impl Session { /// ASIO lifecycle stage 2 ("initialized") -> stage 3 ("prepared") /// - See ASIO specification section II.2 fn prepare( - self : &Arc, - cfg_in : simplex::Config, - cfg_out : simplex::Config, + self: &Arc, + cfg_in: simplex::Config, + cfg_out: simplex::Config, frame_count: FrameCount, - data_cb : data_cb_type!(), - error_cb : error_cb_type!() + data_cb: data_cb_type!(), + error_cb: error_cb_type!(), ) -> CpalResult>> { let channel_ids: Vec<_> = [cfg_in, cfg_out] .into_iter() @@ -223,17 +234,21 @@ impl Session { // 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, "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, "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 simplex_in = simplex::WithScratch::new(cfg_in .format, frame_count, buffers_in ); + let simplex_in = simplex::WithScratch::new(cfg_in.format, frame_count, buffers_in); let simplex_out = simplex::WithScratch::new(cfg_out.format, frame_count, buffers_out); - callbacks.as_mut().prime(Arc::clone(self), data_cb, error_cb, simplex_in, simplex_out); + callbacks + .as_mut() + .prime(Arc::clone(self), data_cb, error_cb, simplex_in, simplex_out); Ok(callbacks) } diff --git a/src/host/asio_new/simplex.rs b/src/host/asio_new/simplex.rs index b39bc201a..b6a9b880a 100644 --- a/src/host/asio_new/simplex.rs +++ b/src/host/asio_new/simplex.rs @@ -5,26 +5,28 @@ 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, "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, "channel_info"))? + .sample_type + .pipe(sample_format_asio2cpal); + if actual_format != Some(self.format) { + return err(UnsupportedConfig, "Sample format mismatch"); + } + Ok(id) + }) } } @@ -32,7 +34,7 @@ impl Config { pub struct Head { 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, dbuf_side: usize) -> *mut u8 { - self.buf_ptrs[channel] - .0[dbuf_side] - .cast() + self.buf_ptrs[channel].0[dbuf_side].cast() } fn get_buf<'buf>(&self, channel: usize, dbuf_side: usize) -> &'buf [u8] { @@ -85,32 +85,34 @@ impl Head { pub struct WithScratch { head: Head, - scratch: Box<[u8]> + scratch: Box<[u8]>, } impl WithScratch { pub fn new(format: SampleFormat, frame_count: FrameCount, buf_ptrs: Vec) -> Self { - let head = Head { format, frame_count, buf_ptrs }; + 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 } } pub fn data(&mut self, dbuf_side: usize) -> Data { let ptr = match self.head.channel_count() { - 0 => ptr::null_mut(), - 1 => self.head.get_buf_ptr(0, dbuf_side).cast(), - 2.. => self.scratch.as_mut_ptr().cast() + 0 => ptr::null_mut(), + 1 => self.head.get_buf_ptr(0, dbuf_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) } } /// copies channel data to the scratch buffer, interleaving it in the process @@ -119,18 +121,16 @@ impl WithScratch { // 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, dbuf_side) - [pos..][..stride] + self.head.get_buf(i_channel, dbuf_side)[pos..][..stride] .pipe_ref(|slice| scratch_sample.copy_from_slice(slice)); } } @@ -141,18 +141,16 @@ impl WithScratch { // 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, dbuf_side) - [pos..][..stride] + self.head.get_buf_mut(i_channel, dbuf_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 e6ae68620..4a467ebc5 100644 --- a/src/host/asio_new/utils.rs +++ b/src/host/asio_new/utils.rs @@ -33,11 +33,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 { @@ -45,7 +65,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 }