Per-app send fixes + apps-mode UI rework (no send-all) + truly-global remembered lists

Per-app capture (the "foobar alone = no sound" report):
- ProcessLoopbackCapture: take ActivateAudioInterfaceAsync's out operation as a raw
  IntPtr (released after completion) instead of a typed interface. The eager RCW cast
  of the not-yet-realised operation object threw InvalidCastException / E_NOINTERFACE
  and killed EVERY specific-app capture at start.
- CompositeCaptureBackend: single IsPushEligible authority shared by Start,
  UpdateSources and the coalesced rebuild. The update paths were missing the
  ProcessLoopback exclusion, so switching whole-device -> one app kept the push
  backend and fed it "proc:<pid>" (GetDevice ArgumentException).
- PushModeWasapiBackend: loud backstop rejecting process-loopback specs.
- Self-test: lifecycle test now FAILS on an activation error (it previously passed
  green with the feature completely dead) + pure routing-rule checks.

Apps-mode UI (Ed 2026-07-16): the "Send all applications" checkbox is GONE from the
main window - applications mode always means picking specific apps; whole-system
audio is devices mode's job. Active list = running apps + any ticked app that is not
running ("(not running)" so it can be unticked); Remembered list = global address
book minus whatever is ticked. In-place list reconcile (no Clear+rebuild) kills the
NVDA double-read of the toggled row. Profile.SendAllApplications stays for the
SERVICE (deliberate divergence - a headless lock-screen sender wants system audio).

Remembered lists now genuinely machine-wide (AppConfig-backed): the settings store is
an intra-process cache, so remembered applications were forgotten on every exit and
remembered peers were per-profile in practice. Both books moved to AppConfig; legacy
per-profile peers are unioned in on profile load; profile save snapshots the global
book back for old-build compat. Cross-instance persistence pinned by self-test.

Service (issue #23): 15s capture pulse (callbacks/bytes/pre-encode peak/frames sent)
while sending - distinguishes "endpoint mix is genuinely silent at the lock screen"
from a pipeline fault, which callbacks alone cannot.

Gate: 38/38.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-16 22:50:38 +01:00
co-authored by Claude Opus 4.8
parent 92ad77ff52
commit 1003bbfdf2
10 changed files with 337 additions and 150 deletions
+21
View File
@@ -71,9 +71,29 @@ public sealed class ServiceSendHost : IDisposable
private long captureWatchStartTick;
private bool loggedFirstCallback;
private bool loggedZeroCallbacks;
private long lastCapturePulseTick;
// How often the periodic capture pulse is written while the service is sending. 15s keeps a
// boot-to-login window (typically 30s+) covered by at least two readings without bloating the log.
private const int CapturePulseIntervalMs = 15_000;
private void WatchCaptureHealth()
{
// Jonathan's follow-up log proved callbacks CAN flow at the lock screen while peers still hear
// nothing — callbacks alone can't distinguish real sound from the silence keepalive feeding back.
// So while sending, also pulse the loudest pre-encode sample + frames actually sent every 15s:
// peak≈0.000 pre-login flipping to real values at login = the captured endpoint mix is genuinely
// silent on the lock screen (device/session routing), NOT a service pipeline fault.
var now = Environment.TickCount64;
if (now - lastCapturePulseTick >= CapturePulseIntervalMs)
{
lastCapturePulseTick = now;
var peak = sender.TakeMaxSenderPreEncodePeak();
var frames = sender.TakeSenderAudioFramesSent();
log?.Invoke($"service: capture pulse — callbacks={sender.CaptureCallbacks} bytes={sender.CaptureBytes} peak={peak:F3} framesSent={frames}"
+ (peak < 0.001f ? " (capturing SILENCE — nothing audible in the endpoint mix)" : ""));
}
if (loggedFirstCallback) return;
if (sender.CaptureCallbacks > 0)
{
@@ -139,6 +159,7 @@ public sealed class ServiceSendHost : IDisposable
captureWatchStartTick = Environment.TickCount64;
loggedFirstCallback = false;
loggedZeroCallbacks = false;
lastCapturePulseTick = Environment.TickCount64; // first pulse lands one interval after start
// Come up on the network too, so the peers can discover and connect to us — not just receive a
// blind push. Same well-known audio port and the same components the interactive app uses.
presence.Start(RemPacket.DefaultPort, endpoints);