using System; using System.Threading.Tasks; using Microsoft.Win32; namespace RemSound.App; /// /// Subscribes to Windows' system power-state changes and fires a single callback on /// — 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 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 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 of /// the previous one so the audio backend isn't torn down and rebuilt twice for one wake. /// internal sealed class PowerResumeHandler : IDisposable { /// How long to wait after Resume before firing the callback — gives the USB /// bus and audio drivers time to finish re-enumerating. public static readonly TimeSpan SettleDelay = TimeSpan.FromMilliseconds(1500); /// A second Resume event within this window of the first is treated as a /// duplicate and ignored. public static readonly TimeSpan DebounceWindow = TimeSpan.FromSeconds(5); private readonly Action onResume; private readonly Action? log; private readonly object gate = new(); private DateTime lastResumeUtc = DateTime.MinValue; private bool disposed; /// 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. /// Optional sink for diagnostic lines — wire to the app's log if you /// want resume events visible there. public PowerResumeHandler(Action onResume, Action? 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 */ } } }