Release v4.2: fix connect-freeze (#10) and new-profile minimize (#12); smoother WASAPI timing

- Connect no longer freezes: PushDiscoveryUnicastHints resolved remembered
  hostnames with synchronous Dns.GetHostAddresses on the UI thread, blocking the
  whole window for the DNS timeout on an unresolvable name. A screen-reader user
  experiences that as the entire machine locking up. Resolution now runs off the
  UI thread. Same class of bug as the v3.0.1 UPnP-on-the-UI-thread hang. (#10)

- New profile / profile switch no longer hides the window: OnShown ORed the
  global StartMinimised into every instance, so creating a new profile while
  Start minimised was on dropped the window to the tray and looked like a crash.
  StartMinimised now applies only to a genuine cold launch; relaunches honour the
  explicit per-instance flag. (#12)

- Smoother WASAPI audio: the receive producer loop and sender mix loop pace
  themselves with WaitHandle.WaitOne, bound by the system timer (~15.6ms default).
  Without a fine timer the 10ms feed slips to ~16-31ms and delivers audio in
  chunky bursts (the desktop-render chunkiness behind Andre's dropouts/lag). New
  SystemTimerResolution holds a 1ms timer whenever a stream is live, independent
  of the opt-in Priority mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-15 09:21:04 +01:00
co-authored by Claude Opus 4.8
parent 5abbeefe29
commit 034a7de632
7 changed files with 165 additions and 47 deletions
@@ -0,0 +1,50 @@
using System.Runtime.InteropServices;
namespace RemSound.Core;
/// <summary>
/// Holds the Windows system timer resolution at a fine grain (1 ms by default) for the lifetime of
/// this scope, via timeBeginPeriod / timeEndPeriod (winmm).
///
/// Why the audio engine needs this on its own, independent of the opt-in "Priority mode": a
/// real-time loop that paces itself with a Thread / WaitHandle wait is only as precise as the
/// system timer, whose DEFAULT resolution is ~15.6 ms. Windows wakes a waiting thread on the next
/// coarse tick, so a loop asking to run every 10 ms actually slips to ~16 ms — or ~31 ms when it
/// misses a tick. On a machine where nothing else happens to be holding a fine timer, that lumpy
/// wake-up made the receive producer feed audio to the output devices in chunky ~1631 ms bursts
/// instead of smooth 10 ms frames — the desktop-render chunkiness behind Andre's overnight dropouts
/// and lag (diagnosed 2026-06-15: render-callback gaps clustered at 2534 ms ≈ 2× a 15.6 ms tick).
/// Priority mode already requested 1 ms via the same call, but it's off by default and the audio
/// path must not depend on a user toggle to deliver smoothly; the producer / mix loops hold this
/// scope directly whenever a stream is live.
///
/// timeBeginPeriod is process-global and reference-counted by Windows, so multiple scopes (and a
/// concurrent Priority-mode request) nest safely — each Begin needs its own matching End. The cost
/// of a fine timer is a small increase in power draw, acceptable while audio is actively streaming.
/// Best-effort: a failed request just leaves the default resolution; nothing here throws into the
/// audio path.
/// </summary>
public sealed class SystemTimerResolution : IDisposable
{
private readonly uint periodMs;
private readonly bool acquired;
public SystemTimerResolution(uint periodMs = 1)
{
this.periodMs = periodMs;
try { acquired = timeBeginPeriod(periodMs) == 0; } // TIMERR_NOERROR == 0
catch { acquired = false; }
}
public void Dispose()
{
if (!acquired) return;
try { timeEndPeriod(periodMs); } catch { /* leave it — process exit reclaims it */ }
}
[DllImport("winmm.dll")]
private static extern uint timeBeginPeriod(uint uMilliseconds);
[DllImport("winmm.dll")]
private static extern uint timeEndPeriod(uint uMilliseconds);
}