* New "Opus, live latency" codec mode: 2.5 ms frames (120 samples/ch at 48 kHz)
via the float-input encode path. End-to-end codec delay drops to ~5 ms (vs
~12.5 ms at standard 10 ms Opus). Test on LAN: 400 pps/lane, zero missed /
reordered / duplicate packets, ~15 ms one-way saved end-to-end.
* Wire-format change: AudioFormatInfo.FrameDurationMilliseconds renamed to
FrameSamplesPerChannel (int sample-count at announced sample rate). Removes
the lossy 48000*ms/1000 conversion that couldn't represent 2.5 ms. v3 <-> v3
exact; v3 <-> v2 still passes audio (Opus decoder is self-describing from
packet TOC) but v2 side over-sizes its buffer wildly. v2.x profiles auto-
migrate via <120 sentinel rule in RemSoundSettingsStore (anything below 120
is treated as legacy ms and multiplied by 48). Profile JSON key kept as
OpusFrameMilliseconds via [JsonPropertyName] so old profile files still load.
* Codec dropdown rebuilt with use-case names: "PCM 48K 24 bit - uncompressed",
"Opus, broadcast quality - loss tolerant", "Opus, live latency - for jamming
and monitoring". Middle 10 ms option retired; saved 480-sample profiles
collapse to broadcast quality (safer-side default).
* Profile auto-resume after self-update: RemSoundUpdater writes a one-shot
_resume-after-update.txt sentinel containing the active profile title before
exit; Program.Main reads + deletes it on next start and silently loads that
profile, skipping the picker. Helper batch's robocopy /XF excludes the
sentinel and the failure-branch cleans it up if the install aborts. Falls
through to normal startup behaviour (StartWithProfileTitle or picker) if the
sentinel is missing, empty, or names a profile that no longer exists.
* Read-only profile saves now go through on explicit Ctrl+S / File -> Save
with a one-time TaskDialog warning ("Save anyway" / "Cancel" + Do-not-show-
again). Lock continues to suppress the automatic unsaved-changes prompt on
close / profile switch (its main job). AppConfig.SaveOnReadOnlyMessageSuppressed
renamed to SaveOnReadOnlyWarningSuppressed; v2.x suppression flag is silently
discarded since the behaviour changed and the user needs to see the warning
once on each machine.
* Manual (readme.html) updated: codec table rewritten with the three new
choices and corrected bandwidth figures, send-rate description updated, new
sections "The same profile picks up automatically after an update" and
"Saving on purpose while a profile is locked".
* Subsumes the never-separately-released v2.2 work: native Opus encoder
(~97% less per-second memory churn on Opus send path via Concentus.Native),
efficiency tidy-ups (item 4 ASIO probe rate, item 6 WaitHandle, item 7
snapshot cache, items 14/16 heartbeat + discovery), legacy cleanup
(items 30/34/35/36: KeepAlive infrastructure, drift drop/repeat/accumulator
fields, fan-out cache stat). New diagnostic columns cpu/memMB/wsMB/
allocKBps/captureMs/sendMs/recvMs/renderMs gated on Enable-logs.
About dialog updated with v3.0 block at top; v2.2 block retained for the
subsumed work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Single biggest change: added the Concentus.Native NuGet package. Concentus
2.0+ auto-detects native libopus at runtime and routes encode calls
through it; encoder state lives on the C side and is reused across calls
rather than `new`ing ~15 working buffers per call (Concentus issue #22,
open since 2018). Measured on the desktop test at 15:36:55 — Opus 10 ms
allocation rate dropped from 4,625 KB/s to 108 KB/s, a 97.7% reduction.
Process CPU dropped from 4.7% to 1.6% in the same config. Audio is bit-
for-bit identical (it's literally the same encoder, just better
packaged). `OpusEncoderState.cs` itself unchanged on the call site.
Diagnostic / measurement layer (gated on Enable-logs, zero cost when off):
* ProcessSelfMeter: CPU%, managed heap MB, working set MB, allocation
rate per second, GC counts per generation
* Per-thread work-time counters: captureMs / sendMs / recvMs / renderMs
expressed as milliseconds of CPU consumed by each audio thread per
second
* Inter-packet arrival gap measured at the user-space UDP socket
(rxNetGapMs) — pinpoints whether arrival jitter is in the network or
our own dispatch path
Small efficiency wins (each one was small but cumulative):
* deviceRefreshTimer interval 1s -> 3s (item 4)
* WaitHandle array allocations eliminated in MixingEngine.MixLoop and
MultiOutputPlayout.ProduceLoop (item 6)
* MultiOutputPlayout caches its output-buffer snapshot and only rebuilds
on SetOutputDevices, instead of rebuilding every 10 ms (item 7)
* HeartbeatService reuses an outbound ping byte[] instead of allocating
per send (item 14)
* PeerDiscoveryService caches broadcast addresses and invalidates on
Windows' NetworkChange event instead of walking all NICs every 1.5 s
(item 16)
Legacy / dead-code removal:
* KeepAlive packet's implementation (struct, enums, writer, reader, size
constant) — all dead since HeartbeatService landed 2026-05-06. Kept
the RemPacketType.KeepAlive enum value and silent-drop dispatch for
wire compat with any pre-2026-05-06 build still in the wild (item 30)
* driftDropFramesTotal / driftRepeatFramesTotal fields and accessors —
Phase-2 splice corrector relics, never incremented since Phase-4
resampler design landed; backed five always-zero diag log columns
(items 34 + 35)
* DriftAccumulator (always returned 0) — same shape, removed alongside
the driftAcc= column (item 35)
* TakeMaxFanOutCacheBytes / Ms + fanCacheMs column — FanOutSource was
retired in May (item 36)
Project documentation:
* RemSoundefficiency.md added as the canonical record of the efficiency
analysis, every item's status, and the measured wins from this round
* Honest item-by-item review of the original 50-item list — several
items I had sized optimistically in the original analysis turned out
to be already-done (item 20), already-optimal (item 22), or below
the meter floor (items 9, 15, 17, 25). Recorded so future passes
don't re-investigate.
Wire format and audio pipeline unchanged from v1.5 onward — v1.5 through
v2.2 peers interoperate.
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.
Opening an ASIO driver takes 1-3 seconds, synchronously, during MainForm
construction — confirmed in logs as a ~2.8s dead gap on an ASIO-profile
launch (a WASAPI-only launch is ~131ms for the same stretch). During that
gap the main window is blank / "Not Responding" and looks hung.
New AsioLoadingSplash shows a small "Loading audio driver, please wait..."
window on its OWN dedicated STA thread with its own message loop, so it
stays painted while the main thread is blocked opening the driver.
Program.cs starts it before new MainForm() and dismisses it after.
Deliberately, the ASIO driver open stays on the main UI thread — that
STA/message-pump thread is what ASIO/COM drivers are most compatible with,
and moving the open off it risks breaking drivers that can't be tested.
Only the cosmetic splash moved to a side thread; no ASIO/driver code is
touched. Splash shows only for profiles with an ASIO driver selected;
WASAPI-only profiles build fast and get no splash.
Version bumped to 2.0.0 to mark the milestone.
No wire-format or audio-pipeline changes — v1.5 through v2.0 interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The install helper's robocopy line was:
robocopy "{stagingRoot}" "{installDir}" ...
installDir is AppContext.BaseDirectory, which always ends in a directory
separator, so the destination argument was a quoted path ending in a
backslash: "D:\...\publish\". Windows command-line parsing reads the \"
as an escaped quote, so robocopy never received a valid destination,
rejected the command line, and exited 16 (usage error, nothing copied)
instantly. The auto-updater has never worked in any release because of
this — v1.0-v1.2 failed silently, v1.3+ detected the failure and wrote
update-failed.txt but never fixed the robocopy line.
Fix: BuildInstallScript now strips trailing separators —
stagingArg = stagingRoot.TrimEnd('\','/'), installArg likewise — and
the robocopy line uses the trimmed forms. Verified by running the
corrected robocopy against real staged files: exit 3 (success), files
copied.
The broken helper is baked into every shipped build including v1.8, and
the helper is generated by the running version — so v1.8 and earlier
cannot auto-install v1.9. v1.9 must be installed by hand once; from v1.9
onward the updater works.
No wire-format or audio-pipeline changes — v1.5 through v1.9 interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updater (RemSoundUpdater.cs), all from Andre's feedback:
* CheckForUpdateAsync now requests /releases?per_page=100 instead of the
default 30-item page, so a burst of server-vX.Y relay releases can't
push the newest client release off page 1.
* The install helper's robocopy now excludes remsound.config.json and the
logs / profiles / recordings folders — an update replaces app files
only and can never overwrite the user's own config or data.
* On a successful update the helper now also deletes _update-helper.log
and any stale update-failed.txt (the _update folder was already
removed), leaving a tidy install folder. The failure branch still keeps
them all for diagnosis.
* update-failed.txt rewritten as plain user-facing instructions: numbered
steps, no brand names, names the real _update folder, no robocopy
jargon. The exit code now goes to _update-helper.log only.
Manual (readme.html): rewritten in plain language — developer jargon
removed or explained in everyday terms — and a second pass removed the
keystroke-navigation choreography and screen-reader narration from the
prose. The Keyboard shortcuts section and all shortcut reference are
kept intact.
No wire-format or audio-pipeline changes — v1.5 through v1.8 interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The client and the relay server are published from the same GitHub repo;
the server's releases use "server-" prefixed tags. RemSoundUpdater hit
/releases/latest, which is repo-wide — when a server release was newest,
the updater fed "server-v2.3" to ParseTag (-> a bogus 0.0.3) and concluded
"up to date", silently skipping real client updates.
CheckForUpdateAsync now lists /releases and picks the highest-versioned
release whose tag is a RemSound client tag (new IsClientReleaseTag: after
an optional leading "v", first char must be a digit). Drafts and
pre-releases are skipped. The server-side updater already filters to
"server-" tags, so client + server coexist in one repo cleanly.
Also rewrites build-release.ps1 with a data-safety check: it publishes to
a fresh staging folder and aborts the release if any logs/, profiles/,
recordings/ folder, .log file or remsound.config.json is present in the
staged output or the finished zip — preventing a repeat of the v1.5/v1.6
zips that shipped with developer logs and profiles.
No wire-format or audio-pipeline changes — v1.5/v1.6/v1.7 interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three reliability fixes. Wire format and audio pipeline unchanged from
v1.4 / v1.5 — all interoperate.
Peer address recovery:
* When a tracked peer goes Unreachable (its resolved address — often a
stale DNS / Pi-hole record, or a peer that rebooted onto a new IP) but
the same peer is still heartbeat-pinging us from a different address,
RemSound now adopts the live address instead of transmitting to a dead
one. HeartbeatService records untracked ping sources; MainForm's
TryAdoptLiveHeartbeatAddress (1 Hz) re-points the sender, heartbeat
tracking and receiver allow-list. Conservative: fires only on the
unambiguous one-unreachable-and-one-source case, private-range (RFC1918)
addresses only so a relay can't hijack the sender, 10 s cooldown.
Reconnect crash:
* Fixed IndexOutOfRangeException in MainForm.SyncConnectedList. A churny
peer-list rebuild (peer reboot) left SelectedIndex pointing past the
rebuilt item array; the 1 Hz status timer read SelectedItem and crashed
the app. New SafeSelectedItem bounds-checks the index; applied to all
three timer-driven sync methods. The status tick is also wrapped in
try/catch so a transient WinForms hiccup logs instead of crashing.
Long-run memory / CPU leak:
* A receiver left running for hours grew to gigabytes and climbing CPU.
Decoder sessions orphaned by peer reconnects were not reaped — every
reconnect mints a fresh (endpoint, streamId) key, and PruneIdleSessions
silently skipped sessions whose PlayoutEngine lookup missed. Rewrote it
to reap on each session's own LastWriteUtc (no cross-dictionary lookup),
added a hard MaxLiveSessions cap as a backstop, and a "stream sessions
live: N" diagnostic line. Bounds both memory and render-thread CPU.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug fixes:
* BothIndependent recording was double-tapped — when both WASAPI and
ASIO outputs were ticked, recordings came out garbled and ~2x the
expected duration. AudioRecorder now uses four per-lane rings
(sent-wasapi, sent-asio, recv-wasapi, recv-asio) plus a writer-
thread mix step that drains min(wasapi, asio) frames and sum-mixes
with the soft-tanh limiter. Tap signatures gained a RenderRoute
parameter; Mixed maps to the wasapi slot.
* A peer announcing on the WASAPI lane was inaudible when the
receiver only had an ASIO output ticked (and vice versa). The
session opened, samples flowed into the SessionPlayout ring, but
ReadForRoute(AsioLane) skipped any session whose Route was
WasapiLane so nothing drained the ring. PlayoutEngine now tracks
per-lane "is this lane backed by a device" via volatile bools
settable through SetLaneActive(RenderRoute, bool), called from
CompositeRenderBackend.SetOutputDevices whenever the device split
changes. ReadForRoute admits orphan sessions when the other lane
is inactive.
Menu reorganisation:
* New Options menu (Alt+O) holds Recording settings (Alt+S), Keyboard
shortcuts (Alt+K, Ctrl+K), Startup behaviour (Alt+T), Preferences
(Alt+P, Ctrl+P). Pre-v1.5 these were scattered across File menu,
Record menu, and inside the Preferences dialog itself.
* Record menu mnemonic moved from Alt+O to Alt+K, rendered as
"Record (Alt+K)" so the chord is visible despite K not being a
letter in "Record". Alt+R is taken by Receive audio.
* File menu — new Recent profiles submenu (Alt+F, R) listing the
five most-recently-opened profiles. Press 1..5 inside the submenu
to jump to a slot. Missing files are skipped from the menu but
kept in storage.
* Rename current profile moves to Alt+M (was R), Minimise to tray
moves to Alt+N (was M).
* Lock to audio clock was Alt+K, now Alt+D.
UX additions:
* Ctrl+O = Open profile (matches the menu chord).
* New global hotkey: Start / Stop recording. Pickable from Options
-> Keyboard shortcuts. Unbound by default. Works system-wide.
Wire format and audio pipeline unchanged from v1.4 — v1.4 and v1.5
peers interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ships the dialog reorganisation and mnemonic adjustments from
commit 45b709d. No wire-format or audio-pipeline changes — v1.3 and
v1.4 peers interoperate.
About dialog gets a v1.4 release-notes block; csproj Version
property bumped 1.3.0 → 1.4.0; RELEASE_NOTES.md rewritten with the
v1.4 highlights and a one-paragraph upgrading-from-v1.3 note (clean
auto-update via Help → Check for updates is now expected to work on
Dropbox-installed copies since v1.3's helper hardening took effect).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Recording settings dialog
=========================
Promote what were row attributes inside the format-attributes table
into their own dedicated listboxes. Pre-rework each format's
attribute list contained a row per (quality knob × channel mode)
combination — doubling the row count for no real benefit. WAV had
6 rows, MP3 / OGG-Opus had 8, FLAC had 4. Channel mode is now its
own listbox; per-format attribute lists shrink to 3 (WAV), 4
(MP3 / OGG-Opus), 2 (FLAC).
FLAC compression level was previously a hidden RecordingSettings
field locked at the libFLAC default of 5. Now exposed as its own
conditional listbox with all 9 levels (0..8). Visible only when the
selected file format is FLAC; the column collapses to 0% width
otherwise so the dialog reads cleanly as 4 columns for non-FLAC
formats and 5 for FLAC.
Mnemonics:
* Source Alt+S (unchanged)
* File format Alt+F (unchanged)
* Attributes Alt+A (unchanged)
* Compression Alt+L (new, FLAC-only visible)
* Channels Alt+C (new)
* OK Alt+O (unchanged)
* Cancel Alt+N (was Alt+C — moved to free C for Channels)
Record menu mnemonics
=====================
Item mnemonics inside the Record menu reorganised so the keystroke
inside the menu matches the same letter used for Ctrl+R outside it,
and Settings reads naturally:
* Start recording / Stop recording Alt+R (was Alt+S)
* Recording settings Alt+S (was Alt+T)
* Open recordings folder Alt+O (unchanged)
* Change recordings folder Alt+C (unchanged)
UpdateStartStopRecordingMenuLabel keeps the underline on a literal
R in both states ("Sta&rt recording" → "Stop &recording") so the
keystroke does the same job regardless of which label is active.
No version bump — the running version stays at 1.3.0. These changes
will be bundled with the next public release alongside whatever else
is in flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Anyone on v1.0 / v1.1 / v1.2 in a Dropbox-synced folder will hit the
auto-updater bug v1.3 fixes — but the fix lives in the running
version's helper, so their installed binary can't use it. Adds a
prominent "Already on an older version and inside a Dropbox folder?
Read this first" section at the top of the release notes with the
manual-install steps, and renames the existing install steps to
"Install (clean machine)" for clarity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v1.2's self-updater silently failed when the install folder lived
inside a Dropbox sync path. Dropbox held write locks on the existing
RemSound.exe / DLLs during the brief window between the parent exiting
and the helper script copying the new files in. The helper's robocopy
(/R:5 /W:1) gave up after 5 seconds, and the helper then
unconditionally relaunched the OLD binary — so the user saw the same
version they started with after pressing "Yes" on the install prompt,
with no visible error.
Helper script (BuildInstallScript) changes:
* Robocopy retries bumped to /R:60 /W:1 — up to 60 seconds per file.
Dropbox lock release happens reliably within that window in
practice.
* Robocopy exit code is captured and checked. Codes >= 8 are real
failures. On a failure the helper writes update-failed.txt to the
install folder with the cause + recovery steps, leaves the staging
folder intact, and does NOT relaunch the old binary. Earlier
versions silently relaunched the unmodified old binary, hiding the
failure.
* Helper appends a step-by-step trace to _update-helper.log (in the
install folder), with robocopy's own output included via /LOG+:.
* update-failed.txt, _update-helper.log, and _apply-update.cmd are
added to the /XF exclusion list so the helper's own state files
don't get copied to themselves on a repeat update run.
DownloadAndStageInstallAsync also clears any stale update-failed.txt
at the start of every new attempt, so a successful run leaves the
install folder clean.
readme.html "If install fails" section expanded with the new
update-failed.txt marker file behaviour and the _update-helper.log
location.
Wire format, audio pipeline, and recording feature unchanged from
v1.2 — this is updater-machinery-only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New user-facing features:
* Recording. Dedicated Record menu (Alt+O — moved from Alt+R to
avoid clashing with the Receive audio checkbox), Start/Stop on
Ctrl+R, settings dialog, per-profile source / format / bit-depth /
channel-mode / folder. Three source modes (received only, sent
only, both). Files are crash-resilient — a process crash
mid-recording leaves a playable file containing everything up to
the last header refresh (~5 seconds).
* Four output formats, all functional:
- WAV: 16/24-bit PCM or 32-bit float, custom writer with
periodic RIFF re-patching.
- MP3: LAME 128–320 kbps CBR (via NAudio.Lame).
- OGG-Opus: 96–256 kbps VBR (via Concentus.Oggfile, reusing the
Concentus encoder from the wire path).
- FLAC: 16/24-bit lossless (via CUETools.Codecs.FLAKE — pure
managed, no native DLL).
* Recording start/stop sound cues. record start.wav and
record stop.wav play around the recording transition. Played via
System.Media.SoundPlayer to the default Windows output, separate
from the recording pipeline so a normal recording does not contain
the cue.
* Per-cue Preferences. The old single "Mute connect/disconnect
sounds" checkbox is replaced by a CheckedListBox: Connect /
Disconnect / Recording start / Recording stop. Old profiles with
the legacy MuteConnectionCues=true are honoured on first load via
a migration path in the new Load* helpers.
* Receiver-side drift compensation switched from discrete
single-frame splices to a continuous WdlResampler at a smoothed
rate ratio. SessionPlayout.cs rewrite.
Diagnostics (only active with Enable logs ticked):
* Per-stage discontinuity probes — sender raw capture (per backend,
PushModeWasapi + Asio both wired), sender pre-encode (now per
lane in BothIndependent, fixing a cross-stream artefact), receiver
post-decode, post-ring, post-resampler.
* Wire-level packet sequence tracking on each PCM stream — in-order
/ missed / reordered / duplicated counts in the diag log.
* Clipped-sample delta in the diag log.
* New AudioStepProbe in RemSound.Core with per-channel scan helper.
UI changes:
* Record menu uses Alt+O (Rec&ord). Inside the menu, item mnemonics
unchanged (S / T / O / C).
* Auto-tune interval combo label is mode-aware: "Auto-tune latency
interval" in classic modes, "Auto-tune interval — WASAPI and ASIO"
in BothIndependent. The combo's Enabled state now follows EITHER
lane's auto-tune checkbox (was only the WASAPI one — bug).
Wire format and audio pipeline unchanged from v1.1 — v1.1 and v1.2
peers interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a v1.1 block above the existing v1.0 block in the About dialog,
covering the priority-mode feature, always-on network packet priority,
larger UDP kernel buffers, MMCSS Critical bump, and the logging-toggle
bug fix. Version property in the .csproj bumped from 1.0.0 to 1.1.0 so
the About dialog heading and the self-updater's CurrentVersion both
report 1.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>