Audio cues: add send/receive on-off and minimise/restore cues (2 sounds each), drop .sfk byproducts

Six new machine-wide cues, each with the same numbered-variant + Preferences treatment as
the others (enable tickbox, Choose default sound picker, Play/Browse):

- Send turned on / off, Receive turned on / off: fire from OnStreamingCheckboxChanged, so
  they sound whether the user clicked the in-window tickbox or pressed the mute shortcut
  (the hotkey flips .Checked, which routes through the same handler). Suppressed during
  profile load by the existing password-gate guard, so loading a profile doesn't blast them.
- Minimise (hide) / Restore (show): fire from the tray controller's Minimize()/Restore() on a
  genuine visibility transition (guarded against no-op / startup-minimise).

Enable flags + custom-WAV overrides for these six live machine-wide in AppConfig
(EnableSendOnCue.., MachineCueCustomPaths) - they're app-level feedback, not per-profile
audio - so no Profile/settings-cache plumbing. TryLoadCueSound now also honours the
machine-wide custom path. PreferencesDialog gains a MachineRow helper + the six rows.

Sounds: shipped via the existing sounds\*.wav wildcard. Fixed an obvious typo in the
supplied files ("rcieve off 1.wav" -> "recieve off 1.wav") so receive-off has both variants.
Renamed the old single-name cue WAVs to Ed's numbered-variant set; added key/passkey and the
new cue sounds.

build-release.ps1: new step deletes the SoundForge .sfk peak-file byproducts from sounds\
before packaging (they never shipped - build is *.wav only - this just keeps the tree tidy).

