Add priority mode + always-on network packet priority

Priority mode (per-profile toggle, Audio profile tab, Alt+U):
- Bundled PROCESS_POWER_THROTTLING_STATE with EXECUTION_SPEED and
  IGNORE_TIMER_RESOLUTION flags (keeps CPU at full clock + 1 ms quantum
  pinned regardless of OS idle decisions)
- PowerSetRequest(EXECUTION_REQUIRED) + SetThreadExecutionState so the
  system can't decide we're idle and downclock
- timeBeginPeriod(1) for 1 ms scheduler quantum
- ProcessPriorityClass.High, ProcessMemoryPriority normal, working-set
  minimum locked via SetProcessWorkingSetSizeEx(HARDWS_MIN_ENABLE)
- Fully reversed on toggle-off and on form close
- Off by default (battery cost on laptops); explicit opt-in per profile

MMCSS thread priority on hot threads bumped from High to Critical
(network listener, mix loop, render thread) — always on.

Network packet priority (always on, no toggle):
- qWAVE flow on the outbound UDP socket at QOS_TRAFFIC_TYPE_VOICE with
  QOS_NON_ADAPTIVE_FLOW (Voice priority, DSCP 46/EF, WMM Voice on Wi-Fi)
- Silent fallback if qwave.dll missing or QoS service disabled
- Sender + receiver kernel UDP buffers bumped to 1 MB each (was
  256 KB send / 512 KB receive) for resilience against ~30 ms stalls

Bug fix: PreferencesDialog logging toggle no longer flags the profile
as dirty. Logging is a machine-local AppConfig setting; the spurious
ChangedAnyProfileSetting flag was triggering the unsaved-changes prompt
on exit after toggling logs alone.

Manual updated: new "Use CPU and Windows performance settings in high
priority mode (Alt+U)" subsection on the Audio profile tab, new
"Network packet priority (qWAVE / Voice / WMM)" subsection in the
network chapter, Alt+U row added to keyboard-shortcuts table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-14 11:21:58 +01:00
co-authored by Claude Opus 4.7
parent 146e76c6e6
commit 768875cebb
10 changed files with 731 additions and 14 deletions
+22 -2
View File
@@ -44,6 +44,12 @@ public sealed class AudioSender : IDisposable
private ICaptureBackend engine;
private IReadOnlyList<CaptureSourceSpec> pendingSources = [];
private readonly UdpClient udp;
// qWAVE attachment for the outbound UDP socket. Marks our packets at DSCP Voice (EF/46)
// and gives them NIC-scheduler priority over best-effort traffic. Wins on LAN and Wi-Fi
// (WMM Voice access category); neutral across the public internet (most ISPs strip DSCP).
// Always on — no toggle. Failure (qwave.dll missing, QoS service disabled) is logged and
// ignored; the socket continues unprioritised.
private readonly NetworkPriority networkPriority = new();
// Two lanes. defaultLane carries every output in the three classic modes (Mixed route).
// In BothIndependent mode defaultLane carries WASAPI-only audio (route WasapiLane) and
// asioLane carries ASIO-only audio (route AsioLane), each producing its own UDP stream
@@ -141,8 +147,11 @@ public sealed class AudioSender : IDisposable
public AudioSender()
{
udp = new UdpClient(AddressFamily.InterNetwork);
udp.Client.SendBufferSize = 256 * 1024;
udp.Client.ReceiveBufferSize = 256 * 1024;
// 1 MB kernel buffers each way — big enough to absorb GC pauses or scheduler hiccups
// up to ~30 ms at typical PCM-stereo bitrates without dropping packets on the kernel
// side. The old 256 KB ceiling was the actual cap on resilience to short stalls.
udp.Client.SendBufferSize = 1024 * 1024;
udp.Client.ReceiveBufferSize = 1024 * 1024;
// Explicit bind to port 0 (OS picks an ephemeral). Two reasons:
// 1. ReceiveFrom on an unbound UDP socket throws SocketException (WSAEINVAL) on
// Windows — the receive thread we start below would then CPU-spin in its
@@ -152,6 +161,12 @@ public sealed class AudioSender : IDisposable
// this; LAN peer-to-peer is unaffected (we still send from this port, peer just
// sends to its own well-known port as before).
udp.Client.Bind(new IPEndPoint(IPAddress.Any, 0));
// Attach the bound socket to qWAVE Voice flow. Must happen after Bind — qWAVE
// inspects the local endpoint when it registers the flow. Diagnostics route through
// the same sink as everything else; if Diagnostic hasn't been wired yet (typical at
// construction time), the lines are silently dropped, which is acceptable for a
// success/no-op outcome. On failure the socket keeps working without prioritisation.
networkPriority.TryAttach(udp.Client, msg => diagnostic?.Invoke(msg));
defaultLane = new SenderLane(this, opusFrameMs, OpusBitrateLan);
asioLane = new SenderLane(this, opusFrameMs, OpusBitrateLan);
// WasapiOnly at startup — no ASIO needed yet, so persistentAsio stays null.
@@ -528,6 +543,11 @@ public sealed class AudioSender : IDisposable
// own it here and close the driver as part of app shutdown.
try { persistentAsio?.Dispose(); } catch { /* ignore */ }
persistentAsio = null;
// Detach the qWAVE flow before closing the socket — the qwave handle holds a
// reference into the kernel-side socket state, and closing the socket first leaves
// the QOS flow handle pointing at freed state. Order matters even though both calls
// are wrapped in try/catch.
try { networkPriority.Dispose(); } catch { /* ignore */ }
udp.Dispose();
}