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..4968379d6 --- /dev/null +++ b/src/host/asio_new/callbacks.rs @@ -0,0 +1,295 @@ +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..d5262f25d --- /dev/null +++ b/src/host/asio_new/capabilities.rs @@ -0,0 +1,82 @@ +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..5e36e673b --- /dev/null +++ b/src/host/asio_new/mod.rs @@ -0,0 +1,313 @@ +//! 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..2d063b949 --- /dev/null +++ b/src/host/asio_new/session.rs @@ -0,0 +1,369 @@ +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..2992a3bc3 --- /dev/null +++ b/src/host/asio_new/simplex.rs @@ -0,0 +1,188 @@ +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..f7403bb3f --- /dev/null +++ b/src/host/asio_new/utils.rs @@ -0,0 +1,108 @@ +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,