Release v4.2: fix connect-freeze (#10) and new-profile minimize (#12); smoother WASAPI timing

- Connect no longer freezes: PushDiscoveryUnicastHints resolved remembered
  hostnames with synchronous Dns.GetHostAddresses on the UI thread, blocking the
  whole window for the DNS timeout on an unresolvable name. A screen-reader user
  experiences that as the entire machine locking up. Resolution now runs off the
  UI thread. Same class of bug as the v3.0.1 UPnP-on-the-UI-thread hang. (#10)

- New profile / profile switch no longer hides the window: OnShown ORed the
  global StartMinimised into every instance, so creating a new profile while
  Start minimised was on dropped the window to the tray and looked like a crash.
  StartMinimised now applies only to a genuine cold launch; relaunches honour the
  explicit per-instance flag. (#12)

- Smoother WASAPI audio: the receive producer loop and sender mix loop pace
  themselves with WaitHandle.WaitOne, bound by the system timer (~15.6ms default).
  Without a fine timer the 10ms feed slips to ~16-31ms and delivers audio in
  chunky bursts (the desktop-render chunkiness behind Andre's dropouts/lag). New
  SystemTimerResolution holds a 1ms timer whenever a stream is live, independent
  of the opt-in Priority mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-15 09:21:04 +01:00
co-authored by Claude Opus 4.8
parent 5abbeefe29
commit 034a7de632
7 changed files with 165 additions and 47 deletions
+30
View File
@@ -20,6 +20,36 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary>
private const string ReleaseNotes =
"""
RemSound v4.2
Three fixes, one of them a real annoyance gone.
Connecting to someone no longer risks a freeze.
If you had a peer saved by name and that name
couldn't be looked up quickly, RemSound used to
stall for a few seconds while it waited and
because your screen reader waits on RemSound, the
whole computer could seem to lock up. That lookup
now happens out of the way, so connecting stays
responsive.
Creating a new profile (Ctrl+N), or switching
profiles, no longer drops the window to the tray
when you have "start minimised" turned on.
Starting minimised is meant for when RemSound
first launches, not for something you did on
purpose so a new profile now comes up the normal
way (or stays in the tray only if that's where you
already were), instead of vanishing and looking
like a crash.
And WASAPI audio is smoother. RemSound now keeps
Windows' timing fine while it's streaming, so audio
moves in even steps instead of arriving in clumps.
On machines where playback was breaking up or
running laggy on WASAPI, this should help and you
no longer need Priority mode switched on to get it.
RemSound v4.1
A couple of small fixes for screen-reader users. When
+59 -37
View File
@@ -1265,13 +1265,21 @@ public sealed class MainForm : Form
// missing (deferred so it lands after the window is fully up and can surface).
BeginInvoke(CheckForMissingEnabledCues);
// Honour AppConfig.StartMinimised drop straight to the tray after the
// window finishes loading. Wrapped in BeginInvoke so the minimise happens
// *after* Shown completes (otherwise the form-show + form-hide collide and
// some virtual-machine drivers throw a redraw exception). The pending-profile
// apply path above is unaffected — settings/devices/peers are already wired
// up before we hide the window.
var minimizeThisInstance = AppConfig.Load().StartMinimised || startNextInstanceMinimized;
// "Start minimised" is a COLD-BOOT preference: drop straight to the tray when the app
// first launches. It must NOT apply when the user deliberately creates a new profile or
// switches profiles — those relaunch the window through the Program.cs loop. Those paths
// already set startNextInstanceMinimized to "stay in the tray only if we were already
// there", which is the right intent; but ORing in the global StartMinimised used to
// override that and hide the window on every new-profile / switch, which read to the user
// as a crash (issue #12). So gate StartMinimised to the genuine first launch, and let
// relaunches honour only the explicit per-instance flag.
// BeginInvoke so the minimise happens *after* Shown completes (otherwise the form-show +
// form-hide collide and some virtual-machine drivers throw a redraw exception). The
// pending-profile apply path above is unaffected — settings/devices/peers are already
// wired up before we hide the window.
var coldStart = isFirstLaunch;
isFirstLaunch = false;
var minimizeThisInstance = startNextInstanceMinimized || (coldStart && AppConfig.Load().StartMinimised);
startNextInstanceMinimized = false;
if (minimizeThisInstance)
{
@@ -1783,6 +1791,12 @@ public sealed class MainForm : Form
// "loading audio driver" splash in that case.
internal static bool startNextInstanceMinimized;
// True until the first MainForm of the process has shown its window. Distinguishes a genuine
// cold launch (where the "Start minimised" preference applies) from an in-session new-profile or
// profile-switch relaunch (where it must NOT — those would otherwise hide the window and look
// like a crash, issue #12). Flipped false in the first OnShown.
private static bool isFirstLaunch = true;
/// <summary>Switch to the profile at <paramref name="path"/> via the same close-and-relaunch
/// flow OpenProfileFromPicker uses. The active profile gets pushed to the front of the
/// recents list by the next MainForm constructor when it sees the loaded path.</summary>
@@ -5062,43 +5076,51 @@ public sealed class MainForm : Form
/// </summary>
private void PushDiscoveryUnicastHints()
{
var hints = new HashSet<IPAddress>();
// Manual peers store IPEndPoint already.
foreach (var peer in manualPeers.Values)
// Snapshot the UI-thread-owned inputs HERE, then resolve hostnames OFF the UI thread.
//
// The comment that used to live here claimed Dns.GetHostAddresses "returns near-instantly".
// It does for a parsed IP or an already-cached name — but for a remembered HOSTNAME that
// can't currently resolve (an offline peer, or a Tailscale/WireGuard name while the VPN is
// down) it BLOCKS for the system DNS timeout, seconds per entry. This method runs on the UI
// thread on every connect / disconnect / add-peer (it's how discovery learns its VPN unicast
// targets), so that block froze the whole window for a few seconds — which a screen-reader
// user experiences as the entire machine locking up (issue #10). Same class of bug as the
// v3.0.1 UPnP-on-the-UI-thread hang, in a newer feature.
//
// SetUnicastPeerAddresses just swaps a snapshot reference and fires an announcement, and is
// already called from the discovery receive loop's own thread, so it's safe to call from a
// background thread here. The hints are advisory and re-pushed frequently, so a slightly
// stale result from an overlapping resolution is harmless.
var seedAddresses = manualPeers.Values.Select(p => p.Address).ToList();
var rememberedEntries = settings.LoadRememberedPeers().ToList();
Task.Run(() =>
{
hints.Add(peer.Address);
}
// Remembered peers are stored as string entries (IP or hostname). Try to parse as IP;
// for hostnames try a quick non-blocking DNS lookup. We do this synchronously here
// because the remembered list is small (typically 110 entries) and Dns.GetHostAddresses
// returns near-instantly for either a parsed IP or a cached hostname.
foreach (var entry in settings.LoadRememberedPeers())
{
if (string.IsNullOrWhiteSpace(entry)) continue;
if (IPAddress.TryParse(entry, out var direct))
var hints = new HashSet<IPAddress>(seedAddresses);
foreach (var entry in rememberedEntries)
{
hints.Add(direct);
continue;
}
try
{
foreach (var addr in Dns.GetHostAddresses(entry))
if (string.IsNullOrWhiteSpace(entry)) continue;
if (IPAddress.TryParse(entry, out var direct))
{
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
hints.Add(direct);
continue;
}
try
{
foreach (var addr in Dns.GetHostAddresses(entry))
{
hints.Add(addr);
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
hints.Add(addr);
}
}
}
catch
{
// Not resolvable right now — skip; re-pushed next time this method runs.
}
}
catch
{
// Hostname not resolvable right now — skip silently. Will retry next time
// PushDiscoveryUnicastHints is called.
}
}
discovery.SetUnicastPeerAddresses(hints);
discovery.SetUnicastPeerAddresses(hints);
});
}
+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.1</Version>
<Version>4.2</Version>
</PropertyGroup>
<ItemGroup>