From 0db36f278d0cfeeaa7a897e8b0e60cb336ae42f4 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:20:47 +0100 Subject: [PATCH] Service: remove the "send all applications" option (finish what the main app did) Ed asked back on 2026-07-16 to drop "send all applications" from the app AND the service - applications mode = pick specific apps only. The main app was done; the service kept it. Worse, a code comment had pinned it in place as a DELIBERATE divergence ("the headless service streams system audio, so send-all stays"), which is exactly why the earlier removal skipped the service. Standing rule bit us: the service silently diverged from the main app. Removed across the whole service path, not just the checkbox: - ServiceProfileDialog: dropped the "Send all applications" AccessibleCheckBox, its event wiring and visibility branch; applications mode now always shows the specific- apps list. Renumbered the tab's Alt hints (apps Alt+3, inputs Alt+4) now the box is gone. Save forces SendAllApplications=false so a stale profile can't resurrect it. - ServiceSendHost.BuildSendSpecs: deleted the SendAllApplications branch (and the now- dead ResolveDefaultRenderId helper). Apps mode builds one process-loopback spec per ticked app - identical to the main app. - Profile.SendAllApplications: marked vestigial (kept only so old profiles deserialize; nothing reads it now). Guard so it can't drift again: the Service-sender-parity self-test now asserts apps mode produces only per-application specs and NEVER a whole-system loopback spec, even with SendAllApplications=true. (The old parity test only exercised devices mode, which is why it missed this.) Service UI audit (Ed's question): the whole service UI is house-controls throughout - QuietTabControl, AccessibleCheckBox, MnemonicLabel, Theme.SectionHeader/AppIcon, FormLayoutRows, CheckedListAccessibility. No raw CheckBox/TabControl (which are silent to NVDA on .NET 10) anywhere in Service*.cs. Gate 43/43. Co-Authored-By: Claude Opus 4.8 --- src/RemSound.App/SelfTest.cs | 17 +++++++++++++- src/RemSound.App/ServiceProfileDialog.cs | 29 +++++++++--------------- src/RemSound.App/ServiceSendHost.cs | 29 +++++------------------- src/RemSound.Core/Profile.cs | 10 ++++---- 4 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index bd5e257..df8c9d2 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -914,7 +914,22 @@ internal static class SelfTest $"the service must FORCE Opus regardless of the profile codec (got {cfg.Codec})"); Check(cfg.Frame == 120, $"the service must force the 2.5 ms live Opus frame (120 samples), regardless of the profile (got {cfg.Frame})"); - return "service forces Opus + 2.5ms frame + lock-to-clock regardless of profile; crypto matches"; + + // Applications-mode parity with the main app: specific apps only. Even with the legacy + // SendAllApplications flag set true, the service must NOT emit a whole-system "all applications" + // loopback spec — it must build one process-loopback spec per ticked app. (Guards the drift where + // the service kept the removed "send all applications" checkbox + code path — Ed, 2026-07-17.) + if (RemSound.Sender.ProcessLoopbackCapture.IsSupported) + { + var appsProfile = new Profile { WasapiSendMode = "applications", SendAllApplications = true }; + appsProfile.SelectedSendApplications.Add("nonexistent-proc-for-test"); + var appSpecs = ServiceSendHost.BuildSendSpecs(appsProfile); + Check(!appSpecs.Any(s => s.Kind == CaptureKind.Loopback), + "applications mode must NOT produce a whole-system loopback spec, even with SendAllApplications=true"); + Check(appSpecs.All(s => s.Kind is CaptureKind.ProcessLoopback or CaptureKind.Input), + "applications mode must build only per-application (or input) specs — the 'send all' path is gone"); + } + return "service forces Opus + 2.5ms frame + lock-to-clock; apps mode is specific-apps-only; crypto matches"; } /// The lock-screen service's app-yield token: while a hold is active the service must see an diff --git a/src/RemSound.App/ServiceProfileDialog.cs b/src/RemSound.App/ServiceProfileDialog.cs index 2fa2ead..b84bab1 100644 --- a/src/RemSound.App/ServiceProfileDialog.cs +++ b/src/RemSound.App/ServiceProfileDialog.cs @@ -28,13 +28,9 @@ internal sealed class ServiceProfileDialog : Form private readonly ListBox sendModeList = new() { Width = 460, Height = 40, IntegralHeight = false, AccessibleName = "How to send WASAPI audio (Alt+1)" }; private readonly CheckedListBox outputsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "WASAPI audio outputs to send (Alt+2)" }; private readonly Label outputsStatus = new() { AutoSize = true, Text = "No output device selected." }; - // DELIBERATE divergence from the main window (which dropped its send-all option 2026-07-16): the - // headless service's whole point is streaming the machine's system audio from the lock screen, so - // "send all applications" stays here as the sensible default. - private readonly AccessibleCheckBox sendAllAppsBox = new() { Text = "Send all applications (Alt+&3)", AccessibleName = "Send all applications", AutoSize = true, Checked = true }; - private readonly CheckedListBox appsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "Applications to send (Alt+4)" }; + private readonly CheckedListBox appsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "Applications to send (Alt+3)" }; private readonly Label appsStatus = new() { AutoSize = true, Text = "No application selected." }; - private readonly CheckedListBox inputsList = new() { CheckOnClick = true, Width = 460, Height = 90, AccessibleName = "WASAPI audio inputs to send (Alt+5)" }; + private readonly CheckedListBox inputsList = new() { CheckOnClick = true, Width = 460, Height = 90, AccessibleName = "WASAPI audio inputs to send (Alt+4)" }; private readonly Label inputsStatus = new() { AutoSize = true, Text = "No input device selected." }; private MnemonicLabel? sendModeLabel, outputsLabel, appsLabel, inputsLabel; @@ -154,15 +150,11 @@ internal sealed class ServiceProfileDialog : Form sendModeList.Items.Add("Send specific applications"); sendModeList.SelectedIndex = 0; sendModeList.SelectedIndexChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); }; - sendAllAppsBox.CheckedChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); }; sendModeLabel = AddListRow(panel, 0, "How to send WASAPI audio (Alt+&1)", sendModeList); outputsLabel = FormLayoutRows.AddCheckedListRow(panel, 1, "WASAPI audio outputs to send (Alt+&2)", outputsList, outputsStatus, l => l.Focus()); - var allAppsWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; - allAppsWrap.Controls.Add(sendAllAppsBox); - panel.Controls.Add(allAppsWrap, 1, 2); - appsLabel = FormLayoutRows.AddCheckedListRow(panel, 3, "Applications to send (Alt+&4)", appsList, appsStatus, l => l.Focus()); - inputsLabel = FormLayoutRows.AddCheckedListRow(panel, 4, "WASAPI audio inputs to send (Alt+&5)", inputsList, inputsStatus, l => l.Focus()); + appsLabel = FormLayoutRows.AddCheckedListRow(panel, 2, "Applications to send (Alt+&3)", appsList, appsStatus, l => l.Focus()); + inputsLabel = FormLayoutRows.AddCheckedListRow(panel, 3, "WASAPI audio inputs to send (Alt+&4)", inputsList, inputsStatus, l => l.Focus()); CheckedListAccessibility.Wire(outputsList, outputsStatus, "output device"); CheckedListAccessibility.Wire(appsList, appsStatus, "application"); @@ -211,7 +203,6 @@ internal sealed class ServiceProfileDialog : Form var appsMode = ProcessLoopbackCapture.IsSupported && string.Equals(working.WasapiSendMode, "applications", StringComparison.OrdinalIgnoreCase); sendModeList.SelectedIndex = appsMode ? 1 : 0; - sendAllAppsBox.Checked = working.SendAllApplications; PopulateAppsList(); peersList.Items.Clear(); @@ -233,7 +224,10 @@ internal sealed class ServiceProfileDialog : Form working.SelectedWasapiSendOutputs = CheckedIds(outputsList); working.SelectedWasapiSendInputs = CheckedIds(inputsList); working.WasapiSendMode = sendModeList.SelectedIndex == 1 ? "applications" : "devices"; - working.SendAllApplications = sendAllAppsBox.Checked; + // Applications mode = pick specific apps only (matches the main app; the "send all applications" + // option was removed from both). Force the legacy flag off so a stale profile that still carries + // SendAllApplications=true can't make the service quietly capture the whole system. + working.SendAllApplications = false; working.SelectedSendApplications = appsList.CheckedItems.OfType().Select(a => a.ProcessName).Distinct().ToList(); // Audio profile is fixed for the service (no tab): Opus live-jamming frame, Small packets, locked @@ -289,10 +283,9 @@ internal sealed class ServiceProfileDialog : Form var appsMode = supported && sendModeList.SelectedIndex == 1; if (outputsLabel is not null) outputsLabel.Visible = !appsMode; SetRowVisible(outputsList, !appsMode); - SetRowVisible(sendAllAppsBox, appsMode); - var showApps = appsMode && !sendAllAppsBox.Checked; - if (appsLabel is not null) appsLabel.Visible = showApps; - SetRowVisible(appsList, showApps); + // Applications mode always shows the specific-apps list (no "send all" option any more). + if (appsLabel is not null) appsLabel.Visible = appsMode; + SetRowVisible(appsList, appsMode); } private static void SetRowVisible(Control c, bool visible) diff --git a/src/RemSound.App/ServiceSendHost.cs b/src/RemSound.App/ServiceSendHost.cs index 1639955..2fb9e17 100644 --- a/src/RemSound.App/ServiceSendHost.cs +++ b/src/RemSound.App/ServiceSendHost.cs @@ -573,17 +573,12 @@ public sealed class ServiceSendHost : IDisposable && string.Equals(p.WasapiSendMode, "applications", StringComparison.OrdinalIgnoreCase); if (appsMode) { - if (p.SendAllApplications) - { - var def = ResolveDefaultRenderId(); - if (def is not null) specs.Add(new CaptureSourceSpec(def, CaptureKind.Loopback, "All applications (system audio)")); - } - else - { - foreach (var name in p.SelectedSendApplications.Distinct(StringComparer.OrdinalIgnoreCase)) - foreach (var pid in AudioAppEnumerator.PidsForProcessName(name)) - specs.Add(new CaptureSourceSpec(ProcessLoopbackId.Format(pid), CaptureKind.ProcessLoopback, name)); - } + // Specific applications only — matches the main app. The old "send all applications" + // path (SendAllApplications) was removed from both the app and the service; the profile + // flag is ignored here so a stale profile can't resurrect whole-system capture. + foreach (var name in p.SelectedSendApplications.Distinct(StringComparer.OrdinalIgnoreCase)) + foreach (var pid in AudioAppEnumerator.PidsForProcessName(name)) + specs.Add(new CaptureSourceSpec(ProcessLoopbackId.Format(pid), CaptureKind.ProcessLoopback, name)); } else { @@ -634,18 +629,6 @@ public sealed class ServiceSendHost : IDisposable return int.TryParse(text[(colon + 1)..], out var port) && port is >= 1 and <= 65535 ? (host, port) : (text, null); } - private static string? ResolveDefaultRenderId() - { - try - { - using var en = new MMDeviceEnumerator(); - if (!en.HasDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia)) return null; - using var d = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); - return d.ID; - } - catch { return null; } - } - public void Dispose() { lock (gate) diff --git a/src/RemSound.Core/Profile.cs b/src/RemSound.Core/Profile.cs index ec90ccb..a1f9c9d 100644 --- a/src/RemSound.Core/Profile.cs +++ b/src/RemSound.Core/Profile.cs @@ -79,11 +79,11 @@ public sealed class Profile /// SERVICE profiles only (ServiceProfileDialog / ServiceSendHost): when true (the default) /// in "applications" send mode every app's audio is sent — i.e. exactly the same result as sending /// the whole system's default output. When false, only the apps named in - /// are sent. The MAIN window no longer has a "send all - /// applications" option (removed 2026-07-16): there, applications mode always means specific ticked - /// apps, and whole-system audio is devices mode's job. The main window never READS this to drive - /// behaviour; it does still write the default true when it saves a profile (harmless — main-app and - /// service profiles live in separate stores and are never the same file). + /// are sent. VESTIGIAL as of 2026-07-17: neither the main + /// window NOR the service honours this any more — applications mode always means the specific ticked + /// apps in both, and whole-system audio is devices mode's job. The "send all applications" option was + /// removed from both UIs and both send-spec builders ignore this flag. Kept only so existing profile + /// files still deserialize; the service now writes it false on save. Do not add new reads. public bool SendAllApplications { get; set; } = true; /// In "applications" send mode: the process /// names (lower-case, no path/extension, e.g. "vlc", "firefox") whose audio to send. Tracked by