fix(coreaudio): honour the full timeout when waiting for a sample rate change - #1348
Open
dylanpulver wants to merge 1 commit into
Open
fix(coreaudio): honour the full timeout when waiting for a sample rate change#1348dylanpulver wants to merge 1 commit into
dylanpulver wants to merge 1 commit into
Conversation
…e change The wait loop subtracted the total elapsed time from the already-reduced remainder on every iteration, so the budget shrank super-linearly and the device could be abandoned long before the caller's timeout expired.
roderickvd
requested changes
Sep 1, 2026
roderickvd
left a comment
Member
There was a problem hiding this comment.
Thanks for your PR for what indeed is a real bug. I propose another solution though, consistent with what we do in ALSA.
| - **ASIO**: Fix loading a driver while a previous driver was still unloading. | ||
| - **AudioWorklet**: Fix processor construction failures not being reported to `error_callback`. | ||
| - **AudioWorklet**: Fix dropouts in output streams when the callback buffer grows. | ||
| - **CoreAudio**: A sample rate change no longer gives up early when the device reports other rates first; the caller's timeout is now honoured in full. |
Member
There was a problem hiding this comment.
Shorter & sweeter would be:
CoreAudio: Fix sample rate changes timing out early when the device reports other rates first.
| /// A listener can report rates other than the target before it reports the new one, e.g. a | ||
| /// device stepping through rates. The whole timeout must remain available across those. | ||
| #[test] | ||
| fn wait_for_rate_honours_the_full_timeout_across_repeated_events() { |
Member
There was a problem hiding this comment.
Please don't wait 500 ms in every test run.
| let mut remaining = timeout; | ||
|
|
||
| loop { | ||
| match receiver.recv_timeout(remaining) { |
Member
There was a problem hiding this comment.
This still manually implements the deadline by subtracting every loop iteration. In ALSA we've got something prettier by computing one deadline up front and checking saturating_duration_since each iteration:
fn wait_for_rate(
receiver: &Receiver<f64>,
target_sample_rate: SampleRate,
timeout: Duration,
) -> Result<(), Error> {
let deadline = Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"Sample rate update timed out",
));
}
match receiver.recv_timeout(remaining) {
Ok(reported_rate) => {
if (reported_rate - target_sample_rate as f64).abs() < 1.0 {
return Ok(());
}
}
Err(RecvTimeoutError::Timeout) => {
return Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"Sample rate update timed out",
));
}
Err(RecvTimeoutError::Disconnected) => {
return Err(Error::with_message(
ErrorKind::StreamInvalidated,
"Sample rate listener disconnected unexpectedly",
));
}
}
}
}As bonus, this also gets rid of a blocking recv_timeout(ZERO) edge case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
set_sample_ratewaits forRateListenerto report the new rate, and re-arms when a notification carries some other rate. The re-arm subtractedstart.elapsed()— the total elapsed time — from the already-reducedremaining, so the budget shrank super-linearly and hit zero well before the caller's timeout:With a 500 ms timeout and a listener emitting a non-matching rate every few ms, the loop gave up after 82 ms and returned
Sample rate update timed outon a device that was still settling. Perversely, the chattier the device, the shorter the effective timeout. Subtracting from the originaltimeoutinstead makes it a single deadline; the same measurement then runs the full 500 ms.Non-matching notifications are reachable:
RateListener's callback re-reads the property and sends whatever it gets, including0.0when that read fails — which matches no target rate.The loop is extracted into
wait_for_rateso it can be tested without a device. Two tests: one asserts the whole timeout is spent across repeated non-matching events (fails at 82 ms on the current code, and also on asaturating_subvariant of it, so it pins the accounting rather than the helper); one asserts it returns as soon as the target rate arrives.cargo teston macOS goes 18 → 20 passing, including the existing device tests. fmt,clippy --all --all-targets -- -D warningsandcargo docare clean.The other backends call
recv_timeout(dur)once and never re-arm, so this is CoreAudio-only.Honest limit: I could not reproduce the intermediate-rate notification on real hardware — the built-in mic and speakers here settle in a single notification — so that half is unverified. The evidence is the extracted unit test and the arithmetic.