Tests + manual updated for the six new cues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-13 00:18:09 +01:00
co-authored by Claude Opus 4.8
parent e526014e6a
commit 8763470037
44 changed files with 141 additions and 4 deletions
+5 -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**. Nine 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**. Fifteen events have a cue:
Cue| Plays when Cue| Plays when
---|--- ---|---
@@ -879,6 +879,10 @@ Cue| Plays when
**Profile menu open sound**| The Quick profile switch popup opens. **Profile menu open sound**| The Quick profile switch popup opens.
**Update sound**| An update is about to install — it plays just before RemSound closes to update itself. Handy when updates install silently in the background, so you're not caught off guard when RemSound restarts. Plays whether you ran the update by hand or it installed on its own. **Update sound**| An update is about to install — it plays just before RemSound closes to update itself. Handy when updates install silently in the background, so you're not caught off guard when RemSound restarts. Plays whether you ran the update by hand or it installed on its own.
**Startup sound**| RemSound has finished starting up. It plays once at launch, even when RemSound opens straight to the notification area, so you know it's running. **Startup sound**| RemSound has finished starting up. It plays once at launch, even when RemSound opens straight to the notification area, so you know it's running.
**Send turned on / off sound**| You turn sending your audio on or off — whether by ticking the **Send my audio** box in the window or by pressing its mute shortcut. There's 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).
**Restore (show) sound**| RemSound's window is brought back from the notification area (shows).
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.)
+9
View File
@@ -35,6 +35,15 @@ $distDir = Join-Path $repo 'dist'
$zipPath = Join-Path $distDir "RemSound-$Tag.zip" $zipPath = Join-Path $distDir "RemSound-$Tag.zip"
$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("remsound-release-" + [guid]::NewGuid().ToString('N')) $staging = Join-Path ([System.IO.Path]::GetTempPath()) ("remsound-release-" + [guid]::NewGuid().ToString('N'))
# 0a. SoundForge drops a .sfk peak file next to every .wav it opens. They're byproducts that must
# never ship (the build only bundles *.wav, so they wouldn't anyway) - clear them from the
# source sounds\ folder so they don't accumulate and clutter the working tree.
$sfk = @(Get-ChildItem -LiteralPath (Join-Path $repo 'sounds') -Filter '*.sfk' -ErrorAction SilentlyContinue)
if ($sfk.Count -gt 0) {
Write-Host "Removing $($sfk.Count) SoundForge .sfk byproduct(s) from sounds\..." -ForegroundColor Cyan
$sfk | Remove-Item -Force
}
# 0. Keep the GitHub-facing MANUAL.md in sync with the bundled readme.html. # 0. Keep the GitHub-facing MANUAL.md in sync with the bundled readme.html.
# Two copies of the manual exist on purpose: readme.html ships inside RemSound (F1 # Two copies of the manual exist on purpose: readme.html ships inside RemSound (F1
# inside the app opens it), MANUAL.md is the Markdown rendition rendered on the # inside the app opens it), MANUAL.md is the Markdown rendition rendered on the
+5 -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>. Nine 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>. Fifteen events have a cue:</p>
<table> <table>
<tr><th>Cue</th><th>Plays when</th></tr> <tr><th>Cue</th><th>Plays when</th></tr>
@@ -932,6 +932,10 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<tr><td><strong>Profile menu open sound</strong></td><td>The Quick profile switch popup opens.</td></tr> <tr><td><strong>Profile menu open sound</strong></td><td>The Quick profile switch popup opens.</td></tr>
<tr><td><strong>Update sound</strong></td><td>An update is about to install &mdash; it plays just before RemSound closes to update itself. Handy when updates install silently in the background, so you're not caught off guard when RemSound restarts. Plays whether you ran the update by hand or it installed on its own.</td></tr> <tr><td><strong>Update sound</strong></td><td>An update is about to install &mdash; it plays just before RemSound closes to update itself. Handy when updates install silently in the background, so you're not caught off guard when RemSound restarts. Plays whether you ran the update by hand or it installed on its own.</td></tr>
<tr><td><strong>Startup sound</strong></td><td>RemSound has finished starting up. It plays once at launch, even when RemSound opens straight to the notification area, so you know it's running.</td></tr> <tr><td><strong>Startup sound</strong></td><td>RemSound has finished starting up. It plays once at launch, even when RemSound opens straight to the notification area, so you know it's running.</td></tr>
<tr><td><strong>Send turned on / off sound</strong></td><td>You turn sending your audio on or off &mdash; whether by ticking the <strong>Send my audio</strong> box in the window or by pressing its mute shortcut. There's 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>Restore (show) sound</strong></td><td>RemSound's window is brought back from the notification area (shows).</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')) { foreach ($base in @('connect', 'disconnect', 'start up', 'send on', 'send off', 'recieve on', 'recieve off', 'minimise', 'maximise')) {
$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.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+66
View File
@@ -334,6 +334,13 @@ public sealed class MainForm : Form
private CuePlayer? profileSwitchSound; private CuePlayer? profileSwitchSound;
private CuePlayer? profileMenuOpenSound; private CuePlayer? profileMenuOpenSound;
private CuePlayer? updateSound; private CuePlayer? updateSound;
// Machine-wide cues (2026-06-13): send/receive toggled on/off, and minimise(hide)/restore(show).
private CuePlayer? sendOnSound;
private CuePlayer? sendOffSound;
private CuePlayer? receiveOnSound;
private CuePlayer? receiveOffSound;
private CuePlayer? hideSound;
private CuePlayer? showSound;
// Labels for the three send/receive device lists, captured at layout time so they can be // Labels for the three send/receive device lists, captured at layout time so they can be
// re-titled when the user toggles between WASAPI mode (Windows devices) and ASIO mode // re-titled when the user toggles between WASAPI mode (Windows devices) and ASIO mode
// (driver channel pairs). null until BuildLayout has run. // (driver channel pairs). null until BuildLayout has run.
@@ -986,6 +993,12 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound); TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
TryLoadCueSound(CueId.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound); TryLoadCueSound(CueId.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound);
TryLoadCueSound(CueId.Update, "update.wav", out updateSound); TryLoadCueSound(CueId.Update, "update.wav", out updateSound);
TryLoadCueSound(CueId.SendOn, "send on.wav", out sendOnSound);
TryLoadCueSound(CueId.SendOff, "send off.wav", out sendOffSound);
TryLoadCueSound(CueId.ReceiveOn, "recieve on.wav", out receiveOnSound);
TryLoadCueSound(CueId.ReceiveOff, "recieve off.wav", out receiveOffSound);
TryLoadCueSound(CueId.Hide, "minimise.wav", out hideSound);
TryLoadCueSound(CueId.Show, "maximise.wav", out showSound);
LoadAudioDevices(); LoadAudioDevices();
// Apply persisted ASIO mode from settings — switches sender/receiver backends so the // Apply persisted ASIO mode from settings — switches sender/receiver backends so the
@@ -6164,6 +6177,39 @@ public sealed class MainForm : Form
if (!EnsureStreamingPassword(box)) return; if (!EnsureStreamingPassword(box)) return;
HandleCapabilityChange(); HandleCapabilityChange();
MarkProfileDirty(); MarkProfileDirty();
// Audible feedback for the toggle, whether the user clicked the checkbox or pressed the
// mute hotkey (the hotkey flips .Checked, which routes through here too). Suppressed during
// profile load by the suppressStreamingPasswordGate guard above, so loading a profile that
// has send/receive on doesn't blast the cues.
PlayStreamToggleCue(box);
}
/// <summary>Play the send/receive turned-on / turned-off cue for a streaming checkbox toggle.
/// Machine-wide cues (enable flags in AppConfig); silent when the cue is unticked or absent.</summary>
private void PlayStreamToggleCue(AccessibleCheckBox box)
{
var on = box.Checked;
var cfg = AppConfig.Load();
if (box == sendMyAudioCheckbox)
{
if (on) { if (cfg.EnableSendOnCue) sendOnSound?.Play(); }
else { if (cfg.EnableSendOffCue) sendOffSound?.Play(); }
}
else if (box == receiveAudioCheckbox)
{
if (on) { if (cfg.EnableReceiveOnCue) receiveOnSound?.Play(); }
else { if (cfg.EnableReceiveOffCue) receiveOffSound?.Play(); }
}
}
/// <summary>Play the minimise(hide) or restore(show) cue. Called by the tray controller when the
/// window actually transitions to/from hidden. Machine-wide enable flags; silent when unticked
/// or the cue WAV is absent.</summary>
public void PlayWindowVisibilityCue(bool show)
{
var cfg = AppConfig.Load();
if (show) { if (cfg.EnableShowCue) showSound?.Play(); }
else { if (cfg.EnableHideCue) hideSound?.Play(); }
} }
/// <summary>True while a peer-security warning dialog is on screen — set so the 1 Hz status /// <summary>True while a peer-security warning dialog is on screen — set so the 1 Hz status
@@ -6458,6 +6504,14 @@ public sealed class MainForm : Form
// enable flag and custom-path live machine-wide in AppConfig, not the per-profile // enable flag and custom-path live machine-wide in AppConfig, not the per-profile
// settings store. This id is still used by the Preferences cue list for display/keying. // settings store. This id is still used by the Preferences cue list for display/keying.
public const string Startup = "startup"; public const string Startup = "startup";
// Send/receive toggle + minimise(hide)/restore(show) cues (2026-06-13). Also machine-wide
// (enable flags + custom paths in AppConfig) - app-level feedback, not per-profile audio.
public const string SendOn = "send-on";
public const string SendOff = "send-off";
public const string ReceiveOn = "receive-on";
public const string ReceiveOff = "receive-off";
public const string Hide = "hide";
public const string Show = "show";
} }
/// <summary>Load one cue sound. Resolution order: /// <summary>Load one cue sound. Resolution order:
@@ -6479,6 +6533,12 @@ public sealed class MainForm : Form
{ {
string? path = null; string? path = null;
var customPath = settings.LoadCustomCuePath(cueId); var customPath = settings.LoadCustomCuePath(cueId);
if (string.IsNullOrWhiteSpace(customPath)
&& AppConfig.Load().MachineCueCustomPaths.TryGetValue(cueId, out var machinePath))
{
// Machine-wide cues (send/receive/hide/show) keep their custom override in AppConfig.
customPath = machinePath;
}
if (!string.IsNullOrWhiteSpace(customPath) && File.Exists(customPath)) if (!string.IsNullOrWhiteSpace(customPath) && File.Exists(customPath))
{ {
path = customPath; path = customPath;
@@ -6523,6 +6583,12 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound); TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
TryLoadCueSound(CueId.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound); TryLoadCueSound(CueId.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound);
TryLoadCueSound(CueId.Update, "update.wav", out updateSound); TryLoadCueSound(CueId.Update, "update.wav", out updateSound);
TryLoadCueSound(CueId.SendOn, "send on.wav", out sendOnSound);
TryLoadCueSound(CueId.SendOff, "send off.wav", out sendOffSound);
TryLoadCueSound(CueId.ReceiveOn, "recieve on.wav", out receiveOnSound);
TryLoadCueSound(CueId.ReceiveOff, "recieve off.wav", out receiveOffSound);
TryLoadCueSound(CueId.Hide, "minimise.wav", out hideSound);
TryLoadCueSound(CueId.Show, "maximise.wav", out showSound);
} }
/// <summary> /// <summary>
@@ -201,6 +201,8 @@ internal sealed class MainFormTrayController : IDisposable
public void Restore() public void Restore()
{ {
// Only sound the "show" cue on a genuine hidden -> shown transition, not a no-op restore.
var wasHidden = !owner.Visible || owner.WindowState == FormWindowState.Minimized;
owner.Show(); owner.Show();
if (owner.WindowState == FormWindowState.Minimized) if (owner.WindowState == FormWindowState.Minimized)
{ {
@@ -218,6 +220,7 @@ internal sealed class MainFormTrayController : IDisposable
// to actually hear the new content. // to actually hear the new content.
try { SetForegroundWindow(owner.Handle); } catch { /* harmless — Restore still mostly worked */ } try { SetForegroundWindow(owner.Handle); } catch { /* harmless — Restore still mostly worked */ }
trayIcon.Visible = false; trayIcon.Visible = false;
if (wasHidden) (owner as MainForm)?.PlayWindowVisibilityCue(show: true);
} }
[DllImport("user32.dll")] [DllImport("user32.dll")]
@@ -226,7 +229,11 @@ internal sealed class MainFormTrayController : IDisposable
public void Minimize() public void Minimize()
{ {
// Only sound the "hide" cue on a genuine shown -> hidden transition (not a startup-minimise
// before the window has ever been shown, nor a repeat call once already hidden).
var wasVisible = owner.Visible;
owner.Hide(); owner.Hide();
if (wasVisible) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
// Refresh the tooltip BEFORE showing the icon so the shell's NIM_ADD call carries // Refresh the tooltip BEFORE showing the icon so the shell's NIM_ADD call carries
// the current live state (peer count, send / receive routing, recording timer), // the current live state (peer count, send / receive routing, recording timer),
// not a stale "starting up" string set earlier. The shell tends to cache hover // not a stale "starting up" string set earlier. The shell tends to cache hover
+26
View File
@@ -143,6 +143,17 @@ internal sealed class PreferencesDialog : Form
() => load(settings), v => save(settings, v), () => load(settings), v => save(settings, v),
() => settings.LoadCustomCuePath(id), p => settings.SaveCustomCuePath(id, p)); () => settings.LoadCustomCuePath(id), p => settings.SaveCustomCuePath(id, p));
// Machine-wide cue (enable flag + custom path in AppConfig, not the profile). Persists
// immediately and never flags a profile save (IsProfileSetting=false). Same shape as the
// Startup row below, factored out for the send/receive/hide/show cues.
CueRowDescriptor MachineRow(string name, string id, string file,
Func<AppConfig, bool> loadEnabled, Action<AppConfig, bool> saveEnabled) =>
new(name, id, file, false,
() => loadEnabled(AppConfig.Load()),
v => { var c = AppConfig.Load(); saveEnabled(c, v); TrySaveConfig(c); },
() => AppConfig.Load().MachineCueCustomPaths.TryGetValue(id, out var p) ? p : null,
p => { var c = AppConfig.Load(); if (string.IsNullOrWhiteSpace(p)) c.MachineCueCustomPaths.Remove(id); else c.MachineCueCustomPaths[id] = p!; TrySaveConfig(c); });
return return
[ [
ProfileRow("Connect sound", MainForm.CueId.Connect, "connect.wav", ProfileRow("Connect sound", MainForm.CueId.Connect, "connect.wav",
@@ -168,6 +179,21 @@ internal sealed class PreferencesDialog : Form
v => { var c = AppConfig.Load(); c.EnableStartupCue = v; TrySaveConfig(c); }, v => { var c = AppConfig.Load(); c.EnableStartupCue = v; TrySaveConfig(c); },
() => AppConfig.Load().StartupCueCustomPath, () => AppConfig.Load().StartupCueCustomPath,
p => { var c = AppConfig.Load(); c.StartupCueCustomPath = p; TrySaveConfig(c); }), p => { var c = AppConfig.Load(); c.StartupCueCustomPath = p; TrySaveConfig(c); }),
// Send/receive toggle + minimise(hide)/restore(show) cues (machine-wide). The Receive
// file bases use the spelling of the shipped files ("recieve ...") so variant discovery
// matches; the display names use the correct spelling.
MachineRow("Send turned on sound", MainForm.CueId.SendOn, "send on.wav",
c => c.EnableSendOnCue, (c, v) => c.EnableSendOnCue = v),
MachineRow("Send turned off sound", MainForm.CueId.SendOff, "send off.wav",
c => c.EnableSendOffCue, (c, v) => c.EnableSendOffCue = v),
MachineRow("Receive turned on sound", MainForm.CueId.ReceiveOn, "recieve on.wav",
c => c.EnableReceiveOnCue, (c, v) => c.EnableReceiveOnCue = v),
MachineRow("Receive turned off sound", MainForm.CueId.ReceiveOff, "recieve off.wav",
c => c.EnableReceiveOffCue, (c, v) => c.EnableReceiveOffCue = v),
MachineRow("Minimise (hide) sound", MainForm.CueId.Hide, "minimise.wav",
c => c.EnableHideCue, (c, v) => c.EnableHideCue = v),
MachineRow("Restore (show) sound", MainForm.CueId.Show, "maximise.wav",
c => c.EnableShowCue, (c, v) => c.EnableShowCue = v),
]; ];
} }
+5 -1
View File
@@ -303,7 +303,11 @@ internal static class SelfTest
Check(Directory.Exists(soundsDir), "the runtime sounds folder must exist (cues are consolidated at startup)"); Check(Directory.Exists(soundsDir), "the runtime sounds folder must exist (cues are consolidated at startup)");
// Cues ship as numbered variants ("connect 1.wav", ...); each required cue must have at // Cues ship as numbered variants ("connect 1.wav", ...); each required cue must have at
// least one variant present. // least one variant present.
foreach (var cue in new[] { "connect.wav", "disconnect.wav", "start up.wav" }) foreach (var cue in new[]
{
"connect.wav", "disconnect.wav", "start up.wav",
"send on.wav", "send off.wav", "recieve on.wav", "recieve off.wav", "minimise.wav", "maximise.wav",
})
{ {
Check(CueSounds.Variants(cue).Count > 0, Check(CueSounds.Variants(cue).Count > 0,
$"no sound variant present for the '{Path.GetFileNameWithoutExtension(cue)}' cue (was the shipped sounds\\ folder empty?)"); $"no sound variant present for the '{Path.GetFileNameWithoutExtension(cue)}' cue (was the shipped sounds\\ folder empty?)");
+17
View File
@@ -96,6 +96,23 @@ public sealed class AppConfig
/// Machine-wide; the user unticks "Play keyboard clicks" in Preferences to silence it.</summary> /// Machine-wide; the user unticks "Play keyboard clicks" in Preferences to silence it.</summary>
public bool EnableKeyboardClicks { get; set; } = true; public bool EnableKeyboardClicks { get; set; } = true;
/// <summary>Per-cue enable flags for the machine-wide cues added 2026-06-13: the send/receive
/// on/off toggle cues and the minimise(hide)/restore(show) cues. Machine-wide (like the startup
/// cue) rather than per-profile - they're app-level feedback for an action, not a per-profile
/// audio setting. All default on; the user unticks them in Preferences like any other cue.</summary>
public bool EnableSendOnCue { get; set; } = true;
public bool EnableSendOffCue { get; set; } = true;
public bool EnableReceiveOnCue { get; set; } = true;
public bool EnableReceiveOffCue { get; set; } = true;
public bool EnableHideCue { get; set; } = true;
public bool EnableShowCue { get; set; } = true;
/// <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
/// live on a profile. Empty = use the chosen default variant. The startup cue keeps its own
/// <see cref="StartupCueCustomPath"/> field for backward compatibility.</summary>
public Dictionary<string, string> MachineCueCustomPaths { get; set; } = new();
/// <summary>If true, RemSound writes a tab-separated diagnostic log to /// <summary>If true, RemSound writes a tab-separated diagnostic log to
/// <c>&lt;exe&gt;\logs\</c>. Lives here (not in <see cref="Profile"/>) because logging /// <c>&lt;exe&gt;\logs\</c>. Lives here (not in <see cref="Profile"/>) because logging
/// is a debugging affordance for the installation, not a user-facing audio preference — /// is a debugging affordance for the installation, not a user-facing audio preference —