Bump to v3.1.0: new audio cues (save, profile-switch), per-profile custom cue paths, redesigned tray menu
Two new cues join the existing connect / disconnect / record-start /
record-stop set:
* profile-save cue (sounds\save.wav by default) fires in
SaveProfileTo after a successful Save or Save As. Honours the
Profile.EnableSaveCue per-profile flag.
* profile-switch cue (sounds\profile.wav by default) fires in the
MainForm Shown handler after a profile finishes loading - covers
both startup-with-profile and mid-session profile switches.
Honours the Profile.EnableProfileSwitchCue flag. Because cue
loading runs AFTER settings.ApplyProfile in the ctor (line 542),
the profile being entered determines which sound plays - not the
one being left, exactly as Ed asked.
The audio cue UI in PreferencesDialog stays as a CheckedListBox (up /
down navigates, Space toggles) with TWO action buttons below that
operate on whichever cue is selected:
* Play [cue name] (Alt+P) - previews via SoundPlayer.Play on the
resolved path (custom override if set, default in sounds\
otherwise). Independent of the tick state.
* Browse for [cue name]... (Alt+B) - opens a WAV picker. If the
user picks a file inside RemSound's own sounds\ folder, it's
treated as "use default" and the override is cleared - avoids
pinning the user to a specific shipped default that a future
release might replace. Right-click "Use default sound" reverts.
Custom cue paths AND enable flags are per-profile. Lives on
Profile.CustomCuePaths (Dictionary<string,string>) and the per-cue
EnableXxxCue bool? properties. Cache mirror in
RemSoundSettingsStore.Settings.
All default WAVs moved from the install-root flat layout into a
sounds\ subfolder (csproj Content rules updated). save.wav and
profile.wav are bundled defaults.
Tray menu rewritten (MainFormTrayController) per Ed's spec:
* Show RemSound (W) - now uses Win32 SetForegroundWindow after the
standard Activate() because WinForms Activate is blocked by the
foreground-lock when invoked from a tray-menu click, which left
screen-reader users having to Alt+Tab to reach the restored
window.
* Enable sending (S) / Enable receiving (R) - tickable, reflect
current state, TOGGLE rather than always-on.
* Profiles (P) - submenu populated from AppConfig.RecentProfiles
with the same &1..&5 mnemonics the File menu uses. Pre-populated
once at construction so WinForms recognises it as a submenu and
fires DropDownOpening - originally I relied entirely on the
open event, which the framework skipped for items with no
DropDownItems, producing the "Profiles does nothing" bug.
* Exit (X).
Tray tooltip now built dynamically from snapshot tick (1 Hz):
"RemSound - [recording for MM:SS,] N peer(s), sending (lane),
receiving (lane)". Recording timer only included while
RecordingController.IsRecording is true (added
RecordingStartedUtc accessor for the elapsed calculation). Lane is
derived from which device-list ticks are active, not just the audio-
mode setting, so a BothIndependent user with only WASAPI inputs ticked
honestly reads as "sending (WASAPI)".
Fixes:
* Initial tooltip "RemSound" produced a "RemSound RemSound" read on
NVDA because the process name and tooltip matched. Set to
"RemSound - starting up" so the duplicate disappears.
* Recent profile menu items no longer carry a "Recent profile N:"
AccessibleName prefix in either the tray submenu or the File
menu's Recent profiles - now just the profile name. Number-key
mnemonics (&1..&5) untouched.
Manual (readme.html) updated: new section 17 "Audio cue sounds"
documents all six cues, the Play/Browse buttons, the right-click
"Use default sound", and the per-profile semantics. Sections 17-21
renumbered to 18-22. New "System tray icon and its menu" subsection
inside section 4 documents the redesigned right-click menu and the
hover tooltip. MANUAL.md regenerated via sync-manual.py. About box
gets a v3.1 block at the top. RELEASE_NOTES.md fully rewritten for
v3.1.
No wire format change - v3.1 talks to other v3.0.x machines exactly
as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0189b12668
commit
70c2669d70
@@ -24,11 +24,12 @@ It was built for playing music together over the internet — a guitarist on one
|
||||
1. [Global hotkeys (work even when minimised)](#14-global-hotkeys-work-even-when-minimised)
|
||||
1. [Remote control: adjusting a peer's listening volume from your end](#15-remote-control-adjusting-a-peers-listening-volume-from-your-end)
|
||||
1. [Startup behaviour](#16-startup-behaviour)
|
||||
1. [Updating RemSound](#17-updating-remsound)
|
||||
1. [Recording to a file](#18-recording-to-a-file)
|
||||
1. [Logs and diagnostics](#19-logs-and-diagnostics)
|
||||
1. [Troubleshooting](#20-troubleshooting)
|
||||
1. [Glossary](#21-glossary)
|
||||
1. [Audio cue sounds](#17-audio-cue-sounds)
|
||||
1. [Updating RemSound](#18-updating-remsound)
|
||||
1. [Recording to a file](#19-recording-to-a-file)
|
||||
1. [Logs and diagnostics](#20-logs-and-diagnostics)
|
||||
1. [Troubleshooting](#21-troubleshooting)
|
||||
1. [Glossary](#22-glossary)
|
||||
|
||||
|
||||
## 1. What RemSound does
|
||||
@@ -183,6 +184,30 @@ Tab| What it's for
|
||||
**Audio inputs and outputs**| The ASIO driver picker (when an ASIO driver is installed), the Receive audio and Send my audio checkboxes, and all the device lists. Choosing a real driver in the picker brings up the ASIO device lists alongside the ordinary Windows ones; choosing _(none)_ hides them.
|
||||
**Audio profile**| Codec, packet size, lock-to-audio-clock, latency, continuous auto-tune, buffer smoothness, artefact sound. Split into an _Audio send parameters_ group and an _Audio receive parameters_ group.
|
||||
|
||||
### The system tray icon and its menu
|
||||
|
||||
When RemSound is **minimised to the tray** (via **File → Minimise to tray**, the “Show or hide window” global hotkey, or by starting minimised on launch), the main window hides and an icon appears in your Windows system tray (the small icons cluster next to the clock).
|
||||
|
||||
**Hovering over the tray icon** shows a short summary of what RemSound is doing right now — the number of healthy peers, whether you're sending or receiving and in which mode (WASAPI, ASIO, or both), and how long any current recording has been running. The summary refreshes every second. Examples:
|
||||
|
||||
* _RemSound — not connected_
|
||||
* _RemSound — 2 peers, sending (WASAPI), receiving (WASAPI)_
|
||||
* _RemSound — recording for 5:23, 1 peer, sending (WASAPI + ASIO), receiving (WASAPI + ASIO)_
|
||||
|
||||
|
||||
|
||||
**Right-clicking the tray icon** opens a small menu with everything you might want to reach without re-opening the main window:
|
||||
|
||||
Item| Shortcut| What it does
|
||||
---|---|---
|
||||
**Show RemSound**| W| Brings the main window back to the front and gives it focus. Double-clicking the tray icon does the same thing.
|
||||
**Enable sending** (tickable)| S| Toggles “Send my audio” on or off, the same way as the checkbox on the Audio inputs and outputs tab. The tick reflects the current state — ticked means sending, unticked means not.
|
||||
**Enable receiving** (tickable)| R| Toggles “Receive audio” on or off. Same tick-reflects-state rule.
|
||||
**Profiles →**| P| A submenu listing your recent profiles, most recent first. Each row has a single-digit shortcut: while the submenu is open, press **1** for the most recent, **2** for the next, and so on up to **5**. Selecting one switches the active profile, exactly the same way as the File menu's Recent profiles submenu. Greyed out as “(No recent profiles)” when you haven't loaded any yet.
|
||||
**Exit**| X| Closes RemSound entirely.
|
||||
|
||||
**Keyboard access:** the tray icon is reachable through standard Windows shortcuts — **Windows + B** moves focus to the notification area, arrow keys navigate, Enter activates, and the application context-menu key (or Shift+F10) opens the right-click menu without a mouse.
|
||||
|
||||
## 5. Menus (File, Record, Options, Help)
|
||||
|
||||
There are four menus on the main window: **File (Alt+F)** , **Record (Alt+K)** , **Options (Alt+O)** and **Help (Alt+H)**. The Record menu opens with Alt+K rather than Alt+R because Alt+R is already used by the **Receive audio** checkbox on the main window. The menu's title is shown as “Record (Alt+K)” so you can find the shortcut even though there is no K in the word.
|
||||
@@ -194,12 +219,12 @@ The File menu holds everything to do with profiles — opening, saving, renaming
|
||||
Item| Shortcut| What it does
|
||||
---|---|---
|
||||
**Open profile …**| Ctrl+O, or Alt+F, O| Opens a Windows file picker showing your profiles folder. Pick a profile, and RemSound reloads using it (the window closes and reopens with all that profile's device choices, peers and settings restored). To delete a profile, right-click its entry inside the file picker and choose Delete — that lets Windows handle the deletion.
|
||||
**Recent profiles →**| Alt+F, R| A submenu listing the last five profiles you've opened, most recent first. Each row has a single-digit shortcut: while the submenu is open, press **1** for the most recent, **2** for the next, and so on up to **5**. Or just select the one you want. It reloads the profile the same way Open profile does. If a recent profile's file has been deleted or moved away, it's left out of the submenu (it stays in the list in case the file comes back later — for example when you reconnect an external drive). If the list is empty, you see a greyed-out “(No recent profiles)” entry.
|
||||
**Recent profiles →**| Alt+F, R| A submenu listing the last five profiles you've opened, most recent first. Each row has a single-digit shortcut: while the submenu is open, press **1** for the most recent, **2** for the next, and so on up to **5**. Or just select the one you want. It reloads the profile the same way Open profile does. If a recent profile's file has been deleted or moved away, it's left out of the submenu (it stays in the list in case the file comes back later — for example when you reconnect an external drive). If the list is empty, you see a greyed-out “(No recent profiles)” entry. The same list appears in the system-tray icon's **Profiles** submenu, with the same number shortcuts, so you can switch profiles without re-opening the main window.
|
||||
**Save**| Ctrl+S| Updates the current profile with your current settings. If there's no current profile (you're on the blank template), this becomes Save as automatically.
|
||||
**Save as …**| Alt+F, A| Asks for a name and saves a copy. Use it to save your current setup under a new name, or to save for the first time from the blank template.
|
||||
**Rename current profile …**| Alt+F, M| Renames the current profile's file and updates the window title. Does nothing on the blank template (there's no profile to rename).
|
||||
**Lock profile (read-only)** (tickable)| Alt+F, L| When ticked, the current profile is loaded for use but RemSound will not save any of your changes back to it. The window title shows “(read-only)” so you can tell at a glance. Save (Ctrl+S) politely refuses with a hint to use Save as instead, and closing RemSound never asks “save changes?” — it just closes. Anything you've changed during the session is forgotten when RemSound closes; the file on disk is left exactly as it was. The lock setting is saved on the profile itself, so it sticks across launches. See Locking a profile for the full story.
|
||||
**Minimise to tray**| Alt+F, N| Hides the window down to the system tray (the small icons near the clock). To bring the window back, click the tray icon or use the “Show or hide window” global hotkey (set in the Keyboard shortcuts dialog, default Ctrl+Shift+F10).
|
||||
**Minimise to tray**| Alt+F, N| Hides the window down to the system tray (the small icons near the clock). The tray icon's hover summary tells you what RemSound is doing, and right-clicking it gives you Show RemSound, Enable sending, Enable receiving, your Profiles submenu, and Exit — see The system tray icon and its menu for the full rundown. To bring the window back, double-click the tray icon, pick “Show RemSound” from its menu, or use the “Show or hide window” global hotkey (set in the Keyboard shortcuts dialog, default Ctrl+Shift+F10).
|
||||
**Exit**| Alt+F, X (or Alt+F4)| Closes RemSound. If you have unsaved profile changes (and the profile isn't locked), it asks you first.
|
||||
|
||||
### Record menu
|
||||
@@ -221,7 +246,7 @@ Item| Shortcut| What it does
|
||||
**Recording settings …**| Alt+O, S| Opens the Recording settings dialog. Up to five lists: _Recording source_ (Alt+S), _File format_ (Alt+F), _Audio format attributes_ (Alt+A), _FLAC compression level_ (Alt+L — only shown when FLAC is chosen), and _Channels_ (Alt+C). The attributes list changes to match the format you pick. OK saves to the current profile; Cancel discards.
|
||||
**Keyboard shortcuts …**| Ctrl+K, or Alt+O, K| Opens the global hotkey dialog (mute, volume, show/hide window, start/stop recording, remote-control commands).
|
||||
**Startup behaviour …**| Alt+O, T| Opens the Startup behaviour dialog. Choose whether to launch automatically with Windows, which profile to load by default, and whether to start hidden in the tray.
|
||||
**Preferences …**| Ctrl+P, or Alt+O, P| Opens the Preferences dialog. This holds the settings that stay the same on this computer no matter which profile is loaded: the profiles folder, the **Audio cue sounds (Alt+N)** checklist (one tickable item per cue: connect, disconnect, recording start, recording stop), whether to accept remote volume commands, whether to check for updates on startup, how often to check for updates after that, a manual check-for-updates button, whether to install updates quietly, whether to ask your router to open the audio port for you (UPnP), whether to keep logs, and a button to write logs now. Esc or the Close button dismisses it.
|
||||
**Preferences …**| Ctrl+P, or Alt+O, P| Opens the Preferences dialog. Settings here are a mix of machine-level (the profiles folder, updates, logging, UPnP) and per-profile (the audio cue list and its tick states). It holds: the profiles folder; the **Audio cue sounds (Alt+N)** list with Play and Browse buttons (see Audio cue sounds); whether to accept remote volume commands; whether to check for updates on startup; how often to check after that; a manual check-for-updates button; whether to install updates quietly; whether to ask your router to open the audio port for you (UPnP); whether to keep logs; and a button to write logs now. Esc or the Close button dismisses it.
|
||||
|
||||
### Help menu
|
||||
|
||||
@@ -750,7 +775,64 @@ Toggle| What it does
|
||||
|
||||
> **Where these are stored:** the start-minimised choice and the start-with-profile name are kept in a small settings file on this computer. The auto-start toggle is kept in Windows' standard startup list — you turn it on or off from this dialog, or from Task Manager → Startup.
|
||||
|
||||
## 17. Updating RemSound
|
||||
## 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:
|
||||
|
||||
Cue| Plays when
|
||||
---|---
|
||||
**Connect sound**| A peer goes from “trying” or “unreachable” to actually connected.
|
||||
**Disconnect sound**| A previously-connected peer drops off (network blip, peer closed RemSound, computer went to sleep, etc).
|
||||
**Recording start sound**| You start 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 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.
|
||||
|
||||
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.)
|
||||
|
||||
### 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.
|
||||
|
||||
Use the up and down arrow keys to move between cues; press **Space** to toggle the highlighted cue's tick on or off.
|
||||
|
||||
The tick settings are **saved with the active profile** , so different profiles can have different combinations of cues on. For example, a “quiet listening” profile might have all cues off, while a “live monitoring” profile keeps them on. When you save the profile (Ctrl+S), the new settings travel with it.
|
||||
|
||||
### Previewing and choosing a different sound
|
||||
|
||||
Below the list are two buttons that act on whichever cue is currently highlighted in the list:
|
||||
|
||||
* **Play [cue name] (Alt+P)** — previews the cue's currently-configured sound through your default Windows output, so you can hear it without having to trigger the event. Works regardless of whether the cue is ticked (so you can listen before deciding to enable it).
|
||||
* **Browse for [cue name] … (Alt+B)** — opens a Windows file picker so you can choose your own WAV file for this cue, replacing the default. RemSound only accepts `.wav` files. Once picked, the button's label changes to “(custom)” to remind you the cue is using your file rather than the default. The next time the event fires, your custom sound plays.
|
||||
|
||||
|
||||
|
||||
Both buttons' labels update as you arrow through the list, so you always know which cue you're about to act on.
|
||||
|
||||
Custom sound choices are **saved with the active profile** , the same way the tick states are. Different profiles can have completely different cue palettes — a “studio” profile might use one set of sounds, a “broadcast” profile another. The custom files themselves stay where you picked them on your disk; the profile just remembers their paths.
|
||||
|
||||
### Going back to the default sound
|
||||
|
||||
To revert a cue to its default sound, **right-click** the _Browse for [cue name] …_ button and pick **Use default sound**. The custom path is forgotten and the cue goes back to playing the default WAV that ships with RemSound. The right-click option is greyed out when the cue is already using its default. (Alternatively, click _Browse_ and pick a file from inside RemSound's own `sounds` folder — RemSound recognises that as “use default” and clears the override automatically.)
|
||||
|
||||
### Where the default sounds live
|
||||
|
||||
The default WAV files are in the `sounds` folder next to `RemSound.exe`. If you don't pick a custom file for a cue, RemSound plays the matching default from there:
|
||||
|
||||
* `sounds\connect.wav` — connect cue
|
||||
* `sounds\disconnect.wav` — disconnect cue
|
||||
* `sounds\record start.wav` — recording start cue
|
||||
* `sounds\record stop.wav` — recording stop cue
|
||||
* `sounds\save.wav` — profile saved cue
|
||||
* `sounds\profile.wav` — profile switched cue
|
||||
|
||||
|
||||
|
||||
If a cue's WAV file is missing — either the default file doesn't exist or a custom path points at a file you've since deleted — the cue stays silent rather than producing an error. RemSound logs a note in the diagnostic log (if logging is on) so you can see what happened.
|
||||
|
||||
> **Tip for sound designers:** the defaults are deliberately short and simple so they stay out of the way. If you'd like the cues to feel more in-character with a particular profile, the custom-sound feature is designed for exactly that. Keep WAV files short (well under a second usually works best) so cues don't overlap with each other on a busy day.
|
||||
|
||||
## 18. Updating RemSound
|
||||
|
||||
RemSound can check for a newer version on a schedule you choose, prompt you to install it, and either ask first or do it quietly. There's also a one-press “check now” button so you don't have to wait for the timer.
|
||||
|
||||
@@ -811,7 +893,7 @@ A step-by-step record of every helper run is kept in a helper log file in the in
|
||||
|
||||
To see which version you're on without checking for updates, open **Help → About RemSound** (Alt+H, A). The dialog shows the version number and the release notes for the version you're running, in a scrollable read-only box. Close (or Esc) dismisses it.
|
||||
|
||||
## 18. Recording to a file
|
||||
## 19. Recording to a file
|
||||
|
||||
RemSound can save the sound passing through it to a file on your computer — useful for keeping a copy of a music session, capturing a long jam for editing later, or just saving a one-off voice exchange you want to come back to.
|
||||
|
||||
@@ -840,9 +922,7 @@ Format| What you get| When to pick it
|
||||
|
||||
### 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. The cues are `record start.wav` and `record stop.wav` in the RemSound install folder — you can drop in your own sounds to customise them, as long as you keep the same filenames.
|
||||
|
||||
Each cue can be turned off on its own via **Preferences → Audio cue sounds (Alt+N)**. The same checklist also controls the connect and disconnect cues. The cues play through your default Windows output device, separately from the recording, so a normal RemSound recording doesn't contain the cue. The one exception: if your sending side is capturing the very same output device the cue plays through, then the cue gets captured along with everything else from that device. In that case you'll have to live with the start cue appearing in the file, unless you turn it off.
|
||||
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`.
|
||||
|
||||
### Where recordings go
|
||||
|
||||
@@ -879,7 +959,7 @@ List| Shortcut| What goes in it
|
||||
|
||||
OK (Alt+O) saves your choices to the current profile. Cancel (Alt+N) or Esc discards them. Settings are saved with the profile as usual — changes here mark the profile as having unsaved changes, and you'll be asked about them on exit if you haven't saved.
|
||||
|
||||
## 19. Logs and diagnostics
|
||||
## 20. Logs and diagnostics
|
||||
|
||||
If logging is turned on (the **Enable logs** checkbox in the Preferences dialog — Options → Preferences, or Ctrl+P — on by default), RemSound writes a log file each session into a `logs` folder next to the RemSound program. One file per launch.
|
||||
|
||||
@@ -903,7 +983,7 @@ Logs are plain text and can be opened in any text editor, or in a spreadsheet. T
|
||||
|
||||
|
||||
|
||||
## 20. Troubleshooting
|
||||
## 21. Troubleshooting
|
||||
|
||||
### I don't hear my friend
|
||||
|
||||
@@ -986,7 +1066,7 @@ The most common reasons:
|
||||
|
||||
If none of those apply, just fall back to Tailscale — it works without involving the router at all.
|
||||
|
||||
## 21. Glossary
|
||||
## 22. Glossary
|
||||
|
||||
Term| Meaning
|
||||
---|---
|
||||
|
||||
@@ -30,7 +30,7 @@ Inside RemSound there's just one main decision: which quality and delay you want
|
||||
## How to install it
|
||||
|
||||
1. Go to the [latest release](https://github.com/Ednunp/RemSound/releases/latest).
|
||||
2. Download the file called `RemSound-v3.0.2.zip` (the version number changes over time — pick whichever is newest).
|
||||
2. Download the file called `RemSound-v3.1.zip` (the version number changes over time — pick whichever is newest).
|
||||
3. Extract the zip into a folder of your choice.
|
||||
4. Double-click `RemSound.exe` and away you go.
|
||||
|
||||
|
||||
+48
-17
@@ -1,40 +1,71 @@
|
||||
# RemSound v3.0.2
|
||||
# RemSound v3.1
|
||||
|
||||
Hot-fix for a slow memory leak on the receiving side. If you leave RemSound running for many hours receiving audio, its memory use was creeping up over time. Small at first, big enough after a day to slow the computer down and make audio feel slightly laggy.
|
||||
A meaningful round of work on **audio cue sounds** and the **system tray**. Two new cues, a way to swap in your own WAV files per profile, and a rebuilt tray menu with a live status tooltip. Plus a couple of smaller bug fixes. No wire format change — v3.1 talks to v3.0.x machines exactly as before.
|
||||
|
||||
## What was happening
|
||||
## New audio cue sounds
|
||||
|
||||
RemSound uses a small library called Concentus to decode incoming Opus audio. In version 2.2 we switched from a pure software version of that library to a faster native one. The native version keeps some memory in Windows itself rather than in RemSound, and you're supposed to tell it explicitly when you're done with it. Our code wasn't doing that — it was relying on the system to notice and tidy up eventually, but a setting we use to keep audio smooth also stops Windows from doing that tidy-up.
|
||||
Two new sounds join the existing connect / disconnect / record-start / record-stop cues:
|
||||
|
||||
So memory built up steadily over hours and never came back. A user left RemSound running for nearly a full day receiving audio and his desktop ended up using 3.5 gigabytes of memory before he restarted it. That was getting in the way of everything else on the computer and was almost certainly the cause of a feeling that audio latency was "drifting" over a long session — at that point the CPU was working hard enough that audio scheduling wasn't as tight as it should be.
|
||||
* **Profile saved.** Plays a short cue whenever a profile is saved — either via File → Save (Ctrl+S) or Save as. An audible "yes, that took" so you don't have to look at the screen.
|
||||
* **Profile switched.** Plays a short cue whenever a profile finishes loading. Fires at startup if you started with a profile, and after every mid-session profile switch. **It plays the new profile's cue, not the old one's** — so if you give each profile a different switched-to sound, you can hear which profile you're now on without checking the title bar.
|
||||
|
||||
## What's fixed
|
||||
## Custom cue sounds, per profile
|
||||
|
||||
Two things. First, the part of RemSound that uses Concentus now properly releases its memory at the right moments — when a session ends, when you change codec, and when a recording stops. That stops the leak at its source.
|
||||
The **Audio cue sounds** list in Preferences now has six entries (Connect, Disconnect, Recording start, Recording stop, Profile saved, Profile switched), each with a tickbox to enable or silence it.
|
||||
|
||||
Second, as a safety net in case anything else in RemSound or any other library we use ever has the same kind of bug, RemSound now does a quick "release any leftover memory" pass once every five minutes. The pass runs in the background, doesn't affect audio in any way, and finishes long before the next audio packet arrives.
|
||||
Two new buttons sit just below the list, both acting on whichever cue is currently highlighted:
|
||||
|
||||
## What to expect
|
||||
* **Play [cue name]** (Alt+P) previews the currently-configured sound through your default Windows output. Works regardless of whether the cue is ticked, so you can listen before deciding to enable.
|
||||
* **Browse for [cue name]…** (Alt+B) opens a Windows file picker so you can choose your own WAV file to replace the default sound for that cue. Right-clicking the Browse button offers a **Use default sound** item to undo the swap.
|
||||
|
||||
After updating, your memory use should settle at around 100-200 megabytes and stay there as long as you leave RemSound running. You can leave it running overnight, or for days, and the memory should hold roughly flat instead of climbing.
|
||||
Both the tick states AND the custom sound choices are **saved with the active profile**. A "quiet listening" profile can have all cues off; a "studio" profile can use a distinct set of sounds. When you switch profiles, the cues switch too.
|
||||
|
||||
If you noticed audio feeling slightly laggier after long sessions and had been working around it by restarting RemSound, that workaround should no longer be needed.
|
||||
The default WAV files have moved out of the install folder root into a new `sounds\` subfolder, keeping the install layout tidier.
|
||||
|
||||
## Nothing else has changed
|
||||
## System tray icon redesigned
|
||||
|
||||
Same wire format as v3.0 and v3.0.1, same codec list, same everything else. v3.0.2 talks to other v3.0.x machines exactly as before. If you've not noticed any slowdown after long sessions, the fix is still worth having because the leak was happening under the surface even if you didn't see it.
|
||||
**Hover summary.** The tray icon's tooltip now shows a live summary that refreshes every second:
|
||||
|
||||
* `RemSound — not connected`
|
||||
* `RemSound — 2 peers, sending (WASAPI), receiving (WASAPI)`
|
||||
* `RemSound — recording for 5:23, 1 peer, sending (WASAPI + ASIO), receiving (WASAPI + ASIO)`
|
||||
|
||||
The recording timer only shows when you're actually recording. The "(WASAPI)", "(ASIO)", or "(WASAPI + ASIO)" tag reflects which lanes the corresponding direction is actually using.
|
||||
|
||||
**Right-click menu rebuilt.** Five items, each with a single-letter shortcut:
|
||||
|
||||
| Item | Key |
|
||||
|---|---|
|
||||
| Show RemSound | W |
|
||||
| Enable sending (tickable, shows current state) | S |
|
||||
| Enable receiving (tickable, shows current state) | R |
|
||||
| Profiles (submenu of recent profiles) | P |
|
||||
| Exit | X |
|
||||
|
||||
* **Show RemSound** now reliably brings the window to the front AND gives it focus, fixing a case where screen-reader users had to Alt+Tab to actually reach the restored window.
|
||||
* **Enable sending / Enable receiving** now **toggle** the state instead of always switching it on — so you can use them to turn off as well.
|
||||
* The **Profiles submenu** shows your five most recent profiles (same list the File menu uses) with number-key shortcuts. While the submenu is open, press 1 for the most recent, 2 for the next, and so on. Switching from the tray works the same way as from the File menu — RemSound reloads under the new profile, your devices and peers come back as the new profile has them.
|
||||
|
||||
## Smaller bug fixes
|
||||
|
||||
* **Tray icon's first-launch tooltip read as "RemSound RemSound"** for some screen-reader users because the tooltip text matched the process name. Initial tooltip is now "RemSound — starting up", which avoids the duplicate read. Within a second the live state takes over and reads cleanly from then on.
|
||||
* **Recent profiles in both the File menu and the new tray menu** no longer announce a "Recent profile N:" prefix on each item — they just read the profile name. The 1..5 number-key shortcuts still work either way.
|
||||
|
||||
## Compatibility
|
||||
|
||||
No wire format change. v3.1 talks to other v3.0.x machines (v3.0, v3.0.1, v3.0.2) exactly the same as before. Profiles created or saved on v3.1 carry the new per-cue settings and a v3.0.x build loading one will silently ignore the new fields (so the profile still works, just without the new cue-customisation state).
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `RemSound-v3.0.2.zip` from this release.
|
||||
1. Download `RemSound-v3.1.zip` from this release.
|
||||
2. Close RemSound.
|
||||
3. Extract the zip over your existing RemSound folder, overwriting program files when prompted. The zip is program files only — it will not 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 will not touch your profiles, settings or recordings.
|
||||
4. Run `RemSound.exe`. Press F1 for the user manual.
|
||||
|
||||
Requires the .NET 10 Desktop Runtime. If it's missing, Windows offers to fetch it on first launch.
|
||||
|
||||
## Upgrading
|
||||
|
||||
**v1.9, v2.0, v2.1, v3.0, v3.0.1:** Help → Check for updates works — it will fetch and install v3.0.2 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.0.2 installs itself shortly after launch and RemSound reopens on whichever profile you were running (a feature added in v3.0).
|
||||
**v1.9 through v3.0.2:** Help → Check for updates works — it will fetch and install v3.1 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.1 installs itself shortly after launch and RemSound reopens on whichever profile you were running.
|
||||
|
||||
**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.0.2 but not apply it. Install v3.0.2 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.1 but not apply it. Install v3.1 by hand using the steps above — just this once. From the build you install onward, updates are automatic.
|
||||
|
||||
+95
-11
@@ -43,6 +43,7 @@ ul, ol { padding-left: 1.4em; }
|
||||
<li><a href="#hotkeys">Global hotkeys (mute, volume, tray, recording)</a></li>
|
||||
<li><a href="#remote-control">Remote control: adjusting a peer's listening volume from your end</a></li>
|
||||
<li><a href="#startup">Startup behaviour</a></li>
|
||||
<li><a href="#audio-cues">Audio cue sounds</a></li>
|
||||
<li><a href="#updates">Updating RemSound</a></li>
|
||||
<li><a href="#recording">Recording to a file</a></li>
|
||||
<li><a href="#logs">Logs and diagnostics</a></li>
|
||||
@@ -212,6 +213,31 @@ ul, ol { padding-left: 1.4em; }
|
||||
<tr><td><strong>Audio profile</strong></td><td>Codec, packet size, lock-to-audio-clock, latency, continuous auto-tune, buffer smoothness, artefact sound. Split into an <em>Audio send parameters</em> group and an <em>Audio receive parameters</em> group.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>The system tray icon and its menu</h3>
|
||||
|
||||
<p>When RemSound is <strong>minimised to the tray</strong> (via <strong>File → Minimise to tray</strong>, the “Show or hide window” global hotkey, or by starting minimised on launch), the main window hides and an icon appears in your Windows system tray (the small icons cluster next to the clock).</p>
|
||||
|
||||
<p><strong>Hovering over the tray icon</strong> shows a short summary of what RemSound is doing right now — the number of healthy peers, whether you're sending or receiving and in which mode (WASAPI, ASIO, or both), and how long any current recording has been running. The summary refreshes every second. Examples:</p>
|
||||
|
||||
<ul>
|
||||
<li><em>RemSound — not connected</em></li>
|
||||
<li><em>RemSound — 2 peers, sending (WASAPI), receiving (WASAPI)</em></li>
|
||||
<li><em>RemSound — recording for 5:23, 1 peer, sending (WASAPI + ASIO), receiving (WASAPI + ASIO)</em></li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Right-clicking the tray icon</strong> opens a small menu with everything you might want to reach without re-opening the main window:</p>
|
||||
|
||||
<table>
|
||||
<tr><th>Item</th><th>Shortcut</th><th>What it does</th></tr>
|
||||
<tr><td><strong>Show RemSound</strong></td><td>W</td><td>Brings the main window back to the front and gives it focus. Double-clicking the tray icon does the same thing.</td></tr>
|
||||
<tr><td><strong>Enable sending</strong> (tickable)</td><td>S</td><td>Toggles “Send my audio” on or off, the same way as the checkbox on the Audio inputs and outputs tab. The tick reflects the current state — ticked means sending, unticked means not.</td></tr>
|
||||
<tr><td><strong>Enable receiving</strong> (tickable)</td><td>R</td><td>Toggles “Receive audio” on or off. Same tick-reflects-state rule.</td></tr>
|
||||
<tr><td><strong>Profiles →</strong></td><td>P</td><td>A submenu listing your recent profiles, most recent first. Each row has a single-digit shortcut: while the submenu is open, press <strong>1</strong> for the most recent, <strong>2</strong> for the next, and so on up to <strong>5</strong>. Selecting one switches the active profile, exactly the same way as the File menu's Recent profiles submenu. Greyed out as “(No recent profiles)” when you haven't loaded any yet.</td></tr>
|
||||
<tr><td><strong>Exit</strong></td><td>X</td><td>Closes RemSound entirely.</td></tr>
|
||||
</table>
|
||||
|
||||
<p><strong>Keyboard access:</strong> the tray icon is reachable through standard Windows shortcuts — <strong>Windows + B</strong> moves focus to the notification area, arrow keys navigate, Enter activates, and the application context-menu key (or Shift+F10) opens the right-click menu without a mouse.</p>
|
||||
|
||||
<h2 id="menu-bar">5. Menus (File, Record, Options, Help)</h2>
|
||||
|
||||
<p>There are four menus on the main window: <strong>File (Alt+F)</strong>, <strong>Record (Alt+K)</strong>, <strong>Options (Alt+O)</strong> and <strong>Help (Alt+H)</strong>. The Record menu opens with Alt+K rather than Alt+R because Alt+R is already used by the <strong>Receive audio</strong> checkbox on the main window. The menu's title is shown as “Record (Alt+K)” so you can find the shortcut even though there is no K in the word.</p>
|
||||
@@ -223,12 +249,12 @@ ul, ol { padding-left: 1.4em; }
|
||||
<table>
|
||||
<tr><th>Item</th><th>Shortcut</th><th>What it does</th></tr>
|
||||
<tr><td><strong>Open profile…</strong></td><td>Ctrl+O, or Alt+F, O</td><td>Opens a Windows file picker showing your profiles folder. Pick a profile, and RemSound reloads using it (the window closes and reopens with all that profile's device choices, peers and settings restored). To delete a profile, right-click its entry inside the file picker and choose Delete — that lets Windows handle the deletion.</td></tr>
|
||||
<tr><td><strong>Recent profiles →</strong></td><td>Alt+F, R</td><td>A submenu listing the last five profiles you've opened, most recent first. Each row has a single-digit shortcut: while the submenu is open, press <strong>1</strong> for the most recent, <strong>2</strong> for the next, and so on up to <strong>5</strong>. Or just select the one you want. It reloads the profile the same way Open profile does. If a recent profile's file has been deleted or moved away, it's left out of the submenu (it stays in the list in case the file comes back later — for example when you reconnect an external drive). If the list is empty, you see a greyed-out “(No recent profiles)” entry.</td></tr>
|
||||
<tr><td><strong>Recent profiles →</strong></td><td>Alt+F, R</td><td>A submenu listing the last five profiles you've opened, most recent first. Each row has a single-digit shortcut: while the submenu is open, press <strong>1</strong> for the most recent, <strong>2</strong> for the next, and so on up to <strong>5</strong>. Or just select the one you want. It reloads the profile the same way Open profile does. If a recent profile's file has been deleted or moved away, it's left out of the submenu (it stays in the list in case the file comes back later — for example when you reconnect an external drive). If the list is empty, you see a greyed-out “(No recent profiles)” entry. The same list appears in the system-tray icon's <strong>Profiles</strong> submenu, with the same number shortcuts, so you can switch profiles without re-opening the main window.</td></tr>
|
||||
<tr><td><strong>Save</strong></td><td>Ctrl+S</td><td>Updates the current profile with your current settings. If there's no current profile (you're on the blank template), this becomes Save as automatically.</td></tr>
|
||||
<tr><td><strong>Save as…</strong></td><td>Alt+F, A</td><td>Asks for a name and saves a copy. Use it to save your current setup under a new name, or to save for the first time from the blank template.</td></tr>
|
||||
<tr><td><strong>Rename current profile…</strong></td><td>Alt+F, M</td><td>Renames the current profile's file and updates the window title. Does nothing on the blank template (there's no profile to rename).</td></tr>
|
||||
<tr><td><strong>Lock profile (read-only)</strong> (tickable)</td><td>Alt+F, L</td><td>When ticked, the current profile is loaded for use but RemSound will not save any of your changes back to it. The window title shows “(read-only)” so you can tell at a glance. Save (Ctrl+S) politely refuses with a hint to use Save as instead, and closing RemSound never asks “save changes?” — it just closes. Anything you've changed during the session is forgotten when RemSound closes; the file on disk is left exactly as it was. The lock setting is saved on the profile itself, so it sticks across launches. See <a href="#profile-lock">Locking a profile</a> for the full story.</td></tr>
|
||||
<tr><td><strong>Minimise to tray</strong></td><td>Alt+F, N</td><td>Hides the window down to the system tray (the small icons near the clock). To bring the window back, click the tray icon or use the “Show or hide window” global hotkey (set in the Keyboard shortcuts dialog, default Ctrl+Shift+F10).</td></tr>
|
||||
<tr><td><strong>Minimise to tray</strong></td><td>Alt+F, N</td><td>Hides the window down to the system tray (the small icons near the clock). The tray icon's hover summary tells you what RemSound is doing, and right-clicking it gives you Show RemSound, Enable sending, Enable receiving, your Profiles submenu, and Exit — see <a href="#main-window">The system tray icon and its menu</a> for the full rundown. To bring the window back, double-click the tray icon, pick “Show RemSound” from its menu, or use the “Show or hide window” global hotkey (set in the Keyboard shortcuts dialog, default Ctrl+Shift+F10).</td></tr>
|
||||
<tr><td><strong>Exit</strong></td><td>Alt+F, X (or Alt+F4)</td><td>Closes RemSound. If you have unsaved profile changes (and the profile isn't locked), it asks you first.</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -252,7 +278,7 @@ ul, ol { padding-left: 1.4em; }
|
||||
<tr><td><strong>Recording settings…</strong></td><td>Alt+O, S</td><td>Opens the Recording settings dialog. Up to five lists: <em>Recording source</em> (Alt+S), <em>File format</em> (Alt+F), <em>Audio format attributes</em> (Alt+A), <em>FLAC compression level</em> (Alt+L — only shown when FLAC is chosen), and <em>Channels</em> (Alt+C). The attributes list changes to match the format you pick. OK saves to the current profile; Cancel discards.</td></tr>
|
||||
<tr><td><strong>Keyboard shortcuts…</strong></td><td>Ctrl+K, or Alt+O, K</td><td>Opens the global hotkey dialog (mute, volume, show/hide window, start/stop recording, remote-control commands).</td></tr>
|
||||
<tr><td><strong>Startup behaviour…</strong></td><td>Alt+O, T</td><td>Opens the Startup behaviour dialog. Choose whether to launch automatically with Windows, which profile to load by default, and whether to start hidden in the tray.</td></tr>
|
||||
<tr><td><strong>Preferences…</strong></td><td>Ctrl+P, or Alt+O, P</td><td>Opens the Preferences dialog. This holds the settings that stay the same on this computer no matter which profile is loaded: the profiles folder, the <strong>Audio cue sounds (Alt+N)</strong> checklist (one tickable item per cue: connect, disconnect, recording start, recording stop), whether to accept remote volume commands, whether to check for updates on startup, how often to check for updates after that, a manual check-for-updates button, whether to install updates quietly, whether to ask your router to open the audio port for you (UPnP), whether to keep logs, and a button to write logs now. Esc or the Close button dismisses it.</td></tr>
|
||||
<tr><td><strong>Preferences…</strong></td><td>Ctrl+P, or Alt+O, P</td><td>Opens the Preferences dialog. Settings here are a mix of machine-level (the profiles folder, updates, logging, UPnP) and per-profile (the audio cue list and its tick states). It holds: the profiles folder; the <strong>Audio cue sounds (Alt+N)</strong> list with Play and Browse buttons (see <a href="#audio-cues">Audio cue sounds</a>); whether to accept remote volume commands; whether to check for updates on startup; how often to check after that; a manual check-for-updates button; whether to install updates quietly; whether to ask your router to open the audio port for you (UPnP); whether to keep logs; and a button to write logs now. Esc or the Close button dismisses it.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Help menu</h3>
|
||||
@@ -799,7 +825,67 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
<strong>Where these are stored:</strong> the start-minimised choice and the start-with-profile name are kept in a small settings file on this computer. The auto-start toggle is kept in Windows' standard startup list — you turn it on or off from this dialog, or from Task Manager → Startup.
|
||||
</div>
|
||||
|
||||
<h2 id="updates">17. Updating RemSound</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>
|
||||
|
||||
<table>
|
||||
<tr><th>Cue</th><th>Plays when</th></tr>
|
||||
<tr><td><strong>Connect sound</strong></td><td>A peer goes from “trying” or “unreachable” to actually connected.</td></tr>
|
||||
<tr><td><strong>Disconnect sound</strong></td><td>A previously-connected peer drops off (network blip, peer closed RemSound, computer went to sleep, etc).</td></tr>
|
||||
<tr><td><strong>Recording start sound</strong></td><td>You start 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 — whether via File → Save or File → 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 — using the new profile's cue, not the old one's.</td></tr>
|
||||
</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>
|
||||
|
||||
<h3>Turning each cue on or off</h3>
|
||||
|
||||
<p>Open <strong>File → 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>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>The tick settings are <strong>saved with the active profile</strong>, so different profiles can have different combinations of cues on. For example, a “quiet listening” profile might have all cues off, while a “live monitoring” profile keeps them on. When you save the profile (Ctrl+S), the new settings travel with it.</p>
|
||||
|
||||
<h3>Previewing and choosing a different sound</h3>
|
||||
|
||||
<p>Below the list are two buttons that act on whichever cue is currently highlighted in the list:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Play [cue name] (Alt+P)</strong> — previews the cue's currently-configured sound through your default Windows output, so you can hear it without having to trigger the event. Works regardless of whether the cue is ticked (so you can listen before deciding to enable it).</li>
|
||||
<li><strong>Browse for [cue name]… (Alt+B)</strong> — opens a Windows file picker so you can choose your own WAV file for this cue, replacing the default. RemSound only accepts <code>.wav</code> files. Once picked, the button's label changes to “(custom)” to remind you the cue is using your file rather than the default. The next time the event fires, your custom sound plays.</li>
|
||||
</ul>
|
||||
|
||||
<p>Both buttons' labels update as you arrow through the list, so you always know which cue you're about to act on.</p>
|
||||
|
||||
<p>Custom sound choices are <strong>saved with the active profile</strong>, the same way the tick states are. Different profiles can have completely different cue palettes — a “studio” profile might use one set of sounds, a “broadcast” profile another. The custom files themselves stay where you picked them on your disk; the profile just remembers their paths.</p>
|
||||
|
||||
<h3>Going back to the default sound</h3>
|
||||
|
||||
<p>To revert a cue to its default sound, <strong>right-click</strong> the <em>Browse for [cue name]…</em> button and pick <strong>Use default sound</strong>. The custom path is forgotten and the cue goes back to playing the default WAV that ships with RemSound. The right-click option is greyed out when the cue is already using its default. (Alternatively, click <em>Browse</em> and pick a file from inside RemSound's own <code>sounds</code> folder — RemSound recognises that as “use default” and clears the override automatically.)</p>
|
||||
|
||||
<h3>Where the default sounds live</h3>
|
||||
|
||||
<p>The default WAV files are in the <code>sounds</code> folder next to <code>RemSound.exe</code>. If you don't pick a custom file for a cue, RemSound plays the matching default from there:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>sounds\connect.wav</code> — connect cue</li>
|
||||
<li><code>sounds\disconnect.wav</code> — disconnect cue</li>
|
||||
<li><code>sounds\record start.wav</code> — recording start cue</li>
|
||||
<li><code>sounds\record stop.wav</code> — recording stop cue</li>
|
||||
<li><code>sounds\save.wav</code> — profile saved cue</li>
|
||||
<li><code>sounds\profile.wav</code> — profile switched cue</li>
|
||||
</ul>
|
||||
|
||||
<p>If a cue's WAV file is missing — either the default file doesn't exist or a custom path points at a file you've since deleted — the cue stays silent rather than producing an error. RemSound logs a note in the diagnostic log (if logging is on) so you can see what happened.</p>
|
||||
|
||||
<div class="note">
|
||||
<strong>Tip for sound designers:</strong> the defaults are deliberately short and simple so they stay out of the way. If you'd like the cues to feel more in-character with a particular profile, the custom-sound feature is designed for exactly that. Keep WAV files short (well under a second usually works best) so cues don't overlap with each other on a busy day.
|
||||
</div>
|
||||
|
||||
<h2 id="updates">18. Updating RemSound</h2>
|
||||
|
||||
<p>RemSound can check for a newer version on a schedule you choose, prompt you to install it, and either ask first or do it quietly. There's also a one-press “check now” button so you don't have to wait for the timer.</p>
|
||||
|
||||
@@ -861,7 +947,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
|
||||
<p>To see which version you're on without checking for updates, open <strong>Help → About RemSound</strong> (Alt+H, A). The dialog shows the version number and the release notes for the version you're running, in a scrollable read-only box. Close (or Esc) dismisses it.</p>
|
||||
|
||||
<h2 id="recording">18. Recording to a file</h2>
|
||||
<h2 id="recording">19. Recording to a file</h2>
|
||||
|
||||
<p>RemSound can save the sound passing through it to a file on your computer — useful for keeping a copy of a music session, capturing a long jam for editing later, or just saving a one-off voice exchange you want to come back to.</p>
|
||||
|
||||
@@ -891,9 +977,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
|
||||
<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. The cues are <code>record start.wav</code> and <code>record stop.wav</code> in the RemSound install folder — you can drop in your own sounds to customise them, as long as you keep the same filenames.</p>
|
||||
|
||||
<p>Each cue can be turned off on its own via <strong>Preferences → Audio cue sounds (Alt+N)</strong>. The same checklist also controls the connect and disconnect cues. The cues play through your default Windows output device, separately from the recording, so a normal RemSound recording doesn't contain the cue. The one exception: if your sending side is capturing the very same output device the cue plays through, then the cue gets captured along with everything else from that device. In that case you'll have to live with the start cue appearing in the file, unless you turn it off.</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 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>
|
||||
|
||||
<h3>Where recordings go</h3>
|
||||
|
||||
@@ -930,7 +1014,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
|
||||
<p>OK (Alt+O) saves your choices to the current profile. Cancel (Alt+N) or Esc discards them. Settings are saved with the profile as usual — changes here mark the profile as having unsaved changes, and you'll be asked about them on exit if you haven't saved.</p>
|
||||
|
||||
<h2 id="logs">19. Logs and diagnostics</h2>
|
||||
<h2 id="logs">20. Logs and diagnostics</h2>
|
||||
|
||||
<p>If logging is turned on (the <strong>Enable logs</strong> checkbox in the Preferences dialog — Options → Preferences, or Ctrl+P — on by default), RemSound writes a log file each session into a <code>logs</code> folder next to the RemSound program. One file per launch.</p>
|
||||
|
||||
@@ -954,7 +1038,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
<li><strong>OpusUnrecoveredGaps</strong> — a running total of multi-packet losses that Opus couldn't repair. Each one is an audible click. It stays at 0 on a clean connection; small numbers are normal over the internet.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="troubleshooting">20. Troubleshooting</h2>
|
||||
<h2 id="troubleshooting">21. Troubleshooting</h2>
|
||||
|
||||
<h3>I don't hear my friend</h3>
|
||||
<ol>
|
||||
@@ -1023,7 +1107,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
|
||||
</ul>
|
||||
<p>If none of those apply, just fall back to Tailscale — it works without involving the router at all.</p>
|
||||
|
||||
<h2 id="glossary">21. Glossary</h2>
|
||||
<h2 id="glossary">22. Glossary</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Term</th><th>Meaning</th></tr>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -20,6 +20,83 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v3.1
|
||||
|
||||
Two big rounds of work on audio cues and the system
|
||||
tray, plus a handful of smaller fixes.
|
||||
|
||||
New audio cue sounds:
|
||||
* Profile saved. Plays a short cue whenever a
|
||||
profile is saved (Ctrl+S or File menu Save as).
|
||||
Gives you an audible "yes, that took" so you
|
||||
don't have to look at the screen.
|
||||
* Profile switched. Plays a short cue whenever a
|
||||
profile finishes loading - at startup and after
|
||||
you pick a different profile. It plays the new
|
||||
profile's cue, not the old one's, so if you
|
||||
give each profile a different switched-to
|
||||
sound you can tell at a glance which profile
|
||||
you're now on.
|
||||
|
||||
Per-profile custom cue sounds:
|
||||
* The Audio cue sounds list in Preferences now has
|
||||
six entries (Connect, Disconnect, Recording
|
||||
start, Recording stop, Profile saved, Profile
|
||||
switched), each with a tick to enable or
|
||||
silence it.
|
||||
* Two new buttons sit under the list. Play
|
||||
previews the currently-highlighted cue through
|
||||
your default Windows output. Browse opens a
|
||||
file picker so you can replace the default
|
||||
sound with any WAV file from your own disk.
|
||||
Right-clicking Browse offers "Use default
|
||||
sound" to undo the swap.
|
||||
* Both the tick states AND the custom sound
|
||||
choices are saved with the active profile, so
|
||||
different profiles can carry different cue
|
||||
palettes. A "quiet listening" profile can have
|
||||
all cues off; a "studio" profile can use a
|
||||
distinct set of sounds.
|
||||
* The default WAV files have moved from sitting
|
||||
loose next to RemSound.exe into a new sounds
|
||||
subfolder, keeping the install folder tidier.
|
||||
|
||||
System tray icon redesigned:
|
||||
* Hovering shows a live summary - peer count,
|
||||
send / receive mode (WASAPI, ASIO, or both),
|
||||
and how long any current recording has been
|
||||
running. Refreshes every second.
|
||||
* Right-click menu reworked. The items are now
|
||||
Show RemSound (W), Enable sending (S, ticks
|
||||
reflect state), Enable receiving (R, same),
|
||||
Profiles (P, submenu of your recent profiles
|
||||
with number-key shortcuts), and Exit (X).
|
||||
* Show RemSound now reliably brings the window
|
||||
forward and focuses it, fixing a case where
|
||||
screen reader users had to Alt+Tab to actually
|
||||
reach the restored window.
|
||||
* Enable sending and Enable receiving toggle the
|
||||
state instead of always switching it on, so
|
||||
you can use them to turn off too.
|
||||
* The Profiles submenu lets you switch profiles
|
||||
from the tray without re-opening the main
|
||||
window. Number keys 1..5 jump straight to a
|
||||
slot.
|
||||
|
||||
Bug fixes:
|
||||
* The tray icon's initial hover text was reading
|
||||
as "RemSound RemSound" on screen readers
|
||||
because the tooltip text matched the process
|
||||
name. Now reads cleanly as "RemSound - starting
|
||||
up" at first, then the live state takes over.
|
||||
* Recent profiles in both menus no longer
|
||||
announce a "Recent profile N:" prefix - they
|
||||
just read the profile name. The number-key
|
||||
shortcuts still work.
|
||||
|
||||
No wire format change. v3.1 talks to other v3.0.x
|
||||
machines exactly as v3.0 / v3.0.1 / v3.0.2 did.
|
||||
|
||||
RemSound v3.0.2
|
||||
|
||||
Hot-fix for a slow memory leak in the receive side. If
|
||||
|
||||
+223
-16
@@ -321,6 +321,11 @@ public sealed class MainForm : Form
|
||||
// anything the recorder is doing.
|
||||
private System.Media.SoundPlayer? recordStartSound;
|
||||
private System.Media.SoundPlayer? recordStopSound;
|
||||
// Profile-save and profile-switch cues, added 2026-05-28 alongside the move of all
|
||||
// default WAVs into a sounds\ subfolder. Save fires after a successful File → Save /
|
||||
// Save As; Profile fires immediately after a profile finishes loading in MainForm.
|
||||
private System.Media.SoundPlayer? saveSound;
|
||||
private System.Media.SoundPlayer? profileSwitchSound;
|
||||
// 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.
|
||||
@@ -606,9 +611,18 @@ public sealed class MainForm : Form
|
||||
hotkeyController.OnHotkeyChanged = MarkProfileDirty;
|
||||
trayController = new MainFormTrayController(
|
||||
this,
|
||||
() => sendMyAudioCheckbox.Checked = true,
|
||||
() => receiveAudioCheckbox.Checked = true,
|
||||
Close);
|
||||
// getSending / toggleSending — the tray's "Enable sending" checkable item reads
|
||||
// and toggles the main-window send checkbox. Toggle (not set-to-true) so right-
|
||||
// clicking it twice doesn't get the user stuck on. 2026-05-28 redesign.
|
||||
getSending: () => sendMyAudioCheckbox.Checked,
|
||||
toggleSending: () => sendMyAudioCheckbox.Checked = !sendMyAudioCheckbox.Checked,
|
||||
getReceiving: () => receiveAudioCheckbox.Checked,
|
||||
toggleReceiving: () => receiveAudioCheckbox.Checked = !receiveAudioCheckbox.Checked,
|
||||
// Profiles submenu — list of recent profile file paths read live from AppConfig
|
||||
// every time the submenu opens, so newly-loaded profiles appear immediately.
|
||||
getRecentProfilePaths: () => AppConfig.Load().RecentProfiles,
|
||||
switchToProfile: path => SwitchToRecentProfile(path),
|
||||
exit: Close);
|
||||
|
||||
recordingController = new RecordingController(
|
||||
sender,
|
||||
@@ -833,12 +847,16 @@ public sealed class MainForm : Form
|
||||
sender.Diagnostic = msg => logFile.Event($"sender: {msg}");
|
||||
receiver.Diagnostic = msg => logFile.Event($"receiver: {msg}");
|
||||
|
||||
// Pre-load peer-state cue sounds so the first playback isn't delayed by file I/O.
|
||||
// Files are deployed alongside the .exe (see RemSound.App.csproj Content rules).
|
||||
TryLoadCueSound("connect.wav", out connectSound);
|
||||
TryLoadCueSound("disconnect.wav", out disconnectSound);
|
||||
TryLoadCueSound("record start.wav", out recordStartSound);
|
||||
TryLoadCueSound("record stop.wav", out recordStopSound);
|
||||
// Pre-load all cue sounds so the first playback isn't delayed by file I/O. Default
|
||||
// WAVs are deployed to a sounds\ subfolder under RemSound.exe (see RemSound.App
|
||||
// .csproj Content rules); the per-cue custom-path overrides in AppConfig.CustomCuePaths
|
||||
// are honoured by TryLoadCueSound when set.
|
||||
TryLoadCueSound(CueId.Connect, "connect.wav", out connectSound);
|
||||
TryLoadCueSound(CueId.Disconnect, "disconnect.wav", out disconnectSound);
|
||||
TryLoadCueSound(CueId.RecordStart, "record start.wav", out recordStartSound);
|
||||
TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound);
|
||||
TryLoadCueSound(CueId.Save, "save.wav", out saveSound);
|
||||
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
|
||||
|
||||
LoadAudioDevices();
|
||||
// Apply persisted ASIO mode from settings — switches sender/receiver backends so the
|
||||
@@ -1030,6 +1048,17 @@ public sealed class MainForm : Form
|
||||
// blank-template case (no pendingProfile) we schedule it here.
|
||||
if (pendingProfile is null) ScheduleBaselineCapture();
|
||||
ApplyPendingProfileToControls();
|
||||
// Profile-switch cue (2026-05-28): fires once after the profile finishes loading
|
||||
// into the UI. Covers BOTH startup (user picks a profile from the picker) and
|
||||
// mid-session switch (user picks a different profile from the menu — Program.Main
|
||||
// re-creates MainForm under the new profile). Skipped when the user is on the
|
||||
// blank template, where there's no profile to announce. Honours the per-profile
|
||||
// EnableProfileSwitchCue flag set in Preferences.
|
||||
if (pendingProfile is not null
|
||||
&& settings.LoadEnableProfileSwitchCue())
|
||||
{
|
||||
profileSwitchSound?.Play();
|
||||
}
|
||||
// Show/hide the Update vs Save-as buttons based on whether we're on a loaded
|
||||
// profile or the blank template.
|
||||
UpdateProfileButtonsVisibility();
|
||||
@@ -1393,9 +1422,13 @@ public sealed class MainForm : Form
|
||||
if (string.IsNullOrWhiteSpace(path)) continue;
|
||||
if (!File.Exists(path)) continue; // skip missing files; keep in storage in case they reappear
|
||||
var title = Path.GetFileNameWithoutExtension(path);
|
||||
// Visible Text has the &1..&5 mnemonic for number-key access; AccessibleName is
|
||||
// just the profile name so NVDA reads the menu item naturally rather than
|
||||
// prefixing every entry with "Recent profile N:" (which was the original cut
|
||||
// and Ed flagged it as noisy / unwanted).
|
||||
var item = new ToolStripMenuItem($"&{slot} {title}")
|
||||
{
|
||||
AccessibleName = $"Recent profile {slot}: {title}",
|
||||
AccessibleName = title,
|
||||
// Stash the path on the menu item so the click handler doesn't depend on
|
||||
// closure capture of the loop variable.
|
||||
Tag = path,
|
||||
@@ -1795,6 +1828,11 @@ public sealed class MainForm : Form
|
||||
unsubscribeUpnpStatusChanged: handler => routerPortMapper.StatusChanged -= handler);
|
||||
dialog.ShowDialog(this);
|
||||
if (dialog.ChangedAnyProfileSetting) MarkProfileDirty();
|
||||
// The Preferences dialog includes per-cue Browse buttons that can change custom
|
||||
// WAV paths in AppConfig.CustomCuePaths. Reload the cached SoundPlayer instances
|
||||
// here unconditionally — cheap, only six small files, and guarantees the next
|
||||
// play uses whatever the user just picked without waiting for the next launch.
|
||||
ReloadAllCueSounds();
|
||||
}
|
||||
|
||||
/// <summary>User pressed "Check for updates" (Help menu or Preferences button). Always
|
||||
@@ -4169,6 +4207,10 @@ public sealed class MainForm : Form
|
||||
// serialised on the network-thread lock inside the receiver, so doing it from the UI
|
||||
// tick is safe.
|
||||
receiver.PruneIdleSessions();
|
||||
// Refresh the tray icon's hover tooltip so it reflects the current peer count and
|
||||
// send / receive routing (WASAPI / ASIO / both). 1 Hz cadence is fine — the user is
|
||||
// hovering, not staring at a counter — and BuildTrayTooltip is allocation-cheap.
|
||||
trayController.SetTooltip(BuildTrayTooltip());
|
||||
// Periodic native-memory reaper. SustainedLowLatency GC mode (set in Program.Main)
|
||||
// explicitly avoids gen2 collections to keep audio scheduling smooth — but that same
|
||||
// suppression means finalizers for IDisposable wrappers that didn't get explicit
|
||||
@@ -4892,6 +4934,11 @@ public sealed class MainForm : Form
|
||||
{
|
||||
SaveCurrentStateToProfileFile(title);
|
||||
AppendLogEntry($"profile saved: \"{title}\"");
|
||||
// Save cue (2026-05-28): fires after any successful save — Save AND Save As, since
|
||||
// both routes funnel through this single method. Honours the EnableSaveCue per-
|
||||
// profile flag; the cue is silent if the user has unticked it in Preferences or if
|
||||
// sounds\save.wav doesn't exist and no custom override has been set.
|
||||
if (settings.LoadEnableSaveCue()) saveSound?.Play();
|
||||
// Refresh the unsaved-changes baseline so this saved state becomes the new
|
||||
// "no changes" reference. The Title field changes on save-as, so the next
|
||||
// diff comparison must use the new state as baseline, not the pre-save one.
|
||||
@@ -5153,16 +5200,162 @@ public sealed class MainForm : Form
|
||||
// tab — see BuildProfilesPrefsTab + SwitchSelectedProfile / RenameSelectedProfile /
|
||||
// DeleteSelectedProfile.
|
||||
|
||||
private void TryLoadCueSound(string fileName, out System.Media.SoundPlayer? player)
|
||||
/// <summary>Builds the live tooltip shown over the system-tray icon — sums up peer count
|
||||
/// and send / receive routing into a single readable line. Kept under the 127-character
|
||||
/// NotifyIcon limit by construction; the tray controller truncates with an ellipsis as a
|
||||
/// belt-and-braces if a future addition ever pushes it over.
|
||||
///
|
||||
/// Examples:
|
||||
/// * RemSound — not connected
|
||||
/// * RemSound — recording for 2:34 — not connected
|
||||
/// * RemSound — 2 peers, sending (WASAPI), receiving (WASAPI)
|
||||
/// * RemSound — recording for 1:23:45, 2 peers, sending (WASAPI), receiving (WASAPI)
|
||||
/// </summary>
|
||||
private string BuildTrayTooltip()
|
||||
{
|
||||
// Healthy-peer count. Heartbeats define "connected" — a peer ticked in the list but
|
||||
// never reachable doesn't count, because the user cares about who they can actually
|
||||
// talk to right now, not who they intend to.
|
||||
var healthyPeers = 0;
|
||||
if (heartbeatService is not null)
|
||||
{
|
||||
foreach (var ph in heartbeatService.GetAllPeerHealth())
|
||||
{
|
||||
if (ph.State == PeerHealthState.Healthy) healthyPeers++;
|
||||
}
|
||||
}
|
||||
// Recording timer — only included when a recording is actually running. Slots in
|
||||
// right after the "RemSound" leader as Ed asked, so it reads as a status on the
|
||||
// app itself rather than a property of the peer list.
|
||||
string? recordingPart = null;
|
||||
if (recordingController.IsRecording && recordingController.RecordingStartedUtc is { } startedUtc)
|
||||
{
|
||||
recordingPart = $"recording for {FormatRecordingElapsed(DateTime.UtcNow - startedUtc)}";
|
||||
}
|
||||
if (healthyPeers == 0 && !sendMyAudioCheckbox.Checked && !receiveAudioCheckbox.Checked)
|
||||
{
|
||||
return recordingPart is null
|
||||
? "RemSound — not connected"
|
||||
: $"RemSound — {recordingPart} — not connected";
|
||||
}
|
||||
|
||||
var parts = new List<string> { "RemSound" };
|
||||
if (recordingPart is not null) parts.Add(recordingPart);
|
||||
var peerText = healthyPeers switch
|
||||
{
|
||||
0 => "no peers",
|
||||
1 => "1 peer",
|
||||
_ => $"{healthyPeers} peers",
|
||||
};
|
||||
parts.Add(peerText);
|
||||
|
||||
// Direction lines — only added when the corresponding direction is actually on. The
|
||||
// lane label (WASAPI / ASIO / WASAPI + ASIO) is derived from which device-list ticks
|
||||
// are active, NOT from the audio-mode setting alone: in BothIndependent a user can
|
||||
// still have ticked WASAPI inputs only, in which case the tray should honestly say
|
||||
// "WASAPI" rather than "WASAPI + ASIO".
|
||||
if (sendMyAudioCheckbox.Checked)
|
||||
{
|
||||
var hasWasapiSend = AnyChecked(sendInputDevicesList) || AnyChecked(sendOutputDevicesList);
|
||||
var hasAsioSend = AnyChecked(asioSendDevicesList);
|
||||
parts.Add($"sending ({DescribeLanes(hasWasapiSend, hasAsioSend)})");
|
||||
}
|
||||
if (receiveAudioCheckbox.Checked)
|
||||
{
|
||||
var hasWasapiReceive = AnyChecked(receiveOutputDevicesList);
|
||||
var hasAsioReceive = AnyChecked(asioReceiveOutputDevicesList);
|
||||
parts.Add($"receiving ({DescribeLanes(hasWasapiReceive, hasAsioReceive)})");
|
||||
}
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
|
||||
/// <summary>Convenience for BuildTrayTooltip — true if the given CheckedListBox has at
|
||||
/// least one ticked item. Defensive against null lists (the layout builders run async on
|
||||
/// startup, so the tooltip refresh CAN fire one tick before they exist).</summary>
|
||||
private static bool AnyChecked(CheckedListBox? list)
|
||||
{
|
||||
if (list is null) return false;
|
||||
return list.CheckedItems.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>Compact duration formatter for the tray tooltip's "recording for X" segment.
|
||||
/// Under an hour shows MM:SS; from an hour on it shows H:MM:SS — the same shape Windows
|
||||
/// uses for media-player elapsed-time displays, so it reads naturally to people who
|
||||
/// don't otherwise know it's a custom format.</summary>
|
||||
private static string FormatRecordingElapsed(TimeSpan elapsed)
|
||||
{
|
||||
// Negative elapsed (clock skew across a sleep cycle) gets clamped to zero — better
|
||||
// than displaying "-00:01" mid-tooltip.
|
||||
if (elapsed < TimeSpan.Zero) elapsed = TimeSpan.Zero;
|
||||
return elapsed.TotalHours >= 1
|
||||
? $"{(int)elapsed.TotalHours}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}"
|
||||
: $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}";
|
||||
}
|
||||
|
||||
/// <summary>Pretty-print "WASAPI", "ASIO", "WASAPI + ASIO", or "no devices" depending
|
||||
/// on which of the two flags are set. "no devices" covers the awkward case where the
|
||||
/// user has the send/receive checkbox on but hasn't ticked anything for the engine to
|
||||
/// chew on — better to say so in the tooltip than imply silent activity.</summary>
|
||||
private static string DescribeLanes(bool wasapi, bool asio) =>
|
||||
(wasapi, asio) switch
|
||||
{
|
||||
(true, true) => "WASAPI + ASIO",
|
||||
(true, false) => "WASAPI",
|
||||
(false, true) => "ASIO",
|
||||
_ => "no devices",
|
||||
};
|
||||
|
||||
/// <summary>Well-known cue identifiers used as keys into
|
||||
/// <see cref="AppConfig.CustomCuePaths"/>. Stable strings — don't rename without writing
|
||||
/// a migration, because users' Preferences-set custom paths are stored under these keys
|
||||
/// in <c>remsound.config.json</c>. Centralised here so the Preferences dialog and the
|
||||
/// MainForm load path agree on the spellings.</summary>
|
||||
internal static class CueId
|
||||
{
|
||||
public const string Connect = "connect";
|
||||
public const string Disconnect = "disconnect";
|
||||
public const string RecordStart = "record-start";
|
||||
public const string RecordStop = "record-stop";
|
||||
public const string Save = "save";
|
||||
public const string ProfileSwitch = "profile-switch";
|
||||
}
|
||||
|
||||
/// <summary>Load one cue sound. Resolution order:
|
||||
/// (1) if the active profile has a custom path for <paramref name="cueId"/> AND the
|
||||
/// referenced file exists, use that — the user-supplied override.
|
||||
/// (2) otherwise the default WAV in <c>sounds\</c> next to RemSound.exe, named
|
||||
/// <paramref name="defaultFileName"/>.
|
||||
/// (3) otherwise null — the cue silently doesn't play. New cues without a shipped
|
||||
/// default WAV (e.g. save.wav and profile.wav before the project owner supplies
|
||||
/// them) land here and the rest of the app keeps working.
|
||||
///
|
||||
/// Custom paths are per-profile (changed from machine-wide in v3.0.3 development) so
|
||||
/// each profile can carry its own cue palette. The settings cache mirrors the active
|
||||
/// profile's CustomCuePaths dictionary and is the runtime source of truth.</summary>
|
||||
private void TryLoadCueSound(string cueId, string defaultFileName, out System.Media.SoundPlayer? player)
|
||||
{
|
||||
player = null;
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, fileName);
|
||||
if (!File.Exists(path))
|
||||
string? path = null;
|
||||
var customPath = settings.LoadCustomCuePath(cueId);
|
||||
if (!string.IsNullOrWhiteSpace(customPath) && File.Exists(customPath))
|
||||
{
|
||||
logFile.Event($"cue sound missing: {fileName} (looked at {path})");
|
||||
return;
|
||||
path = customPath;
|
||||
logFile.Event($"cue sound '{cueId}': using custom path {customPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultPath = Path.Combine(AppContext.BaseDirectory, "sounds", defaultFileName);
|
||||
if (File.Exists(defaultPath))
|
||||
{
|
||||
path = defaultPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
logFile.Event($"cue sound '{cueId}': default missing ({defaultPath}) and no custom override set — cue will be silent");
|
||||
return;
|
||||
}
|
||||
}
|
||||
var sp = new System.Media.SoundPlayer(path);
|
||||
sp.LoadAsync();
|
||||
@@ -5170,10 +5363,24 @@ public sealed class MainForm : Form
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logFile.Event($"cue sound load failed for {fileName}: {ex.GetType().Name}: {ex.Message}");
|
||||
logFile.Event($"cue sound load failed for '{cueId}': {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Re-load all cue sounds. Called by PreferencesDialog after the user picks a
|
||||
/// new custom WAV for any cue — re-runs <see cref="TryLoadCueSound"/> for the lot so
|
||||
/// the cached SoundPlayer instances point at the right file from the next play onward.
|
||||
/// </summary>
|
||||
public void ReloadAllCueSounds()
|
||||
{
|
||||
TryLoadCueSound(CueId.Connect, "connect.wav", out connectSound);
|
||||
TryLoadCueSound(CueId.Disconnect, "disconnect.wav", out disconnectSound);
|
||||
TryLoadCueSound(CueId.RecordStart, "record start.wav", out recordStartSound);
|
||||
TryLoadCueSound(CueId.RecordStop, "record stop.wav", out recordStopSound);
|
||||
TryLoadCueSound(CueId.Save, "save.wav", out saveSound);
|
||||
TryLoadCueSound(CueId.ProfileSwitch, "profile.wav", out profileSwitchSound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares current peer-health states to the last-seen states and plays a connect /
|
||||
/// disconnect cue on the relevant transitions. Driven from the 1 Hz snapshot tick. Rules:
|
||||
|
||||
@@ -1,25 +1,153 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the system-tray icon RemSound shows while it's minimised. The icon's right-click
|
||||
/// menu provides quick access to the most common actions without restoring the window —
|
||||
/// useful for users who park RemSound in the tray and never look at the main window again.
|
||||
///
|
||||
/// Menu (2026-05-28 rewrite — the original four-item menu was just Show / Sending /
|
||||
/// Receiving / Exit, with no recent-profiles entry and no live state on the checkable items):
|
||||
///
|
||||
/// * Show RemSound (Alt+W) — restore the main window
|
||||
/// * Enable sending (Alt+S) — checkable, reflects sendMyAudioCheckbox state
|
||||
/// * Enable receiving (Alt+R) — checkable, reflects receiveAudioCheckbox state
|
||||
/// * Profiles (Alt+P) — submenu of AppConfig.RecentProfiles, picking one
|
||||
/// switches the active profile
|
||||
/// * Exit (Alt+X) — close the app
|
||||
///
|
||||
/// Tooltip (NotifyIcon.Text): set dynamically from MainForm's snapshot tick via
|
||||
/// <see cref="SetTooltip"/>. Default is "RemSound" until the first refresh. Capped at
|
||||
/// 127 characters because Windows truncates anything beyond that.
|
||||
/// </summary>
|
||||
internal sealed class MainFormTrayController : IDisposable
|
||||
{
|
||||
/// <summary>Maximum NotifyIcon.Text length on Windows 10+. Windows truncates anything
|
||||
/// longer; truncating ourselves means the tooltip ends with our own "..." rather than
|
||||
/// being chopped mid-word.</summary>
|
||||
private const int MaxTooltipLength = 127;
|
||||
|
||||
private readonly Form owner;
|
||||
private readonly NotifyIcon trayIcon = new();
|
||||
|
||||
public MainFormTrayController(Form owner, Action enableSending, Action enableReceiving, Action exit)
|
||||
private readonly Func<bool> getSending;
|
||||
private readonly Action toggleSending;
|
||||
private readonly Func<bool> getReceiving;
|
||||
private readonly Action toggleReceiving;
|
||||
private readonly Func<IReadOnlyList<string>> getRecentProfilePaths;
|
||||
private readonly Action<string> switchToProfile;
|
||||
private readonly Action exit;
|
||||
|
||||
private readonly ToolStripMenuItem sendingItem;
|
||||
private readonly ToolStripMenuItem receivingItem;
|
||||
private readonly ToolStripMenuItem profilesItem;
|
||||
|
||||
public MainFormTrayController(
|
||||
Form owner,
|
||||
Func<bool> getSending,
|
||||
Action toggleSending,
|
||||
Func<bool> getReceiving,
|
||||
Action toggleReceiving,
|
||||
Func<IReadOnlyList<string>> getRecentProfilePaths,
|
||||
Action<string> switchToProfile,
|
||||
Action exit)
|
||||
{
|
||||
this.owner = owner;
|
||||
trayIcon.Text = "RemSound";
|
||||
this.getSending = getSending;
|
||||
this.toggleSending = toggleSending;
|
||||
this.getReceiving = getReceiving;
|
||||
this.toggleReceiving = toggleReceiving;
|
||||
this.getRecentProfilePaths = getRecentProfilePaths;
|
||||
this.switchToProfile = switchToProfile;
|
||||
this.exit = exit;
|
||||
|
||||
// Initial tooltip text — deliberately NOT just "RemSound" because some screen
|
||||
// readers (NVDA in particular) read tray icons as "<process name>, <tooltip>",
|
||||
// which with a single-word "RemSound" tooltip on a "RemSound" process renders as
|
||||
// "RemSound RemSound" until the first snapshot tick (~1s after launch) overwrites
|
||||
// it. Picking a sensible startup-state string avoids the duplicate read entirely;
|
||||
// the snapshot tick refreshes this with live peer / send / receive info from then
|
||||
// on.
|
||||
trayIcon.Text = "RemSound — starting up";
|
||||
trayIcon.Icon = SystemIcons.Application;
|
||||
trayIcon.Visible = false;
|
||||
trayIcon.DoubleClick += (_, _) => Restore();
|
||||
|
||||
var menu = new ContextMenuStrip();
|
||||
menu.Items.Add("Show", null, (_, _) => Restore());
|
||||
menu.Items.Add("Enable sending", null, (_, _) => enableSending());
|
||||
menu.Items.Add("Enable receiving", null, (_, _) => enableReceiving());
|
||||
menu.Items.Add("Exit", null, (_, _) => exit());
|
||||
|
||||
var showItem = new ToolStripMenuItem("Sho&w RemSound")
|
||||
{
|
||||
AccessibleName = "Show RemSound",
|
||||
};
|
||||
showItem.Click += (_, _) => Restore();
|
||||
|
||||
sendingItem = new ToolStripMenuItem("Enable &sending")
|
||||
{
|
||||
CheckOnClick = false, // we set Checked manually in RefreshMenuState; toggle drives the actual app state via the callback
|
||||
AccessibleName = "Enable sending",
|
||||
};
|
||||
sendingItem.Click += (_, _) => toggleSending();
|
||||
|
||||
receivingItem = new ToolStripMenuItem("Enable &receiving")
|
||||
{
|
||||
CheckOnClick = false,
|
||||
AccessibleName = "Enable receiving",
|
||||
};
|
||||
receivingItem.Click += (_, _) => toggleReceiving();
|
||||
|
||||
profilesItem = new ToolStripMenuItem("&Profiles")
|
||||
{
|
||||
AccessibleName = "Profiles",
|
||||
};
|
||||
// Populate ONCE at construction so WinForms recognises this as a real submenu
|
||||
// (an empty DropDownItems collection means the framework treats the item as a
|
||||
// plain command, never opens the submenu, and DropDownOpening never fires —
|
||||
// which produced "Profiles does nothing" in the first cut of this controller).
|
||||
// After that, every DropDownOpening rebuilds the items so a profile loaded since
|
||||
// the last menu open shows up immediately.
|
||||
RebuildProfilesSubmenu();
|
||||
profilesItem.DropDownOpening += (_, _) => RebuildProfilesSubmenu();
|
||||
|
||||
var exitItem = new ToolStripMenuItem("E&xit")
|
||||
{
|
||||
AccessibleName = "Exit RemSound",
|
||||
};
|
||||
exitItem.Click += (_, _) => exit();
|
||||
|
||||
menu.Items.Add(showItem);
|
||||
menu.Items.Add(sendingItem);
|
||||
menu.Items.Add(receivingItem);
|
||||
menu.Items.Add(profilesItem);
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
menu.Items.Add(exitItem);
|
||||
|
||||
// Refresh the checkable items' state every time the menu opens so the visible
|
||||
// ticks match the current main-window state (which can have changed while the
|
||||
// user was clicking around elsewhere).
|
||||
menu.Opening += (_, _) => RefreshMenuState();
|
||||
|
||||
trayIcon.ContextMenuStrip = menu;
|
||||
}
|
||||
|
||||
/// <summary>Replace the tooltip the OS shows over the tray icon. Called from MainForm's
|
||||
/// 1 Hz snapshot tick to keep the text current with peer count + send/receive state.
|
||||
/// Truncated to the Windows 10+ limit (127 chars) — anything longer is silently chopped
|
||||
/// by the shell, so chopping ourselves keeps the truncation point visible.</summary>
|
||||
public void SetTooltip(string text)
|
||||
{
|
||||
// Same anti-duplicate rule as the ctor: avoid a single-word "RemSound" tooltip on
|
||||
// the "RemSound" process, since some screen readers render that as "RemSound RemSound".
|
||||
if (string.IsNullOrEmpty(text)) text = "RemSound — running";
|
||||
if (text.Length > MaxTooltipLength)
|
||||
{
|
||||
text = text[..(MaxTooltipLength - 1)] + "…";
|
||||
}
|
||||
// NotifyIcon.Text throws on the same string-assigning path under some shell
|
||||
// conditions (rare race during a session-end). Best-effort: swallow.
|
||||
try { trayIcon.Text = text; } catch { /* harmless */ }
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
if (owner.Visible && owner.WindowState != FormWindowState.Minimized) Minimize();
|
||||
@@ -29,11 +157,28 @@ internal sealed class MainFormTrayController : IDisposable
|
||||
public void Restore()
|
||||
{
|
||||
owner.Show();
|
||||
owner.WindowState = FormWindowState.Normal;
|
||||
if (owner.WindowState == FormWindowState.Minimized)
|
||||
{
|
||||
owner.WindowState = FormWindowState.Normal;
|
||||
}
|
||||
owner.BringToFront();
|
||||
owner.Activate();
|
||||
// WinForms' Activate() is best-effort: Windows' foreground-lock feature blocks
|
||||
// arbitrary processes from stealing focus, and Activate() doesn't always win even
|
||||
// for a process that's clearly user-initiated. Calling SetForegroundWindow directly
|
||||
// bypasses the lock because the caller (a tray-menu click handler) is on a UI
|
||||
// thread that received recent user input — which Windows recognises as the
|
||||
// legitimate "user asked for this" case. Without this fix, "Show RemSound" puts
|
||||
// the window on screen but doesn't focus it, leaving NVDA users having to Alt+Tab
|
||||
// to actually hear the new content.
|
||||
try { SetForegroundWindow(owner.Handle); } catch { /* harmless — Restore still mostly worked */ }
|
||||
trayIcon.Visible = false;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
public void Minimize()
|
||||
{
|
||||
owner.Hide();
|
||||
@@ -41,4 +186,57 @@ internal sealed class MainFormTrayController : IDisposable
|
||||
}
|
||||
|
||||
public void Dispose() => trayIcon.Dispose();
|
||||
|
||||
private void RefreshMenuState()
|
||||
{
|
||||
// Live state of the two togglable items — read on demand from MainForm so a checkbox
|
||||
// change made via the main window or a global hotkey is reflected in the tray menu
|
||||
// the next time the user opens it.
|
||||
try { sendingItem.Checked = getSending(); } catch { sendingItem.Checked = false; }
|
||||
try { receivingItem.Checked = getReceiving(); } catch { receivingItem.Checked = false; }
|
||||
}
|
||||
|
||||
private void RebuildProfilesSubmenu()
|
||||
{
|
||||
profilesItem.DropDownItems.Clear();
|
||||
IReadOnlyList<string> paths;
|
||||
try { paths = getRecentProfilePaths(); }
|
||||
catch { paths = Array.Empty<string>(); }
|
||||
|
||||
var slot = 1;
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) continue;
|
||||
if (!File.Exists(path)) continue; // skip missing files; the AppConfig list keeps the entry in case it reappears
|
||||
var title = Path.GetFileNameWithoutExtension(path);
|
||||
// Mnemonic prefix matches the File menu's Recent profiles submenu (&1..&5) so
|
||||
// muscle-memory between the in-app menu and the tray menu carries over. The
|
||||
// visible Text carries the number; AccessibleName is just the profile name so
|
||||
// NVDA reads "MyProfile, menu item, one of five" rather than the noisier
|
||||
// "Recent profile 1: MyProfile" that the original code was reading out.
|
||||
var item = new ToolStripMenuItem($"&{slot} {title}")
|
||||
{
|
||||
AccessibleName = title,
|
||||
Tag = path,
|
||||
};
|
||||
item.Click += (s, _) =>
|
||||
{
|
||||
var sender = (ToolStripMenuItem)s!;
|
||||
var profilePath = (string)sender.Tag!;
|
||||
try { switchToProfile(profilePath); }
|
||||
catch { /* the switch path surfaces its own errors via MainForm */ }
|
||||
};
|
||||
profilesItem.DropDownItems.Add(item);
|
||||
slot++;
|
||||
}
|
||||
|
||||
if (profilesItem.DropDownItems.Count == 0)
|
||||
{
|
||||
profilesItem.DropDownItems.Add(new ToolStripMenuItem("(No recent profiles)")
|
||||
{
|
||||
Enabled = false,
|
||||
AccessibleName = "No recent profiles",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,14 @@ internal sealed class PreferencesDialog : Form
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
// Per-cue enable list (2026-05-15). Replaces the single "mute connect/disconnect"
|
||||
// checkbox with one item per cue sound, ticked = play, unticked = silent. Same
|
||||
// CheckOnClick / mnemonic-via-label pattern as the audio device lists on the main
|
||||
// form — visually familiar and NVDA-friendly. The Items collection order MUST match
|
||||
// the CueIndex enum below so the ItemCheck handler can dispatch by index.
|
||||
// Audio cue UI (2026-05-28 revised after Ed's feedback that one-control-per-cue blew
|
||||
// out the tab order). Back to a single CheckedListBox — up/down arrows move between
|
||||
// cues, Space toggles enable, exactly as it always was. Two buttons sit BELOW the list:
|
||||
// a Play button to preview, and a Browse button to pick a custom WAV. Both act on
|
||||
// whichever cue is currently selected in the list. Their labels update live as the
|
||||
// selection changes ("Play disconnect sound", "Browse for disconnect sound...") so
|
||||
// sighted and NVDA users alike know which cue they're about to act on. Tab order in
|
||||
// the cue section is just: list → Play → Browse (three tab stops, not eighteen).
|
||||
private readonly Label cueListLabel = new()
|
||||
{
|
||||
Text = "Audio cue sou&nds (Alt+N):",
|
||||
@@ -57,18 +60,49 @@ internal sealed class PreferencesDialog : Form
|
||||
{
|
||||
CheckOnClick = true,
|
||||
IntegralHeight = false,
|
||||
Height = 100,
|
||||
Height = 130,
|
||||
Width = 360,
|
||||
AccessibleName = "Audio cue sounds",
|
||||
};
|
||||
|
||||
private enum CueIndex
|
||||
private readonly Button playSelectedCueButton = new()
|
||||
{
|
||||
Connect = 0,
|
||||
Disconnect = 1,
|
||||
RecordStart = 2,
|
||||
RecordStop = 3,
|
||||
}
|
||||
AutoSize = true,
|
||||
Padding = new Padding(6, 2, 6, 2),
|
||||
};
|
||||
|
||||
private readonly Button browseSelectedCueButton = new()
|
||||
{
|
||||
AutoSize = true,
|
||||
Padding = new Padding(6, 2, 6, 2),
|
||||
};
|
||||
|
||||
/// <summary>Describes one cue. <see cref="DisplayName"/> ends up in the listbox row;
|
||||
/// <see cref="CueId"/> is the well-known key from <see cref="MainForm.CueId"/>; the
|
||||
/// LoadEnabled / SaveEnabled pair routes the checkbox state to the right
|
||||
/// <see cref="RemSoundSettingsStore"/> getter/setter so we don't need a hard-coded
|
||||
/// switch on index.</summary>
|
||||
private sealed record CueRowDescriptor(
|
||||
string DisplayName,
|
||||
string CueId,
|
||||
Func<RemSoundSettingsStore, bool> LoadEnabled,
|
||||
Action<RemSoundSettingsStore, bool> SaveEnabled);
|
||||
|
||||
private static readonly CueRowDescriptor[] CueRows =
|
||||
[
|
||||
new("Connect sound", MainForm.CueId.Connect,
|
||||
s => s.LoadEnableConnectCue(), (s, v) => s.SaveEnableConnectCue(v)),
|
||||
new("Disconnect sound", MainForm.CueId.Disconnect,
|
||||
s => s.LoadEnableDisconnectCue(), (s, v) => s.SaveEnableDisconnectCue(v)),
|
||||
new("Recording start sound", MainForm.CueId.RecordStart,
|
||||
s => s.LoadEnableRecordStartCue(), (s, v) => s.SaveEnableRecordStartCue(v)),
|
||||
new("Recording stop sound", MainForm.CueId.RecordStop,
|
||||
s => s.LoadEnableRecordStopCue(), (s, v) => s.SaveEnableRecordStopCue(v)),
|
||||
new("Profile saved sound", MainForm.CueId.Save,
|
||||
s => s.LoadEnableSaveCue(), (s, v) => s.SaveEnableSaveCue(v)),
|
||||
new("Profile switched sound", MainForm.CueId.ProfileSwitch,
|
||||
s => s.LoadEnableProfileSwitchCue(), (s, v) => s.SaveEnableProfileSwitchCue(v)),
|
||||
];
|
||||
|
||||
private readonly AccessibleCheckBox acceptRemoteVolumeBox = new()
|
||||
{
|
||||
@@ -223,30 +257,80 @@ internal sealed class PreferencesDialog : Form
|
||||
"Profiles folder updated", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
};
|
||||
|
||||
// Populate the cue list — order must match CueIndex enum. Each item is ticked from
|
||||
// its corresponding settings flag; the toggle handler dispatches by index so adding
|
||||
// a future cue is just two lines (enum value + Items.Add + Save case).
|
||||
// Populate the cue listbox — order matches the CueRows array, and the index of a
|
||||
// selected row maps 1:1 to a CueRowDescriptor. Each row's ticked state is loaded
|
||||
// from the active profile's per-cue enable flag via the descriptor.
|
||||
cueList.Items.Clear();
|
||||
cueList.Items.Add("Connect sound", settings.LoadEnableConnectCue());
|
||||
cueList.Items.Add("Disconnect sound", settings.LoadEnableDisconnectCue());
|
||||
cueList.Items.Add("Recording start sound", settings.LoadEnableRecordStartCue());
|
||||
cueList.Items.Add("Recording stop sound", settings.LoadEnableRecordStopCue());
|
||||
foreach (var c in CueRows)
|
||||
{
|
||||
cueList.Items.Add(c.DisplayName, c.LoadEnabled(settings));
|
||||
}
|
||||
if (cueList.Items.Count > 0) cueList.SelectedIndex = 0;
|
||||
cueList.ItemCheck += (_, e) =>
|
||||
{
|
||||
// ItemCheck fires BEFORE the visual state actually flips; e.NewValue is what
|
||||
// it's about to become. Use that for the persist call so the saved value
|
||||
// matches what the user just clicked.
|
||||
// it's about to become, so the persisted value matches what the user just
|
||||
// clicked.
|
||||
if (e.Index < 0 || e.Index >= CueRows.Length) return;
|
||||
var nowEnabled = e.NewValue == CheckState.Checked;
|
||||
switch ((CueIndex)e.Index)
|
||||
{
|
||||
case CueIndex.Connect: settings.SaveEnableConnectCue(nowEnabled); break;
|
||||
case CueIndex.Disconnect: settings.SaveEnableDisconnectCue(nowEnabled); break;
|
||||
case CueIndex.RecordStart: settings.SaveEnableRecordStartCue(nowEnabled); break;
|
||||
case CueIndex.RecordStop: settings.SaveEnableRecordStopCue(nowEnabled); break;
|
||||
}
|
||||
CueRows[e.Index].SaveEnabled(settings, nowEnabled);
|
||||
ChangedAnyProfileSetting = true;
|
||||
};
|
||||
|
||||
// Selection changes update the two action buttons' labels so they always tell the
|
||||
// user which cue they're about to act on. Refreshed eagerly at construction time
|
||||
// for the initial selection too.
|
||||
cueList.SelectedIndexChanged += (_, _) => RefreshCueActionButtons(settings);
|
||||
RefreshCueActionButtons(settings);
|
||||
|
||||
playSelectedCueButton.Click += (_, _) =>
|
||||
{
|
||||
if (cueList.SelectedIndex < 0 || cueList.SelectedIndex >= CueRows.Length) return;
|
||||
OnPlayClicked(CueRows[cueList.SelectedIndex], settings);
|
||||
};
|
||||
browseSelectedCueButton.Click += (_, _) =>
|
||||
{
|
||||
if (cueList.SelectedIndex < 0 || cueList.SelectedIndex >= CueRows.Length) return;
|
||||
OnBrowseClicked(browseSelectedCueButton, CueRows[cueList.SelectedIndex], settings);
|
||||
RefreshCueActionButtons(settings);
|
||||
};
|
||||
|
||||
// Right-click "Use default sound" context menu lives on the Browse button. It acts
|
||||
// on whichever cue is currently selected — same as a left click. Disabled when no
|
||||
// override is set so it can't accidentally do nothing.
|
||||
var browseCtx = new ContextMenuStrip();
|
||||
var useDefaultItem = new ToolStripMenuItem("Use default sound");
|
||||
useDefaultItem.Click += (_, _) =>
|
||||
{
|
||||
if (cueList.SelectedIndex < 0 || cueList.SelectedIndex >= CueRows.Length) return;
|
||||
var cue = CueRows[cueList.SelectedIndex];
|
||||
if (settings.LoadCustomCuePath(cue.CueId) is not null)
|
||||
{
|
||||
settings.SaveCustomCuePath(cue.CueId, null);
|
||||
ChangedAnyProfileSetting = true;
|
||||
RefreshCueActionButtons(settings);
|
||||
}
|
||||
};
|
||||
browseCtx.Opening += (_, _) =>
|
||||
{
|
||||
if (cueList.SelectedIndex < 0 || cueList.SelectedIndex >= CueRows.Length)
|
||||
{
|
||||
useDefaultItem.Enabled = false;
|
||||
useDefaultItem.Text = "Use default sound";
|
||||
}
|
||||
else
|
||||
{
|
||||
var cue = CueRows[cueList.SelectedIndex];
|
||||
useDefaultItem.Enabled = settings.LoadCustomCuePath(cue.CueId) is not null;
|
||||
useDefaultItem.Text = $"Use default {cue.DisplayName.ToLowerInvariant()}";
|
||||
useDefaultItem.AccessibleName = useDefaultItem.Text;
|
||||
}
|
||||
};
|
||||
browseCtx.Items.Add(useDefaultItem);
|
||||
browseSelectedCueButton.ContextMenuStrip = browseCtx;
|
||||
|
||||
cueListLabel.Click += (_, _) => cueList.Focus();
|
||||
|
||||
acceptRemoteVolumeBox.Checked = settings.LoadAcceptRemoteVolumeCommands();
|
||||
acceptRemoteVolumeBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
@@ -344,22 +428,25 @@ internal sealed class PreferencesDialog : Form
|
||||
for (var i = 0; i < 12; i++) panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
// Tab order top-to-bottom: browse, cue-sound list, accept remote, check-on-startup,
|
||||
// frequency, check-now, silent install, UPnP, enable logs, write logs now, close.
|
||||
// Updates sit above the log row so a user setting up the app meets them first. The
|
||||
// Startup behaviour button used to live here at tab index 3; it moved to the Options
|
||||
// menu in the 2026-05-15 reorg.
|
||||
// Tab order top-to-bottom: browse-profiles-folder → cue list → Play selected →
|
||||
// Browse for selected → accept remote → check-on-startup → frequency → check-now →
|
||||
// silent install → UPnP → enable logs → write logs now → close. The cue section is
|
||||
// three tab stops total: the list itself (where up/down navigates between cues and
|
||||
// Space toggles enable), then the two action buttons that operate on whichever cue
|
||||
// is currently selected in the list.
|
||||
browseProfilesFolderButton.TabIndex = 0;
|
||||
cueList.TabIndex = 1;
|
||||
acceptRemoteVolumeBox.TabIndex = 2;
|
||||
checkForUpdatesOnStartupBox.TabIndex = 3;
|
||||
updateFrequencyBox.TabIndex = 4;
|
||||
checkForUpdatesNowButton.TabIndex = 5;
|
||||
silentlyInstallUpdatesBox.TabIndex = 6;
|
||||
upnpEnabledBox.TabIndex = 7;
|
||||
loggingBox.TabIndex = 8;
|
||||
writeLogsNowButton.TabIndex = 9;
|
||||
closeButton.TabIndex = 10;
|
||||
playSelectedCueButton.TabIndex = 2;
|
||||
browseSelectedCueButton.TabIndex = 3;
|
||||
acceptRemoteVolumeBox.TabIndex = 4;
|
||||
checkForUpdatesOnStartupBox.TabIndex = 5;
|
||||
updateFrequencyBox.TabIndex = 6;
|
||||
checkForUpdatesNowButton.TabIndex = 7;
|
||||
silentlyInstallUpdatesBox.TabIndex = 8;
|
||||
upnpEnabledBox.TabIndex = 9;
|
||||
loggingBox.TabIndex = 10;
|
||||
writeLogsNowButton.TabIndex = 11;
|
||||
closeButton.TabIndex = 12;
|
||||
|
||||
// Group the frequency label + combo on one FlowLayoutPanel row so the visible label
|
||||
// sits inline next to the combo while keeping the combo as the focusable target.
|
||||
@@ -375,21 +462,34 @@ internal sealed class PreferencesDialog : Form
|
||||
freqRow.Controls.Add(updateFrequencyLabel);
|
||||
freqRow.Controls.Add(updateFrequencyBox);
|
||||
|
||||
// Wrap label + list as one logical group so they share the same row in the
|
||||
// top-level layout. The label's Alt+N mnemonic focuses the list when activated.
|
||||
// Group the cue label + list + the two action buttons into a single panel that
|
||||
// occupies one row in the outer layout. The action buttons sit side-by-side under
|
||||
// the list so they read as "buttons that act on the list above" without taking up
|
||||
// a second row of vertical space.
|
||||
var cueGroup = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoSize = true,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
RowCount = 3,
|
||||
};
|
||||
cueGroup.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
cueGroup.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
cueGroup.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
cueGroup.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
var cueActions = new FlowLayoutPanel
|
||||
{
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
WrapContents = false,
|
||||
Padding = new Padding(0, 4, 0, 0),
|
||||
};
|
||||
cueActions.Controls.Add(playSelectedCueButton);
|
||||
cueActions.Controls.Add(browseSelectedCueButton);
|
||||
cueGroup.Controls.Add(cueListLabel, 0, 0);
|
||||
cueGroup.Controls.Add(cueList, 0, 1);
|
||||
cueListLabel.Click += (_, _) => cueList.Focus();
|
||||
cueGroup.Controls.Add(cueActions, 0, 2);
|
||||
|
||||
panel.Controls.Add(browseProfilesFolderButton, 0, 0);
|
||||
panel.Controls.Add(cueGroup, 0, 1);
|
||||
@@ -429,6 +529,156 @@ internal sealed class PreferencesDialog : Form
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Refresh the Play and Browse action buttons so their visible text and
|
||||
/// AccessibleName reflect the currently-selected cue. Called on every selection change
|
||||
/// in the cue listbox AND immediately after a Browse pick (the "(custom)" tag flips
|
||||
/// based on whether a custom path is set). When the selection is empty — e.g. the
|
||||
/// listbox briefly clears during a profile reload — both buttons get a generic label
|
||||
/// and are disabled so a stray click can't act on a stale index.</summary>
|
||||
private void RefreshCueActionButtons(RemSoundSettingsStore settings)
|
||||
{
|
||||
var idx = cueList.SelectedIndex;
|
||||
if (idx < 0 || idx >= CueRows.Length)
|
||||
{
|
||||
playSelectedCueButton.Text = "&Play selected sound";
|
||||
playSelectedCueButton.AccessibleName = "Play selected sound";
|
||||
playSelectedCueButton.Enabled = false;
|
||||
browseSelectedCueButton.Text = "&Browse for selected sound...";
|
||||
browseSelectedCueButton.AccessibleName = "Browse for selected sound";
|
||||
browseSelectedCueButton.Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var cue = CueRows[idx];
|
||||
playSelectedCueButton.Enabled = true;
|
||||
playSelectedCueButton.Text = $"&Play {cue.DisplayName.ToLowerInvariant()}";
|
||||
playSelectedCueButton.AccessibleName = $"Play {cue.DisplayName.ToLowerInvariant()}";
|
||||
|
||||
var customPath = settings.LoadCustomCuePath(cue.CueId);
|
||||
browseSelectedCueButton.Enabled = true;
|
||||
if (string.IsNullOrWhiteSpace(customPath))
|
||||
{
|
||||
browseSelectedCueButton.Text = $"&Browse for {cue.DisplayName.ToLowerInvariant()}...";
|
||||
browseSelectedCueButton.AccessibleName = $"Browse for {cue.DisplayName.ToLowerInvariant()}, currently using the default sound";
|
||||
}
|
||||
else
|
||||
{
|
||||
var filename = Path.GetFileName(customPath);
|
||||
browseSelectedCueButton.Text = $"&Browse for {cue.DisplayName.ToLowerInvariant()}... (custom)";
|
||||
browseSelectedCueButton.AccessibleName = $"Browse for {cue.DisplayName.ToLowerInvariant()}, currently using your file {filename}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves the WAV file currently configured for a cue: the user's custom
|
||||
/// override if set and on disk, otherwise the bundled default in <c>sounds\</c>.
|
||||
/// Returns null when neither resolves to an existing file (typical for save.wav /
|
||||
/// profile.wav before the project owner supplies them) so the caller can stay silent.
|
||||
/// Mirrors the resolution order in MainForm.TryLoadCueSound — the Play button must
|
||||
/// preview exactly what the cue would play if it fired now. Reads through the settings
|
||||
/// cache so we see whatever the user has changed in this dialog session, including
|
||||
/// custom paths not yet persisted to the profile JSON.</summary>
|
||||
private static string? ResolveCueFilePath(CueRowDescriptor cue, RemSoundSettingsStore settings)
|
||||
{
|
||||
var customPath = settings.LoadCustomCuePath(cue.CueId);
|
||||
if (!string.IsNullOrWhiteSpace(customPath) && File.Exists(customPath))
|
||||
{
|
||||
return customPath;
|
||||
}
|
||||
// Default WAV filename is built from the cue ID — same convention as MainForm.
|
||||
// The dictionary kept here makes the mapping explicit and lets us pretty-print
|
||||
// "record start" / "record stop" with the space rather than the cue ID's hyphen.
|
||||
var defaultFileName = cue.CueId switch
|
||||
{
|
||||
MainForm.CueId.Connect => "connect.wav",
|
||||
MainForm.CueId.Disconnect => "disconnect.wav",
|
||||
MainForm.CueId.RecordStart => "record start.wav",
|
||||
MainForm.CueId.RecordStop => "record stop.wav",
|
||||
MainForm.CueId.Save => "save.wav",
|
||||
MainForm.CueId.ProfileSwitch => "profile.wav",
|
||||
_ => null,
|
||||
};
|
||||
if (defaultFileName is null) return null;
|
||||
var defaultPath = Path.Combine(AppContext.BaseDirectory, "sounds", defaultFileName);
|
||||
return File.Exists(defaultPath) ? defaultPath : null;
|
||||
}
|
||||
|
||||
/// <summary>Preview a cue's currently-configured WAV through the system default audio
|
||||
/// output. Plays asynchronously (SoundPlayer.Play loads + plays on a thread-pool thread),
|
||||
/// so the dialog stays responsive even if the file is briefly slow to load. When no file
|
||||
/// resolves — e.g. a cue without a default WAV and no custom path — show a small popup
|
||||
/// so the user knows why nothing happened, rather than silently doing nothing and
|
||||
/// leaving them wondering whether the Play button worked.</summary>
|
||||
private void OnPlayClicked(CueRowDescriptor cue, RemSoundSettingsStore settings)
|
||||
{
|
||||
var path = ResolveCueFilePath(cue, settings);
|
||||
if (path is null)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
$"No sound is currently configured for the {cue.DisplayName.ToLowerInvariant()}. " +
|
||||
$"Use the Browse button on this row to pick a WAV file.",
|
||||
"RemSound", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var sp = new System.Media.SoundPlayer(path);
|
||||
sp.Play();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
$"Could not play {Path.GetFileName(path)}: {ex.Message}",
|
||||
"RemSound", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Open a WAV file picker for the given cue. The picker defaults to the user's
|
||||
/// previously-set custom path if one exists, falling back to the bundled sounds\ folder
|
||||
/// next to RemSound.exe — so picking a file from inside that folder is treated as
|
||||
/// "use the default" and the override is cleared rather than re-pointed at the same
|
||||
/// file (which would leave the user stuck with a stale copy if a future RemSound update
|
||||
/// replaces the default WAV). Writes through the settings cache, since custom cue paths
|
||||
/// are per-profile — clearing here also flips ChangedAnyProfileSetting so the save-prompt
|
||||
/// fires on the way out.</summary>
|
||||
private void OnBrowseClicked(Button btn, CueRowDescriptor cue, RemSoundSettingsStore settings)
|
||||
{
|
||||
var soundsFolder = Path.Combine(AppContext.BaseDirectory, "sounds");
|
||||
var existing = settings.LoadCustomCuePath(cue.CueId);
|
||||
var initialDir = !string.IsNullOrWhiteSpace(existing) && File.Exists(existing)
|
||||
? Path.GetDirectoryName(existing) ?? soundsFolder
|
||||
: soundsFolder;
|
||||
using var picker = new OpenFileDialog
|
||||
{
|
||||
Title = $"Choose a WAV file for {cue.DisplayName}",
|
||||
Filter = "WAV files (*.wav)|*.wav",
|
||||
CheckFileExists = true,
|
||||
InitialDirectory = initialDir,
|
||||
DereferenceLinks = true,
|
||||
};
|
||||
if (picker.ShowDialog(this) != DialogResult.OK) return;
|
||||
if (string.IsNullOrWhiteSpace(picker.FileName)) return;
|
||||
|
||||
var pickedFullPath = Path.GetFullPath(picker.FileName);
|
||||
var soundsFolderFullPath = Path.GetFullPath(soundsFolder);
|
||||
|
||||
// If the user picked a file inside the bundled sounds\ folder, treat it as a "use
|
||||
// default" — clear the override rather than store the path. Avoids freezing the
|
||||
// user on a specific shipped-default file across updates.
|
||||
if (pickedFullPath.StartsWith(soundsFolderFullPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
settings.SaveCustomCuePath(cue.CueId, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.SaveCustomCuePath(cue.CueId, pickedFullPath);
|
||||
}
|
||||
ChangedAnyProfileSetting = true;
|
||||
// Refresh the visible action-button labels so the "(custom)" tag appears or
|
||||
// disappears right away. Belt-and-braces: the caller also refreshes, but doing it
|
||||
// here makes the function self-consistent.
|
||||
RefreshCueActionButtons(settings);
|
||||
}
|
||||
|
||||
/// <summary>Pull the latest UPnP snapshot and update the inline status label. Always
|
||||
/// called on the UI thread (either inline from a change handler or marshaled in from
|
||||
/// the StatusChanged subscription).</summary>
|
||||
|
||||
@@ -33,6 +33,12 @@ internal sealed class RecordingController
|
||||
|
||||
public bool IsRecording => active is not null;
|
||||
|
||||
/// <summary>UTC clock at which the current recording started, or null when nothing is
|
||||
/// recording. Captured by <see cref="Start"/> and cleared by <see cref="Stop"/>. Used
|
||||
/// by the system-tray tooltip builder in MainForm to surface "recording for MM:SS"
|
||||
/// alongside the peer count. 2026-05-28.</summary>
|
||||
public DateTime? RecordingStartedUtc { get; private set; }
|
||||
|
||||
/// <summary>Optional callback fired when the user starts or stops a recording. The
|
||||
/// MainForm hooks this to flip the menu item text "Start recording" ↔ "Stop recording"
|
||||
/// and announce the change to NVDA.</summary>
|
||||
@@ -64,6 +70,7 @@ internal sealed class RecordingController
|
||||
// whether to actually write the samples.
|
||||
sender.OnSentSamples = active.WriteSent;
|
||||
receiver.OnReceivedSamples = active.WriteReceived;
|
||||
RecordingStartedUtc = DateTime.UtcNow;
|
||||
diagnostic($"recording: started → {active.FilePath} (source={s.Source}, format={s.FileFormat}, channels={s.ChannelMode})");
|
||||
RecordingStateChanged?.Invoke(true);
|
||||
}
|
||||
@@ -81,6 +88,7 @@ internal sealed class RecordingController
|
||||
receiver.OnReceivedSamples = null;
|
||||
|
||||
active = null;
|
||||
RecordingStartedUtc = null;
|
||||
try
|
||||
{
|
||||
recorder.Stop();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>3.0.2</Version>
|
||||
<Version>3.1.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -57,24 +57,37 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Peer-state notification sounds. Copied next to RemSound.exe so SoundPlayer can find
|
||||
them via AppContext.BaseDirectory. -->
|
||||
<Content Include="..\..\connect.wav">
|
||||
<Link>connect.wav</Link>
|
||||
<!-- All cue sounds live in a sounds\ subfolder under the publish output (moved out of
|
||||
the install-root flat layout 2026-05-28). Default cue WAVs ship inside that folder;
|
||||
user-supplied custom cue paths set in Preferences override the defaults at runtime.
|
||||
Filenames containing a space (e.g. "record start.wav") are preserved verbatim on
|
||||
copy so the load-by-filename path in TryLoadCueSound finds them exactly as written. -->
|
||||
<Content Include="..\..\sounds\connect.wav">
|
||||
<Link>sounds\connect.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\disconnect.wav">
|
||||
<Link>disconnect.wav</Link>
|
||||
<Content Include="..\..\sounds\disconnect.wav">
|
||||
<Link>sounds\disconnect.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- Recording start/stop cues. Filenames contain a space — preserve on copy so the
|
||||
load-by-filename path in TryLoadCueSound finds them exactly as written. -->
|
||||
<Content Include="..\..\record start.wav">
|
||||
<Link>record start.wav</Link>
|
||||
<Content Include="..\..\sounds\record start.wav">
|
||||
<Link>sounds\record start.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\record stop.wav">
|
||||
<Link>record stop.wav</Link>
|
||||
<Content Include="..\..\sounds\record stop.wav">
|
||||
<Link>sounds\record stop.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- New cues introduced 2026-05-28: save.wav fires after a successful Save / Save As;
|
||||
profile.wav fires immediately after a profile finishes loading. Both default WAVs
|
||||
to be supplied by the project owner; absent files are gracefully ignored at load
|
||||
time (the cue just doesn't play) so the build is fine without them. -->
|
||||
<Content Include="..\..\sounds\save.wav" Condition="Exists('..\..\sounds\save.wav')">
|
||||
<Link>sounds\save.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\sounds\profile.wav" Condition="Exists('..\..\sounds\profile.wav')">
|
||||
<Link>sounds\profile.wav</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- User manual. F1 anywhere in the app opens this via the user's default browser
|
||||
|
||||
@@ -97,11 +97,25 @@ public sealed class Profile
|
||||
/// falls back to the legacy <see cref="MuteConnectionCues"/> migration path; once the
|
||||
/// user touches the new UI we write a concrete <c>true</c>/<c>false</c> and the legacy
|
||||
/// field stops mattering. Defaults to "play the sound" (true) for both cases — the
|
||||
/// audio cues are part of the normal user feedback loop, not opt-in. 2026-05-15.</summary>
|
||||
/// audio cues are part of the normal user feedback loop, not opt-in. 2026-05-15.
|
||||
///
|
||||
/// EnableSaveCue / EnableProfileSwitchCue added 2026-05-28 alongside the new
|
||||
/// save.wav / profile.wav defaults. Same nullable + default-true semantics as the rest.</summary>
|
||||
public bool? EnableConnectCue { get; set; }
|
||||
public bool? EnableDisconnectCue { get; set; }
|
||||
public bool? EnableRecordStartCue { get; set; }
|
||||
public bool? EnableRecordStopCue { get; set; }
|
||||
public bool? EnableSaveCue { get; set; }
|
||||
public bool? EnableProfileSwitchCue { get; set; }
|
||||
|
||||
/// <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>profile-switch</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
|
||||
/// 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".
|
||||
/// Empty dictionary on a fresh profile.</summary>
|
||||
public Dictionary<string, string> CustomCuePaths { get; set; } = new();
|
||||
public int MaxLatencyMs { get; set; } = 80;
|
||||
public int Smoothness { get; set; } = 3;
|
||||
public bool ContinuousAutoTuneEnabled { get; set; }
|
||||
|
||||
@@ -408,6 +408,60 @@ public sealed class RemSoundSettingsStore
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public bool LoadEnableSaveCue() =>
|
||||
Try(() => Load()?.EnableSaveCue) ?? true;
|
||||
|
||||
public void SaveEnableSaveCue(bool value)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.EnableSaveCue = value;
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public bool LoadEnableProfileSwitchCue() =>
|
||||
Try(() => Load()?.EnableProfileSwitchCue) ?? true;
|
||||
|
||||
public void SaveEnableProfileSwitchCue(bool value)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.EnableProfileSwitchCue = value;
|
||||
Save(s);
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// profiles can carry different cue palettes.</summary>
|
||||
public string? LoadCustomCuePath(string cueId)
|
||||
{
|
||||
return Try(() =>
|
||||
{
|
||||
var s = Load();
|
||||
if (s?.CustomCuePaths is { } dict && dict.TryGetValue(cueId, out var path)
|
||||
&& !string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
return (string?)null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Set a custom WAV path for a given cue. Pass null or empty to clear the
|
||||
/// override (the cue reverts to the bundled default in <c>sounds\</c>).</summary>
|
||||
public void SaveCustomCuePath(string cueId, string? path)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.CustomCuePaths ??= new Dictionary<string, string>();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
s.CustomCuePaths.Remove(cueId);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.CustomCuePaths[cueId] = path;
|
||||
}
|
||||
Save(s);
|
||||
}
|
||||
|
||||
/// <summary>The whole recording-settings bag for the current profile. Loaded as a
|
||||
/// CLONE so callers can mutate the returned object without inadvertently writing
|
||||
/// back to the cache. Save flushes the object atomically.</summary>
|
||||
@@ -531,6 +585,12 @@ public sealed class RemSoundSettingsStore
|
||||
EnableDisconnectCue = profile.EnableDisconnectCue,
|
||||
EnableRecordStartCue = profile.EnableRecordStartCue,
|
||||
EnableRecordStopCue = profile.EnableRecordStopCue,
|
||||
EnableSaveCue = profile.EnableSaveCue,
|
||||
EnableProfileSwitchCue = profile.EnableProfileSwitchCue,
|
||||
// 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
|
||||
// the session and is written back via CopyTo on save.
|
||||
CustomCuePaths = profile.CustomCuePaths is null ? new() : new Dictionary<string, string>(profile.CustomCuePaths),
|
||||
RecordingSettings = profile.RecordingSettings?.Clone() ?? new RecordingSettings(),
|
||||
};
|
||||
}
|
||||
@@ -581,6 +641,11 @@ public sealed class RemSoundSettingsStore
|
||||
profile.EnableDisconnectCue = s.EnableDisconnectCue;
|
||||
profile.EnableRecordStartCue = s.EnableRecordStartCue;
|
||||
profile.EnableRecordStopCue = s.EnableRecordStopCue;
|
||||
profile.EnableSaveCue = s.EnableSaveCue;
|
||||
profile.EnableProfileSwitchCue = s.EnableProfileSwitchCue;
|
||||
profile.CustomCuePaths = s.CustomCuePaths is null
|
||||
? new Dictionary<string, string>()
|
||||
: new Dictionary<string, string>(s.CustomCuePaths);
|
||||
if (s.RecordingSettings is RecordingSettings rs) profile.RecordingSettings = rs.Clone();
|
||||
}
|
||||
|
||||
@@ -646,6 +711,9 @@ public sealed class RemSoundSettingsStore
|
||||
public bool? EnableDisconnectCue { get; set; }
|
||||
public bool? EnableRecordStartCue { get; set; }
|
||||
public bool? EnableRecordStopCue { get; set; }
|
||||
public bool? EnableSaveCue { get; set; }
|
||||
public bool? EnableProfileSwitchCue { get; set; }
|
||||
public Dictionary<string, string>? CustomCuePaths { get; set; }
|
||||
public RecordingSettings? RecordingSettings { get; set; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user