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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
- **ALSA**: Try to resume from hardware after a system suspend.
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
- **ASIO**: Timestamps now include driver-reported hardware latency.
- **CoreAudio**: Timestamps now include device latency and safety offset.
- **JACK**: Timestamps now use the precise hardware deadline.
- **Linux/BSD**: Default host now is, in order from first to last available: PipeWire, PulseAudio, ALSA.
- **WASAPI**: Timestamps now include hardware pipeline latency.
- **WebAudio**: Timestamps now include base and output latency.

### Fixed

Expand Down
6 changes: 6 additions & 0 deletions asio-sys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Added `Driver::latencies()`

## [0.2.6] - 2026-02-18

### Fixed
Expand Down Expand Up @@ -82,6 +87,7 @@ Initial release.
- Support for MSVC toolchain on Windows
- Basic error types: `AsioError`, `LoadDriverError`

[Unreleased]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.6...HEAD
[0.2.6]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.5...asio-sys-v0.2.6
[0.2.5]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.4...asio-sys-v0.2.5
[0.2.4]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.3...asio-sys-v0.2.4
Expand Down
7 changes: 7 additions & 0 deletions asio-sys/asio_stub_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ pub unsafe extern "C" fn ASIOGetChannels(_ins: *mut c_long, _outs: *mut c_long)
0
}
#[no_mangle]
pub unsafe extern "C" fn ASIOGetLatencies(
_in_latency: *mut c_long,
_out_latency: *mut c_long,
) -> ASIOError {
0
}
#[no_mangle]
pub unsafe extern "C" fn ASIOGetChannelInfo(_info: *mut ASIOChannelInfo) -> ASIOError {
0
}
Expand Down
1 change: 1 addition & 0 deletions asio-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ fn create_bindings(cpal_asio_dir: &PathBuf) {
.allowlist_function("ASIOGetChannels")
.allowlist_function("ASIOGetChannelInfo")
.allowlist_function("ASIOGetBufferSize")
.allowlist_function("ASIOGetLatencies")
.allowlist_function("ASIOGetSamplePosition")
.allowlist_function("ASIOOutputReady")
.allowlist_function("get_sample_rate")
Expand Down
13 changes: 13 additions & 0 deletions asio-sys/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,19 @@ impl Driver {
Ok(channel)
}

/// Get the input and output hardware latency in frames.
pub fn latencies(&self) -> Result<(c_long, c_long), AsioError> {
let mut input_latency: c_long = 0;
let mut output_latency: c_long = 0;
unsafe {
asio_result!(ai::ASIOGetLatencies(
&mut input_latency,
&mut output_latency
))?;
}
Ok((input_latency, output_latency))
}

/// Get the min and max supported buffersize of the driver.
pub fn buffersize_range(&self) -> Result<(c_long, c_long), AsioError> {
let buffer_sizes = asio_get_buffer_sizes()?;
Expand Down
54 changes: 52 additions & 2 deletions src/host/asio/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ impl Device {
let playing = Arc::clone(&stream_playing);
let asio_streams = self.asio_streams.clone();

// Query hardware input latency (order matters: needs buffers created above).
let hardware_input_latency = driver
.latencies()
.map(|(input, _)| input.max(0) as usize)
.unwrap_or(0);

// Set the input callback.
// This is most performance critical part of the ASIO bindings.
let callback_id = driver.add_callback(move |callback_info| unsafe {
Expand All @@ -113,6 +119,7 @@ impl Device {

/// 1. Write from the ASIO buffer to the interleaved CPAL buffer.
/// 2. Deliver the CPAL buffer to the user callback.
#[allow(clippy::too_many_arguments)]
unsafe fn process_input_callback<A, D, F>(
data_callback: &mut D,
interleaved: &mut [u8],
Expand All @@ -121,6 +128,7 @@ impl Device {
sample_rate: crate::SampleRate,
format: SampleFormat,
from_endianness: F,
hardware_latency_frames: usize,
) where
A: Copy,
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
Expand All @@ -147,6 +155,7 @@ impl Device {
asio_info,
sample_rate,
format,
hardware_latency_frames,
);
}

Expand All @@ -160,6 +169,7 @@ impl Device {
config.sample_rate,
SampleFormat::I16,
from_le,
hardware_input_latency,
);
}
(&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => {
Expand All @@ -171,6 +181,7 @@ impl Device {
config.sample_rate,
SampleFormat::I16,
from_be,
hardware_input_latency,
);
}

Expand All @@ -183,6 +194,7 @@ impl Device {
config.sample_rate,
SampleFormat::F32,
from_le,
hardware_input_latency,
);
}
(&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => {
Expand All @@ -194,6 +206,7 @@ impl Device {
config.sample_rate,
SampleFormat::F32,
from_be,
hardware_input_latency,
);
}

Expand All @@ -206,6 +219,7 @@ impl Device {
config.sample_rate,
SampleFormat::I32,
from_le,
hardware_input_latency,
);
}
(&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => {
Expand All @@ -217,6 +231,7 @@ impl Device {
config.sample_rate,
SampleFormat::I32,
from_be,
hardware_input_latency,
);
}

Expand All @@ -229,6 +244,7 @@ impl Device {
config.sample_rate,
SampleFormat::F64,
from_le,
hardware_input_latency,
);
}
(&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => {
Expand All @@ -240,6 +256,7 @@ impl Device {
config.sample_rate,
SampleFormat::F64,
from_be,
hardware_input_latency,
);
}

Expand All @@ -251,6 +268,7 @@ impl Device {
callback_info,
config.sample_rate,
true,
hardware_input_latency,
);
}
(&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => {
Expand All @@ -261,6 +279,7 @@ impl Device {
callback_info,
config.sample_rate,
false,
hardware_input_latency,
);
}

Expand Down Expand Up @@ -334,6 +353,12 @@ impl Device {
let playing = Arc::clone(&stream_playing);
let asio_streams = self.asio_streams.clone();

// Query hardware output latency (order matters: needs buffers created above).
let hardware_output_latency = driver
.latencies()
.map(|(_, output)| output.max(0) as usize)
.unwrap_or(0);

let callback_id = driver.add_callback(move |callback_info| unsafe {
// If not playing, return early.
if !playing.load(Ordering::SeqCst) {
Expand Down Expand Up @@ -372,6 +397,7 @@ impl Device {
sample_rate: crate::SampleRate,
format: SampleFormat,
mix_samples: F,
hardware_latency_frames: usize,
) where
A: Copy,
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
Expand All @@ -385,6 +411,7 @@ impl Device {
asio_info,
sample_rate,
format,
hardware_latency_frames,
);
let n_channels = interleaved.len() / asio_stream.buffer_size as usize;
let buffer_index = asio_info.buffer_index as usize;
Expand Down Expand Up @@ -415,6 +442,7 @@ impl Device {
|old_sample, new_sample| {
from_le(old_sample).saturating_add(new_sample).to_le()
},
hardware_output_latency,
);
}
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => {
Expand All @@ -429,6 +457,7 @@ impl Device {
|old_sample, new_sample| {
from_be(old_sample).saturating_add(new_sample).to_be()
},
hardware_output_latency,
);
}
(SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => {
Expand All @@ -445,6 +474,7 @@ impl Device {
.to_bits()
.to_le()
},
hardware_output_latency,
);
}

Expand All @@ -462,6 +492,7 @@ impl Device {
.to_bits()
.to_be()
},
hardware_output_latency,
);
}

Expand All @@ -477,6 +508,7 @@ impl Device {
|old_sample, new_sample| {
from_le(old_sample).saturating_add(new_sample).to_le()
},
hardware_output_latency,
);
}
(SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => {
Expand All @@ -491,6 +523,7 @@ impl Device {
|old_sample, new_sample| {
from_be(old_sample).saturating_add(new_sample).to_be()
},
hardware_output_latency,
);
}

Expand All @@ -508,6 +541,7 @@ impl Device {
.to_bits()
.to_le()
},
hardware_output_latency,
);
}

Expand All @@ -525,6 +559,7 @@ impl Device {
.to_bits()
.to_be()
},
hardware_output_latency,
);
}

Expand All @@ -537,6 +572,7 @@ impl Device {
asio_stream,
callback_info,
config.sample_rate,
hardware_output_latency,
);
}

Expand All @@ -549,6 +585,7 @@ impl Device {
asio_stream,
callback_info,
config.sample_rate,
hardware_output_latency,
);
}

Expand Down Expand Up @@ -853,6 +890,7 @@ fn i24_bytes_to_i32(i24_bytes: &[u8; 3], little_endian: bool) -> i32 {
}
}

