Skip to content

wasapi: don't wake every period while a stream is paused - #1392

Open
dennisgr7 wants to merge 2 commits into
RustAudio:masterfrom
dennisgr7:wasapi-paused-wait
Open

dennisgr7 wants to merge 2 commits into
RustAudio:masterfrom
dennisgr7:wasapi-paused-wait

Conversation

@dennisgr7

@dennisgr7 dennisgr7 commented Oct 6, 2026 •

Copy link
Copy Markdown

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

  • Windows 11 ARM64 (Snapdragon X), Rust 1.98.0: cargo fmt --check, cargo clippy --all-targets (the only warning, frames_to_duration unused, is already on master for this target), cargo check --all-targets.
  • Measured on 0.18.2, where the change was made first: Spotifast paused in the tray with a browser playing on the same device went from 117-132 to 17-22 wake-ups a second for the whole app. Play, pause, resume and default-device changes behave as before.
  • On 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 after start(). 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.

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.
Comment thread src/host/wasapi/stream.rs Outdated
Comment on lines 881 to 892

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_event
  • stream_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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants