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..ae68a0934 --- /dev/null +++ b/src/host/asio_new/callbacks.rs @@ -0,0 +1,265 @@ +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..ea7e9700f --- /dev/null +++ b/src/host/asio_new/capabilities.rs @@ -0,0 +1,84 @@ +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..db5611a01 --- /dev/null +++ b/src/host/asio_new/mod.rs @@ -0,0 +1,318 @@ +//! 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..40bd2b140 --- /dev/null +++ b/src/host/asio_new/session.rs @@ -0,0 +1,263 @@ +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..b6a9b880a --- /dev/null +++ b/src/host/asio_new/simplex.rs @@ -0,0 +1,158 @@ +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..4a467ebc5 --- /dev/null +++ b/src/host/asio_new/utils.rs @@ -0,0 +1,98 @@ +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,