#[allow(clippy::too_many_arguments)]
unsafe fn process_output_callback_i24<D>(
data_callback: &mut D,
interleaved: &mut [u8],
Expand All @@ -861,6 +899,7 @@ unsafe fn process_output_callback_i24<D>(
asio_stream: &mut sys::AsioStream,
asio_info: &sys::CallbackInfo,
sample_rate: crate::SampleRate,
hardware_latency_frames: usize,
) where
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
{
Expand All @@ -873,6 +912,7 @@ unsafe fn process_output_callback_i24<D>(
asio_info,
sample_rate,
format,
hardware_latency_frames,
);

// Size of samples in the ASIO buffer (has to be 3 in this case)
Expand Down Expand Up @@ -930,6 +970,7 @@ unsafe fn process_input_callback_i24<D>(
asio_info: &sys::CallbackInfo,
sample_rate: crate::SampleRate,
little_endian: bool,
hardware_latency_frames: usize,
) where
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
{
Expand Down Expand Up @@ -969,6 +1010,7 @@ unsafe fn process_input_callback_i24<D>(
asio_info,
sample_rate,
format,
hardware_latency_frames,
);
}

Expand All @@ -980,6 +1022,7 @@ unsafe fn apply_output_callback_to_data<A, D>(
asio_info: &sys::CallbackInfo,
sample_rate: crate::SampleRate,
sample_format: SampleFormat,
hardware_latency_frames: usize,
) where
A: Copy,
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
Expand All @@ -990,7 +1033,10 @@ unsafe fn apply_output_callback_to_data<A, D>(
sample_format,
);
let callback = system_time_to_stream_instant(asio_info.system_time);
let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate);
let delay = frames_to_duration(
asio_stream.buffer_size as usize + hardware_latency_frames,
sample_rate,
);
let playback = callback
.add(delay)
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
Expand All @@ -1007,6 +1053,7 @@ unsafe fn apply_input_callback_to_data<A, D>(
asio_info: &sys::CallbackInfo,
sample_rate: crate::SampleRate,
format: SampleFormat,
hardware_latency_frames: usize,
) where
A: Copy,
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
Expand All @@ -1017,7 +1064,10 @@ unsafe fn apply_input_callback_to_data<A, D>(
format,
);
let callback = system_time_to_stream_instant(asio_info.system_time);
let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate);
let delay = frames_to_duration(
asio_stream.buffer_size as usize + hardware_latency_frames,
sample_rate,
);
let capture = callback
.sub(delay)
.expect("`capture` occurs before origin of alsa `StreamInstant`");
Expand Down
Loading
Loading