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:
co-authored by
Claude Opus 4.8
parent
e526014e6a
commit
8763470037
@@ -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
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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?)");
|
||||
|
||||
@@ -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><exe>\logs\</c>. Lives here (not in <see cref="Profile"/>) because logging
|
||||
/// is a debugging affordance for the installation, not a user-facing audio preference —
|
||||
|
||||
Reference in New Issue
Block a user