Issue #23: deaf-capture detector via endpoint meter — react in ~1s, not 15s
The 15s silence-pulse trigger was far too slow (Ed: the boot tune would be over before the first check) and silence alone was always a weak signal — a quiet machine and a deaf capture look identical from inside the stream. New detector: read the endpoint's OWN output meter (IAudioMeterInformation .MasterPeakValue) every watch tick, independently of our capture stream, and compare it with what the capture is hearing. Device audibly playing (meter >= 0.003) while the capture has heard only silence since it opened = the capture is provably DEAF -> re-open it immediately so it re-attaches to the live audio graph. - Service loop tick 1000ms -> 500ms; deafness threshold is TIME-based (450ms of continuous divergence) so reaction lands ~1s after the first audible sound — fast enough that the boot tune itself comes through — and a fast test cadence can't trip it (a healthy capture hears real sound within ~200ms). - Zero churn risk: a quiet machine reads quiet on BOTH sides, so healthy captures never re-open (the old design would have re-opened 3x on any quiet stint). - Frozen callbacks (2s+) still re-open regardless of loudness. - Ladder: max 3 re-opens per stint, 2s spacing, ends at the first real audio heard; refilled on Resume and power resume. - Meter readers swapped per (re)apply, disposed on suspend; per-device catch absorbs a disposed/invalidated endpoint mid-read. - 15s pulse is now purely diagnostic and logs capPeak + meterPeak maxima with an explicit "(DEVICE AUDIBLE BUT CAPTURE SILENT)" flag. - Decision core (ShouldReopenCapture) pure + pinned by updated self-test. Gate: 39/39. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c72169c7f5
commit
950a61cbf3
@@ -1548,23 +1548,25 @@ internal static class SelfTest
|
||||
return "reachable armed; long-unreachable dropped; grace-window kept; recovery re-arms (issues #8/#15)";
|
||||
}
|
||||
|
||||
/// <summary>Issue #23 boot self-heal decision core: a capture that has heard only silence since it
|
||||
/// opened (or whose callbacks froze) gets re-opened, capped at 3 attempts per stint, and the ladder
|
||||
/// ends for good once real audio has been heard. The scenario: at the boot lock screen the machine's
|
||||
/// <summary>Issue #23 boot self-heal decision core. Scenario: at the boot lock screen the machine's
|
||||
/// speakers audibly play (Windows tune, NVDA) but a capture attached in the first seconds of boot
|
||||
/// taps an engine mix the logon-session audio was never wired into — re-attaching lands on the live
|
||||
/// graph.</summary>
|
||||
/// taps an engine mix the logon-session audio was never wired into — the endpoint's own METER shows
|
||||
/// sound while the capture hears none, which proves the capture deaf, and a re-open re-attaches it
|
||||
/// to the live graph. Quiet machines read quiet on both sides, so healthy captures never re-open;
|
||||
/// frozen callbacks re-open regardless; the ladder is capped and ends once real audio is heard.</summary>
|
||||
private static string? ServiceSilentCaptureSelfHeal()
|
||||
{
|
||||
Check(ServiceSendHost.ShouldReopenSilentCapture(stalled: false, everHeardAudio: false, attemptsSoFar: 0),
|
||||
"a capture that has never heard audio must be re-opened");
|
||||
Check(ServiceSendHost.ShouldReopenSilentCapture(stalled: true, everHeardAudio: true, attemptsSoFar: 0),
|
||||
Check(ServiceSendHost.ShouldReopenCapture(captureDeaf: true, everHeardAudio: false, stalled: false, attemptsSoFar: 0),
|
||||
"a provably deaf capture (device audible, capture silent since open) must be re-opened");
|
||||
Check(ServiceSendHost.ShouldReopenCapture(captureDeaf: false, everHeardAudio: true, stalled: true, attemptsSoFar: 0),
|
||||
"a stalled capture must be re-opened even after audio has been heard");
|
||||
Check(!ServiceSendHost.ShouldReopenSilentCapture(stalled: false, everHeardAudio: true, attemptsSoFar: 0),
|
||||
"a healthy capture that has heard real audio must be left alone");
|
||||
Check(!ServiceSendHost.ShouldReopenSilentCapture(stalled: false, everHeardAudio: false, attemptsSoFar: 3),
|
||||
Check(!ServiceSendHost.ShouldReopenCapture(captureDeaf: false, everHeardAudio: false, stalled: false, attemptsSoFar: 0),
|
||||
"a quiet machine (silent on both sides) must never trigger a re-open");
|
||||
Check(!ServiceSendHost.ShouldReopenCapture(captureDeaf: true, everHeardAudio: true, stalled: false, attemptsSoFar: 0),
|
||||
"once real audio has been heard, later silence must not re-open a healthy capture");
|
||||
Check(!ServiceSendHost.ShouldReopenCapture(captureDeaf: true, everHeardAudio: false, stalled: false, attemptsSoFar: 3),
|
||||
"the re-open ladder must stop at the attempt cap");
|
||||
return "silent-since-open and stalled captures re-open; heard-audio healthy captures don't; capped at 3";
|
||||
return "deaf and stalled captures re-open; quiet or heard-audio captures don't; capped at 3";
|
||||
}
|
||||
|
||||
/// <summary>The fix for "a saved app that launches later never gets captured": the send engine
|
||||
|
||||
Reference in New Issue
Block a user