Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
73 changes: 73 additions & 0 deletions src/host/asio_new/capabilities.rs
Original file line number Diff line number Diff line change
@@ -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<const INPUT: bool>(driver: &Driver) -> CpalResult<i32> {
channel_counts(driver)
.map(|counts|
if INPUT { counts.in_ }
else { counts.out }
)
}

pub fn channel_counts(driver: &Driver) -> CpalResult<ChannelCounts> {
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<i32> {
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<const INPUT: bool>(driver: &Driver, ch_count: i32) -> CpalResult<impl Iterator<Item=SampleFormat>> {
(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::<CpalResult<HashSet<_>>>()? // aggregates errors and deduplicates the values
.into_iter()
.filter_map(sample_format_azo2cpal)
.pipe(Ok)
}
32 changes: 32 additions & 0 deletions src/host/asio_new/enumerate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use super::*;

#[derive(Debug, Clone)]
pub struct Sessions(worker::Handle, vec::IntoIter<azo::DriverMetadata>);

impl Sessions {
pub fn new(com_worker: worker::Handle) -> azo::WinResult<Self> {
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::Item> {
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::Item> {
self.0.next().map(Device::new)
}
}

pub type SupportedConfigs = vec::IntoIter<SupportedStreamConfigRange>;
225 changes: 225 additions & 0 deletions src/host/asio_new/ffi_callbacks.rs
Original file line number Diff line number Diff line change
@@ -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<T> = 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<Session>,
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<BufferSwitch>,
sample_rate_did_change : Bare<SampleRateDidChange>,
asio_message : Bare<AsioMessage>,
buffer_switch_time_info: Bare<BufferSwitchTimeInfo>
}

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<Mutex<error_cb_type!()>>,
session : Arc<Session>,
bsti_ptr: BufferSwitchTimeInfo
) -> Bare<BufferSwitch> {
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<Mutex<error_cb_type!()>>) -> Bare<SampleRateDidChange> {
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<Mutex<error_cb_type!()>>) -> Bare<AsioMessage> {
let closure = move |selector, value, _message, _opt| {
match selector {
MessageSelector::SELECTOR_SUPPORTED =>
SUPPORTED_MESSAGE_SELECTORS
.contains(&MessageSelector(value))
.conv::<Bool>()
.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<Mutex<error_cb_type!()>>,
mut data_callback: data_cb_type!(),
[mut in_, mut out]: [simplex::WithScratch; 2]
) -> Bare<BufferSwitchTimeInfo> {
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_cb_type!()>, error: Error) {
error_cb.lock().expect("mutex poisoned")(error);
}
Loading
Loading