diff --git a/MANUAL.md b/MANUAL.md index 37bd5f3..468778b 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -866,7 +866,7 @@ Toggle| What it does ## 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 ---|--- @@ -879,6 +879,10 @@ Cue| Plays when **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. **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.) diff --git a/build-release.ps1 b/build-release.ps1 index 6bf1593..278d128 100644 --- a/build-release.ps1 +++ b/build-release.ps1 @@ -35,6 +35,15 @@ $distDir = Join-Path $repo 'dist' $zipPath = Join-Path $distDir "RemSound-$Tag.zip" $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. # 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 diff --git a/readme.html b/readme.html index 15f0624..9bb6219 100644 --- a/readme.html +++ b/readme.html @@ -919,7 +919,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift

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:

@@ -932,6 +932,10 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift + + + +
CuePlays when
Profile menu open soundThe Quick profile switch popup opens.
Update soundAn 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 soundRemSound 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 soundYou 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 soundYou 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) soundRemSound's window minimises to the notification area (hides).
Restore (show) soundRemSound'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.)

diff --git a/run-tests.ps1 b/run-tests.ps1 index 77fe57b..ed03535 100644 --- a/run-tests.ps1 +++ b/run-tests.ps1 @@ -48,7 +48,7 @@ $soundsPath = Join-Path $publishDir 'sounds' $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" } # 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) if ($variants.Count -gt 0) { Pass "'$base' cue has $($variants.Count) sound variant(s)" } else { Fail "no sound variant for the '$base' cue" } } diff --git a/sounds/Profile menu open.wav b/sounds/Profile menu open 1.wav similarity index 100% rename from sounds/Profile menu open.wav rename to sounds/Profile menu open 1.wav diff --git a/sounds/connect.wav b/sounds/connect 1.wav similarity index 100% rename from sounds/connect.wav rename to sounds/connect 1.wav diff --git a/sounds/connect 2.wav b/sounds/connect 2.wav new file mode 100644 index 0000000..f7e0187 Binary files /dev/null and b/sounds/connect 2.wav differ diff --git a/sounds/disconnect.wav b/sounds/disconnect 1.wav similarity index 100% rename from sounds/disconnect.wav rename to sounds/disconnect 1.wav diff --git a/sounds/disconnect 2.wav b/sounds/disconnect 2.wav new file mode 100644 index 0000000..5e4c68b Binary files /dev/null and b/sounds/disconnect 2.wav differ diff --git a/sounds/key 1.wav b/sounds/key 1.wav new file mode 100644 index 0000000..185e482 Binary files /dev/null and b/sounds/key 1.wav differ diff --git a/sounds/key 2.wav b/sounds/key 2.wav new file mode 100644 index 0000000..75518b9 Binary files /dev/null and b/sounds/key 2.wav differ diff --git a/sounds/key 3.wav b/sounds/key 3.wav new file mode 100644 index 0000000..dc27952 Binary files /dev/null and b/sounds/key 3.wav differ diff --git a/sounds/key 4.wav b/sounds/key 4.wav new file mode 100644 index 0000000..5cd1fd2 Binary files /dev/null and b/sounds/key 4.wav differ diff --git a/sounds/maximise 1.wav b/sounds/maximise 1.wav new file mode 100644 index 0000000..eeb5232 Binary files /dev/null and b/sounds/maximise 1.wav differ diff --git a/sounds/maximise 2.wav b/sounds/maximise 2.wav new file mode 100644 index 0000000..8151356 Binary files /dev/null and b/sounds/maximise 2.wav differ diff --git a/sounds/minimise 1.wav b/sounds/minimise 1.wav new file mode 100644 index 0000000..ea6bf1b Binary files /dev/null and b/sounds/minimise 1.wav differ diff --git a/sounds/minimise 2.wav b/sounds/minimise 2.wav new file mode 100644 index 0000000..4250aca Binary files /dev/null and b/sounds/minimise 2.wav differ diff --git a/sounds/passkey.wav b/sounds/passkey.wav new file mode 100644 index 0000000..4559a4e Binary files /dev/null and b/sounds/passkey.wav differ diff --git a/sounds/profile.wav b/sounds/profile 1.wav similarity index 100% rename from sounds/profile.wav rename to sounds/profile 1.wav diff --git a/sounds/profile 2.wav b/sounds/profile 2.wav new file mode 100644 index 0000000..584ebbd Binary files /dev/null and b/sounds/profile 2.wav differ diff --git a/sounds/profile menu open 2.wav b/sounds/profile menu open 2.wav new file mode 100644 index 0000000..b232a73 Binary files /dev/null and b/sounds/profile menu open 2.wav differ diff --git a/sounds/recieve off 1.wav b/sounds/recieve off 1.wav new file mode 100644 index 0000000..759497c Binary files /dev/null and b/sounds/recieve off 1.wav differ diff --git a/sounds/recieve off 2.wav b/sounds/recieve off 2.wav new file mode 100644 index 0000000..500e8ec Binary files /dev/null and b/sounds/recieve off 2.wav differ diff --git a/sounds/recieve on 1.wav b/sounds/recieve on 1.wav new file mode 100644 index 0000000..25e270c Binary files /dev/null and b/sounds/recieve on 1.wav differ diff --git a/sounds/recieve on 2.wav b/sounds/recieve on 2.wav new file mode 100644 index 0000000..c8b8ce6 Binary files /dev/null and b/sounds/recieve on 2.wav differ diff --git a/sounds/record start.wav b/sounds/record start 1.wav similarity index 100% rename from sounds/record start.wav rename to sounds/record start 1.wav diff --git a/sounds/record start 2.wav b/sounds/record start 2.wav new file mode 100644 index 0000000..20fca1b Binary files /dev/null and b/sounds/record start 2.wav differ diff --git a/sounds/record stop.wav b/sounds/record stop 1.wav similarity index 100% rename from sounds/record stop.wav rename to sounds/record stop 1.wav diff --git a/sounds/record stop 2.wav b/sounds/record stop 2.wav new file mode 100644 index 0000000..5edcd4e Binary files /dev/null and b/sounds/record stop 2.wav differ diff --git a/sounds/save.wav b/sounds/save 1.wav similarity index 100% rename from sounds/save.wav rename to sounds/save 1.wav diff --git a/sounds/save 2.wav b/sounds/save 2.wav new file mode 100644 index 0000000..7e13f4c Binary files /dev/null and b/sounds/save 2.wav differ diff --git a/sounds/send off 1.wav b/sounds/send off 1.wav new file mode 100644 index 0000000..207f505 Binary files /dev/null and b/sounds/send off 1.wav differ diff --git a/sounds/send off 2.wav b/sounds/send off 2.wav new file mode 100644 index 0000000..f5813f0 Binary files /dev/null and b/sounds/send off 2.wav differ diff --git a/sounds/send on 1.wav b/sounds/send on 1.wav new file mode 100644 index 0000000..04807c5 Binary files /dev/null and b/sounds/send on 1.wav differ diff --git a/sounds/send on 2.wav b/sounds/send on 2.wav new file mode 100644 index 0000000..f873eba Binary files /dev/null and b/sounds/send on 2.wav differ diff --git a/sounds/start up.wav b/sounds/start up 1.wav similarity index 100% rename from sounds/start up.wav rename to sounds/start up 1.wav diff --git a/sounds/start up 2.wav b/sounds/start up 2.wav new file mode 100644 index 0000000..2f39340 Binary files /dev/null and b/sounds/start up 2.wav differ diff --git a/sounds/update.wav b/sounds/update 1.wav similarity index 100% rename from sounds/update.wav rename to sounds/update 1.wav diff --git a/sounds/update 2.wav b/sounds/update 2.wav new file mode 100644 index 0000000..9466858 Binary files /dev/null and b/sounds/update 2.wav differ diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 3a8f25a..08341a2 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -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); + } + + /// 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. + 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(); } + } + } + + /// 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. + public void PlayWindowVisibilityCue(bool show) + { + var cfg = AppConfig.Load(); + if (show) { if (cfg.EnableShowCue) showSound?.Play(); } + else { if (cfg.EnableHideCue) hideSound?.Play(); } } /// 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"; } /// 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); } /// diff --git a/src/RemSound.App/MainFormTrayController.cs b/src/RemSound.App/MainFormTrayController.cs index e91080f..5d8a822 100644 --- a/src/RemSound.App/MainFormTrayController.cs +++ b/src/RemSound.App/MainFormTrayController.cs @@ -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 diff --git a/src/RemSound.App/PreferencesDialog.cs b/src/RemSound.App/PreferencesDialog.cs index 0842608..ca9f53c 100644 --- a/src/RemSound.App/PreferencesDialog.cs +++ b/src/RemSound.App/PreferencesDialog.cs @@ -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 loadEnabled, Action 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), ]; } diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index 497d955..2ac80cf 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -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?)"); diff --git a/src/RemSound.Core/AppConfig.cs b/src/RemSound.Core/AppConfig.cs index 0cbdb97..a047bf6 100644 --- a/src/RemSound.Core/AppConfig.cs +++ b/src/RemSound.Core/AppConfig.cs @@ -96,6 +96,23 @@ public sealed class AppConfig /// Machine-wide; the user unticks "Play keyboard clicks" in Preferences to silence it. public bool EnableKeyboardClicks { get; set; } = true; + /// 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. + 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; + + /// Custom WAV overrides for the machine-wide cues above, keyed by cue id. The + /// equivalent of but machine-wide, since these cues don't + /// live on a profile. Empty = use the chosen default variant. The startup cue keeps its own + /// field for backward compatibility. + public Dictionary MachineCueCustomPaths { get; set; } = new(); + /// If true, RemSound writes a tab-separated diagnostic log to /// <exe>\logs\. Lives here (not in ) because logging /// is a debugging affordance for the installation, not a user-facing audio preference —