Skip to content

fix(audio): release the device before reopening it exclusively - #606

Draft
InstaZDLL wants to merge 2 commits into
mainfrom
fix/604-exclusive-device-collision
Draft

fix(audio): release the device before reopening it exclusively#606
InstaZDLL wants to merge 2 commits into
mainfrom
fix/604-exclusive-device-collision

Conversation

@InstaZDLL

Copy link
Copy Markdown
Owner

Addresses the reproducible half of #604.

What the logs actually say

0x8889000A is AUDCLNT_E_DEVICE_IN_USE, and the question was who held the
device. Three facts, measured on Windows 11 with a throwaway probe rather
than reasoned about:

Question Answer
Does a shared client of ours block our exclusive open? No. Initialize in exclusive mode succeeds while one of our own shared streams is open and running on the same endpoint.
Does an exclusive client block it? Yes, with exactly 0x8889000A.
How long does the block outlive the client? ~19 ms after the drop.

The third one matters because it rules out the leading hypothesis. A
release still in flight cannot explain a loop that ran for twelve seconds.
The holder was still alive.

Who was holding it

set_output_device opens the replacement stream before releasing the
old one, and says why: the two streams target different endpoints, so a
failed open costs nothing and playback survives. That premise is what a
vanished device breaks. pick_device falls back to the default
endpoint when the pinned name is no longer enumerated — which is precisely
the state after a Bluetooth headset disconnects — and that endpoint can be
the one the old exclusive stream is still holding. The open is then refused
against ourselves.

The rule for this already existed. must_release_before_reopening was
added for the same class of defect on macOS, and both force_rebuild_output
and set_exclusive_output consult it. The switch path never did.

A shared old stream still keeps the spawn-first order and the rollback it
buys, because the first measurement above says it is no obstacle.

A correction

The doc comment being touched here claimed Windows evicts the shared client
when the endpoint is seized. That was written from reasoning, not from a
run, and it is backwards in a way that mattered: it is exactly the sentence
that made a shared client look like the suspect. It now carries the
measurement instead.

The other commit

Clippy runs on the Linux slot only, on the stated grounds that its answer
cannot differ by operating system. True for portable code, false for code
behind a cfg: wasapi_exclusive.rs is never compiled where clippy runs,
so a needless_borrow sat in it from the day the shared period fill
landed. coreaudio_exclusive.rs has no job at all and is linted nowhere.
The lint is fixed here; whether to run clippy on the Windows slot is a
cost decision left to the maintainer
, since the current arrangement is
documented as deliberate.

What this does not close

The reporter describes silence; the logs also show a storm of failed opens.
They are two distinct moments — 04:58 and 06:20 — and only one symptom was
reported. Nothing here establishes they share a cause, and #604 should stay
open until its author confirms.

The silent redirect itself is untouched: a pinned device that has vanished
still becomes the default one with only a log line to say so, and the
picker still shows the old selection. That is #597, and #604 is what it
looks like from the outside.

Clippy runs on the Linux slot only, on the stated grounds that its answer
cannot differ by operating system. That holds for portable code and not
for code behind a cfg: the Windows backend is never compiled where clippy
runs, so needless_borrow sat in it from the day the shared period fill
landed. The macOS backend has no job at all and is linted nowhere.
A device switch opens the new stream before tearing the old one down, so
a failed open costs nothing and the stream being listened to survives.
The premise is that the two streams target different endpoints. A device
that has gone away breaks it: when the pinned name is no longer
enumerated, pick_device falls back to the default endpoint, which can be
the one the old stream already holds exclusively. Windows then refuses
the open against ourselves, we drop to shared mode, and nothing says why.

The rule for this already existed and both other rebuild paths consult
it; the switch path did not.

Three things measured on Windows 11 rather than assumed, because the
comment being corrected here asserted the second one backwards. An
exclusive client does block a second exclusive open of the same endpoint.
A shared client blocks nothing: an exclusive Initialize succeeds while
one of our own shared streams is open and running. And the block clears
about 19 milliseconds after the client is dropped, which rules out a
release still in flight as the explanation for a loop lasting seconds.

What this does not establish is that the silence the reporter describes
has the same cause as the storm of failed opens in their log. They are
two distinct moments and only one symptom was reported.
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) type: fix Bug fix size: m 50-200 lines labels Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) size: m 50-200 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant