Repository navigation
Conversation
A paused stream's run loop still waited on the WASAPI audio event. With another client keeping the endpoint running, that event can keep being signalled every engine period after IAudioClient::Stop, so the stream thread woke about 100 times a second with nothing to render. While stopped, the loop now waits only for commands and device-change events, and maps the signalled index back to the usual handle layout. Priming returns before the wait, so it is unaffected. Measured on cpal 0.18.2 with Spotifast on Windows 11 ARM64 (Snapdragon X), paused in the tray with a browser playing on the same device: the app's wake-ups fell from 117-132 to 17-22 a second, with playback unchanged.
There was a problem hiding this comment.
This (pre-existing) code immediately below yours seems to be misleading, as it implies that handles.len() > 3 was possible, which AFAICT is not the case. To me it looks like the vec always consists of
pending_scheduled_eventstream_inner.event- (optional)
monitor_event
in that order, which means it should be possible to avoid constructing a new vec (and instead take a subslice of the existing one) if the order of the HANDLEs was rearranged. Could you try this?
There was a problem hiding this comment.
You're right, there are at most three handles, one of each kind, and the voices comment was a leftover from when one loop served several streams.
I've pushed c0d2d9f, which puts the audio event last: commands, then the default-device change event (default-device streams only), then audio. While paused the loop now waits on &handles[..len - 1], so there is no new Vec and no index mapping; commands stay at 0 and the device change at 1 either way. I rewrote the layout comment on RunContext::handles and replaced the >= 2 check with a match on commands, audio and the rest.
One side effect of the order: WaitForMultipleObjectsEx reports the lowest signalled index, so a device change that fires together with the audio event is now handled first. Both are auto-reset events and the wait only resets the one it reports, so the audio event stays signalled and is handled on the next pass. The comment says so.
I also measured it on master this time, with a small program playing silence on two output streams on the default device and pausing one: the paused stream's thread went from about 102 wake-ups a second on master to under 1 with this PR, while the playing one stayed around 105. Callbacks stop while paused and resume after start().
I kept it as a separate commit so it's easy to review; happy to squash it into the first one if you prefer.
Put the stream's audio event last among the handles the run loop waits on, after the command and default-device change events. A paused stream then waits on all but the last handle instead of collecting a new Vec, and the signalled index needs no mapping back. The layout comments implied more than three handles and still referred to a list of voices; there is at most one handle of each kind.
Summary
A paused WASAPI stream's run loop still waited on the stream's audio event. With another client keeping the endpoint running, Windows keeps signalling that event every engine period after
IAudioClient::Stop, so the stream thread woke about 100 times a second with nothing to render.While the stream is stopped, the loop now waits only for the command event and the default-device-change event. The audio event is the last of the handles the loop waits on, so a stopped stream waits on all but the last, with no allocation and no index mapping. Priming returns before the wait, so it is unaffected. Playing streams wait on the same events as before; when a device change and the audio event are signalled together, the device change is now handled first and the audio on the next pass.
Testing
cargo fmt --check,cargo clippy --all-targets(the only warning,frames_to_durationunused, is already onmasterfor this target),cargo check --all-targets.master, with a small program playing silence on two output streams on the default device and pausing one: the paused stream's thread went from about 102 wake-ups a second to under 1, while the playing one stayed around 105. Callbacks stop while paused and resume afterstart(). I did not change the default device during this run.A changelog entry is under Unreleased, Fixed. This was written with AI assistance; I have read and understand every line, and I measured the result myself on real hardware.