Release v3.2: update sound cue + single-instance bring-to-front fix

- New "Update sound" cue (CueId.Update / update.wav). Plays just before an
  update starts installing, on every path (manual, background-silent,
  startup-silent), so a silent background update gives an audible heads-up
  before RemSound closes to restart. Per-profile mute + custom-sound override
  in Preferences, same infrastructure as the other cues:
    * Profile.EnableUpdateCue + RemSoundSettingsStore Load/Save + round-trip
    * MainForm updateSound field, TryLoadCueSound, play in InstallUpdateAsync
      gated by LoadEnableUpdateCue
    * PreferencesDialog "Update sound" CueRow + ResolveCueFilePath mapping
    * update.wav shipped in sounds\ via csproj Content
- Single-instance "switch to the running copy" now actually brings the window
  to the front. The second copy grants the running copy foreground rights via
  AllowSetForegroundWindow before signalling, and lingers briefly so it can
  raise itself before we exit — without this, Windows' foreground lock left
  the running window only flashing in the taskbar (Ed's report).
- Docs: About box v3.2 block, RELEASE_NOTES.md, manual cue section (now seven
  cues, with the update cue described), MANUAL.md regenerated.
- Version bumped 3.1.3 -> 3.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-31 17:58:25 +01:00
co-authored by Claude Opus 4.8
parent 9ab0a1a20a
commit cdcac859c4
11 changed files with 130 additions and 27 deletions
+5 -4
View File
@@ -787,7 +787,7 @@ Toggle| What it does
## 17. Audio cue sounds ## 17. 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**. Six 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**. Seven events have a cue:
Cue| Plays when Cue| Plays when
---|--- ---|---
@@ -797,12 +797,13 @@ Cue| Plays when
**Recording stop sound**| You stop a recording. **Recording stop sound**| You stop a recording.
**Profile saved sound**| A profile is saved — whether via File → Save or File → Save as. **Profile saved sound**| A profile is saved — whether via File → Save or File → Save as.
**Profile switched sound**| A profile finishes loading. Plays at startup if you started with a profile, and after every profile switch — using the new profile's cue, not the old one's. **Profile switched sound**| A profile finishes loading. Plays at startup if you started with a profile, and after every profile switch — using the new profile's cue, not the old one's.
**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.
All six 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 seven 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.)
### Turning each cue on or off ### Turning each cue on or off
Open **File → Preferences** (or Ctrl+P). The **Audio cue sounds (Alt+N)** list shows all six cues with a tickbox next to each. Tick to play the cue when the corresponding event happens; untick to silence it. Open **File → Preferences** (or Ctrl+P). The **Audio cue sounds (Alt+N)** list shows all seven cues with a tickbox next to each. Tick to play the cue when the corresponding event happens; untick to silence it.
Use the up and down arrow keys to move between cues; press **Space** to toggle the highlighted cue's tick on or off. Use the up and down arrow keys to move between cues; press **Space** to toggle the highlighted cue's tick on or off.
@@ -932,7 +933,7 @@ Format| What you get| When to pick it
### Start and stop sound cues ### Start and stop sound cues
RemSound plays a short ding when a recording starts and another when it stops, so you have an audible confirmation that the toggle actually took effect. These are two of the six cues described in Audio cue sounds. You can turn either or both off, replace them with your own WAV files, and preview them from Preferences. The defaults live at `sounds\record start.wav` and `sounds\record stop.wav` next to `RemSound.exe`. RemSound plays a short ding when a recording starts and another when it stops, so you have an audible confirmation that the toggle actually took effect. These are two of the seven cues described in Audio cue sounds. You can turn either or both off, replace them with your own WAV files, and preview them from Preferences. The defaults live at `sounds\record start.wav` and `sounds\record stop.wav` next to `RemSound.exe`.
### Where recordings go ### Where recordings go
+22 -16
View File
@@ -1,35 +1,41 @@
# RemSound v3.1.3 # RemSound v3.2
An important reliability fix. After an update, a chain of small faults could line up and leave RemSound misbehaving — in the worst case, more than one copy running at once with the sound getting louder and louder. This release breaks that chain in several places, the most important being that RemSound now flatly refuses to run as two copies at the same time. A new audio cue plus a round of reliability work. The headline is a sound that warns you an update is about to install — and underneath it, RemSound now flatly refuses to run as two copies at once.
## What could go wrong (and now can't) ## New: an "Update sound" cue
A few separate problems were feeding into each other: RemSound now plays a short sound **just before it closes to install an update** — whether you started the update by hand or it installed silently in the background. So a silent update no longer catches you off guard: you hear that RemSound is about to restart.
- **Locked profiles could quietly unlock themselves.** If you'd marked a profile as read-only (locked) and then deliberately saved it, the save was silently dropping the lock. Next time you opened that profile it was editable again — so it started asking "save your changes?" when you didn't expect it. Like every other cue, it's **per-profile**, you can **mute** it, and you can **swap in your own WAV** — all from **File → Preferences → Audio cue sounds**, where it's the new seventh entry in the list. Preview it with the Play button, point it at your own sound with Browse, or right-click Browse to go back to the default.
- **That "save your changes?" question could block an update.** When RemSound updates itself it has to close and restart. If the save question popped up at that moment, a stray Enter or Escape landed on "Cancel" and quietly stopped the restart — so the update never finished cleanly.
- **Nothing stopped extra copies running.** With the restart half-done, RemSound could end up with two copies going at once — and then more — each one playing incoming audio, which is why the sound climbed to a deafening level. The only way out was force-closing them all.
## What's fixed ## Only one copy of RemSound at a time
- **Only one copy at a time.** RemSound now refuses to run as two copies. If you open it while it's already running, it asks what you'd like to do: **switch to the copy that's already running** (it may be tucked in the system tray, by the clock), or — if that copy is stuck — **force it closed and start fresh**. This makes the stacking-copies runaway impossible. RemSound now refuses to run as two copies at once. If you open it while it's already running, it asks what you'd like to do:
- **Updates can't be interrupted.** When RemSound updates and restarts, nothing is allowed to get in the way — no question can cancel the restart.
- **Locked means locked.** Marking a profile read-only now survives saving it, deliberately or otherwise. - **Switch to the copy that's already running** — brings it back to the front (even if it was minimised to the system tray, down by the clock).
- **No double-installs.** A single copy can no longer kick off two update installs at once. - **Force the running copy to close and start fresh** — for when a copy is stuck or not responding.
This closes off a problem where, after an update, RemSound could end up with several copies running at once, each playing audio and getting louder — the only way out being to force-close them all.
## Updates are more dependable
- **Nothing can interrupt an update's restart any more.** Previously an "unsaved changes?" question could pop up at the wrong moment and quietly cancel it.
- **A locked (read-only) profile stays locked when you save it.** Before, deliberately saving a locked profile silently dropped the lock — which then brought the save question back and could trip up an update.
- **A single copy can't kick off two updates at once.**
## Nothing else has changed ## Nothing else has changed
Same wire format, same codec list, same audio cues, same everything else from v3.1. v3.1.3 talks to v3.0.x and v3.1.x peers exactly as before. Same wire format, same codec list, same audio cues (plus the new one), same everything else from v3.1. v3.2 talks to v3.0.x and v3.1.x peers exactly as before.
## Install ## Install
1. Download `RemSound-v3.1.3.zip` from this release. 1. Download `RemSound-v3.2.zip` from this release.
2. Close RemSound. 2. Close RemSound.
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your profiles, settings or recordings. 3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your profiles, settings or recordings.
4. Run `RemSound.exe`. 4. Run `RemSound.exe`.
## Upgrading ## Upgrading
**v1.9 through v3.1.2:** Help → Check for updates works — it will fetch and install v3.1.3 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.1.3 installs itself shortly after launch. **v1.9 through v3.1.3:** Help → Check for updates works — it will fetch and install v3.2 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.2 installs itself shortly after launch (and you'll hear the new update cue as it does).
**v1.8 and earlier:** the auto-updater in those versions has a fault that prevents it from installing updates, so Check for updates will download v3.1.3 but not apply it. Install v3.1.3 by hand using the steps above — just this once. From the build you install onward, updates are automatic. **v1.8 and earlier:** the auto-updater in those versions has a fault that prevents it from installing updates, so Check for updates will download v3.2 but not apply it. Install v3.2 by hand using the steps above — just this once. From the build you install onward, updates are automatic.
+5 -4
View File
@@ -837,7 +837,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<h2 id="audio-cues">17. Audio cue sounds</h2> <h2 id="audio-cues">17. 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>. Six 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>. Seven 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>
@@ -847,13 +847,14 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<tr><td><strong>Recording stop sound</strong></td><td>You stop a recording.</td></tr> <tr><td><strong>Recording stop sound</strong></td><td>You stop a recording.</td></tr>
<tr><td><strong>Profile saved sound</strong></td><td>A profile is saved &mdash; whether via File &rarr; Save or File &rarr; Save as.</td></tr> <tr><td><strong>Profile saved sound</strong></td><td>A profile is saved &mdash; whether via File &rarr; Save or File &rarr; Save as.</td></tr>
<tr><td><strong>Profile switched sound</strong></td><td>A profile finishes loading. Plays at startup if you started with a profile, and after every profile switch &mdash; using the new profile's cue, not the old one's.</td></tr> <tr><td><strong>Profile switched sound</strong></td><td>A profile finishes loading. Plays at startup if you started with a profile, and after every profile switch &mdash; using the new profile's cue, not the old one's.</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>
</table> </table>
<p>All six 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 seven 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>
<h3>Turning each cue on or off</h3> <h3>Turning each cue on or off</h3>
<p>Open <strong>File &rarr; Preferences</strong> (or Ctrl+P). The <strong>Audio cue sounds (Alt+N)</strong> list shows all six cues with a tickbox next to each. Tick to play the cue when the corresponding event happens; untick to silence it.</p> <p>Open <strong>File &rarr; Preferences</strong> (or Ctrl+P). The <strong>Audio cue sounds (Alt+N)</strong> list shows all seven cues with a tickbox next to each. Tick to play the cue when the corresponding event happens; untick to silence it.</p>
<p>Use the up and down arrow keys to move between cues; press <strong>Space</strong> to toggle the highlighted cue's tick on or off.</p> <p>Use the up and down arrow keys to move between cues; press <strong>Space</strong> to toggle the highlighted cue's tick on or off.</p>
@@ -987,7 +988,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<h3>Start and stop sound cues</h3> <h3>Start and stop sound cues</h3>
<p>RemSound plays a short ding when a recording starts and another when it stops, so you have an audible confirmation that the toggle actually took effect. These are two of the six cues described in <a href="#audio-cues">Audio cue sounds</a>. You can turn either or both off, replace them with your own WAV files, and preview them from Preferences. The defaults live at <code>sounds\record start.wav</code> and <code>sounds\record stop.wav</code> next to <code>RemSound.exe</code>.</p> <p>RemSound plays a short ding when a recording starts and another when it stops, so you have an audible confirmation that the toggle actually took effect. These are two of the seven cues described in <a href="#audio-cues">Audio cue sounds</a>. You can turn either or both off, replace them with your own WAV files, and preview them from Preferences. The defaults live at <code>sounds\record start.wav</code> and <code>sounds\record stop.wav</code> next to <code>RemSound.exe</code>.</p>
<h3>Where recordings go</h3> <h3>Where recordings go</h3>
BIN
View File
Binary file not shown.
+28
View File
@@ -20,6 +20,34 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary> /// updates" path.</summary>
private const string ReleaseNotes = private const string ReleaseNotes =
""" """
RemSound v3.2
A new audio cue, plus the reliability work from
the recent updates, rolled into one release.
New "Update sound" cue: RemSound now plays a short
sound just before it closes to install an update
for both manual updates and silent background ones.
So a silent update no longer takes you by surprise;
you hear it coming. Like every cue it's per-profile,
can be muted, and you can swap in your own sound,
all from File Preferences Audio cue sounds.
Only one copy at a time: RemSound refuses to run as
two copies at once. Open it while it's already
running and it offers to switch you to the copy
that's already going (now reliably bringing that
window to the front, even from the system tray), or
if that copy is stuck to force it closed and
start fresh.
Updates can't be blocked or doubled up: nothing can
get in the way of an update's restart any more, and
a single copy can't kick off two installs at once.
Locked profiles stay locked: marking a profile as
read-only now survives saving it.
RemSound v3.1.3 RemSound v3.1.3
An important reliability fix. After an update, a An important reliability fix. After an update, a
+12
View File
@@ -326,6 +326,7 @@ public sealed class MainForm : Form
// Save As; Profile fires immediately after a profile finishes loading in MainForm. // Save As; Profile fires immediately after a profile finishes loading in MainForm.
private System.Media.SoundPlayer? saveSound; private System.Media.SoundPlayer? saveSound;
private System.Media.SoundPlayer? profileSwitchSound; private System.Media.SoundPlayer? profileSwitchSound;
private System.Media.SoundPlayer? updateSound;
// 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.
@@ -892,6 +893,7 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound); TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound);
TryLoadCueSound(CueId.Save, "save.wav", out saveSound); TryLoadCueSound(CueId.Save, "save.wav", out saveSound);
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound); TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
TryLoadCueSound(CueId.Update, "update.wav", out updateSound);
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
@@ -2081,6 +2083,14 @@ public sealed class MainForm : Form
} }
updateInstallStarted = true; updateInstallStarted = true;
// Update cue — an audible heads-up that an update is going in, played just before it
// starts. Fires for every install path (manual, background-silent, startup-silent), so
// a silent background update isn't completely silent: the user hears that RemSound is
// about to close and update. Played before the download so it sounds well before the
// app vanishes; the download comfortably outlasts the short cue. Honours the per-profile
// EnableUpdateCue flag set in Preferences.
if (settings.LoadEnableUpdateCue()) updateSound?.Play();
var ok = await updater.DownloadAndStageInstallAsync(info, currentProfileTitle).ConfigureAwait(true); var ok = await updater.DownloadAndStageInstallAsync(info, currentProfileTitle).ConfigureAwait(true);
if (!ok) if (!ok)
{ {
@@ -5426,6 +5436,7 @@ public sealed class MainForm : Form
public const string RecordStop = "record-stop"; public const string RecordStop = "record-stop";
public const string Save = "save"; public const string Save = "save";
public const string ProfileSwitch = "profile-switch"; public const string ProfileSwitch = "profile-switch";
public const string Update = "update";
} }
/// <summary>Load one cue sound. Resolution order: /// <summary>Load one cue sound. Resolution order:
@@ -5487,6 +5498,7 @@ public sealed class MainForm : Form
TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound); TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound);
TryLoadCueSound(CueId.Save, "save.wav", out saveSound); TryLoadCueSound(CueId.Save, "save.wav", out saveSound);
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound); TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
TryLoadCueSound(CueId.Update, "update.wav", out updateSound);
} }
/// <summary> /// <summary>
+3
View File
@@ -102,6 +102,8 @@ internal sealed class PreferencesDialog : Form
s => s.LoadEnableSaveCue(), (s, v) => s.SaveEnableSaveCue(v)), s => s.LoadEnableSaveCue(), (s, v) => s.SaveEnableSaveCue(v)),
new("Profile switched sound", MainForm.CueId.ProfileSwitch, new("Profile switched sound", MainForm.CueId.ProfileSwitch,
s => s.LoadEnableProfileSwitchCue(), (s, v) => s.SaveEnableProfileSwitchCue(v)), s => s.LoadEnableProfileSwitchCue(), (s, v) => s.SaveEnableProfileSwitchCue(v)),
new("Update sound", MainForm.CueId.Update,
s => s.LoadEnableUpdateCue(), (s, v) => s.SaveEnableUpdateCue(v)),
]; ];
private readonly AccessibleCheckBox acceptRemoteVolumeBox = new() private readonly AccessibleCheckBox acceptRemoteVolumeBox = new()
@@ -595,6 +597,7 @@ internal sealed class PreferencesDialog : Form
MainForm.CueId.RecordStop => "record stop.wav", MainForm.CueId.RecordStop => "record stop.wav",
MainForm.CueId.Save => "save.wav", MainForm.CueId.Save => "save.wav",
MainForm.CueId.ProfileSwitch => "profile.wav", MainForm.CueId.ProfileSwitch => "profile.wav",
MainForm.CueId.Update => "update.wav",
_ => null, _ => null,
}; };
if (defaultFileName is null) return null; if (defaultFileName is null) return null;
+8 -1
View File
@@ -14,7 +14,7 @@
tag_name on the latest GitHub release; bump it on every public release. The tag_name on the latest GitHub release; bump it on every public release. The
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
is what the About dialog and the updater both read. --> is what the About dialog and the updater both read. -->
<Version>3.1.3</Version> <Version>3.2.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -90,6 +90,13 @@
<Link>sounds\profile.wav</Link> <Link>sounds\profile.wav</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<!-- update.wav (2026-05-31): plays just before an update starts installing, so a silent
background update still gives an audible heads-up. Per-profile mute + custom-sound
override available in Preferences, same as the other cues. -->
<Content Include="..\..\sounds\update.wav" Condition="Exists('..\..\sounds\update.wav')">
<Link>sounds\update.wav</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- User manual. F1 anywhere in the app opens this via the user's default browser <!-- User manual. F1 anywhere in the app opens this via the user's default browser
(HelpLauncher.OpenManual). The PreserveNewest mode means a fresh publish overwrites (HelpLauncher.OpenManual). The PreserveNewest mode means a fresh publish overwrites
the published copy whenever the source is newer; manually-edited copies inside the published copy whenever the source is newer; manually-edited copies inside
+29 -1
View File
@@ -1,5 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices;
namespace RemSound.App; namespace RemSound.App;
@@ -113,10 +114,31 @@ internal sealed class SingleInstanceCoordinator : IDisposable
} }
} }
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllowSetForegroundWindow(int dwProcessId);
/// <summary>Signal whichever copy currently owns the lock to bring itself to the front. /// <summary>Signal whichever copy currently owns the lock to bring itself to the front.
/// Called by a SECOND copy that chose "switch to the running copy".</summary> /// Called by a SECOND copy that chose "switch to the running copy".
///
/// Windows blocks <c>SetForegroundWindow</c> from a process that didn't receive the most
/// recent user input. When the user launches this second copy from (say) an Explorer
/// window and clicks "switch", the input belongs to US, not the running copy — so the
/// running copy's own SetForegroundWindow is denied and its window only flashes in the
/// taskbar instead of coming forward (the bug Ed reported). The documented fix is for the
/// process that currently holds the foreground right — us, right now — to hand it to the
/// running copy via <see cref="AllowSetForegroundWindow"/> BEFORE signalling. We then
/// linger briefly so the running copy can raise itself while the grant is fresh and the
/// foreground hasn't churned from us exiting.</summary>
public static void SignalExistingToActivate() public static void SignalExistingToActivate()
{ {
// Grant every other RemSound process the one-shot right to pull itself to the front.
foreach (var p in OtherInstances(Environment.ProcessId))
{
try { AllowSetForegroundWindow(p.Id); } catch { /* best-effort */ }
p.Dispose();
}
try try
{ {
if (EventWaitHandle.TryOpenExisting(ActivateEventName, out var ev)) if (EventWaitHandle.TryOpenExisting(ActivateEventName, out var ev))
@@ -128,6 +150,12 @@ internal sealed class SingleInstanceCoordinator : IDisposable
{ {
// Best-effort — if the signal can't be delivered the user can click the tray icon. // Best-effort — if the signal can't be delivered the user can click the tray icon.
} }
// Stay alive a beat so the running copy can raise itself while we're still the
// foreground process and the grant is fresh. If we vanished instantly the foreground
// would churn and the grant could be consumed before it's used. Invisible to the user —
// our dialog has already closed and we have no window.
try { Thread.Sleep(600); } catch { /* ignore */ }
} }
/// <summary>Force every OTHER RemSound process to terminate. Returns true if no other /// <summary>Force every OTHER RemSound process to terminate. Returns true if no other
+5 -1
View File
@@ -107,10 +107,14 @@ public sealed class Profile
public bool? EnableRecordStopCue { get; set; } public bool? EnableRecordStopCue { get; set; }
public bool? EnableSaveCue { get; set; } public bool? EnableSaveCue { get; set; }
public bool? EnableProfileSwitchCue { get; set; } public bool? EnableProfileSwitchCue { get; set; }
/// <summary>Plays the update cue just before an update starts installing (manual or
/// silent). Null = unset → defaults to on, so a silent background update still gives an
/// audible heads-up. Added 2026-05-31.</summary>
public bool? EnableUpdateCue { get; set; }
/// <summary>Per-cue custom WAV file overrides, keyed by the well-known cue id (<c>connect</c>, /// <summary>Per-cue custom WAV file overrides, keyed by the well-known cue id (<c>connect</c>,
/// <c>disconnect</c>, <c>record-start</c>, <c>record-stop</c>, <c>save</c>, /// <c>disconnect</c>, <c>record-start</c>, <c>record-stop</c>, <c>save</c>,
/// <c>profile-switch</c>) and valued with the absolute filesystem path to the user's chosen /// <c>profile-switch</c>, <c>update</c>) and valued with the absolute filesystem path to the user's chosen
/// WAV. Per-profile (moved here from AppConfig 2026-05-28) so a "live monitoring" profile /// WAV. Per-profile (moved here from AppConfig 2026-05-28) so a "live monitoring" profile
/// can have one set of custom sounds and a "recording" profile a different set. Missing /// can have one set of custom sounds and a "recording" profile a different set. Missing
/// keys mean "use the default sound shipped in the sounds\ folder next to RemSound.exe". /// keys mean "use the default sound shipped in the sounds\ folder next to RemSound.exe".
@@ -428,6 +428,16 @@ public sealed class RemSoundSettingsStore
Save(s); Save(s);
} }
public bool LoadEnableUpdateCue() =>
Try(() => Load()?.EnableUpdateCue) ?? true;
public void SaveEnableUpdateCue(bool value)
{
var s = Load() ?? new Settings();
s.EnableUpdateCue = value;
Save(s);
}
/// <summary>The user's custom WAV path for a given cue, or null when they're using the /// <summary>The user's custom WAV path for a given cue, or null when they're using the
/// default. Per-profile (lives on <see cref="Profile.CustomCuePaths"/>) so different /// default. Per-profile (lives on <see cref="Profile.CustomCuePaths"/>) so different
/// profiles can carry different cue palettes.</summary> /// profiles can carry different cue palettes.</summary>
@@ -587,6 +597,7 @@ public sealed class RemSoundSettingsStore
EnableRecordStopCue = profile.EnableRecordStopCue, EnableRecordStopCue = profile.EnableRecordStopCue,
EnableSaveCue = profile.EnableSaveCue, EnableSaveCue = profile.EnableSaveCue,
EnableProfileSwitchCue = profile.EnableProfileSwitchCue, EnableProfileSwitchCue = profile.EnableProfileSwitchCue,
EnableUpdateCue = profile.EnableUpdateCue,
// Defensive copy so cache mutations don't leak into the in-memory Profile graph // Defensive copy so cache mutations don't leak into the in-memory Profile graph
// (and vice-versa). Profile is loaded once at startup; the cache evolves through // (and vice-versa). Profile is loaded once at startup; the cache evolves through
// the session and is written back via CopyTo on save. // the session and is written back via CopyTo on save.
@@ -643,6 +654,7 @@ public sealed class RemSoundSettingsStore
profile.EnableRecordStopCue = s.EnableRecordStopCue; profile.EnableRecordStopCue = s.EnableRecordStopCue;
profile.EnableSaveCue = s.EnableSaveCue; profile.EnableSaveCue = s.EnableSaveCue;
profile.EnableProfileSwitchCue = s.EnableProfileSwitchCue; profile.EnableProfileSwitchCue = s.EnableProfileSwitchCue;
profile.EnableUpdateCue = s.EnableUpdateCue;
profile.CustomCuePaths = s.CustomCuePaths is null profile.CustomCuePaths = s.CustomCuePaths is null
? new Dictionary<string, string>() ? new Dictionary<string, string>()
: new Dictionary<string, string>(s.CustomCuePaths); : new Dictionary<string, string>(s.CustomCuePaths);
@@ -713,6 +725,7 @@ public sealed class RemSoundSettingsStore
public bool? EnableRecordStopCue { get; set; } public bool? EnableRecordStopCue { get; set; }
public bool? EnableSaveCue { get; set; } public bool? EnableSaveCue { get; set; }
public bool? EnableProfileSwitchCue { get; set; } public bool? EnableProfileSwitchCue { get; set; }
public bool? EnableUpdateCue { get; set; }
public Dictionary<string, string>? CustomCuePaths { get; set; } public Dictionary<string, string>? CustomCuePaths { get; set; }
public RecordingSettings? RecordingSettings { get; set; } public RecordingSettings? RecordingSettings { get; set; }
} }