Skip to content

fix: recover from WASAPI device invalidation instead of going silent#55

Merged
chrisuthe merged 2 commits into
masterfrom
fix/recover-device-invalidation
Jun 17, 2026
Merged

fix: recover from WASAPI device invalidation instead of going silent#55
chrisuthe merged 2 commits into
masterfrom
fix/recover-device-invalidation

Conversation

@chrisuthe

Copy link
Copy Markdown
Owner

The bug (from a live log)

Playback went permanently silent. The log showed:

[ERR] WasapiAudioPlayer: Playback stopped due to error
System.Runtime.InteropServices.COMException (0x88890004)
   at IAudioClient.GetCurrentPadding
   at WasapiOut.PlayThread()
Player state: "Playing" -> "Error"
…
[Buffer] Overrun #2640: dropped 96ms … buffer full at 120000ms   (forever)

0x88890004 is AUDCLNT_E_DEVICE_INVALIDATED — the output device was pulled out from under WASAPI (default device changed, device disabled/unplugged/reformatted, or grabbed in exclusive mode). The player dropped to a terminal Error state and never recovered; the network kept feeding the buffer, which pinned at capacity and just churned overruns with nobody reading. Silent until app restart.

Fix

Detect that HResult in OnPlaybackStopped and re-initialize the output on the current device, resuming from the buffered audio, instead of dying:

  • Recovery reuses the existing device-switch rebuild path (which preserves the buffer and re-anchors timing), via a new internal forceResume flag.
  • Retries up to 5× with a short increasing backoff so Windows can settle a new default device; only falls back to Error if every attempt fails.
  • The previous output's Stop() is now guarded — a dead/invalidated device throws on Stop().

Net effect: a device change that used to silence playback forever becomes a brief blip that self-recovers.

Scope / risk

App-only, independent of the SDK PRs. No change to the normal Clear()/play paths; the only edit to the device-switch path is the additive forceResume parameter and the guarded Stop().

Verify

Manual (it's a hardware/OS event): play audio, then in Windows Sound settings change the default output device (or disable the playing device). Expect a ~½–1s gap, then audio resumes and the log shows Recovered audio output after device invalidation. Build green, 99/99 tests pass.

AUDCLNT_E_DEVICE_INVALIDATED (0x88890004) fires when the output device is pulled
out from under WASAPI - the default device changes, a device is disabled,
unplugged or reformatted, or another app grabs it in exclusive mode. The player
dropped to a terminal Error state and stayed silent until the app was restarted
(the buffer just filled and overran with nobody reading it).

Detect that HResult in OnPlaybackStopped and re-initialize the output on the
current device, resuming from the buffered audio. Recovery reuses the
device-switch rebuild path (preserves the buffer, re-anchors timing) via a new
forceResume flag, retries up to 5 times with a short backoff so Windows can
settle a new default device, and only falls back to Error if all attempts fail.
The previous output's Stop() is now guarded, since a dead device throws on Stop().
…ured, not guessed

GetActualOutputLatency() queried IAudioClient.StreamLatency before WasapiOut.Init()
ran, so every call threw AUDCLNT_E_NOT_INITIALIZED (0x88890001) and fell back to a
fixed 115ms estimate - wrong for any DAC whose real latency differs, which is what
forces a per-device manual static-delay offset.

Move the query to after Init() (SetSampleSource and the device-switch path), where the
AudioClient is initialized and StreamLatency returns the real device latency.
InitializeAsync keeps a preliminary estimate only for its own init log. Promotes the
shared latency constants to class scope.

Note: making this drive auto-compensation still needs SDK-side work - the pipeline
reads OutputLatencyMs before SetSampleSource (so it gets the estimate) and treats
OutputLatencyMicroseconds as informational rather than subtracting it from the schedule.
@chrisuthe
chrisuthe merged commit b733b5d into master Jun 17, 2026
2 checks passed
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.

1 participant