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:
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to Windows' system power-state changes and fires a single callback on
|
||||
/// <see cref="PowerModes.Resume"/> — i.e. when the system has just come back from sleep
|
||||
/// (S3) or hibernate (S4). Both states raise the same Resume event, so the one hook covers
|
||||
/// both. Used by <see cref="MainForm"/> to re-initialise the audio backend after wake:
|
||||
/// after a sleep cycle the USB audio device (ASIO / WASAPI) can come back in a degraded
|
||||
/// state where the pipeline appears to run but no sound actually comes out of the
|
||||
/// interface, and a clean close-and-reopen of the audio backend clears it.
|
||||
///
|
||||
/// Threading: PowerModeChanged is raised on a system-message thread, NOT the UI thread.
|
||||
/// The handler returns from that thread quickly (no work done inline) and schedules the
|
||||
/// real reset on a background task — which then marshals onto the UI thread via the
|
||||
/// caller's callback. The caller's callback is responsible for any UI-thread marshaling.
|
||||
///
|
||||
/// USB settle delay: Windows raises Resume early, sometimes before USB devices have
|
||||
/// finished re-enumerating. The handler waits <see cref="SettleDelay"/> before firing the
|
||||
/// callback so the audio backend re-init has a fully-ready USB stack to talk to.
|
||||
///
|
||||
/// Debounce: Windows can in rare cases fire Resume twice in quick succession after a
|
||||
/// short sleep. The handler ignores Resume events within <see cref="DebounceWindow"/> of
|
||||
/// the previous one so the audio backend isn't torn down and rebuilt twice for one wake.
|
||||
/// </summary>
|
||||
internal sealed class PowerResumeHandler : IDisposable
|
||||
{
|
||||
/// <summary>How long to wait after Resume before firing the callback — gives the USB
|
||||
/// bus and audio drivers time to finish re-enumerating.</summary>
|
||||
public static readonly TimeSpan SettleDelay = TimeSpan.FromMilliseconds(1500);
|
||||
|
||||
/// <summary>A second Resume event within this window of the first is treated as a
|
||||
/// duplicate and ignored.</summary>
|
||||
public static readonly TimeSpan DebounceWindow = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly Action onResume;
|
||||
private readonly Action<string>? log;
|
||||
private readonly object gate = new();
|
||||
private DateTime lastResumeUtc = DateTime.MinValue;
|
||||
private bool disposed;
|
||||
|
||||
/// <param name="onResume">Invoked once per resume event, on a background thread, after
|
||||
/// the USB-settle delay. The callback is responsible for marshaling onto the UI thread
|
||||
/// if it touches UI or audio state.</param>
|
||||
/// <param name="log">Optional sink for diagnostic lines — wire to the app's log if you
|
||||
/// want resume events visible there.</param>
|
||||
public PowerResumeHandler(Action onResume, Action<string>? log = null)
|
||||
{
|
||||
this.onResume = onResume ?? throw new ArgumentNullException(nameof(onResume));
|
||||
this.log = log;
|
||||
SystemEvents.PowerModeChanged += OnPowerModeChanged;
|
||||
log?.Invoke("subscribed to PowerModeChanged");
|
||||
}
|
||||
|
||||
private void OnPowerModeChanged(object? sender, PowerModeChangedEventArgs e)
|
||||
{
|
||||
if (e.Mode != PowerModes.Resume) return;
|
||||
if (disposed) return;
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - lastResumeUtc < DebounceWindow)
|
||||
{
|
||||
log?.Invoke($"Resume event ignored (within {DebounceWindow.TotalSeconds:0} s debounce of previous)");
|
||||
return;
|
||||
}
|
||||
lastResumeUtc = now;
|
||||
}
|
||||
|
||||
log?.Invoke($"system Resume detected — scheduling audio backend reset in {SettleDelay.TotalMilliseconds:0} ms");
|
||||
// Return from the system message thread immediately; do the work on a background task.
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(SettleDelay).ConfigureAwait(false);
|
||||
if (disposed) return;
|
||||
onResume();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke($"Resume callback failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
try { SystemEvents.PowerModeChanged -= OnPowerModeChanged; } catch { /* shutting down */ }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user