v4.1: two screen-reader fixes — silent start-in-tray, announce on restore

- Starting straight into the tray (StartMinimised / --minimized) no longer plays the "minimise"
  cue; only a genuine user minimise does (the startup path passes playCue:false)
- Restoring the window from the tray now lands focus on a real named control on the active tab so
  NVDA announces it, instead of resting on the role-less QuietTabControl and surfacing silently
- Shared the focus-a-leaf-for-announcement helper between the main window and Preferences

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-13 15:59:59 +01:00
co-authored by Claude Opus 4.8
parent a408d2b56e
commit 5abbeefe29
7 changed files with 73 additions and 54 deletions
+9 -28
View File
@@ -1,48 +1,29 @@
# RemSound v4.0
# RemSound v4.1
A big step up for audible feedback and for getting the latency right by itself. RemSound now has a sound for nearly everything you do, can click as you type, has a clearer four-tab Preferences window, and tunes latency more intelligently and more quietly.
A small follow-up to v4.0, with two fixes for screen-reader users.
## A sound for nearly everything
## Starting in the tray is silent
On top of the connect, disconnect and recording cues, RemSound can now play a short sound when you:
When RemSound starts straight into the notification area (**Start minimised**, or `--minimized`), it no longer plays the "minimise" cue. Booting into the tray isn't you choosing to hide the window, so it shouldn't sound the cue — only a genuine minimise does.
* turn **sending** or **receiving** on or off (a separate sound for each),
* **minimise** to or **return** from the notification area,
* **tick or untick** any box, and
* **switch between tabs**.
## Bringing the window back announces itself
Every one is optional. Under **Options → Preferences → Audio cues** you can silence any cue, pick which built-in sound it uses, or **Browse** for your own WAV file. (The receive-audio on/off sounds were a request on the issue tracker — they're here now.)
## Hear yourself type
RemSound can **click softly as you type** into any box, so you get an audible sense of your keystrokes — with a distinct sound in **password fields** so you always know which kind of box you're in. One tick under Audio cues turns it on or off.
## Preferences, reorganised
The Preferences window is now four clear tabs — **General**, **Audio cues**, **Startup behaviour** and **Update settings** — so everything is easier to find. The startup options (start with Windows, start minimised, start with a chosen profile) have moved here from the Options menu.
## Smarter, quieter latency tuning
The automatic latency tuning used to treat every tiny audio glitch as "the buffer is too small" and keep adding delay — even when the real cause was the receiving computer's own sound card stumbling, which more buffer can't fix. It now tells the two apart, so it stops piling on delay it can't help. And when it does lower the latency, it eases the buffer down smoothly instead of trimming it, so you no longer hear little clicks while it tunes.
## Default sounds that updates can refresh
The built-in default sounds now travel with the program itself, so an update can refresh them — if a better default sound ships in a future update, you'll actually get it. Your own chosen sounds are kept exactly as you set them.
When you restore RemSound from the tray, it now lands focus on a control on whichever tab you'd left showing, so your screen reader announces the window instead of surfacing silently.
## Compatibility
**v4.0 talks to v3.3 through v3.9 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
**v4.1 talks to v3.3 through v4.0 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
## Install
1. Download `RemSound-v4.0.zip` from this release.
1. Download `RemSound-v4.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 won't touch your settings, profiles, logs or recordings.
4. Run `RemSound.exe`.
## Upgrading
**From v3.6 or newer:** Help → Check for updates installs v4.0 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was. The first launch tidies your settings into their current home if they aren't there already; nothing is lost.
**From v3.6 or newer:** Help → Check for updates installs v4.1 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
**From v1.9v3.5:** Check for updates works, but uses your current version's older updater for this one hop. If auto-update has been failing on your machine, install by hand using the steps above.
+11
View File
@@ -20,6 +20,17 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary>
private const string ReleaseNotes =
"""
RemSound v4.1
A couple of small fixes for screen-reader users. When
RemSound starts straight into the notification area, it no
longer plays the "minimise" sound starting in the tray
isn't the same as you choosing to hide the window, so it
shouldn't sound the cue. And when you bring the window back
from the tray, RemSound now lands on a control on whichever
tab you'd left open, so your screen reader announces the
window instead of coming up silent.
RemSound v4.0
RemSound now has a sound for nearly everything you do.
+30
View File
@@ -27,6 +27,36 @@ internal static class WinEventNotifier
NotifyWinEvent(EVENT_OBJECT_FOCUS, control.Handle, OBJID_CLIENT, CHILDID_SELF);
}
}
/// <summary>Land NVDA on a named control so a freshly-shown window or dialog announces itself
/// instead of surfacing silently. Focuses the first focusable leaf inside <paramref name="page"/>
/// (falling back to <paramref name="fallback"/>), forcing a genuine focus CHANGE (ActiveControl=null
/// first) and re-firing the MSAA focus event. Used by the Preferences dialog on open and by the
/// main window when it's restored from the tray. The page sits inside a <see cref="QuietTabControl"/>
/// whose own accessible object is deliberately role-less and nameless, so focusing the tab control
/// itself would be silent — this finds a real leaf (the first interactive control on whatever tab
/// is showing) instead. Best run deferred (BeginInvoke), after the show/foreground settles.</summary>
public static void AnnounceByFocusingLeaf(ContainerControl form, Control? page, Control fallback)
{
var leaf = FirstFocusableLeaf(page) ?? fallback;
form.ActiveControl = null;
leaf.Focus();
if (leaf.IsHandleCreated) NotifyFocus(leaf);
}
/// <summary>The first visible, enabled, tab-stop control inside <paramref name="container"/>,
/// searched depth-first in child order (which matches the order controls were added). Returns a
/// real leaf (button / list / combo / checkbox) — never a layout panel or the role-less tab strip.</summary>
private static Control? FirstFocusableLeaf(Control? container)
{
if (container is null) return null;
foreach (Control c in container.Controls)
{
if (c is { CanSelect: true, TabStop: true, Visible: true, Enabled: true }) return c;
if (FirstFocusableLeaf(c) is { } nested) return nested;
}
return null;
}
}
/// <summary>
+13 -1
View File
@@ -606,6 +606,16 @@ public sealed class MainForm : Form
{
if (InvokeRequired) { BeginInvoke(new Action(RestoreFromTray)); return; }
trayController.Restore();
// Land focus on a real, named control on whichever tab is showing, so NVDA announces
// something when the window comes back from the tray. Without this, focus rests on the
// QuietTabControl (deliberately role-less / nameless) and a screen reader has nothing to
// read, so the window surfaces silently — the same issue the Preferences dialog had.
// Deferred so it runs after the show/foreground settles.
BeginInvoke(new Action(() =>
{
if (IsDisposed) return;
WinEventNotifier.AnnounceByFocusingLeaf(this, mainTabControl.SelectedTab, mainTabControl);
}));
}
catch { /* best-effort — surfacing the window is a convenience, not load-critical */ }
}
@@ -1265,7 +1275,9 @@ public sealed class MainForm : Form
startNextInstanceMinimized = false;
if (minimizeThisInstance)
{
BeginInvoke(() => trayController.Minimize());
// playCue:false — starting up in the tray (StartMinimised / --minimized) isn't the
// user choosing to minimise, so it must not sound the "minimise" cue.
BeginInvoke(() => trayController.Minimize(playCue: false));
}
// Kick off UPnP discovery if the user has the box ticked. Off by default; the
+8 -4
View File
@@ -227,13 +227,17 @@ internal sealed class MainFormTrayController : IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
public void Minimize()
/// <summary>Hide the window to the tray. <paramref name="playCue"/> defaults true for a genuine
/// user-initiated minimise; the startup-minimise path passes false because that's not the user
/// choosing to hide the window — and the usual "was it visible?" guard can't tell the two apart,
/// since by the time the deferred startup minimise runs the window HAS been shown (Visible=true).</summary>
public void Minimize(bool playCue = true)
{
// Only sound the "hide" cue on a genuine shown -> hidden transition (not a startup-minimise
// before the window has ever been shown, nor a repeat call once already hidden).
// Only sound the "hide" cue on a genuine shown -> hidden transition the user asked for: not a
// startup-minimise (playCue:false), and not a repeat call once already hidden (wasVisible).
var wasVisible = owner.Visible;
owner.Hide();
if (wasVisible) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
if (wasVisible && playCue) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
// Refresh the tooltip BEFORE showing the icon so the shell's NIM_ADD call carries
// the current live state (peer count, send / receive routing, recording timer),
// not a stale "starting up" string set earlier. The shell tends to cache hover
+1 -20
View File
@@ -731,29 +731,10 @@ internal sealed class PreferencesDialog : Form
{
if (IsDisposed) return;
if (tabs.TabCount > 0) tabs.SelectedIndex = 0;
var leaf = FirstTabStopLeaf(tabs.SelectedTab) ?? (Control)tabs;
ActiveControl = null;
leaf.Focus();
if (leaf.IsHandleCreated) WinEventNotifier.NotifyFocus(leaf);
WinEventNotifier.AnnounceByFocusingLeaf(this, tabs.SelectedTab, tabs);
}));
}
/// <summary>The first visible, enabled, tab-stop control inside <paramref name="container"/>,
/// searched depth-first in child order (which matches the order controls were added to each tab).
/// Returns a real leaf the dialog can focus on open so NVDA has a named control to announce —
/// never a layout panel or the role-less tab strip.</summary>
private static Control? FirstTabStopLeaf(Control? container)
{
if (container is null) return null;
foreach (Control c in container.Controls)
{
if (c is { CanSelect: true, TabStop: true, Visible: true, Enabled: true })
return c;
if (FirstTabStopLeaf(c) is { } nested) return nested;
}
return null;
}
/// <summary>Wire up the Startup behaviour tab (moved here from StartupBehaviourDialog): load the
/// current state, populate the profile list, and persist each change immediately to AppConfig /
/// the Windows auto-start registry entry, exactly as the old dialog did.</summary>
+1 -1
View File
@@ -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>4.0</Version>
<Version>4.1</Version>
</PropertyGroup>
<ItemGroup>