Skip to content

Commit 793001c

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

12 files changed

Lines changed: 199 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: 50 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 {
@@ -121,6 +127,7 @@ impl Device {
121127
sample_rate: crate::SampleRate,
122128
format: SampleFormat,
123129
from_endianness: F,
130+
hardware_latency_frames: usize,
124131
) where
125132
A: Copy,
126133
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
@@ -147,6 +154,7 @@ impl Device {
147154
asio_info,
148155
sample_rate,
149156
format,
157+
hardware_latency_frames,
150158
);
151159
}
152160

@@ -160,6 +168,7 @@ impl Device {
160168
config.sample_rate,
161169
SampleFormat::I16,
162170
from_le,
171+
hardware_input_latency,
163172
);
164173
}
165174
(&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => {
@@ -171,6 +180,7 @@ impl Device {
171180
config.sample_rate,
172181
SampleFormat::I16,
173182
from_be,
183+
hardware_input_latency,
174184
);
175185
}
176186

@@ -183,6 +193,7 @@ impl Device {
183193
config.sample_rate,
184194
SampleFormat::F32,
185195
from_le,
196+
hardware_input_latency,
186197
);
187198
}
188199
(&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => {
@@ -194,6 +205,7 @@ impl Device {
194205
config.sample_rate,
195206
SampleFormat::F32,
196207
from_be,
208+
hardware_input_latency,
197209
);
198210
}
199211

@@ -206,6 +218,7 @@ impl Device {
206218
config.sample_rate,
207219
SampleFormat::I32,
208220
from_le,
221+
hardware_input_latency,
209222
);
210223
}
211224
(&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => {
@@ -217,6 +230,7 @@ impl Device {
217230
config.sample_rate,
218231
SampleFormat::I32,
219232
from_be,
233+
hardware_input_latency,
220234
);
221235
}
222236

@@ -229,6 +243,7 @@ impl Device {
229243
config.sample_rate,
230244
SampleFormat::F64,
231245
from_le,
246+
hardware_input_latency,
232247
);
233248
}
234249
(&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => {
@@ -240,6 +255,7 @@ impl Device {
240255
config.sample_rate,
241256
SampleFormat::F64,
242257
from_be,
258+
hardware_input_latency,
243259
);
244260
}
245261

@@ -251,6 +267,7 @@ impl Device {
251267
callback_info,
252268
config.sample_rate,
253269
true,
270+
hardware_input_latency,
254271
);
255272
}
256273
(&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => {
@@ -261,6 +278,7 @@ impl Device {
261278
callback_info,
262279
config.sample_rate,
263280
false,
281+
hardware_input_latency,
264282
);
265283
}
266284

@@ -334,6 +352,12 @@ impl Device {
334352
let playing = Arc::clone(&stream_playing);
335353
let asio_streams = self.asio_streams.clone();
336354

355+
// Query hardware input latency (order matters: needs buffers created above).
356+
let hardware_output_latency = driver
357+
.latencies()
358+
.map(|(_, output)| output.max(0) as usize)
359+
.unwrap_or(0);
360+
337361
let callback_id = driver.add_callback(move |callback_info| unsafe {
338362
// If not playing, return early.
339363
if !playing.load(Ordering::SeqCst) {
@@ -372,6 +396,7 @@ impl Device {
372396
sample_rate: crate::SampleRate,
373397
format: SampleFormat,
374398
mix_samples: F,
399+
hardware_latency_frames: usize,
375400
) where
376401
A: Copy,
377402
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
@@ -385,6 +410,7 @@ impl Device {
385410
asio_info,
386411
sample_rate,
387412
format,
413+
hardware_latency_frames,
388414
);
389415
let n_channels = interleaved.len() / asio_stream.buffer_size as usize;
390416
let buffer_index = asio_info.buffer_index as usize;
@@ -415,6 +441,7 @@ impl Device {
415441
|old_sample, new_sample| {
416442
from_le(old_sample).saturating_add(new_sample).to_le()
417443
},
444+
hardware_output_latency,
418445
);
419446
}
420447
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => {
@@ -429,6 +456,7 @@ impl Device {
429456
|old_sample, new_sample| {
430457
from_be(old_sample).saturating_add(new_sample).to_be()
431458
},
459+
hardware_output_latency,
432460
);
433461
}
434462
(SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => {
@@ -445,6 +473,7 @@ impl Device {
445473
.to_bits()
446474
.to_le()
447475
},
476+
hardware_output_latency,
448477
);
449478
}
450479

@@ -462,6 +491,7 @@ impl Device {
462491
.to_bits()
463492
.to_be()
464493
},
494+
hardware_output_latency,
465495
);
466496
}
467497

@@ -477,6 +507,7 @@ impl Device {
477507
|old_sample, new_sample| {
478508
from_le(old_sample).saturating_add(new_sample).to_le()
479509
},
510+
hardware_output_latency,
480511
);
481512
}
482513
(SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => {
@@ -491,6 +522,7 @@ impl Device {
491522
|old_sample, new_sample| {
492523
from_be(old_sample).saturating_add(new_sample).to_be()
493524
},
525+
hardware_output_latency,
494526
);
495527
}
496528

@@ -508,6 +540,7 @@ impl Device {
508540
.to_bits()
509541
.to_le()
510542
},
543+
hardware_output_latency,
511544
);
512545
}
513546

