Bump to v2.1.0: UPnP, read-only profile lock, sleep/hibernate audio fix

Headline features:
* Automatic router port opening (UPnP / NAT-PMP / PCP). Opt-in via
  Preferences; surfaces external address + carrier-grade NAT detection.
* Lock profile (read-only). New File-menu tick that makes a profile
  load-only — session changes don't persist, no save prompt on close.
  Unblocks unattended shutdowns (NVDA gone, remote dropped, hibernate)
  where the existing save prompt could deadlock.
* Check for updates on startup (default on) + brief countdown notice
  before silent updates install, so a launch-time update doesn't make
  the app silently vanish.
* "Cue sounds" -> "Audio cue sounds" label clarification.

Bug fixes:
* No sound after the computer wakes from sleep. PowerResumeHandler
  rebuilds the audio backend automatically on resume; brief
  "Reconnecting to audio driver" splash during the rebuild.
* Receiver audio silent after waking from hibernate. RefreshAudioDeviceLists
  now treats a transient ASIO probe failure (returns -1/-1 because the
  driver is mid-teardown / mid-reinit) as "retry next tick" instead of
  clearing the user's tick selection.

Diagnostic-only changes (gated on the existing Enable-logs checkbox,
zero cost when off):
* AudioStepProbe split into cross-buffer vs within-buffer maxes so log
  inspection can tell a real-content sharp transient apart from a
  pipeline-boundary glitch. Plumbed through every probe owner.
* New rxNetGapMs + gc0/gc1/gc2 delta columns in the diag log to split
  receive-side jitter into network-layer vs managed-runtime causes.

Files touched: RELEASE_NOTES.md + readme.html + 24 source files across
RemSound.Core / RemSound.Sender / RemSound.Receiver / RemSound.App.
Three new app files: PowerResumeHandler, RouterPortMapper,
UpdateInstallNoticeDialog.

Wire format and audio pipeline unchanged from v1.5 onward — v1.5
through v2.1 peers interoperate.
This commit is contained in:
Ednunp
2026-05-22 23:06:07 +01:00
parent 00cb4deef1
commit 79b28b6c02
27 changed files with 1980 additions and 125 deletions
+23
View File
@@ -67,6 +67,29 @@ public sealed class ProfileStore
}
}
/// <summary>Returns whether the profile with the given title has its ReadOnly flag set
/// on disk, without doing a full <see cref="Load"/>. Used by the startup profile picker
/// to label locked profiles in the list ("Title (read-only)") so the user knows what
/// they're picking. Returns false on any error — the picker treats unreadable profiles
/// as not-read-only, which is the safer default (the worst case is the user gets the
/// normal save-prompt behaviour on close, which is what they're already used to).</summary>
public bool IsProfileReadOnly(string title)
{
if (string.IsNullOrWhiteSpace(title)) return false;
var path = PathFor(title);
if (!File.Exists(path)) return false;
try
{
var json = File.ReadAllText(path);
var profile = JsonSerializer.Deserialize<Profile>(json);
return profile?.ReadOnly ?? false;
}
catch
{
return false;
}
}
/// <summary>Loads a profile by title. Returns null if the file is missing or
/// unreadable. Malformed JSON is treated as "not found" rather than throwing —
/// the caller can surface a diagnostic and fall back to a blank template.</summary>