"Use Windows default" is now exclusive: it clears + locks out the specific cards
Ed: ticking "Use Windows default audio device" should uncheck the other cards and stop them being ticked again until the default is turned back off - in both the app and the service. This replaces the old optional "shall I untick the others?" prompt with a hard rule. Behaviour (all follower lists: app received-outputs, outputs-to-send and inputs-to-send; service outputs): - Ticking the follower unticks every specific card in that list. - While the follower is on, trying to tick a specific card is refused (the ItemCheck is vetoed straight back to unticked) - you're either following the Windows default or picking cards, never both. - Unticking the follower frees the cards again. - Enforced on load too, so a hand-edited/legacy profile carrying both comes up clean. Shared, not duplicated: the veto + clear + follower-checked helpers live in AudioDefaultFollower and are used by MainForm and the service dialog alike. Removed the now-obsolete soft prompt and everything that hung off it: - MainForm.MaybeUntickOthersForDefault + ResetDefaultAudioDevicePrompt. - The "Reset the default audio device prompt" Options item (and its Alt+O,R). - AppConfig.UntickOthersWhenUsingDefaultOutput/Input. - Manual + About-box references to the prompt / reset item; the manual now documents the exclusive rule and lists all three follower lists (the outputs-to-send follower had been undocumented). Test: "Default follower exclusivity" self-test - follower on clears + locks specific cards, follower off frees them, unticking and the follower entry itself are never vetoed. Gate 45/45. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4a86a82ec8
commit
5fc1b5bd75
+2
-4
@@ -315,7 +315,6 @@ ul, ol { padding-left: 1.4em; }
|
||||
<tr><td><strong>Keyboard shortcuts…</strong></td><td>Ctrl+K, or Alt+O, K</td><td>Opens the global hotkey dialog (mute, volume, show/hide window, start/stop recording, remote-control commands, speak the status line).</td></tr>
|
||||
<tr><td><strong>Profile passwords…</strong></td><td>Alt+O, W</td><td>Lists every profile alongside its password, so you can view or change any of them in one place.</td></tr>
|
||||
<tr><td><strong>Manage named peers…</strong></td><td>Alt+O, N</td><td>Opens a list of every peer you've given a friendly name to, showing each one's machine name and where and when you last connected to it. Pick one and <strong>Rename (Alt+R</strong>, or F2) to change its name, or <strong>Delete (Alt+D</strong>, or the Del key) to forget it — deleting only drops the name, the peer still connects as normal under its machine name. See <a href="#tab-connectivity">the peer-naming notes on the Connectivity tab</a>.</td></tr>
|
||||
<tr><td><strong>Reset the default audio device prompt</strong></td><td>Alt+O, R</td><td>Brings back the “use only the default audio device?” question if you previously ticked “Don't ask me this again” on it. See <a href="#tab-io">Following the Windows default audio device</a>.</td></tr>
|
||||
<tr><td><strong>Enable / Disable Realtek ASIO</strong></td><td>—</td><td>Only shown if a Realtek ASIO driver is installed. Lets you reverse the choice RemSound offered about disabling that driver (Realtek's generic ASIO driver tends to grab the wrong device and clash with your screen reader).</td></tr>
|
||||
<tr><td><strong>Install RemSound on this PC…</strong> (or <strong>Uninstall…</strong>)</td><td>Alt+O, I (Alt+O, U once installed)</td><td>Turns the copy you're running into a properly installed Windows app — on the Start menu, with a desktop shortcut, and listed in Windows’ Installed apps — or removes it again. Once it's installed, this item changes to <strong>Uninstall RemSound from this PC</strong>. See <a href="#install">Installing RemSound on your PC</a>.</td></tr>
|
||||
<tr><td><strong>Preferences…</strong></td><td>Ctrl+P, or Alt+O, P</td><td>Opens the Preferences dialog, organised into six tabs (move between them with Ctrl+Tab, or the arrow keys when the tab names have focus): <strong>General</strong> — the profiles folder, accept remote volume commands, UPnP router opening, and two buttons to clear the shared address books: <strong>Clear remembered peers list (Alt+P)</strong> and <strong>Clear remembered applications list (Alt+L)</strong> (each asks you to confirm first; these lists are shared across all your profiles, so clearing empties them everywhere); <strong>Appearance</strong> — <strong>Colour theme (Alt+T)</strong> (Match Windows / Light / Dark; changes only how the window looks, takes effect next launch, no effect on the screen reader), “Show the volume, pan and EQ for peers tab” (on by default; hides the <a href="#pan-eq">Volume, pan and EQ for peers tab</a> if you untick it), <strong>Tab order (Alt+O)</strong> — a list of the window's tabs with <strong>Move up (Alt+U)</strong> and <strong>Move down (Alt+N)</strong> buttons to reorder them, and two toggles to <strong>enable the Discovered (Alt+D)</strong> and <strong>Remembered (Alt+R)</strong> peer lists on the Connectivity tab (both on by default — untick one to hide that list); <strong>Audio cues</strong> — the cue list and its sounds (see <a href="#audio-cues">Audio cue sounds</a>); <strong>Startup behaviour</strong> — start minimised / with Windows / with a specific profile; <strong>Update settings</strong> — the update checks and install options; and <strong>Logging</strong> — enable logs, write logs now, and the log-folder housekeeping (see <a href="#logs">Logs and diagnostics</a>). Esc or the Close button dismisses it.</td></tr>
|
||||
@@ -413,9 +412,9 @@ ul, ol { padding-left: 1.4em; }
|
||||
|
||||
<h3>Following the Windows default audio device</h3>
|
||||
|
||||
<p>At the very top of the <strong>WASAPI outputs for received audio</strong> list and the <strong>WASAPI audio inputs to send</strong> list there's a special entry: <strong>Use Windows default audio device, follows Windows changes</strong>. Tick it and RemSound uses whatever Windows currently treats as the default — the default speakers for received sound, the default microphone for sending — and, the useful part, it <strong>follows that default on its own</strong>. Make a headset your default and RemSound switches to it; unplug it and RemSound moves back, all without you touching the list.</p>
|
||||
<p>At the very top of the <strong>WASAPI outputs for received audio</strong> list, the <strong>WASAPI audio outputs to send</strong> list and the <strong>WASAPI audio inputs to send</strong> list there's a special entry: <strong>Use Windows default audio device, follows Windows changes</strong>. Tick it and RemSound uses whatever Windows currently treats as the default — the default speakers for received sound, the default output for what you send, the default microphone for input — and, the useful part, it <strong>follows that default on its own</strong>. Make a headset your default and RemSound switches to it; unplug it and RemSound moves back, all without you touching the list.</p>
|
||||
|
||||
<p>It works alongside the specific devices: tick the default entry <em>and</em> particular cards, and the sound plays out of (or is captured from) all of them at once. When you first tick it, RemSound asks whether you'd like to untick the other devices in that list, so you can choose to use only the default — with a “Don't ask me this again” option. If you tick that and later want the question back, use <strong>Options → Reset the default audio device prompt</strong>.</p>
|
||||
<p>Ticking it is <strong>exclusive</strong>: RemSound unticks every specific card in that list and locks them so they can't be re-ticked while the default entry is on. That keeps things unambiguous — in each list you're either following the Windows default or picking specific cards, never a mix. Untick the default entry and the specific cards become available again.</p>
|
||||
|
||||
<p>Unlike the specific device ticks (which start fresh each session), the “use default audio device” choice is remembered between launches. It's safe to remember because it can never point at the wrong card — it always resolves to whatever Windows is using right now.</p>
|
||||
|
||||
@@ -945,7 +944,6 @@ Audient USB Audio ASIO Driver — Pair 3 (channels 5/6): Loop-back 1 (L) / L
|
||||
<tr><td>Alt+O, K</td><td>Keyboard shortcuts</td></tr>
|
||||
<tr><td>Alt+O, W</td><td>Profile passwords</td></tr>
|
||||
<tr><td>Alt+O, N</td><td>Manage named peers</td></tr>
|
||||
<tr><td>Alt+O, R</td><td>Reset the default audio device prompt</td></tr>
|
||||
<tr><td>Alt+O, P</td><td>Preferences</td></tr>
|
||||
<tr><td>Alt+F, X</td><td>Exit</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -138,14 +138,7 @@ internal sealed class AboutDialog : Form
|
||||
changes" entry at the top. Tick it and RemSound
|
||||
uses whatever Windows is currently using, and
|
||||
switches with it on its own: make a headset your
|
||||
default and RemSound follows, no clicking. You
|
||||
can still tick specific devices alongside it.
|
||||
|
||||
When you turn it on, RemSound offers to untick
|
||||
the other devices so you use only the default;
|
||||
if you hide that question with "Don't ask
|
||||
again", Options → Reset the default audio device
|
||||
prompt brings it back.
|
||||
default and RemSound follows, no clicking.
|
||||
|
||||
RemSound v4.5
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Windows.Forms;
|
||||
using NAudio.CoreAudioApi;
|
||||
using RemSound.Core;
|
||||
|
||||
@@ -33,6 +34,46 @@ internal static class AudioDefaultFollower
|
||||
/// callers that only follow the default output and shouldn't have to name a NAudio DataFlow.</summary>
|
||||
internal static string? ResolveDefaultRenderId() => ResolveDefaultDeviceId(DataFlow.Render);
|
||||
|
||||
// --- exclusivity: "use the default" locks out the specific cards in the same list ---
|
||||
// Ticking the follower means "just use whatever Windows uses" — so the specific device ticks are
|
||||
// cleared and cannot be re-ticked until the follower is turned off again (Ed, 2026-07-17). This
|
||||
// replaced the old optional "shall I untick the others?" prompt.
|
||||
|
||||
/// <summary>Whether the list's "Use Windows default" follower entry is currently ticked.</summary>
|
||||
internal static bool IsFollowerChecked(CheckedListBox list) =>
|
||||
list.CheckedItems.OfType<AudioDeviceChoice>().Any(c => c.IsDefaultFollower);
|
||||
|
||||
/// <summary>Call at the top of a list's ItemCheck handler. If the user is trying to TICK a specific
|
||||
/// (non-follower) device while the follower is on, force it back to unticked and return true — the
|
||||
/// caller should then stop processing that event. Never vetoes the follower itself, an untick, or a
|
||||
/// tick made while the follower is off.</summary>
|
||||
internal static bool VetoRealDeviceCheck(CheckedListBox list, ItemCheckEventArgs e)
|
||||
{
|
||||
if (e.NewValue != CheckState.Checked) return false;
|
||||
if (list.Items[e.Index] is not AudioDeviceChoice choice || choice.IsDefaultFollower) return false;
|
||||
if (!IsFollowerChecked(list)) return false;
|
||||
e.NewValue = CheckState.Unchecked;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Untick every specific (non-follower) device in the list, leaving the follower as-is.
|
||||
/// Returns true if anything changed. The caller MUST already be suppressing its ItemCheck handler
|
||||
/// (SetItemChecked re-raises ItemCheck) — this method does not toggle any suppression flag itself,
|
||||
/// so it nests safely inside an already-suppressed load.</summary>
|
||||
internal static bool UncheckRealDevices(CheckedListBox list)
|
||||
{
|
||||
var changed = false;
|
||||
for (var i = 0; i < list.Items.Count; i++)
|
||||
{
|
||||
if (list.Items[i] is AudioDeviceChoice { IsDefaultFollower: false } && list.GetItemChecked(i))
|
||||
{
|
||||
list.SetItemChecked(i, false);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>The current Windows default endpoint id for the given direction, or null if there isn't
|
||||
/// one (or it can't be read). Render = default speakers/output; Capture = default mic/input.</summary>
|
||||
internal static string? ResolveDefaultDeviceId(DataFlow flow)
|
||||
|
||||
@@ -1215,6 +1215,8 @@ public sealed class MainForm : Form
|
||||
receiveOutputDevicesList.ItemCheck += (_, e) =>
|
||||
{
|
||||
if (suppressDeviceCheckChange) return;
|
||||
// "Use Windows default" is exclusive: while it's on, specific cards can't be ticked.
|
||||
if (AudioDefaultFollower.VetoRealDeviceCheck(receiveOutputDevicesList, e)) return;
|
||||
if (receiveOutputDevicesList.Items[e.Index] is AudioDeviceChoice c)
|
||||
{
|
||||
if (c.IsDefaultFollower)
|
||||
@@ -1222,7 +1224,8 @@ public sealed class MainForm : Form
|
||||
// Machine-wide preference (AppConfig), not part of the profile.
|
||||
var on = e.NewValue == CheckState.Checked;
|
||||
PersistUseDefaultDevice(output: true, on);
|
||||
if (on) BeginInvoke(new Action(() => MaybeUntickOthersForDefault(receiveOutputDevicesList, output: true)));
|
||||
// Turning it on clears the specific outputs and locks them out (the veto above).
|
||||
if (on) BeginInvoke(new Action(() => UntickAllExceptDefaultFollower(receiveOutputDevicesList, output: true)));
|
||||
}
|
||||
else if (c.DeviceId is { } rid)
|
||||
{
|
||||
@@ -1240,14 +1243,17 @@ public sealed class MainForm : Form
|
||||
sendOutputDevicesList.ItemCheck += (_, args) =>
|
||||
{
|
||||
if (suppressDeviceCheckChange) return;
|
||||
// "Use Windows default" is exclusive: while it's on, specific cards can't be ticked.
|
||||
if (AudioDefaultFollower.VetoRealDeviceCheck(sendOutputDevicesList, args)) return;
|
||||
BeginInvoke(ApplyAudioRuntime);
|
||||
if (sendOutputDevicesList.Items[args.Index] is AudioDeviceChoice { IsDefaultFollower: true })
|
||||
{
|
||||
// "Use Windows default output for loopback" is a machine-wide preference (AppConfig),
|
||||
// not part of the profile — a follower can never go stale. No untick-others prompt: the
|
||||
// capture-spec builder de-dupes ids, so following the default never double-captures a
|
||||
// device already ticked explicitly.
|
||||
PersistUseDefaultLoopbackSend(args.NewValue == CheckState.Checked);
|
||||
// not part of the profile — a follower can never go stale. Turning it on clears the
|
||||
// specific outputs and locks them out (the veto above) so only the default is captured.
|
||||
var on = args.NewValue == CheckState.Checked;
|
||||
PersistUseDefaultLoopbackSend(on);
|
||||
if (on) BeginInvoke(new Action(() => UntickAllExceptDefaultFollower(sendOutputDevicesList, output: false)));
|
||||
return;
|
||||
}
|
||||
MarkProfileDirty();
|
||||
@@ -1255,6 +1261,8 @@ public sealed class MainForm : Form
|
||||
sendInputDevicesList.ItemCheck += (_, args) =>
|
||||
{
|
||||
if (suppressDeviceCheckChange) return;
|
||||
// "Use Windows default" is exclusive: while it's on, specific mics can't be ticked.
|
||||
if (AudioDefaultFollower.VetoRealDeviceCheck(sendInputDevicesList, args)) return;
|
||||
logFile.Event($"ui: capture (WASAPI mic) '{sendInputDevicesList.Items[args.Index]}' {(args.NewValue == CheckState.Checked ? "ticked" : "unticked")}");
|
||||
BeginInvoke(ApplyAudioRuntime);
|
||||
if (sendInputDevicesList.Items[args.Index] is AudioDeviceChoice { IsDefaultFollower: true })
|
||||
@@ -1263,7 +1271,8 @@ public sealed class MainForm : Form
|
||||
// (the default mic could be anything; the runtime apply handles a blocked default).
|
||||
var on = args.NewValue == CheckState.Checked;
|
||||
PersistUseDefaultDevice(output: false, on);
|
||||
if (on) BeginInvoke(new Action(() => MaybeUntickOthersForDefault(sendInputDevicesList, output: false)));
|
||||
// Turning it on clears the specific mics and locks them out (the veto above).
|
||||
if (on) BeginInvoke(new Action(() => UntickAllExceptDefaultFollower(sendInputDevicesList, output: false)));
|
||||
return;
|
||||
}
|
||||
MarkProfileDirty();
|
||||
@@ -2097,15 +2106,6 @@ public sealed class MainForm : Form
|
||||
};
|
||||
manageNamedPeersItem.Click += (_, _) => ShowManageNamedPeersDialog();
|
||||
|
||||
// Reset the one-time "untick the other devices?" prompt that appears when you turn on a
|
||||
// "Use Windows default audio device" box — clears the remembered Yes/No so it asks again.
|
||||
// Mirrors the Realtek toggle's "let the user reverse a one-time decision" pattern.
|
||||
var resetDefaultDeviceItem = new ToolStripMenuItem("&Reset the default audio device prompt")
|
||||
{
|
||||
AccessibleName = "Reset the default audio device prompt",
|
||||
};
|
||||
resetDefaultDeviceItem.Click += (_, _) => ResetDefaultAudioDevicePrompt();
|
||||
|
||||
// Realtek ASIO enable/disable toggle — only present when a Realtek ASIO driver is actually
|
||||
// installed. Lets the user reverse the disable decision (or disable a driver they kept).
|
||||
ToolStripMenuItem? realtekToggle = null;
|
||||
@@ -2146,7 +2146,6 @@ public sealed class MainForm : Form
|
||||
keyboardItem,
|
||||
profilePasswordsItem,
|
||||
manageNamedPeersItem,
|
||||
resetDefaultDeviceItem,
|
||||
};
|
||||
if (realtekToggle is not null) optionItems.Add(realtekToggle);
|
||||
optionItems.Add(new ToolStripSeparator());
|
||||
@@ -6552,55 +6551,13 @@ public sealed class MainForm : Form
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The default is exclusive: with it on, no specific card may stay ticked. Enforce it at load
|
||||
// too, so a profile/config that somehow carries both doesn't come up with both ticked.
|
||||
AudioDefaultFollower.UncheckRealDevices(list);
|
||||
}
|
||||
finally { suppressDeviceCheckChange = false; }
|
||||
}
|
||||
|
||||
/// <summary>Offered when the user ticks a "Use Windows default" follower: optionally untick the
|
||||
/// other devices in that list so only the default is used. Remembers the answer if the user ticks
|
||||
/// "Don't ask again".</summary>
|
||||
private void MaybeUntickOthersForDefault(CheckedListBox list, bool output)
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
AppConfig cfg;
|
||||
try { cfg = AppConfig.Load(); }
|
||||
catch { return; }
|
||||
var remembered = output ? cfg.UntickOthersWhenUsingDefaultOutput : cfg.UntickOthersWhenUsingDefaultInput;
|
||||
bool untick;
|
||||
if (remembered is bool r)
|
||||
{
|
||||
untick = r;
|
||||
}
|
||||
else
|
||||
{
|
||||
var verification = new TaskDialogVerificationCheckBox("Don't ask me this again");
|
||||
var page = new TaskDialogPage
|
||||
{
|
||||
Caption = "RemSound",
|
||||
Heading = "Use only the default audio device?",
|
||||
Text = output
|
||||
? "You've turned on “Use Windows default audio device”. Do you want to untick the other outputs in this list, so received sound plays only to whatever Windows is using?\n\nChoose No to keep playing to those outputs as well as the default audio device."
|
||||
: "You've turned on “Use Windows default audio device”. Do you want to untick the other inputs in this list, so only the Windows default audio device is captured?\n\nChoose No to keep capturing those inputs as well as the default audio device.",
|
||||
Icon = TaskDialogIcon.Information,
|
||||
Buttons = { TaskDialogButton.Yes, TaskDialogButton.No },
|
||||
DefaultButton = TaskDialogButton.No,
|
||||
Verification = verification,
|
||||
};
|
||||
untick = TaskDialog.ShowDialog(this, page) == TaskDialogButton.Yes;
|
||||
if (verification.Checked)
|
||||
{
|
||||
try
|
||||
{
|
||||
var c2 = AppConfig.Load();
|
||||
if (output) c2.UntickOthersWhenUsingDefaultOutput = untick; else c2.UntickOthersWhenUsingDefaultInput = untick;
|
||||
c2.Save();
|
||||
}
|
||||
catch { /* harmless */ }
|
||||
}
|
||||
}
|
||||
if (untick) UntickAllExceptDefaultFollower(list, output);
|
||||
}
|
||||
|
||||
private void UntickAllExceptDefaultFollower(CheckedListBox list, bool output)
|
||||
{
|
||||
var anyChanged = false;
|
||||
@@ -6622,34 +6579,6 @@ public sealed class MainForm : Form
|
||||
if (output) ApplyReceiveDevices(); else ApplyAudioRuntime();
|
||||
}
|
||||
|
||||
/// <summary>Options-menu action: clear the remembered answer to the "untick the other devices?"
|
||||
/// prompt (for both output and input), so it asks again next time the user turns on a "Use Windows
|
||||
/// default audio device" box. The discoverable way back from the prompt's "Don't ask again".</summary>
|
||||
private void ResetDefaultAudioDevicePrompt()
|
||||
{
|
||||
try
|
||||
{
|
||||
var c = AppConfig.Load();
|
||||
c.UntickOthersWhenUsingDefaultOutput = null;
|
||||
c.UntickOthersWhenUsingDefaultInput = null;
|
||||
c.Save();
|
||||
logFile.Event("default audio device: reset the untick-others prompt (will ask again)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logFile.Event($"default audio device: reset prompt failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
var page = new TaskDialogPage
|
||||
{
|
||||
Caption = "RemSound",
|
||||
Heading = "Default audio device prompt reset",
|
||||
Text = "RemSound will ask again next time you turn on a “Use Windows default audio device” option.",
|
||||
Icon = TaskDialogIcon.Information,
|
||||
Buttons = { TaskDialogButton.OK },
|
||||
};
|
||||
TaskDialog.ShowDialog(this, page);
|
||||
}
|
||||
|
||||
/// <summary>A receive-output card that was unplugged drops out of the WASAPI list (and its tick
|
||||
/// with it). When it returns, re-tick it from <see cref="rememberedReceiveOutputIds"/> so audio
|
||||
/// resumes automatically (issue #5). Only RE-ticks present-but-unticked remembered devices; it
|
||||
|
||||
@@ -65,6 +65,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Service app-yield token", ServiceInteractivePresence);
|
||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||
RunStep(results, "Default-output follower (service follows Windows default)", DefaultOutputFollower);
|
||||
RunStep(results, "Default follower exclusivity (locks out specific cards)", DefaultFollowerExclusivity);
|
||||
RunStep(results, "Service profile isolation (location + hidden from pickers)", ServiceProfileIsolation);
|
||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||
RunStep(results, "Service network presence (reachable + shell teardown)", ServiceNetworkPresenceReachable);
|
||||
@@ -883,6 +884,43 @@ internal static class SelfTest
|
||||
/// AND the fingerprint from the password (a missing fingerprint gets the encrypted stream rejected at
|
||||
/// the peer), and apply the send-rate-adjusted Opus frame (the "Small" rate halves it). Guards the
|
||||
/// divergences found auditing the service against the main app.</summary>
|
||||
/// <summary>Ticking a "Use Windows default" follower must be EXCLUSIVE: it clears the specific cards in
|
||||
/// its list and locks them out (a check attempt is vetoed) until the follower is turned off. Unticking
|
||||
/// a card, and the follower entry itself, are never vetoed. (Ed, 2026-07-17.)</summary>
|
||||
private static string? DefaultFollowerExclusivity()
|
||||
{
|
||||
using var list = new System.Windows.Forms.CheckedListBox();
|
||||
list.Items.Add(AudioDefaultFollower.LoopbackSendChoice()); // 0 = follower
|
||||
list.Items.Add(new AudioDeviceChoice("Card A", "id-a", CaptureKind.Loopback)); // 1
|
||||
list.Items.Add(new AudioDeviceChoice("Card B", "id-b", CaptureKind.Loopback)); // 2
|
||||
|
||||
// Follower OFF: a specific card may be ticked (no veto).
|
||||
var offCheck = new System.Windows.Forms.ItemCheckEventArgs(1, System.Windows.Forms.CheckState.Checked, System.Windows.Forms.CheckState.Unchecked);
|
||||
Check(!AudioDefaultFollower.VetoRealDeviceCheck(list, offCheck), "with the follower off, a specific card must be checkable");
|
||||
Check(offCheck.NewValue == System.Windows.Forms.CheckState.Checked, "no veto must leave the pending check intact");
|
||||
|
||||
// Tick the follower plus both cards, then clearing must leave ONLY the follower.
|
||||
list.SetItemChecked(0, true);
|
||||
list.SetItemChecked(1, true);
|
||||
list.SetItemChecked(2, true);
|
||||
Check(AudioDefaultFollower.IsFollowerChecked(list), "the follower must read as checked");
|
||||
Check(AudioDefaultFollower.UncheckRealDevices(list), "clearing must report a change when cards were ticked");
|
||||
Check(list.GetItemChecked(0), "the follower must stay ticked");
|
||||
Check(!list.GetItemChecked(1) && !list.GetItemChecked(2), "every specific card must be cleared");
|
||||
|
||||
// With the follower ON, a fresh attempt to tick a specific card is vetoed back to unticked.
|
||||
var onCheck = new System.Windows.Forms.ItemCheckEventArgs(1, System.Windows.Forms.CheckState.Checked, System.Windows.Forms.CheckState.Unchecked);
|
||||
Check(AudioDefaultFollower.VetoRealDeviceCheck(list, onCheck), "with the follower on, ticking a specific card must be vetoed");
|
||||
Check(onCheck.NewValue == System.Windows.Forms.CheckState.Unchecked, "the vetoed check must be forced back to unticked");
|
||||
|
||||
// Unticking a card, and the follower entry itself, are never vetoed.
|
||||
var untick = new System.Windows.Forms.ItemCheckEventArgs(1, System.Windows.Forms.CheckState.Unchecked, System.Windows.Forms.CheckState.Checked);
|
||||
Check(!AudioDefaultFollower.VetoRealDeviceCheck(list, untick), "unticking a card must never be vetoed");
|
||||
var followerToggle = new System.Windows.Forms.ItemCheckEventArgs(0, System.Windows.Forms.CheckState.Checked, System.Windows.Forms.CheckState.Unchecked);
|
||||
Check(!AudioDefaultFollower.VetoRealDeviceCheck(list, followerToggle), "the follower entry itself must never be vetoed");
|
||||
return "follower on clears + locks specific cards; off frees them; follower/untick never vetoed";
|
||||
}
|
||||
|
||||
/// <summary>The "Use Windows default output" follower (Christopher's request) must be the SAME shared
|
||||
/// sentinel + resolver the main app uses, and the service must resolve it to the LIVE default render
|
||||
/// endpoint — never pass the raw sentinel through as a device id.</summary>
|
||||
|
||||
@@ -160,6 +160,21 @@ internal sealed class ServiceProfileDialog : Form
|
||||
CheckedListAccessibility.Wire(appsList, appsStatus, "application");
|
||||
CheckedListAccessibility.Wire(inputsList, inputsStatus, "input device");
|
||||
|
||||
// "Use Windows default output" is exclusive, same as the main app: while it's ticked the specific
|
||||
// output cards can't be, and ticking it clears them.
|
||||
outputsList.ItemCheck += (_, e) =>
|
||||
{
|
||||
if (suppressAppEvents) return;
|
||||
if (AudioDefaultFollower.VetoRealDeviceCheck(outputsList, e)) return;
|
||||
if (outputsList.Items[e.Index] is AudioDeviceChoice { IsDefaultFollower: true } && e.NewValue == CheckState.Checked)
|
||||
BeginInvoke(new Action(() =>
|
||||
{
|
||||
suppressAppEvents = true;
|
||||
try { AudioDefaultFollower.UncheckRealDevices(outputsList); }
|
||||
finally { suppressAppEvents = false; }
|
||||
}));
|
||||
};
|
||||
|
||||
page.Controls.Add(panel);
|
||||
tabs.TabPages.Add(page);
|
||||
}
|
||||
@@ -203,6 +218,9 @@ internal sealed class ServiceProfileDialog : Form
|
||||
var outputChoices = new List<AudioDeviceChoice> { AudioDefaultFollower.LoopbackSendChoice() };
|
||||
outputChoices.AddRange(AudioDeviceCatalog.LoadOutputs());
|
||||
PopulateDeviceList(outputsList, outputChoices, working.SelectedWasapiSendOutputs);
|
||||
// Default is exclusive: if the profile carries the follower, drop any specific outputs it also
|
||||
// carried (already inside the suppressAppEvents guard set at the top of LoadFromProfile).
|
||||
if (AudioDefaultFollower.IsFollowerChecked(outputsList)) AudioDefaultFollower.UncheckRealDevices(outputsList);
|
||||
PopulateDeviceList(inputsList, AudioDeviceCatalog.LoadInputs(), working.SelectedWasapiSendInputs);
|
||||
|
||||
var appsMode = ProcessLoopbackCapture.IsSupported
|
||||
|
||||
@@ -248,12 +248,6 @@ public sealed class AppConfig
|
||||
/// device, following it if the default changes. Machine-wide, opt-in, default false.</summary>
|
||||
public bool UseDefaultLoopbackSend { get; set; }
|
||||
|
||||
/// <summary>Remembered answer to the "untick the other outputs/inputs when you turn on Use Windows
|
||||
/// default?" prompt. Null = ask each time; true = always untick; false = never untick. Set when the
|
||||
/// user ticks "Don't ask again" on that prompt. Machine-wide.</summary>
|
||||
public bool? UntickOthersWhenUsingDefaultOutput { get; set; }
|
||||
public bool? UntickOthersWhenUsingDefaultInput { get; set; }
|
||||
|
||||
/// <summary>If non-null and a profile with this title exists, RemSound skips the
|
||||
/// startup profile picker and loads this profile directly. Combine with
|
||||
/// <see cref="StartMinimised"/> + the Windows auto-start registry entry
|
||||
|
||||
Reference in New Issue
Block a user