Service: offer "Use Windows default output" (Christopher's request), reusing the app's follower

The service profile could only pick named output devices, so it couldn't "just send
whatever this machine plays and follow the Windows default". The main app already had
exactly that (its DefaultLoopbackSendFollower + ResolveDefaultDeviceId) - so rather
than invent a parallel mechanism (which would silently diverge), this pulls the shared
piece out and has the service reuse it.

- New AudioDefaultFollower: one home for the loopback-send sentinel
  ("__use-default-loopback-send__"), the follower list entry, and the default-endpoint
  resolver. MainForm now references it (its DefaultLoopbackSendFollower and
  ResolveDefaultDeviceId delegate to it) so there is a single definition.
- Service config dialog: the Audio send tab lists "Use Windows default audio device,
  follows Windows changes" as the first output choice. Ticking it persists the same
  sentinel the app uses.
- ServiceSendHost.BuildSendSpecs resolves that sentinel to the LIVE default render
  endpoint (with de-dup against explicitly-ticked devices), never passing the raw
  sentinel through. Because the service re-applies its profile on every device-change
  notification - and OnDefaultDeviceChanged is one of them - it FOLLOWS the default:
  change Windows' default output and the service switches to it within a beat.
- Manual: documents the new option in the service section.

Test: new "Default-output follower" self-test - follower is flagged + shares the app's
sentinel, and the service resolves it to the current Windows default render endpoint
(never leaks the raw sentinel into a capture spec). Gate 44/44.

The ASIO "Rea" devices Ed noticed are real registry drivers (Realtek ASIO + REAPER's
ReaRoute), not injected dummies - we only ever list HKLM\SOFTWARE\ASIO. No code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-17 23:08:55 +01:00
co-authored by Claude Opus 4.8
parent 43e249e90e
commit 4a86a82ec8
6 changed files with 103 additions and 16 deletions
+7 -13
View File
@@ -526,8 +526,9 @@ public sealed class MainForm : Form
// "Use Windows default" for the SEND side's "WASAPI audio outputs to send" (system-audio/loopback)
// list: loopback-capture whatever Windows currently uses as the default OUTPUT and follow it. Its own
// sentinel + persisted flag, distinct from the receive-output follower (which plays TO the default).
private static readonly AudioDeviceChoice DefaultLoopbackSendFollower =
new("Use Windows default audio device, follows Windows changes", "__use-default-loopback-send__", CaptureKind.Loopback) { IsDefaultFollower = true };
// The loopback-send follower is shared with the service (AudioDefaultFollower) so both offer and
// resolve the exact same sentinel; the receive-output and send-input followers above stay local.
private static readonly AudioDeviceChoice DefaultLoopbackSendFollower = AudioDefaultFollower.LoopbackSendChoice();
// The Windows-default device id we last routed to while following, per direction. A default-device
// change doesn't change the device SET, so the list-sync wouldn't catch it — we compare against
// these to spot it and re-route (see ReapplyIfFollowedDefaultChanged).
@@ -6479,17 +6480,10 @@ public sealed class MainForm : Form
/// <summary>The id of the current Windows default endpoint for the given direction, or null if
/// there isn't one / it can't be read. Best-effort.</summary>
private static string? ResolveDefaultDeviceId(NAudio.CoreAudioApi.DataFlow flow)
{
try
{
using var enumerator = new NAudio.CoreAudioApi.MMDeviceEnumerator();
if (!enumerator.HasDefaultAudioEndpoint(flow, NAudio.CoreAudioApi.Role.Multimedia)) return null;
using var device = enumerator.GetDefaultAudioEndpoint(flow, NAudio.CoreAudioApi.Role.Multimedia);
return device.ID;
}
catch { return null; }
}
// Shared with the service via AudioDefaultFollower so "the current Windows default" means the same
// thing in both. Thin delegate kept so the existing call sites don't all have to change.
private static string? ResolveDefaultDeviceId(NAudio.CoreAudioApi.DataFlow flow) =>
AudioDefaultFollower.ResolveDefaultDeviceId(flow);
private static bool IsFollowerChecked(CheckedListBox list) =>
list.CheckedItems.OfType<AudioDeviceChoice>().Any(c => c.IsDefaultFollower);