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
+66
View File
@@ -334,6 +334,13 @@ public sealed class MainForm : Form
private CuePlayer? profileSwitchSound;
private CuePlayer? profileMenuOpenSound;
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
// re-titled when the user toggles between WASAPI mode (Windows devices) and ASIO mode
// (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.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound);
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();
// 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;
HandleCapabilityChange();
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
@@ -6458,6 +6504,14 @@ public sealed class MainForm : Form
// 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.
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:
@@ -6479,6 +6533,12 @@ public sealed class MainForm : Form
{
string? path = null;
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))
{
path = customPath;
@@ -6523,6 +6583,12 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
TryLoadCueSound(CueId.ProfileMenuOpen, "profile menu open.wav", out profileMenuOpenSound);
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>
@@ -201,6 +201,8 @@ internal sealed class MainFormTrayController : IDisposable
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();
if (owner.WindowState == FormWindowState.Minimized)
{
@@ -218,6 +220,7 @@ internal sealed class MainFormTrayController : IDisposable
// to actually hear the new content.
try { SetForegroundWindow(owner.Handle); } catch { /* harmless — Restore still mostly worked */ }
trayIcon.Visible = false;
if (wasHidden) (owner as MainForm)?.PlayWindowVisibilityCue(show: true);
}
[DllImport("user32.dll")]
@@ -226,7 +229,11 @@ internal sealed class MainFormTrayController : IDisposable
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();
if (wasVisible) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
// 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),
// 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),
() => 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
[
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); },
() => AppConfig.Load().StartupCueCustomPath,
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)");
// Cues ship as numbered variants ("connect 1.wav", ...); each required cue must have at
// 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,
$"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>
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
/// <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 —