@@ -525,6 +558,7 @@ impl Device {
525558
.to_bits()
526559
.to_be()
527560
},
561+
hardware_output_latency,
528562
);
529563
}
530564

@@ -537,6 +571,7 @@ impl Device {
537571
asio_stream,
538572
callback_info,
539573
config.sample_rate,
574+
hardware_output_latency,
540575
);
541576
}
542577

@@ -549,6 +584,7 @@ impl Device {
549584
asio_stream,
550585
callback_info,
551586
config.sample_rate,
587+
hardware_output_latency,
552588
);
553589
}
554590

@@ -861,6 +897,7 @@ unsafe fn process_output_callback_i24<D>(
861897
asio_stream: &mut sys::AsioStream,
862898
asio_info: &sys::CallbackInfo,
863899
sample_rate: crate::SampleRate,
900+
hardware_latency_frames: usize,
864901
) where
865902
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
866903
{
@@ -873,6 +910,7 @@ unsafe fn process_output_callback_i24<D>(
873910
asio_info,
874911
sample_rate,
875912
format,
913+
hardware_latency_frames,
876914
);
877915

878916
// Size of samples in the ASIO buffer (has to be 3 in this case)
@@ -930,6 +968,7 @@ unsafe fn process_input_callback_i24<D>(
930968
asio_info: &sys::CallbackInfo,
931969
sample_rate: crate::SampleRate,
932970
little_endian: bool,
971+
hardware_latency_frames: usize,
933972
) where
934973
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
935974
{
@@ -969,6 +1008,7 @@ unsafe fn process_input_callback_i24<D>(
9691008
asio_info,
9701009
sample_rate,
9711010
format,
1011+
hardware_latency_frames,
9721012
);
9731013
}
9741014

@@ -980,6 +1020,7 @@ unsafe fn apply_output_callback_to_data<A, D>(
9801020
asio_info: &sys::CallbackInfo,
9811021
sample_rate: crate::SampleRate,
9821022
sample_format: SampleFormat,
1023+
hardware_latency_frames: usize,
9831024
) where
9841025
A: Copy,
9851026
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
@@ -990,7 +1031,10 @@ unsafe fn apply_output_callback_to_data<A, D>(
9901031
sample_format,
9911032
);
9921033
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);
1034+
let delay = frames_to_duration(
1035+
asio_stream.buffer_size as usize + hardware_latency_frames,
1036+
sample_rate,
1037+
);
9941038
let playback = callback
9951039
.add(delay)
9961040
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
@@ -1007,6 +1051,7 @@ unsafe fn apply_input_callback_to_data<A, D>(
10071051
asio_info: &sys::CallbackInfo,
10081052
sample_rate: crate::SampleRate,
10091053
format: SampleFormat,
1054+
hardware_latency_frames: usize,
10101055
) where
10111056
A: Copy,
10121057
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
@@ -1017,7 +1062,10 @@ unsafe fn apply_input_callback_to_data<A, D>(
10171062
format,
10181063
);
10191064
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);
1065+
let delay = frames_to_duration(
1066+
asio_stream.buffer_size as usize + hardware_latency_frames,
1067+
sample_rate,
1068+
);
10211069
let capture = callback
10221070
.sub(delay)
10231071
.expect("`capture` occurs before origin of alsa `StreamInstant`");

0 commit comments

Comments
 (0)