Reset-ASIO button + checkbox tick/untick sounds (parts 1-2 of the cue/preferences overhaul)

Part 1 - "Uncheck all inputs and outputs" button now also resets the ASIO driver to "(none)":
renamed to say so, and UncheckAllDevices sets asioDriverBox to row 0 for a full clean
WASAPI-only, nothing-selected state.

Part 2 - checkbox tick/untick sounds: every checkbox toggle anywhere in RemSound now plays a
short cue (check.wav on tick, uncheck.wav on untick) - instant feedback on which way a box
went, especially in the inputs/outputs lists. New CheckSoundService + two machine-wide cues
(CheckboxOn/Off) with the usual numbered-variant + Preferences treatment. Hooked from
AccessibleCheckBox.OnCheckedChanged and the device lists' WireCheckedListAccessibility, both
gated on the control being Focused so a genuine user toggle clicks but bulk programmatic
(un)checking (profile load, "uncheck all") stays silent. Reloaded at startup and on cue change.

Tests + manual updated; .sfk byproducts cleared.

Remaining for the overhaul (next): tabbed Preferences (General / Audio cues / Startup behaviour /
Update settings), the cue-list redesign with a "none" option replacing per-cue checkboxes, moving
Startup behaviour out of the Options menu, and a front-most "missing sound file" error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-13 00:42:41 +01:00
co-authored by Claude Opus 4.8
parent 8763470037
commit aed555cc2e
14 changed files with 87 additions and 5 deletions
+2 -1
View File
@@ -866,7 +866,7 @@ Toggle| What it does
## 18. Audio cue sounds ## 18. Audio cue sounds
RemSound plays a short sound at moments where you might want an audible confirmation that something just happened. These are called **cue sounds**. Fifteen events have a cue: RemSound plays a short sound at moments where you might want an audible confirmation that something just happened. These are called **cue sounds**. Sixteen kinds of event have a cue:
Cue| Plays when Cue| Plays when
---|--- ---|---
@@ -883,6 +883,7 @@ Cue| Plays when
**Receive turned on / off sound**| You turn receiving audio on or off — from the **Receive audio** box or its mute shortcut. Again, a separate sound for on and for off. **Receive turned on / off sound**| You turn receiving audio on or off — from the **Receive audio** box or its mute shortcut. Again, a separate sound for on and for off.
**Minimise (hide) sound**| RemSound's window minimises to the notification area (hides). **Minimise (hide) sound**| RemSound's window minimises to the notification area (hides).
**Restore (show) sound**| RemSound's window is brought back from the notification area (shows). **Restore (show) sound**| RemSound's window is brought back from the notification area (shows).
**Checkbox ticked / unticked sound**| You tick or untick _any_ checkbox anywhere in RemSound — a click for ticked, a different one for unticked. This gives instant feedback on which way a box just went, which is especially handy in the busy inputs and outputs lists. There's a separate sound for ticking and for unticking.
All of these cues play through your default Windows sound output, which is separate from the audio RemSound is sending or receiving. They don't appear in a normal recording. (The exception: if your sending side is capturing the very output device the cues play through, then they get captured along with everything else from that device.) All of these cues play through your default Windows sound output, which is separate from the audio RemSound is sending or receiving. They don't appear in a normal recording. (The exception: if your sending side is capturing the very output device the cues play through, then they get captured along with everything else from that device.)
+2 -1
View File
@@ -919,7 +919,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<h2 id="audio-cues">18. Audio cue sounds</h2> <h2 id="audio-cues">18. Audio cue sounds</h2>
<p>RemSound plays a short sound at moments where you might want an audible confirmation that something just happened. These are called <strong>cue sounds</strong>. Fifteen events have a cue:</p> <p>RemSound plays a short sound at moments where you might want an audible confirmation that something just happened. These are called <strong>cue sounds</strong>. Sixteen kinds of event have a cue:</p>
<table> <table>
<tr><th>Cue</th><th>Plays when</th></tr> <tr><th>Cue</th><th>Plays when</th></tr>
@@ -936,6 +936,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<tr><td><strong>Receive turned on / off sound</strong></td><td>You turn receiving audio on or off &mdash; from the <strong>Receive audio</strong> box or its mute shortcut. Again, a separate sound for on and for off.</td></tr> <tr><td><strong>Receive turned on / off sound</strong></td><td>You turn receiving audio on or off &mdash; from the <strong>Receive audio</strong> box or its mute shortcut. Again, a separate sound for on and for off.</td></tr>
<tr><td><strong>Minimise (hide) sound</strong></td><td>RemSound's window minimises to the notification area (hides).</td></tr> <tr><td><strong>Minimise (hide) sound</strong></td><td>RemSound's window minimises to the notification area (hides).</td></tr>
<tr><td><strong>Restore (show) sound</strong></td><td>RemSound's window is brought back from the notification area (shows).</td></tr> <tr><td><strong>Restore (show) sound</strong></td><td>RemSound's window is brought back from the notification area (shows).</td></tr>
<tr><td><strong>Checkbox ticked / unticked sound</strong></td><td>You tick or untick <em>any</em> checkbox anywhere in RemSound &mdash; a click for ticked, a different one for unticked. This gives instant feedback on which way a box just went, which is especially handy in the busy inputs and outputs lists. There's a separate sound for ticking and for unticking.</td></tr>
</table> </table>
<p>All of these cues play through your default Windows sound output, which is separate from the audio RemSound is sending or receiving. They don't appear in a normal recording. (The exception: if your sending side is capturing the very output device the cues play through, then they get captured along with everything else from that device.)</p> <p>All of these cues play through your default Windows sound output, which is separate from the audio RemSound is sending or receiving. They don't appear in a normal recording. (The exception: if your sending side is capturing the very output device the cues play through, then they get captured along with everything else from that device.)</p>
+1 -1
View File
@@ -48,7 +48,7 @@ $soundsPath = Join-Path $publishDir 'sounds'
$wavCount = @(Get-ChildItem -LiteralPath $soundsPath -Filter *.wav -ErrorAction SilentlyContinue).Count $wavCount = @(Get-ChildItem -LiteralPath $soundsPath -Filter *.wav -ErrorAction SilentlyContinue).Count
if ($wavCount -ge 3) { Pass "cue sounds bundled ($wavCount .wav)" } else { Fail "cue sounds missing (found $wavCount) - this is the bug that shipped v3.9 with no sounds" } if ($wavCount -ge 3) { Pass "cue sounds bundled ($wavCount .wav)" } else { Fail "cue sounds missing (found $wavCount) - this is the bug that shipped v3.9 with no sounds" }
# Cues ship as numbered variants ("connect 1.wav", ...); each required cue needs at least one. # Cues ship as numbered variants ("connect 1.wav", ...); each required cue needs at least one.
foreach ($base in @('connect', 'disconnect', 'start up', 'send on', 'send off', 'recieve on', 'recieve off', 'minimise', 'maximise')) { foreach ($base in @('connect', 'disconnect', 'start up', 'send on', 'send off', 'recieve on', 'recieve off', 'minimise', 'maximise', 'check', 'uncheck')) {
$variants = @(Get-ChildItem -LiteralPath $soundsPath -Filter "$base*.wav" -ErrorAction SilentlyContinue) $variants = @(Get-ChildItem -LiteralPath $soundsPath -Filter "$base*.wav" -ErrorAction SilentlyContinue)
if ($variants.Count -gt 0) { Pass "'$base' cue has $($variants.Count) sound variant(s)" } else { Fail "no sound variant for the '$base' cue" } if ($variants.Count -gt 0) { Pass "'$base' cue has $($variants.Count) sound variant(s)" } else { Fail "no sound variant for the '$base' cue" }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -60,6 +60,9 @@ internal sealed class AccessibleCheckBox : CheckBox
if (Focused) if (Focused)
{ {
NotifyWinEvent(EVENT_OBJECT_FOCUS, Handle, OBJID_CLIENT, CHILDID_SELF); NotifyWinEvent(EVENT_OBJECT_FOCUS, Handle, OBJID_CLIENT, CHILDID_SELF);
// Audible tick/untick feedback. Gated on Focused so it fires for a genuine user toggle
// (click or spacebar) but stays silent for the bulk programmatic checking on profile load.
CheckSoundService.Play(Checked);
} }
} }
} }
+47
View File
@@ -0,0 +1,47 @@
using RemSound.Core;
namespace RemSound.App;
/// <summary>
/// Plays a short tick / untick sound whenever the user checks or unchecks a checkbox anywhere in
/// RemSound - instant audible feedback of which way a box just went, which matters most in the busy
/// inputs/outputs lists. Two normal machine-wide cues: CheckboxOn (check.wav) and CheckboxOff
/// (uncheck.wav), each with numbered variants + an off option in Preferences, like any other cue.
///
/// Wired from <see cref="AccessibleCheckBox"/> and the device <c>LiveCheckedListBox</c>, both gated
/// on the control being focused - so a genuine user toggle clicks, but the bulk programmatic
/// checking done on profile load or by "uncheck all" stays silent.
/// </summary>
internal static class CheckSoundService
{
private static CuePlayer? checkSound;
private static CuePlayer? uncheckSound;
/// <summary>(Re)load the tick/untick sounds from the current cue configuration. Call at startup
/// and whenever cue settings change.</summary>
public static void Reload()
{
var cfg = AppConfig.Load();
checkSound = LoadCue(MainForm.CueId.CheckboxOn, "check.wav", cfg);
uncheckSound = LoadCue(MainForm.CueId.CheckboxOff, "uncheck.wav", cfg);
}
public static void Play(bool isChecked)
{
var cfg = AppConfig.Load();
if (isChecked) { if (cfg.EnableCheckboxOnCue) checkSound?.Play(); }
else { if (cfg.EnableCheckboxOffCue) uncheckSound?.Play(); }
}
private static CuePlayer? LoadCue(string cueId, string defaultFile, AppConfig cfg)
{
try
{
string? path = cfg.MachineCueCustomPaths.TryGetValue(cueId, out var custom) && File.Exists(custom)
? custom
: CueSounds.ResolveDefaultPath(cueId, defaultFile, cfg);
return path is not null && File.Exists(path) ? new CuePlayer(path) : null;
}
catch { return null; }
}
}
+19 -2
View File
@@ -776,8 +776,8 @@ public sealed class MainForm : Form
// "Uncheck all inputs and outputs on all soundcards" — clears every device tick in one // "Uncheck all inputs and outputs on all soundcards" — clears every device tick in one
// press. Button owns its own &-mnemonic (Alt+U), so AccessibleName stays clean per Ed's // press. Button owns its own &-mnemonic (Alt+U), so AccessibleName stays clean per Ed's
// mnemonic convention. // mnemonic convention.
uncheckAllDevicesButton.Text = "Uncheck all inputs and outputs on all soundcards (Alt+&U)"; uncheckAllDevicesButton.Text = "Uncheck all inputs and outputs on all soundcards and set ASIO driver to none (Alt+&U)";
uncheckAllDevicesButton.AccessibleName = "Uncheck all inputs and outputs on all soundcards"; uncheckAllDevicesButton.AccessibleName = "Uncheck all inputs and outputs on all soundcards and set ASIO driver to none";
uncheckAllDevicesButton.Click += (_, _) => UncheckAllDevices(); uncheckAllDevicesButton.Click += (_, _) => UncheckAllDevices();
// Populate ASIO driver list at startup. Discovers all ASIO drivers via NAudio + a // Populate ASIO driver list at startup. Discovers all ASIO drivers via NAudio + a
@@ -4572,6 +4572,11 @@ public sealed class MainForm : Form
} }
finally { suppressDeviceCheckChange = false; } finally { suppressDeviceCheckChange = false; }
// Also reset the ASIO driver to "(none)" (row 0): the button now does a full reset to a
// clean WASAPI-only, nothing-selected state. Setting the index fires the driver-change
// handler, which saves the change and rebuilds the engine in WASAPI-only mode.
if (asioDriverBox.SelectedIndex > 0) asioDriverBox.SelectedIndex = 0;
ApplyAudioRuntime(); ApplyAudioRuntime();
ApplyReceiveDevices(); ApplyReceiveDevices();
MarkProfileDirty(); MarkProfileDirty();
@@ -6512,6 +6517,9 @@ public sealed class MainForm : Form
public const string ReceiveOff = "receive-off"; public const string ReceiveOff = "receive-off";
public const string Hide = "hide"; public const string Hide = "hide";
public const string Show = "show"; public const string Show = "show";
// Played on every checkbox tick/untick across the whole app (CheckSoundService).
public const string CheckboxOn = "checkbox-on";
public const string CheckboxOff = "checkbox-off";
} }
/// <summary>Load one cue sound. Resolution order: /// <summary>Load one cue sound. Resolution order:
@@ -6589,6 +6597,8 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.ReceiveOff, "recieve off.wav", out receiveOffSound); TryLoadCueSound(CueId.ReceiveOff, "recieve off.wav", out receiveOffSound);
TryLoadCueSound(CueId.Hide, "minimise.wav", out hideSound); TryLoadCueSound(CueId.Hide, "minimise.wav", out hideSound);
TryLoadCueSound(CueId.Show, "maximise.wav", out showSound); TryLoadCueSound(CueId.Show, "maximise.wav", out showSound);
// The app-wide checkbox tick/untick sounds live in their own service; keep them in step.
CheckSoundService.Reload();
} }
/// <summary> /// <summary>
@@ -7209,6 +7219,13 @@ public sealed class MainForm : Form
private void WireCheckedListAccessibility(CheckedListBox list, Label statusLabel, string itemKind) private void WireCheckedListAccessibility(CheckedListBox list, Label statusLabel, string itemKind)
{ {
// Tick/untick sound for the inputs/outputs lists. Gated on the list being focused so a real
// user click/spacebar clicks, but the bulk programmatic (un)checking done on profile load or
// by "uncheck all" (focus is on the button, not the list) stays silent.
list.ItemCheck += (_, e) =>
{
if (list.Focused) CheckSoundService.Play(e.NewValue == CheckState.Checked);
};
list.SelectedIndexChanged += (_, _) => list.SelectedIndexChanged += (_, _) =>
{ {
if (list.SelectedIndex >= 0) lastFocusedListIndices[list] = list.SelectedIndex; if (list.SelectedIndex >= 0) lastFocusedListIndices[list] = list.SelectedIndex;
+5
View File
@@ -194,6 +194,11 @@ internal sealed class PreferencesDialog : Form
c => c.EnableHideCue, (c, v) => c.EnableHideCue = v), c => c.EnableHideCue, (c, v) => c.EnableHideCue = v),
MachineRow("Restore (show) sound", MainForm.CueId.Show, "maximise.wav", MachineRow("Restore (show) sound", MainForm.CueId.Show, "maximise.wav",
c => c.EnableShowCue, (c, v) => c.EnableShowCue = v), c => c.EnableShowCue, (c, v) => c.EnableShowCue = v),
// Played on every checkbox tick / untick across the whole app (CheckSoundService).
MachineRow("Checkbox ticked sound", MainForm.CueId.CheckboxOn, "check.wav",
c => c.EnableCheckboxOnCue, (c, v) => c.EnableCheckboxOnCue = v),
MachineRow("Checkbox unticked sound", MainForm.CueId.CheckboxOff, "uncheck.wav",
c => c.EnableCheckboxOffCue, (c, v) => c.EnableCheckboxOffCue = v),
]; ];
} }
+4
View File
@@ -137,6 +137,10 @@ internal static class Program
KeyClickService.Initialize(AppConfig.Load().EnableKeyboardClicks); KeyClickService.Initialize(AppConfig.Load().EnableKeyboardClicks);
Application.ApplicationExit += (_, _) => KeyClickService.Shutdown(); Application.ApplicationExit += (_, _) => KeyClickService.Shutdown();
// Tick/untick sounds for checkbox toggles app-wide (CheckSoundService). Loaded here; reloaded
// by MainForm.ReloadAllCueSounds whenever cue settings change in Preferences.
CheckSoundService.Reload();
// One-time "your settings moved" notice — only the launch that actually relocated files // One-time "your settings moved" notice — only the launch that actually relocated files
// shows it (idempotent migration ⇒ MovedAnything is false on every later launch). Shown // shows it (idempotent migration ⇒ MovedAnything is false on every later launch). Shown
// here, after the guard and before the profile picker, so the user reads it once up front. // here, after the guard and before the profile picker, so the user reads it once up front.
+1
View File
@@ -307,6 +307,7 @@ internal static class SelfTest
{ {
"connect.wav", "disconnect.wav", "start up.wav", "connect.wav", "disconnect.wav", "start up.wav",
"send on.wav", "send off.wav", "recieve on.wav", "recieve off.wav", "minimise.wav", "maximise.wav", "send on.wav", "send off.wav", "recieve on.wav", "recieve off.wav", "minimise.wav", "maximise.wav",
"check.wav", "uncheck.wav",
}) })
{ {
Check(CueSounds.Variants(cue).Count > 0, Check(CueSounds.Variants(cue).Count > 0,
+3
View File
@@ -106,6 +106,9 @@ public sealed class AppConfig
public bool EnableReceiveOffCue { get; set; } = true; public bool EnableReceiveOffCue { get; set; } = true;
public bool EnableHideCue { get; set; } = true; public bool EnableHideCue { get; set; } = true;
public bool EnableShowCue { get; set; } = true; public bool EnableShowCue { get; set; } = true;
/// <summary>Tick / untick sounds played on every checkbox toggle anywhere in the app.</summary>
public bool EnableCheckboxOnCue { get; set; } = true;
public bool EnableCheckboxOffCue { get; set; } = true;
/// <summary>Custom WAV overrides for the machine-wide cues above, keyed by cue id. The /// <summary>Custom WAV overrides for the machine-wide cues above, keyed by cue id. The
/// equivalent of <see cref="Profile.CustomCuePaths"/> but machine-wide, since these cues don't /// equivalent of <see cref="Profile.CustomCuePaths"/> but machine-wide, since these cues don't