fix(audio): preserve compatible playback and recover failed Windows outputs (#16150)

* fix(audio): preserve compatible playback on replacement failure

* fix(audio): use Windows events for capture worker wakeups

* fix(audio): recover Windows playback after output device failure

* reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): reset decoder when retaining compatible playback

* fix(audio): integrate playback retention and Windows capture wakeups

* revert(audio): remove Windows capture-event notification changes

* docs(audio): explain Windows 7 capture limitations

* docs(audio): clarify capture changes introduced by #16095

* fix(audio): preserve playback through asynchronous Windows startup

Keep a compatible active output until the replacement callback confirms startup, preserving decoder progress on promotion or rollback. Handle superseding formats and simultaneous output failures without losing recovery state. Pin the scoped CPAL WASAPI event-ownership fix and add deterministic and native regressions.

* fix(audio): retain ready output across superseding formats

* fix(audio): update CPAL teardown recovery

Pin the upstream-aligned WASAPI cleanup with shared event lifetime, self-join prevention and fallible destructor diagnostics. Preserve the existing dependency graph and audio implementation.

* Use upstream-style CPAL stream teardown

* Refact: remove low value test

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): retry playback when startup confirmation times out

Signed-off-by: fufesou <linlong1266@gmail.com>

* update cpal

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-09-16 11:55:53 +08:00
committed by GitHub
parent 20e25e743d
commit 28269af9c7
9 changed files with 668 additions and 117 deletions

View File

@@ -391,6 +391,15 @@ mod cpal_impl {
if !audio_input.is_empty() {
return get_audio_input(&audio_input);
}
// The pinned CPAL uses event-driven WASAPI loopback here. Windows versions
// before Windows 10 1703 do not signal capture events, so system audio does
// not work on Win7. #16095 kept the same CPAL revision and loopback path;
// this limitation predates that PR.
// Ordinary microphone input is supported on Win7 and uses the branch above.
// #16095 added its callback-to-encoder wake dependency; see CapturePcmSender::wake
// for the new scheduling risk, whose audible impact on Win7 is unmeasured.
// https://learn.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording
// https://learn.microsoft.com/en-us/windows/win32/coreaudio/capturesharedeventdriven
let device = HOST
.default_output_device()
.with_context(|| "Failed to get default output device for loopback")?;

View File

@@ -97,6 +97,7 @@ impl Drop for CaptureEncoderWorker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.take() {
// Owner-thread shutdown already waits via join(); see CapturePcmSender::wake for Win7.
handle.thread().unpark();
if let Err(error) = handle.join() {
log::error!("Failed to join audio encoder thread: {error:?}");
@@ -209,6 +210,16 @@ impl CapturePcmSender {
fn wake(&self) {
if let Some(thread) = self.handoff.wake_thread.get() {
// #16095 moved Opus encoding and message submission from the capture callback to a worker.
// Previously, the callback did that work directly, with allocations and blocking locks.
// On Win7 with Rust 1.75, if the worker is descheduled after publishing PARKED but before
// NtWaitForKeyedEvent, unpark() waits in NtReleaseKeyedEvent until the worker enters that wait.
// It does not wait for encoding; park_timeout() does not bound the callback's wait.
// Delays can cause gaps or stall teardown; a Win7 microphone regression has not been measured.
// System loopback already failed on Win7 before #16095 (see cpal_impl::get_device),
// so the affected path is microphone/input-device capture, including outgoing voice calls.
// Accept this risk to preserve Win7 input capture without a separate legacy notifier.
// https://github.com/rust-lang/rust/blob/1.75.0/library/std/src/sys/windows/thread_parking.rs
thread.unpark();
}
}