Skip to content

Commit 81c71de

Browse files
committed
feat: add hardware latency reporting to ASIO, CoreAudio, JACK, WASAPI, and WebAudio
1 parent 3cb457f commit 81c71de

12 files changed

Lines changed: 197 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
3232
- **ALSA**: Try to resume from hardware after a system suspend.
3333
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
34+
- **ASIO**: Timestamps now include driver-reported hardware latency.
35+
- **CoreAudio**: Timestamps now include device latency and safety offset.
36+
- **JACK**: Timestamps now use the precise hardware deadline.
3437
- **Linux/BSD**: Default host now is, in order from first to last available: PipeWire, PulseAudio, ALSA.
38+
- **WASAPI**: Timestamps now include hardware pipeline latency.
39+
- **WebAudio**: Timestamps now include base and output latency.
3540

3641
### Fixed
3742

asio-sys/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- Added `Driver::latencies()`
12+
813
## [0.2.6] - 2026-02-18
914

1015
### Fixed
@@ -82,6 +87,7 @@ Initial release.
8287
- Support for MSVC toolchain on Windows
8388
- Basic error types: `AsioError`, `LoadDriverError`
8489

90+
[Unreleased]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.6...HEAD
8591
[0.2.6]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.5...asio-sys-v0.2.6
8692
[0.2.5]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.4...asio-sys-v0.2.5
8793
[0.2.4]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.3...asio-sys-v0.2.4

asio-sys/asio_stub_bindings.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ pub unsafe extern "C" fn ASIOGetChannels(_ins: *mut c_long, _outs: *mut c_long)
137137
0
138138
}
139139
#[no_mangle]
140+
pub unsafe extern "C" fn ASIOGetLatencies(
141+
_in_latency: *mut c_long,
142+
_out_latency: *mut c_long,
143+
) -> ASIOError {
144+
0
145+
}
146+
#[no_mangle]
140147
pub unsafe extern "C" fn ASIOGetChannelInfo(_info: *mut ASIOChannelInfo) -> ASIOError {
141148
0
142149
}

