Bump to v3.0.1: move UPnP discovery off the UI thread (fixes hang on Andre's network)

Bug: ticking the "Automatically open my router for incoming connections
(UPnP)" box in Preferences could freeze the WinForms message pump until
Mono.Nat's NatUtility.StartDiscovery() returned. On Andre's setup it never
did — audio kept flowing (audio threads are independent of the UI thread)
but the window stopped repainting, the system-tray hotkey stopped
responding, and the only way out was Task Manager.

Pre-existing latent bug in the v2.1 UPnP code; we just shipped without
anyone exercising the path on a problematic network (multiple adapters /
VPN / SSDP-swallowing router).

Fix: three call sites moved off the UI thread via Task.Run -
  * MainForm OnShown (startup re-enable from saved AppConfig.UpnpEnabled)
  * MainForm Preferences applyUpnpEnabled callback (user ticks the box)
  * MainForm power-resume handler (Refresh() after sleep/wake)
RouterPortMapper.Start() returns "immediately" only when StartDiscovery
returns quickly; on a slow network it can block synchronously for many
seconds. Same is true of Stop()'s socket teardown and Refresh()'s
teardown-then-restart sequence. All three are now safely backgrounded.

StatusChanged is unaffected - it already fires on the mapper's own thread
and the PreferencesDialog handler BeginInvokes back to the UI thread.
Live status label updates correctly during the new background discovery.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-25 22:41:20 +01:00
co-authored by Claude Opus 4.7
parent af0e7c3fff
commit 5737550453
4 changed files with 79 additions and 52 deletions
+12 -39
View File
@@ -1,49 +1,24 @@
# RemSound v3.0 # RemSound v3.0.1
A big release with two things you'll actually notice — plus everything from the never-separately-released v2.2 work bundled in. v3.0 is a wire-format break: two v3.0 machines talk to each other perfectly, but a v3.0 machine talking to a v2.x machine will have very high latency until you upgrade the second machine too. See **Compatibility** below. Hot-fix for a bug in the **"Automatically open my router for incoming connections (UPnP)"** tickbox in Preferences.
## What's new ## What was broken
* **Opus "live latency" mode.** RemSound now sends sound in tiny 2.5-millisecond chunks instead of the usual 10 or 20 ms. End-to-end delay drops to about 5 ms of codec delay — close to PCM — perfect for playing along with someone in real time. Uses a bit more network bandwidth than the regular Opus mode but still a fraction of PCM. Best on a clean wired network. Pick it from the codec list as **"Opus, live latency — for jamming and monitoring"**. On some network setups — machines with several network adapters, a VPN connected, or a router that doesn't answer the way RemSound's UPnP library expects — ticking the UPnP box could freeze RemSound's window. Audio kept flowing (so peers stayed connected), but you couldn't open the window again, the system-tray hotkey stopped responding, and the only way out was to end the RemSound process from Task Manager.
* **RemSound reopens on the same profile after it updates itself.** If a silent update fires in the middle of a session, the session drops briefly while the new files swap in, then RemSound reopens with the same devices, peers and settings — you don't see the profile picker, and you don't have to be at the computer for it. The next time you launch RemSound yourself, your normal startup choice applies as before. ## Why it happened
## Other changes RemSound was running the router-discovery step on the same thread that draws the window, so when discovery couldn't get a quick answer from the router it blocked the window until it finished — which on some networks meant forever. v3.0.1 moves that work off to a background thread, so the window stays responsive while RemSound looks for the router. The live status label in Preferences still updates as discovery progresses — that was already on the right thread.
* **Codec list is now three choices, with clearer names.** The same fix is applied to the three places UPnP can kick off: at app startup (when you have UPnP ticked from a previous session), when you tick the box in Preferences, and after a sleep/resume (when RemSound re-pokes the router in case the mapping was dropped).
* `PCM 48K 24 bit — uncompressed` — best quality, ~2.3 Mbps
* `Opus, broadcast quality — loss tolerant` — ~200 kbps, ~12 ms codec delay (was "Opus high quality (20 ms)")
* `Opus, live latency — for jamming and monitoring` — ~320 kbps, ~5 ms codec delay (the new one)
The old `Opus lower quality (10 ms)` middle option has been retired — it sat between the other two without a clear reason to pick it. If your saved profile was using it, RemSound silently picks broadcast quality for you on first launch — slightly more delay, more loss tolerance. Pick "live latency" if you want the new low-latency mode. ## No other changes
* **Save on a locked (read-only) profile now goes through when you ask on purpose.** The lock still suppresses the automatic "save your changes?" prompt on close (its main job), but if you press Save (Ctrl+S) or pick File → Save deliberately, a one-time warning explains what's about to happen and lets you confirm or cancel. Once you tick "do not show again", future deliberate saves on a locked profile go through silently. Save as... is unchanged — it always works. Same wire format as v3.0, same codec list, same everything else. If you were already running v3.0 happily without UPnP turned on, this update fixes a problem you may not have hit — install it at your leisure.
## Includes everything from v2.2 (which was never separately released)
* **Native Opus encoder.** RemSound's Opus encoder used to put quite a lot of work on Windows' memory manager — about 4 megabytes per second of "throwaway" memory churn while sending Opus audio. v3.0 ships a native build of the same encoder that does its work in a tighter, faster way. The audio you hear is identical (it really is the same encoder, just packaged better); the memory churn drops by about 97 %. On laptops you should see less background CPU when streaming Opus, and longer sessions are less likely to see brief pauses while Windows tidies up memory.
* **Smaller all-round efficiency tidy-up.** RemSound checks the audio-device list less often, reuses some small bits of memory it used to make fresh each time, and skips some paperwork on the receive side when there's nothing to do. Each one is small on its own; together they cut everyday memory churn modestly.
* **New diagnostic log columns** (only emit when Enable logs is ticked):
* `cpu` — how much of one CPU core RemSound just used
* `memMB` / `wsMB` — managed heap and working set
* `allocKBps` — per-second memory-churn rate (lower is better)
* `captureMs / sendMs / recvMs / renderMs` — how busy each of the four audio threads is
* **Some old diagnostic columns removed.** `fanCacheMs`, `driftDrop`, `driftRep`, `driftAcc` are gone — they were always zero after the playback engine changed in May.
## Compatibility
**v3.0 is a wire-format break.** RemSound describes audio frame sizes to other RemSound machines using a slightly different unit on the wire (sample-count instead of milliseconds) so the new 2.5 ms mode can be expressed cleanly.
* **v3.0 ↔ v3.0**: works perfectly.
* **v3.0 ↔ v2.x**: audio still passes (the underlying Opus / PCM decode is unchanged) but the v2.x side will mis-read the frame-size announcement and build up too much buffer. Expect very high latency on that side until you upgrade it to v3.0.
To avoid the high-latency window: update **both** machines to v3.0 before your next session. The auto-updater on v1.9 and later will handle the upgrade for you, but the timing matters — if one machine updates before the other, latency will be high until the second machine catches up.
**Suppression flags** from v2.x for the "save was blocked on a read-only profile" dialog do **not** carry over. You'll see the new one-time warning once per machine. That's deliberate; the behaviour changed and you need to know.
## Install ## Install
1. Download `RemSound-v3.0.zip` from this release. 1. Download `RemSound-v3.0.1.zip` from this release.
2. Close RemSound. 2. Close RemSound.
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it 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. 4. Run `RemSound.exe`. Press F1 for the user manual.
@@ -52,8 +27,6 @@ Requires the .NET 10 Desktop Runtime. If it's missing, Windows offers to fetch i
## Upgrading ## Upgrading
**v1.9, v2.0, v2.1:** Help → Check for updates works — it will fetch and install v3.0 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.0 installs itself shortly after launch with a brief notice; RemSound then reopens on whichever profile you were running. **v1.9, v2.0, v2.1, v3.0:** Help → Check for updates works — it will fetch and install v3.0.1 automatically. If you've ticked "Check for updates on startup" and "Silently install updates", v3.0.1 installs itself shortly after launch with a brief notice; RemSound then reopens on whichever profile you were running (new in v3.0).
**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 but not apply it. Install v3.0 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.0.1 but not apply it. Install v3.0.1 by hand using the steps above — just this once. From the build you install onward, updates are automatic.
If you installed RemSound inside a synced folder (Dropbox etc.) and your install is v1.0 / v1.1 / v1.2, see the [v1.3 release notes](https://github.com/Ednunp/RemSound/releases/tag/v1.3) for one-time manual install steps.
+26
View File
@@ -20,6 +20,32 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary> /// updates" path.</summary>
private const string ReleaseNotes = private const string ReleaseNotes =
""" """
RemSound v3.0.1
Hot-fix for a bug in the "Automatically open my router
for incoming connections (UPnP)" tickbox.
On some network setups (machines with several network
adapters, a VPN connected, or a router that doesn't
answer the way RemSound's UPnP library expects) ticking
that box could freeze RemSound's window audio kept
flowing, but you couldn't open the window again, even
from the system tray. The only way out was to end the
process from Task Manager.
The fault was that RemSound was doing the router
discovery on the same thread that draws the window, so
a slow router (or no router answering at all) would
block the window until it finished which sometimes
was never. v3.0.1 moves that work off to a background
thread so the window stays responsive while RemSound
looks for the router.
Nothing else has changed from v3.0 same wire format,
same codec list, same everything. If you were already
running v3.0 happily, this update fixes a problem you
may not have hit; you can install it at your leisure.
RemSound v3.0 RemSound v3.0
A big release with two things you'll actually notice: A big release with two things you'll actually notice:
+40 -12
View File
@@ -1051,12 +1051,22 @@ public sealed class MainForm : Form
// Kick off UPnP discovery if the user has the box ticked. Off by default; the // Kick off UPnP discovery if the user has the box ticked. Off by default; the
// mapper itself coalesces redundant Start() calls so a re-enter via Shown after // mapper itself coalesces redundant Start() calls so a re-enter via Shown after
// a sleep cycle is harmless. // a sleep cycle is harmless. Run on a thread-pool thread because
// NatUtility.StartDiscovery() (Mono.Nat 3.0.4) sets up SSDP sockets on every
// network interface and CAN BLOCK FOR TENS OF SECONDS, or indefinitely, on
// unusual network setups (multiple adapters, VPNs, hostile firewalls, routers
// that swallow SSDP). Calling it on the UI thread freezes the WinForms message
// pump — Andre's v3.0 hang was this exact pattern. The status label still
// updates correctly because StatusChanged fires on the mapper's own thread and
// the PreferencesDialog handler BeginInvokes back to the UI thread. 2026-05-23.
var startupCfg = AppConfig.Load(); var startupCfg = AppConfig.Load();
if (startupCfg.UpnpEnabled) if (startupCfg.UpnpEnabled)
{ {
try { routerPortMapper.Start(); } Task.Run(() =>
catch (Exception ex) { logFile.Event($"upnp: start failed: {ex.GetType().Name}: {ex.Message}"); } {
try { routerPortMapper.Start(); }
catch (Exception ex) { logFile.Event($"upnp: start failed: {ex.GetType().Name}: {ex.Message}"); }
});
} }
// Startup update check — separate from the periodic timer because users who // Startup update check — separate from the periodic timer because users who
@@ -1751,17 +1761,28 @@ public sealed class MainForm : Form
applyUpnpEnabled: enabled => applyUpnpEnabled: enabled =>
{ {
// The persist already happened in the dialog; this callback only flips the // The persist already happened in the dialog; this callback only flips the
// live RouterPortMapper. Start kicks off discovery; Stop politely removes any // live RouterPortMapper. Start/Stop both run on a thread-pool thread because
// existing mapping. // NatUtility's discovery + socket teardown CAN BLOCK FOR TENS OF SECONDS, or
// indefinitely, on unusual network setups (multiple adapters, VPNs, hostile
// firewalls). Doing that on the UI thread here would freeze the
// Preferences dialog AND every other UI element until the call returned —
// Andre's v3.0 hang was triggered from this exact handler. See the longer
// explanation on the startup-time UPnP block in OnShown. 2026-05-23.
if (enabled) if (enabled)
{ {
try { routerPortMapper.Start(); } Task.Run(() =>
catch (Exception ex) { logFile.Event($"upnp: start from prefs failed: {ex.GetType().Name}: {ex.Message}"); } {
try { routerPortMapper.Start(); }
catch (Exception ex) { logFile.Event($"upnp: start from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
});
} }
else else
{ {
try { routerPortMapper.Stop(); } Task.Run(() =>
catch (Exception ex) { logFile.Event($"upnp: stop from prefs failed: {ex.GetType().Name}: {ex.Message}"); } {
try { routerPortMapper.Stop(); }
catch (Exception ex) { logFile.Event($"upnp: stop from prefs failed: {ex.GetType().Name}: {ex.Message}"); }
});
} }
}, },
getUpnpSnapshot: () => (routerPortMapper.Status, routerPortMapper.ExternalEndpoint, routerPortMapper.LastError), getUpnpSnapshot: () => (routerPortMapper.Status, routerPortMapper.ExternalEndpoint, routerPortMapper.LastError),
@@ -3631,11 +3652,18 @@ public sealed class MainForm : Form
// Re-poke the router. UPnP/NAT-PMP mappings often survive a sleep, but cheap // Re-poke the router. UPnP/NAT-PMP mappings often survive a sleep, but cheap
// routers and ISP-supplied combo boxes sometimes drop their NAT table — easier // routers and ISP-supplied combo boxes sometimes drop their NAT table — easier
// to just rediscover than to guess. Refresh() is a no-op if UPnP is off. // to just rediscover than to guess. Refresh() is a no-op if UPnP is off. Run on
// a thread-pool thread for the same reason as the other UPnP entry points: the
// NatUtility teardown + restart inside Refresh() can block for tens of seconds
// on unusual networks, and we're on the UI thread during the resume handler.
// 2026-05-23.
if (AppConfig.Load().UpnpEnabled) if (AppConfig.Load().UpnpEnabled)
{ {
try { routerPortMapper.Refresh(); } Task.Run(() =>
catch (Exception ex) { logFile.Event($"upnp: refresh-on-resume failed: {ex.GetType().Name}: {ex.Message}"); } {
try { routerPortMapper.Refresh(); }
catch (Exception ex) { logFile.Event($"upnp: refresh-on-resume failed: {ex.GetType().Name}: {ex.Message}"); }
});
} }
} }
catch (Exception ex) catch (Exception ex)
+1 -1
View File
@@ -14,7 +14,7 @@
tag_name on the latest GitHub release; bump it on every public release. The tag_name on the latest GitHub release; bump it on every public release. The
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
is what the About dialog and the updater both read. --> is what the About dialog and the updater both read. -->
<Version>3.0.0</Version> <Version>3.0.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>