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/capabilities.rs b/src/host/asio_new/capabilities.rs new file mode 100644 index 000000000..fbd819a33 --- /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_azo2cpal}; + +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_azo2cpal) + .pipe(Ok) +} \ No newline at end of file diff --git a/src/host/asio_new/enumerate.rs b/src/host/asio_new/enumerate.rs new file mode 100644 index 000000000..7bff8f881 --- /dev/null +++ b/src/host/asio_new/enumerate.rs @@ -0,0 +1,32 @@ +use super::*; + +#[derive(Debug, Clone)] +pub struct Sessions(worker::Handle, vec::IntoIter); + +impl Sessions { + pub fn new(com_worker: worker::Handle) -> azo::WinResult { + let metas = azo::get_drivers()?.into_iter(); + + Ok(Self(com_worker, metas)) + } +} + +impl Iterator for Sessions { + type Item = Session; + + fn next(&mut self) -> Option { + self.1.find_map(|metadata| Session::try_new(metadata.clsid, &self.0).ok()) + } +} + +pub struct Devices(pub(super) Sessions); + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + self.0.next().map(Device::new) + } +} + +pub type SupportedConfigs = vec::IntoIter; diff --git a/src/host/asio_new/ffi_callbacks.rs b/src/host/asio_new/ffi_callbacks.rs new file mode 100644 index 000000000..4ec63f426 --- /dev/null +++ b/src/host/asio_new/ffi_callbacks.rs @@ -0,0 +1,225 @@ +use super::*; +use azo::sys::*; +use closure_ffi::BareFnMutSync; +use std::ffi::c_long; +use std::fmt::{self, Debug}; +use std::marker::PhantomPinned; +use std::sync::Mutex; + +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::SUPPORTS_TIME_INFO, + MessageSelector::SUPPORTS_TIME_CODE, + MessageSelector::OVERLOAD +]; + +type Bare = BareFnMutSync<'static, T>; + +#[derive(Debug)] +pub struct Container { + pointers: Callbacks, + closures: Closures, + _marker : PhantomPinned +} + +impl Container { + pub const fn pointers(&self) -> &Callbacks { + &self.pointers + } + + pub fn prime( + self: Pin<&mut Self>, + session : Arc, + data_cb : data_cb_type!(), + error_cb : error_cb_type!(), + simplexes: [simplex::WithScratch; 2] + ) { + // SAFETY: + // `self` is not self-referential yet (but we're changing that now) + 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, simplexes); + 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 Container { + fn default() -> Self { + Self { + pointers: Callbacks::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_azo_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!("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 in_, mut out]: [simplex::WithScratch; 2] +) -> 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 + .cast_unsigned() + .pipe(StreamInstant::from_millis) + .pipe(|instant| StreamTimestamp { callback: instant, device: instant }) + .pipe(|stamp| CallbackInfo::new(stamp, false)) + .pipe(|cbi| DuplexCallbackInfo::new(cbi, cbi)); + + in_.interleave(buf_idx as _); + data_callback(&in_.data(buf_idx as _), &mut out.data(buf_idx as _), &callback_info); + out.deinterleave(buf_idx as _); + + time + }; + + Bare::new_system(closure) +} + +fn throw(error_cb: &Mutex, error: Error) { + error_cb.lock().expect("mutex poisoned")(error); +} \ No newline at end of file diff --git a/src/host/asio_new/mod.rs b/src/host/asio_new/mod.rs new file mode 100644 index 000000000..990427f74 --- /dev/null +++ b/src/host/asio_new/mod.rs @@ -0,0 +1,474 @@ +//! Experimental ASIO backend implementation. +//! +//! Available on Windows with the `asio-new` feature. + +use super::com::worker; +use crate::ErrorKind::*; +use crate::traits::*; +use crate::*; +use azo::Driver; +use azo::dto::ChannelCounts; +use std::fmt; +use std::fmt::Debug; +use std::hash::Hash; +use std::hash::Hasher; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use std::vec; +use tap::prelude::*; +use windows_core::{GUID, Interface}; + +#[macro_use] +mod utils; +mod capabilities; +mod enumerate; +mod ffi_callbacks; +mod simplex; + +use self::enumerate::{Devices, Sessions, SupportedConfigs}; +use self::utils::*; + +#[derive(Debug, Clone)] +pub struct Host(worker::Handle); + +impl Host { + /// Required by the `impl_platform_host!` macro + pub fn new() -> CpalResult { + worker::Handle + ::new() + .pipe(Self) + .pipe(Ok) + } + + fn sessions(&self) -> CpalResult { + self.0 + .clone() + .pipe(Sessions::new) + .map_err(|_win_error| Error::new(HostUnavailable)) + } + + #[must_use] + fn default_session(&self) -> Option { + self.sessions() + .ok()? + .find(Session::supports_direction::) + } +} + +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.sessions() + .map(Devices) + } + + fn default_input_device(&self) -> Option { + self.default_session::() + .map(Device::new) + } + + fn default_output_device(&self) -> Option { + self.default_session::() + .map(Device::new) + } + + fn device_by_id(&self, id: &DeviceId) -> Option { + if id.host() != HostId::AsioNew { + return None; + } + + let clsid = id.id().try_into().ok()?; + + Session::try_new(clsid, &self.0) + .ok() + .map(Device::new) + } +} + +#[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, + config : 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: config. input_channels }; + let cfg_out = simplex::Config { format: format_out, channels: config.output_channels }; + let ids_in = cfg_in .validate(&self.0.driver, true ); + let ids_out = cfg_out.validate(&self.0.driver, false); + let channel_ids = ids_in.chain(ids_out) + .collect::>>()?; + + let frame_count = match config.buffer_size { // buffer_size = frame_count because ASIO channels are always mono + BufferSize::Fixed(n) => n, + BufferSize::Default => capabilities::buffer_size_preferred(&self.0.driver)? as FrameCount, + }; + + self.0 + .driver + .can_sample_rate(config.sample_rate as _) + .map_err(|_| Error::with_message(InvalidInput, "sample rate not supported"))?; + + self.0 + .driver + .set_sample_rate(config.sample_rate as _) + .map_err(|azo_error| create_report(&self.0.driver, azo_error, "set_sample_rate"))?; + + let mut ffi_callbacks = ffi_callbacks::Container + ::default() // creates dummy callbacks that do nothing + .pipe(Box::pin); + + let mut double_buffers = + unsafe { self.0.driver.create_buffers(channel_ids, frame_count as _, ffi_callbacks.pointers()) } + .map_err(|error| create_report(&self.0.driver, error, "create_buffers"))? + .map(DoubleBuffer); + + let simplexes = [cfg_in, cfg_out] + .map(|simplex_cfg| + simplex::Head { + format: simplex_cfg.format, + frame_count, + buf_ptrs: double_buffers + .by_ref() + .take(simplex_cfg.channels as _) + .collect() + } + .pipe(simplex::WithScratch::new) + ); + + ffi_callbacks.as_mut().prime( + Arc::clone(&self.0), + data_cb, + error_cb, + simplexes + ); + + Stream { + device: Arc::clone(&self.0), + frame_count, + _ffi_callbacks: ffi_callbacks // keep this alive until the whole stream is dropped + } + .pipe(Ok) + } +} + +#[derive(Debug)] +pub struct Session { + driver: Driver, + init_success: bool, + clsid_string: String, + _com_worker: worker::Handle +} + +impl Session { + fn try_new(clsid: GUID, com_worker: &worker::Handle) -> azo::WinResult { + let driver = com_worker.create_driver(clsid)?; + + Self { + init_success: driver.init(None), + driver, + 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) + } + + fn id(&self) -> CpalResult { + DeviceId::new( + HostId::AsioNew, + self.clsid_string.clone() + ) + .pipe(Ok) + } + + fn description(&self) -> CpalResult { + let name_c = self.driver.name(); + let name = name_c.to_string_lossy(); + + let direction = match self.driver.channel_counts() { + Ok(ChannelCounts { in_: 1.., out: 1.. }) => DeviceDirection::Duplex, + Ok(ChannelCounts { in_: 1.., out: 0 }) => DeviceDirection::Input, + Ok(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("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] + 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 + } + + 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) + } + + 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) + } +} + +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); + } +} + +#[derive(Debug)] +pub struct Stream { + device: Arc, + frame_count: FrameCount, + _ffi_callbacks: Pin> +} + +unsafe impl Send for Stream {} +unsafe impl Sync for Stream {} + +impl StreamTrait for Stream { + fn start(&self) -> CpalResult<()> { + self.device + .driver + .start() + .map_err(|error| create_report(&self.device.driver, error, "start")) + } + + fn pause(&self) -> CpalResult<()> { + self.device + .driver + .stop() + .map_err(|error| create_report(&self.device.driver, error, "stop")) + } + + fn stop(&self, _timeout: Option) -> Result<(), Error> { + self.pause() + } + + fn now(&self) -> StreamInstant { + self.device + .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) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + _ = self.pause(); // might fail if the stream is already halted + _ = self.device.driver.dispose_all_buffers(); // dunno in what kind of scenario this would fail + } +} diff --git a/src/host/asio_new/simplex.rs b/src/host/asio_new/simplex.rs new file mode 100644 index 000000000..cd4164392 --- /dev/null +++ b/src/host/asio_new/simplex.rs @@ -0,0 +1,140 @@ +use std::{ptr, slice}; +use azo::dto::ChannelId; +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Config { + pub format: SampleFormat, + pub channels: u16 +} + +impl Config { + pub fn validate(self, driver: &Driver, input: bool) -> impl Iterator> { + (0..self.channels) + .map(move |i| { + let id = ChannelId { 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_azo2cpal); + 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 + } + + const fn channel_count(&self) -> usize { + self.buf_ptrs.len() + } + + fn sample_size(&self) -> usize { + self.format.sample_size() + } + + const 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(head: Head) -> Self { + // 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 { + 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; + scratch_sample.copy_from_slice(&self.head.get_buf(i_channel, dbuf_side)[pos..][..stride]); + } + } + } + /// copies scratch data to the channels, deinterleaving it in the process + pub fn deinterleave(&self, dbuf_side: usize) { + if self.head.channel_count() < 2 { + 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); + } + } + } +} \ No newline at end of file diff --git a/src/host/asio_new/utils.rs b/src/host/asio_new/utils.rs new file mode 100644 index 000000000..ec406e9ea --- /dev/null +++ b/src/host/asio_new/utils.rs @@ -0,0 +1,85 @@ +use crate::ErrorKind::*; +use crate::*; +use azo::dto::*; +use azo::{Driver, 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, Default, 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 AzoFormat; + +pub const fn sample_format_azo2cpal(azo_format: AzoFormat) -> Option { + cfg_select! { + target_endian = "little" => { + const AZO_I16 : AzoFormat = AzoFormat::PCM_I16_LSB; + const AZO_I24 : AzoFormat = AzoFormat::PCM_I32_LSB_24; + const AZO_I32 : AzoFormat = AzoFormat::PCM_I32_LSB; + const AZO_F32 : AzoFormat = AzoFormat::PCM_F32_LSB; + const AZO_DSD_U8: AzoFormat = AzoFormat::DSD_I8_LSB_1; + }, + target_endian = "big" => { + const AZO_I16 : AzoFormat = AzoFormat::PCM_I16_MSB; + const AZO_I24 : AzoFormat = AzoFormat::PCM_I32_MSB_24; + const AZO_I32 : AzoFormat = AzoFormat::PCM_I32_MSB; + const AZO_F32 : AzoFormat = AzoFormat::PCM_F32_MSB; + const AZO_DSD_U8: AzoFormat = AzoFormat::DSD_I8_MSB_1; + } + } + + match azo_format { + AZO_I16 => Some(CpalFormat::I16), + AZO_I24 => Some(CpalFormat::I24), + AZO_I32 => Some(CpalFormat::I32), + AZO_F32 => Some(CpalFormat::F32), + AZO_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, azo_error: azo::Error, origin: &str) -> Error { + let last_error = driver.last_error(); + + Error::with_message( + BackendError, + format!(".{origin}() failed with `{azo_error}` - {last_error:?}"), + ) +} + +pub fn create_minimal_azo_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,