asio-sys/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ fn create_bindings(cpal_asio_dir: &PathBuf) {
228228
.allowlist_function("ASIOGetChannels")
229229
.allowlist_function("ASIOGetChannelInfo")
230230
.allowlist_function("ASIOGetBufferSize")
231+
.allowlist_function("ASIOGetLatencies")
231232
.allowlist_function("ASIOGetSamplePosition")
232233
.allowlist_function("ASIOOutputReady")
233234
.allowlist_function("get_sample_rate")

asio-sys/src/bindings/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,19 @@ impl Driver {
458458
Ok(channel)
459459
}
460460

461+
/// Get the input and output hardware latency in frames.
462+
pub fn latencies(&self) -> Result<(c_long, c_long), AsioError> {
463+
let mut input_latency: c_long = 0;
464+
let mut output_latency: c_long = 0;
465+
unsafe {
466+
asio_result!(ai::ASIOGetLatencies(
467+
&mut input_latency,
468+
&mut output_latency
469+
))?;
470+
}
471+
Ok((input_latency, output_latency))
472+
}
473+
461474
/// Get the min and max supported buffersize of the driver.
462475
pub fn buffersize_range(&self) -> Result<(c_long, c_long), AsioError> {
463476
let buffer_sizes = asio_get_buffer_sizes()?;

src/host/asio/stream.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ impl Device {
9696
let playing = Arc::clone(&stream_playing);
9797
let asio_streams = self.asio_streams.clone();
9898

99+
// Query hardware input latency (order matters: needs buffers created above).
100+
let hardware_input_latency = driver
101+
.latencies()
102+
.map(|(input, _)| input.max(0) as usize)
103+
.unwrap_or(0);
104+
99105
// Set the input callback.
100106
// This is most performance critical part of the ASIO bindings.
101107
let callback_id = driver.add_callback(move |callback_info| unsafe {
@@ -113,6 +119,7 @@ impl Device {
113119

114120
/// 1. Write from the ASIO buffer to the interleaved CPAL buffer.
115121
/// 2. Deliver the CPAL buffer to the user callback.
122+
#[allow(clippy::too_many_arguments)]
116123
unsafe fn process_input_callback<A, D, F>(
117124
data_callback: &mut D,
118125
interleaved: &mut [u8],
@@ -121,6 +128,7 @@ impl Device {
121128
sample_rate: crate::SampleRate,
122129
format: SampleFormat,
123130
from_endianness: F,
131+
hardware_latency_frames: usize,
124132
) where
125133
A: Copy,
126134
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
@@ -147,6 +155,7 @@ impl Device {
147155
asio_info,
148156
sample_rate,
149157
format,
158+
hardware_latency_frames,
150159
);
151160
}
152161

@@ -160,6 +169,7 @@ impl Device {
160169
config.sample_rate,
161170
SampleFormat::I16,
162171
from_le,
172+
hardware_input_latency,
163173
);
164174
}
165175
(&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => {
@@ -171,6 +181,7 @@ impl Device {
171181
config.sample_rate,
172182
SampleFormat::I16,
173183
from_be,
184+
hardware_input_latency,
174185
);
175186
}
176187

@@ -183,6 +194,7 @@ impl Device {
183194
config.sample_rate,
184195
SampleFormat::F32,
185196
from_le,
197+
hardware_input_latency,
186198
);
187199
}
188200
(&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => {
@@ -194,6 +206,7 @@ impl Device {
194206
config.sample_rate,
195207
SampleFormat::F32,
196208
from_be,
209+
hardware_input_latency,
197210
);
198211
}
199212

@@ -206,6 +219,7 @@ impl Device {
206219
config.sample_rate,
207220
SampleFormat::I32,
208221
from_le,
222+
hardware_input_latency,
209223
);
210224
}
211225
(&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => {
@@ -217,6 +231,7 @@ impl Device {
217231
config.sample_rate,
218232
SampleFormat::I32,
219233
from_be,
234+
hardware_input_latency,
220235
);
221236
}
222237

@@ -229,6 +244,7 @@ impl Device {
229244
config.sample_rate,
230245
SampleFormat::F64,
231246
from_le,
247+
hardware_input_latency,
232248
);
233249
}
234250
(&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => {
@@ -240,6 +256,7 @@ impl Device {
240256
config.sample_rate,
241257
SampleFormat::F64,
242258
from_be,
259+
hardware_input_latency,
243260
);
244261
}
245262

@@ -251,6 +268,7 @@ impl Device {
251268
callback_info,
252269
config.sample_rate,
253270
true,
271+
hardware_input_latency,
254272
);
255273
}
256274
(&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => {
@@ -261,6 +279,7 @@ impl Device {
261279
callback_info,
262280
config.sample_rate,
263281
false,
282+
hardware_input_latency,
264283
);
265284
}
266285

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

356+
// Query hardware input latency (order matters: needs buffers created above).
357+
let hardware_output_latency = driver
358+
.latencies()
359+
.map(|(_, output)| output.max(0) as usize)
360+
.unwrap_or(0);
361+
337362
let callback_id = driver.add_callback(move |callback_info| unsafe {
338363
// If not playing, return early.
339364
if !playing.load(Ordering::SeqCst) {
@@ -372,6 +397,7 @@ impl Device {
372397
sample_rate: crate::SampleRate,
373398
format: SampleFormat,
374399
mix_samples: F,
400+
hardware_latency_frames: usize,
375401
) where
376402
A: Copy,
377403
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
@@ -385,6 +411,7 @@ impl Device {
385411
asio_info,
386412
sample_rate,
387413
format,
414+
hardware_latency_frames,
388415
);
389416
let n_channels = interleaved.len() / asio_stream.buffer_size as usize;
390417
let buffer_index = asio_info.buffer_index as usize;
@@ -415,6 +442,7 @@ impl Device {
415442
|old_sample, new_sample| {
416443
from_le(old_sample).saturating_add(new_sample).to_le()
417444
},
445+
hardware_output_latency,
418446
);
419447
}
420448
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => {
@@ -429,6 +457,7 @@ impl Device {
429457
|old_sample, new_sample| {
430458
from_be(old_sample).saturating_add(new_sample).to_be()
431459
},
460+
hardware_output_latency,
432461
);
433462
}
434463
(SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => {
@@ -445,6 +474,7 @@ impl Device {
445474
.to_bits()
446475
.to_le()
447476
},
477+
hardware_output_latency,
448478
);
449479
}
450480

@@ -462,6 +492,7 @@ impl Device {
462492
.to_bits()
463493
.to_be()
464494
},
495+
hardware_output_latency,
465496
);
466497
}
467498

@@ -477,6 +508,7 @@ impl Device {
477508
|old_sample, new_sample| {
478509
from_le(old_sample).saturating_add(new_sample).to_le()
479510
},
511+
hardware_output_latency,
480512
);
481513
}
482514
(SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => {
@@ -491,6 +523,7 @@ impl Device {
491523
|old_sample, new_sample| {
492524
from_be(old_sample).saturating_add(new_sample).to_be()
493525
},
526+
hardware_output_latency,
494527
);
495528
}
496529

@@ -508,6 +541,7 @@ impl Device {
508541
.to_bits()
509542
.to_le()
510543
},
544+
hardware_output_latency,
511545
);
512546
}
513547

@@ -525,6 +559,7 @@ impl Device {
525559
.to_bits()
526560
.to_be()
527561
},
562+
hardware_output_latency,
528563
);
529564
}
530565

@@ -537,6 +572,7 @@ impl Device {
537572
asio_stream,
538573
callback_info,
539574
config.sample_rate,
575+
hardware_output_latency,
540576
);
541577
}
542578

@@ -549,6 +585,7 @@ impl Device {
549585
asio_stream,
550586
callback_info,
551587
config.sample_rate,
588+
hardware_output_latency,
552589
);
553590
}
554591

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

