Release v4.7: keep trying your configured peer after a reboot (#15)

Removes the heartbeat "adopt a live address" feature (added v1.6). On a LAN with
more than one RemSound machine it could latch a receiver onto an unrelated sender
that happened to be pinging the audio port — then never recover to the real peer,
needing a manual restart (the singer's #15, with log). The feature guessed peer
identity from an untracked ping source with no way to verify it was the same peer;
on the stable VPN/LAN addresses RemSound is actually used with, it only ever caused
harm, since same-address reconnect already works via the continuous heartbeat.
Removed TryAdoptLiveHeartbeatAddress + IsPrivateLanAddress (MainForm) and
GetUntrackedPingSources + recentPingSources (HeartbeatService); the identity-safe
discovery-based following (by verified peer ID) stays. Manual troubleshooting entry
rewritten to match.

Also bundles the held changes since v4.6: status reads line-by-line with GB totals
and double-press-to-copy, CPU/memory in the status, the Install Scripts folder, and
the what's-new-after-failed-update fix. Version 4.7; About + RELEASE_NOTES updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-24 22:41:43 +01:00
co-authored by Claude Opus 4.8
parent 843be9b7fb
commit 64d5cb5a86
7 changed files with 58 additions and 101 deletions
+29
View File
@@ -20,6 +20,35 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary>
private const string ReleaseNotes =
"""
RemSound v4.7
Sound now comes back on its own after a reboot.
If RemSound started before your network or VPN
was up, it could latch onto a different machine
on your network and stay silent until you closed
and reopened it. It now keeps trying the peer you
chose and connects the moment it answers no
manual reconnect. (Thank you to the singer who
reported this and sent the log that pinned it
down.)
A few improvements for screen-reader users. The
"Speak the RemSound status information" hotkey
now reads the status a line at a time, shows big
data totals in gigabytes, and a quick double
press copies the status to the clipboard so you
can share it. The status also now includes how
much CPU and memory RemSound itself is using.
If RemSound ever won't start because the .NET
runtime is missing, there's a new "Install
Scripts" folder next to the program with a
one-click installer for it.
And the "what's new" notes no longer pop up a
second time after an update that didn't finish.
RemSound v4.6
Follow your Windows default audio device.
+1 -57
View File
@@ -322,9 +322,6 @@ public sealed class MainForm : Form
private int continuousTuneIntervalSec = 5;
private long lastObservedUnderrunCount;
private HeartbeatService? heartbeatService;
// Last time TryAdoptLiveHeartbeatAddress re-pointed the sender at a peer's live address.
// Gives a fresh endpoint time to prove healthy before another swap can fire (anti-thrash).
private DateTime lastAddressAdoptionUtc = DateTime.MinValue;
// Tracks whether each peer was last considered CONNECTED, for the connect/disconnect cues.
// "Connected" now means audio is actually flowing OR the heartbeat is healthy — not the
// heartbeat alone (see DetectAndAnnouncePeerHealthTransitions). The bool, rather than the
@@ -1201,7 +1198,6 @@ public sealed class MainForm : Form
UpdateStatus();
SnapshotLogIfDue();
EnsureRequestedAudioRunning();
TryAdoptLiveHeartbeatAddress();
// Refresh the Connectivity tab's peer lists from the same 1 Hz tick — replaces
// the dialog's old 1.5 s dedicated refresh timer. Each Sync* helper short-circuits
// when its signature is unchanged so NVDA isn't spammed with re-announcements.
@@ -5277,7 +5273,7 @@ public sealed class MainForm : Form
// heard as heavy crackle (Tech Singer's Win7-over-VPN report, 2026-05-31). Keeping the
// endpoint pinned while it's still passing heartbeats stops the thrash. A genuine move
// (DHCP renewal, Wi-Fi switch) makes the old endpoint go unreachable first, at which
// point the guard lets the move through; TryAdoptLiveHeartbeatAddress backs it up.
// point the guard lets the move through.
foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList())
{
if (!knownPeers.TryGetValue(id, out var peer)) continue;
@@ -5366,58 +5362,6 @@ public sealed class MainForm : Form
/// messier multi-peer case is left for the user to sort out by hand. Runs once per second
/// from the status ticker. 2026-05-15.
/// </summary>
private void TryAdoptLiveHeartbeatAddress()
{
if (heartbeatService is null || !connected) return;
// Cooldown: adoption re-points the sender; give a freshly-adopted endpoint time to
// prove healthy (or fail) before another swap can fire.
if (DateTime.UtcNow - lastAddressAdoptionUtc < TimeSpan.FromSeconds(10)) return;
var unreachable = heartbeatService.GetAllPeerHealth()
.Where(h => h.State == PeerHealthState.Unreachable)
.ToList();
if (unreachable.Count != 1) return; // 0 = nothing wrong; 2+ = ambiguous
var liveSources = heartbeatService.GetUntrackedPingSources();
if (liveSources.Count != 1) return; // 0 = no candidate; 2+ = ambiguous
var deadEp = unreachable[0].AudioEndpoint;
var liveAddr = liveSources[0];
if (liveAddr.Equals(deadEp.Address)) return; // same machine — nothing to adopt
if (!IsPrivateLanAddress(liveAddr)) return; // never adopt a public / relay source
// Find the selected-peer entry whose endpoint is the dead one.
var match = selectedPeerEndpoints
.FirstOrDefault(kv => kv.Value.Address.Equals(deadEp.Address) && kv.Value.Port == deadEp.Port);
if (match.Key == Guid.Empty) return;
// Reuse the dead endpoint's port — a peer that moved on the LAN keeps its audio port.
var newEp = new IPEndPoint(liveAddr, deadEp.Port);
selectedPeerEndpoints[match.Key] = newEp;
var label = selectedPeerLabels.GetValueOrDefault(match.Key, deadEp.Address.ToString());
logFile.Event($"heartbeat: adopted live address for \"{label}\": {deadEp} unreachable, peer is pinging from {newEp}");
lastAddressAdoptionUtc = DateTime.UtcNow;
// ApplyAudioRuntime re-points BOTH the audio sender (SetReceivers) and heartbeat
// tracking (SetTrackedPeers); PushAllowedReceiveSenders re-points the receiver
// allow-list; PushDiscoveryUnicastHints feeds the new address to discovery too.
ApplyAudioRuntime();
PushAllowedReceiveSenders();
PushDiscoveryUnicastHints();
}
/// <summary>True if <paramref name="addr"/> is an IPv4 RFC1918 private-range address
/// (10/8, 172.16/12, 192.168/16). Gates stale-address adoption so a relay's public
/// source address can never be mistaken for a peer that moved on the LAN.</summary>
private static bool IsPrivateLanAddress(IPAddress addr)
{
if (addr.AddressFamily != AddressFamily.InterNetwork) return false;
var b = addr.GetAddressBytes();
return b[0] == 10
|| (b[0] == 172 && b[1] >= 16 && b[1] <= 31)
|| (b[0] == 192 && b[1] == 168);
}
/// <summary>
/// Wipes the rolling max-gap window and pushes <see cref="lastSourceChangeUtc"/> forward,
/// so the next continuous auto-tune tick has nothing to react to. Called whenever a user
+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.6</Version>
<Version>4.7</Version>
</PropertyGroup>
<ItemGroup>