893+
#[allow(clippy::too_many_arguments)]
856894
unsafe fn process_output_callback_i24<D>(
857895
data_callback: &mut D,
858896
interleaved: &mut [u8],
@@ -861,6 +899,7 @@ unsafe fn process_output_callback_i24<D>(
861899
asio_stream: &mut sys::AsioStream,
862900
asio_info: &sys::CallbackInfo,
863901
sample_rate: crate::SampleRate,
902+
hardware_latency_frames: usize,
864903
) where
865904
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
866905
{
@@ -873,6 +912,7 @@ unsafe fn process_output_callback_i24<D>(
873912
asio_info,
874913
sample_rate,
875914
format,
915+
hardware_latency_frames,
876916
);
877917

878918
// Size of samples in the ASIO buffer (has to be 3 in this case)
@@ -930,6 +970,7 @@ unsafe fn process_input_callback_i24<D>(
930970
asio_info: &sys::CallbackInfo,
931971
sample_rate: crate::SampleRate,
932972
little_endian: bool,
973+
hardware_latency_frames: usize,
933974
) where
934975
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
935976
{
@@ -969,6 +1010,7 @@ unsafe fn process_input_callback_i24<D>(
9691010
asio_info,
9701011
sample_rate,
9711012
format,
1013+
hardware_latency_frames,
9721014
);
9731015
}
9741016

@@ -980,6 +1022,7 @@ unsafe fn apply_output_callback_to_data<A, D>(
9801022
asio_info: &sys::CallbackInfo,
9811023
sample_rate: crate::SampleRate,
9821024
sample_format: SampleFormat,
1025+
hardware_latency_frames: usize,
9831026
) where
9841027
A: Copy,
9851028
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
@@ -990,7 +1033,10 @@ unsafe fn apply_output_callback_to_data<A, D>(
9901033
sample_format,
9911034
);
9921035
let callback = system_time_to_stream_instant(asio_info.system_time);
993-
let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate);
1036+
let delay = frames_to_duration(
1037+
asio_stream.buffer_size as usize + hardware_latency_frames,
1038+
sample_rate,
1039+
);
9941040
let playback = callback
9951041
.add(delay)
9961042
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
@@ -1007,6 +1053,7 @@ unsafe fn apply_input_callback_to_data<A, D>(
10071053
asio_info: &sys::CallbackInfo,
10081054
sample_rate: crate::SampleRate,
10091055
format: SampleFormat,
1056+
hardware_latency_frames: usize,
10101057
) where
10111058
A: Copy,
10121059
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
@@ -1017,7 +1064,10 @@ unsafe fn apply_input_callback_to_data<A, D>(
10171064
format,
10181065
);
10191066
let callback = system_time_to_stream_instant(asio_info.system_time);
1020-
let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate);
1067+
let delay = frames_to_duration(
1068+
asio_stream.buffer_size as usize + hardware_latency_frames,
1069+
sample_rate,
1070+
);
10211071
let capture = callback
10221072
.sub(delay)
10231073
.expect("`capture` occurs before origin of alsa `StreamInstant`");

0 commit comments

Comments
 (0)