Local checkpoint - NOT for public release.
Toward Ed's "every control and every function tested" goal: a new self-test applies a
profile to the ACTUAL main-window controls (via an internal ApplyThenCaptureForTest seam
on the headless form) and reads it straight back, asserting every persisted value
survived - volume, mute, send/receive toggles, peer-shaping master, send mode, send-all-
applications, and the selected applications. This exercises each control's load AND save
logic, not just that it exists (which the accessibility audit already covers).
The apply path pops the "set a password to stream" dialog when enabling send with no
password (would hang a headless test); the seam sets the existing suppressStreamingPasswordGate
around the apply, same gate the app uses internally.
Gate 25/25.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Closes the second test gap.
- RecordingController gets an internal SettingsSourceForTest seam so a self-test can drive
a split (multi-track) recording without writing to the real shared settings store.
- New self-test "Recording split tracks": with SplitTracks on + one connected peer, starts
the real controller, feeds the "your send" track through the tap it wires onto the sender,
and asserts it wrote a FOLDER of tracks (one per peer plus your own) with content - Ed's
multi-track feature, now proven on every build.
Gate 24/24. Both previously-deferred test gaps (split-track + main-window tabs) now closed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Closes the UI-coverage gap Ed pushed on.
- MainForm gets a `headless` ctor flag (defaults false → real startup path byte-for-byte
unchanged). When true it builds the WHOLE window — every tab, control and menu — but
skips the OS touches: global-hotkey registration, the status/device-refresh timers, the
device-change notifier, and the audio-backend mode switch in ApplyAsioMode. The
disruptive startup work (Connect + sockets, UPnP, update check) already lives in the
Shown handler, which never fires when a test constructs the form without showing it — so
a headless construction is naturally side-effect-free.
- New self-test "Main window coverage": constructs the headless main window and audits the
lot — accessible names present, Alt mnemonics unique per group, tab order forms no cycle.
4 tabs, 41 interactive controls.
- It immediately EARNED ITS KEEP: caught a real Alt+L collision — the WASAPI latency and
ASIO latency labels both claimed Alt+L in the same panel, so in WASAPI+ASIO mode the ASIO
field was unreachable by its shortcut. Moved ASIO latency to Alt+I; WASAPI keeps Alt+L.
Gate 23/23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
- New "Recording churn / soak": rapidly start/feed/stop/dispose recordings across all
four formats, asserting handles stay bounded - catches recorder/encoder lifecycle
leaks a single recording wouldn't.
- Both the recording churn and the lifecycle churn now honour REMSOUND_TEST_SOAK=<seconds>:
unset, one quick round in the gate; set, they hammer until the deadline for a real
minutes-long soak. Verified at 8s: 312 lifecycle transitions + 16 record cycles.
- Handle checks now force GC + finalizers to settle before measuring, so the reading
reflects genuine leaks, not collection lag under fast churn (which would false-fail a
long soak). Post-settle the churn shows NEGATIVE handle growth - no leaks.
Gate 22/22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
- The accessibility audit now covers 8 dialogs (was 3): Recording settings,
Preferences, Service profile, About, Add EQ band, Rename peer, Keyboard shortcut
import, Profile selection. Each is constructed and checked for accessible names on
every control + unique Alt mnemonics per group.
- Added a tab-order sanity check to the audit: the GetNextControl walk must terminate
(no cycle) for every audited form - guards a keyboard/screen-reader user from a Tab
trap, and the wrapper-TabIndex trap we've hit before.
Deferred (documented): auditing the MAIN WINDOW's tabs needs a headless construction
seam - MainForm's ctor opens audio backends, registers global hotkeys, binds sockets
and shows a tray icon, interleaved through the ctor. A `headless` flag that skips those
is safe in principle (real path unchanged) but invasive on the critical startup path
and best added with Ed able to test it. The dialog surface (most of the app's controls)
is now covered. Gate 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. First test-suite increment.
New self-test "Recording engine" drives the real AudioRecorder headless by feeding
its audio-thread taps a synthetic tone, then asserting the output files:
- every format written with real content: WAV / MP3 (LAME) / OGG-Opus (Concentus) /
FLAC (FLAKE), native encoders and all.
- source gate: a SentOnly recorder fed only received audio stays (near) empty.
- mono downmix produces a valid smaller WAV.
This is the recording pain Ed flagged - now proven on every build instead of by ear.
Follow-up: split-track (per-peer) recording is orchestrated by RecordingController,
which reads settings from the shared store; testing it without mutating real config
needs a settings-injection seam. Deferred, noted.
Gate 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
- New manual section "24. The lock-screen service (send only)": what it is, the
send-only/WASAPI-only limits, the yield-to-the-app behaviour, the Service menu
(configure/install/start/stop), and the good-to-know notes (direct peers, firewall,
updates picked up on next restart, install both ends for two-way).
- Swept the two stale "Menus (File, Record, Options, Help)" references to include
Service; added a Service-menu pointer at the end of the Menus section.
- Updater needs NO change: UpdateApplier renames the old exe aside (survives the
running service's lock) then copies the new one in, so the service keeps running the
previous version until its next restart and never blocks a swap. Documented.
MANUAL.md regenerates from readme.html at release time (sync-manual.py), so untouched.
Gate 20/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
- ServiceProfileDialog: modal 3-tab editor (Audio send / Audio profile / Connectivity)
reusing the house controls, with a Save and Close / Cancel / Additional options
button row. Send-only: send-mode chooser + WASAPI outputs/apps + inputs (no "Send
my audio" toggle, no receive, no ASIO); codec/rate/lock-to-clock; peers add/remove +
a Set password button. Additional options sub-dialog = play connect/disconnect sound
+ enable service logging. Edits a Profile clone; returns it on OK.
- Service menu in MainForm: status line + Configure / Install / Uninstall / Start / Stop,
items enabled per live state on drop-down. Install/uninstall confirm then elevate.
Configure saves the reserved service profile, points AppConfig at it, stores the
machine-wide logging choice, and restarts a running service to pick up edits.
- ProfileStore.ReservedServiceProfileTitle ("RemSound service"): the service profile
lives with normal profiles (so the service Loads it) but ListProfileTitles hides it
from every picker. ServiceControl reuses that single constant.
- Self-test: the service dialog is now in the accessibility audit (constructs cleanly,
unique mnemonics, all controls named). Gate 20/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
- RemSoundService (ServiceBase): hosts ServiceSendHost.RunLoop on a worker thread,
OnStop cancels + joins. Added System.ServiceProcess.ServiceController package.
- ServiceControl: install (sc.exe create, auto-start, careful binPath quoting) /
uninstall (stop + delete) / start / stop / status. Status query is unprivileged
(menu can poll it); the mutating verbs self-elevate via ShellExecute runas.
- Program.cs: early guards for --run-service (blocks in the SCM dispatcher) and the
one-shot elevated verbs, before the single-instance lock (the service is a
separate role and must never take the interactive lock).
- Self-test "Service registration args" verifies the sc create binPath quoting
survives a spaced exe path. Gate 20/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. First increment of the send-only
Windows service feature (design in memory/project_remsound_service).
THE SPIKE PASSED: the send engine runs fully headless (no window, no message
pump) and streams, proven by a real self-test — the one genuine unknown that
gated the whole feature. Also proves the app-yield model end to end.
What's in this increment (all headless, all tested, 19/19 gate):
- AppConfig: ServiceProfileName + ServiceLoggingEnabled (machine-wide).
- InteractivePresence (Core): the cross-session app-yield token. App holds a
Global\ mutex for its lifetime; the service checks it and yields while an
interactive app is present, resuming when it closes OR crashes (OS frees the
mutex). Name-parameterised internal seams for isolated testing.
- ServiceSendHost (App): loads a send-only profile and streams it to its peers,
WASAPI-only, no ASIO/receive. ApplyProfile/Suspend/Resume + a RunLoop that
drives them from the presence token with a settle delay. v1 sends to direct
peer addresses (LAN/port-forwarded); NAT/relay discovery stays the app's job.
- Program.cs: the interactive app now acquires the presence token at startup so
a future service yields to it.
- Tests: "Service app-yield token" (held=present, released=absent) and "Service
send host (headless stream + yield)" — streams a captured device to a local
receiver over loopback, verifies start/suspend/resume, then drives the full
RunLoop against the token (held=suspended, released=resumes-and-flows).
Still to come (later increments): the --run-service entry + Windows-service
registration, the Service menu, the 3-tab config dialog, updater integration,
docs. None user-facing yet, so nothing deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
Ed's ask: the ASIO-toggle crash was a lifecycle-transition bug, and those take an
age to test by hand but regress easily. Automate tearing features down and adding
them back in every combination.
New self-test "Lifecycle churn" drives a REAL sender+receiver pair over loopback
through a matrix of runtime transitions and asserts no crash + bounded handles:
- audio mode changes
- send sources: empty / device loopback / process-loopback (own pid) / both,
reconfigured repeatedly so the process-loopback capture is torn down and rebuilt
many times (the mechanism that hard-crashed)
- receive outputs on/off
- per-peer pan + parametric EQ: none / volume-only / full pan+EQ chain
- codec (PCM/Opus) and tight-latency toggles
- a rapid WASAPI-only reconfigure loop (no mode changes, never abuses hardware)
Any unsafe teardown crashes the test process and fails the gate; it also checks
handle growth stays bounded across the churn (caught nothing leaking: +25).
Real ASIO hardware cycling is OPT-IN via REMSOUND_TEST_ASIO ("1" = first installed
driver, or a driver name) so routine builds never open - and possibly hang or lock -
a real interface. When set it adds a GENTLE ASIO on/off loop (4 toggles, 600ms settle
- some drivers stall for seconds on a quick close+reopen) with a process-loopback
source live across the toggle, i.e. the exact Ed repro. Verified on the Audient:
52 transitions, no crash.
Gate: 17/17 (default, WASAPI-only path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
Repro (Ed): in applications send mode with an app being captured, turning the
ASIO driver off hard-crashed the process. No log and no crash-report file were
written - and MixingEngine.DisposeEntry swallows managed exceptions - which points
to a native access violation, not a managed throw.
Cause: the ASIO toggle rebuilds the capture backend (ApplyAsioMode -> ApplyAudioRuntime
-> ApplySendSources), which disposes the live ProcessLoopbackCapture. The old design
released the WASAPI COM objects from the disposing thread while the capture thread
could still be inside a native GetBuffer call - a classic use-after-free / AV.
Fix: the capture thread now owns the ENTIRE COM lifecycle. It activates, runs, and
releases every COM object itself, in its finally, only after the loop has exited.
StopRecording/Dispose merely signal and join (2s) - they never touch the COM objects.
If the thread ever wedges in a native call we leak it rather than free from outside
(a rare bounded leak beats a hard crash). The thread is also explicitly MTA, and
activation moved onto it, so the async-activation callback can't stall the UI thread.
bufferReady is volatile and only disposed once the thread has genuinely exited.
Also: ApplyAsioMode force-sets the WASAPI send-list visibility for the new mode, which
resurrected the loopback-outputs list in applications mode; re-assert ApplySendModeVisibility
at the end so the correct list stays shown after an ASIO toggle.
New self-test "Per-application capture lifecycle" runs real start/stop/dispose cycles
of the native capture against our own process on hardware - a bad teardown would AV
and fail the gate. Gate: 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release. Builds on the engine core commit.
Ed's revisions to the plan: the send-mode chooser lives on the Input/Output tab
(not Preferences), right after "Send my audio", and the setting saves per profile.
Input/Output tab:
- New "How to send WASAPI audio" listbox (Alt+6) after the send checkbox, with
two rows: send whole sound devices (classic) or send specific applications.
Switching it swaps the tab live between the loopback-outputs list and the app
section. Hidden entirely on Windows too old for process loopback (mode pinned
to devices), so nothing changes for Win7.
- Applications section: "Send all applications" master checkbox (Alt+7, ticked by
default = whole system audio, same as today) and an "Applications to send"
checked list (Alt+8) shown only when the master is unticked.
- All house controls (AccessibleCheckBox, MnemonicLabel, WireCheckedListAccessibility),
accessible names + Alt-shortcut suffixes, tab order slotted in.
Behaviour:
- App list reconciles on a 3s timer while visible: apps appear/disappear as they
open and close, ticks preserved by process NAME, and a ticked app that closes
stays in the list marked "(not running)" and resumes when it reappears.
- ApplySendSources: devices mode unchanged; applications mode sends either the
default-render loopback (send all) or one process-loopback spec per running
process of each ticked app. WASAPI mics and ASIO run alongside in both modes.
- Process-loopback sources are excluded from the single-source push-mode fast
path (it opens an MMDevice; a "proc:<pid>" id has none) - they go via MixingEngine.
- "Is anything being sent" status/tray gates account for applications mode.
Persistence moved from AppConfig to Profile: WasapiSendMode / SendAllApplications /
SelectedSendApplications. Profile round-trip self-test extended. Gate: 15/15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local checkpoint - NOT for public release.
Fan-out (every received stream to every selected output, no added latency):
- SessionPlayout mirror replicas fed the same decoded bytes; delicate ReadFloats untouched.
- PlayoutEngine reconciles replicas per active output lane; per-route tuner
aggregation keeps lanes from disturbing each other. Recording dispatch gated
to the recording route only.
- PeerDspChain.Clone() gives each output lane independent biquad state.
- ReceiverSelfChecks.FanOutToBothOutputs proves both lanes get audio (self-test).
Offline-marker pile-up fix:
- ResolvePeerDisplayName strips the " (offline)" marker before reuse, so a ghost
peer no longer compounds the suffix hundreds of times in the status line.
Issue #19 (Use-Windows-default follower in the loopback send list):
- DefaultLoopbackSendFollower resolves to the current default render device's
loopback spec, re-applied when the default changes.
Per-application send engine core (issue #20) - WASAPI-only, Win10 19041+ gated:
- CaptureKind.ProcessLoopback + ProcessLoopbackId ("proc:<pid>").
- AudioAppEnumerator: snapshots apps with audio sessions, tracked by process
name, releasing every session object each pass so nothing piles up.
- ProcessLoopbackCapture: IWaveIn over the process-loopback activation API
(hand-rolled COM interop; NAudio has no binding). Fixed 48k/float/stereo.
- CaptureSource IWaveIn overload; MixingEngine opens "proc:<pid>" sources with
no MMDevice and no render keepalive. ASIO path untouched.
- Self-test enumerated real apps on hardware; support gate verified.
UI (Preferences device/app mode + app checklist), ApplySendSources app specs,
and the reconcile timer are still to come. Gate: 15/15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified findings from a multi-dimension code audit, plus the two install-flow bugs:
- Fix Opus encoder use-after-free on a codec/rate change while streaming (guard swap vs encode).
- Fix "both" single-file recording dropping audio + drifting (drain both directions in lockstep).
- Fix broken clip counter, UPnP teardown on exit, auto-update-restart foreground grant, and a
malformed-Opus-format packet orphaning a playout session forever.
- Post-install relaunch now respects start-minimised; uninstall is path-aware so it won't clear a
different copy's run-at-startup.
- Perf/hygiene: cache AppConfig off UI hot paths, fold per-peer EQ+gain into one pass, deterministic
disposal (tray menu, timers, COM shortcut, Process handles, process meter), ring-buffer overflow
guard, receiver session-lock fix, remote-control allow-list moved onto the UI thread.
- Remove dead code (two IsAsioBackend, SessionPlayout.Reset, IsSameEndpoint, RemSoundUpdater
IDisposable); several stale-doc fixes.
Deferred (not in this release): drift-estimator tweak, peer-discovery pruning, uninstall retry-loop,
encryption nonce. Wire format unchanged (interops v3.3-v5.1). Version -> 5.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New Options -> Install / Uninstall RemSound on this PC: a per-user self-installer
(%LOCALAPPDATA%\Programs\RemSound, no admin) with optional desktop + Start-menu
shortcuts, login auto-start (reuses StartupAutoStart), Windows Installed-apps
registration, and copy-across of profiles+config, recordings and logs. Install
state is decided by a marker file, not a folder-path guess; the post-install
relaunch hands over foreground via AllowSetForegroundWindow so the installed copy
comes to the front; uninstall uses a batch remover (no PowerShell) and confirms
with two independent tick-boxes. All new dialogs use the house accessible controls
(AccessibleCheckBox, Theme.Heading).
Also: iOS (TestFlight) companion link alongside Android in README + manual;
slimmed-down default cue WAVs; About/RELEASE_NOTES/manual updated; version -> 5.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jonathan Schuster's iOS client (iPhone/iPad) is in TestFlight beta — linked in the GitHub README and
the bundled manual next to the existing Android app, framed the same way (separate community project,
speaks the same protocol, not maintained by us).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full manual audit before release: fixed the General→Appearance toggle location (3 places), the
Connectivity section-6 tab order (Add-by-IP after Remembered), a new Appearance subsection (theme /
tab order / list toggles) resolving the #appearance links, standardised the recording-source names
and default, added the split-recording friendly-name precedence, the two recording tickboxes to the
Options-menu summary, Alt-keys on the pan/EQ table, Alt+L and Alt+O,N to the shortcut tables, fixed
the glossary (hotkeys not per-profile; remote-control cross-ref) and the section-4 tab-count wording.
About-box v5.0 changelog and version 5.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-track recording drift fix (Ed's question — can the separate tracks drift over an hour?):
* Root: FlushPeerTracks skipped a peer that produced no samples in a render block, so a peer that
went quiet long enough for its session to be pruned (>4 s idle) would have its track fall behind
and desync. Now every peer track is padded to a full render block each cycle (silence when the
peer produced nothing), so all peer tracks stay sample-locked to the single render clock — they
can't drift apart however long the recording runs, and all end the same length. Same padding for
the single-file bypass path. OnRecordBlockComplete now carries the block's float count.
(The peer tracks are already resampled to the render clock per peer, so this makes peer-to-peer
sync exact; your own "me" track is capture-clocked — same soundcard for capture+playback = same
clock = no drift, different interfaces can drift slightly.)
* Self-test: two new steps — "Per-peer shaping DSP" (PeerDspChain unity/master-off/volume/parametric
+ ParametricToPeaking) and "v5 settings and shaping round-trip" (AppConfig defaults, NamedPeers,
MainTabOrder, parametric PeerShaping, recording default = Both).
* Logging (gated by the logging checkbox): master shaping switch, EQ-mode change, parametric band
add/delete, peer rename/clear/delete, and the applied Appearance settings after Preferences close.
* CLI: --list-profiles and --list-named-peers (read-only), in --help.
* Version bumped to 5.0; About-box changelog, RELEASE_NOTES.md and README updated for v5.
Build clean; --selftest passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ctrl and a number selects the Nth tab as it currently appears — positions are live, so they follow
the user's tab reordering and the pan/EQ tab's show/hide. Handled in ProcessCmdKey on both the main
window and the Preferences dialog; focuses the tab strip afterwards so NVDA reads the new tab (like
Ctrl+Tab). Matches Andre's readout app. Manual updated.
Build clean; --selftest passes; deployed to both test folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renamed from "Main tab order" and reworded the accessible name so NVDA reads a clear instruction
instead of the confusing "main tab order, then move up / move down".
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an Appearance tab (after General) and moves the colour theme and "show volume/pan/EQ tab" into
it, per Ed. New on that tab:
* Main tab order — a list of the four main-window tabs (all shown, even when the pan/EQ tab is
hidden) with Move up / Move down buttons to reorder them. Saved to AppConfig.MainTabOrder and
applied by the new ApplyMainTabLayout (rebuilds the tab strip in order, dropping pan/EQ when off,
preserving selection). Replaces RefreshPanEqTabVisibility.
* Enable discovered / remembered peers lists on the Connectivity tab (AppConfig.ShowDiscoveredPeers
/ ShowRememberedPeers, both default on). RefreshConnectivityListVisibility hides the row's label
and its list wrapper when off. Row labels are now captured for this.
All apply when Preferences closes. Manual updated. Build clean; --selftest passes; deployed to both
test folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the repeated failures: each list is wrapped in a FlowLayoutPanel by AddCheckedListRow,
so setting the list's own TabIndex only ordered it inside its (single-child) wrapper and did nothing
to the tab traversal — the wrappers stayed at TabIndex 0 and sorted by add-order. Now the WRAPPERS
(list.Parent) carry the TabIndex, alongside the directly-added controls, giving:
connected, details, rename, discovered, remembered, add-by-IP, lock, status.
Set authoritatively in BuildConnectivityTab; removed the ineffective Connectivity block from
SetTabOrder. Verified against a faithful WinForms mock (GetNextControl walk) before shipping.
Build clean; --selftest passes; deployed. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Order is now connected, details, rename, discovered, remembered, add-by-IP, lock, status — in both
SetTabOrder (authoritative) and the visual layout. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Connectivity tab order: the real driver was SetTabOrder(), whose explicit TabIndex forced
Add-peer-by-IP and Status to the end while the newer Peer details / Rename / Lock controls
defaulted to 0 — so reordering the layout did nothing. SetTabOrder now lists every focusable
control in the intended order: connected, details, rename, add-by-IP, discovered, remembered,
lock, status.
* Parametric bands Left/Right gain nudge now speaks: the new gain is announced via a UIA
notification (NVDA reads it natively — not an extra speech layer), since a plain listbox item
won't announce a value change on its own. The Bands list's accessible name now also hints that
left/right adjust the gain, so it's discoverable.
Build clean; --selftest passes; deployed to both test folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Connectivity tab order: Add peer by IP now sits right after Rename peer (before the Discovered
list), grouped with the connected-peer actions, per Ed's requested order. Dropped the second
section header (only the lock toggle was under it).
* Audio I/O labels: "Set volume for all received audio" → "Master volume for received audio" (the
code never got this rename, only the manual had). Device lists now say "audio": "...for received
audio" and "WASAPI/ASIO audio inputs/outputs to send". Labels + AccessibleNames updated together.
* Add EQ band dialog: the spin/edit boxes now select-all on focus, so a typed value REPLACES what's
there instead of being inserted next to it and reverting (typing 2.5 over 4.0 now works).
* Parametric bands list: Left/Right arrow nudge the selected band's gain by half a dB, live —
up/down still move between bands. NVDA re-reads the band's new dB.
* Recording settings: source list reordered to Both (top, now the default) / Received / Sent;
display order decoupled from the RecordingSource enum. Default RecordingSettings.Source = Both.
* Preferences: Colour theme is now first in the General tab order.
Manual updated. Build clean; --selftest passes; deployed to both test folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A do-no-harm cosmetic pass — all changes are colours/fonts/spacing/icons only; no control types or
accessibility wiring changed, so NVDA behaviour is unchanged. Held for next release.
* Segoe UI 9pt app-wide (ApplicationDefaultFont) — was the legacy default font.
* Follow the Windows light/dark theme via Application.SetColorMode, driven by a new AppConfig.ThemeMode
("system"/"light"/"dark", default system) read at startup in Program.Main (guarded — a failure just
leaves the classic theme). New Preferences → General → "Colour theme (Alt+T)" picker; takes effect
next launch.
* Custom app icon (equalizer-bars motif): multi-size remsound.ico, embedded + set as ApplicationIcon,
on the main window and the tray icon (dialogs are FixedDialog, no title-bar icon).
* New Theme helper (accent colour, section-header + dialog-heading label factories, health colours)
and StatusDot (a small NVDA-invisible colour dot).
* Connectivity tab grouped under "Peers" and "Connect manually, and locking" section headers.
* Connection-health colour dot in the status footer (green streaming / amber idle / grey off); the
health text is unchanged.
* Bold headings on the Rename peer, Add EQ band and Manage named peers dialogs.
* EQ curve: soft translucent fill under the response line.
Audio I/O tab left un-restructured on purpose (its conditional ASIO rows make a blind layout change too
risky); its existing labels already read clearly. Manual updated. Build clean; --selftest passes;
deployed to both test folders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the flat friendly-name map into a proper machine-wide book (AppConfig.NamedPeers), and adds a
management dialog. Held for next release.
* New NamedPeer record (machine name, friendly name, last address, last-seen UTC). AppConfig gains
NamedPeers; the legacy PeerFriendlyNames map is migrated into it once on load, then no longer
written. The book stays machine-wide and profile-independent (it always was — this just enriches it).
* Only deliberately-renamed peers are recorded (per Ed) so the list can't balloon. A named peer's
last address / last-seen are updated in memory each tick while connected; persisted on address
change and on app close (timestamp-only changes don't thrash the disk).
* Options → Manage named peers (Alt+O, N): lists each named peer as "friendly — machine — last seen
date, address"; Rename (Alt+R / F2 / double-click) reuses RenamePeerDialog; Delete (Alt+D / Del)
forgets the name. Edits refresh the connected/discovered lists, the volume/pan/EQ list and details
box live.
* ApplyFriendlyName now records machine name + last address alongside the name.
Manual updated (readme.html + MANUAL.md). Build clean; --selftest passes; deployed to both test folders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add band dialog (from testing feedback):
* Tab order is now Start, End, Gain, OK, Cancel (OK was after Cancel).
* Gain box takes decimals like 1.5 (half-dB steps); parametric list shows one decimal.
* The two frequency boxes start empty — nothing prepopulated to mislead; both required on OK.
New Connectivity-tab feature (held for next release):
* Rename peer (Alt+M) opens a dialog to give a peer a friendly name, with a Clear custom name
button. Names are keyed by the peer's MACHINE NAME (stable across restarts, IP changes and
networks), stored machine-wide in AppConfig.PeerFriendlyNames, and resolved everywhere a peer
shows: both peer lists (via PeerListItem.DisplayNameProvider), the volume/pan/EQ list, the
status line and split-recording filenames. Manual-by-IP peers with no announced name fall back
to keying by address.
* Peer details (Alt+E): a read-only box for the highlighted connected peer showing name, machine
name, IP, connected-for, link health + ping, what they're sending, and whether they're
receiving our audio. "Sending: 2 devices on ASIO at 48 kHz, Opus" is derived from the live
receive streams — each stream is one device and its lane gives WASAPI vs ASIO — so NO protocol
change and no new privacy exposure. AudioReceiver.ActiveFormatsFromAddress added for this.
Manual updated (readme.html + MANUAL.md). Build clean; --selftest passes; deployed to both test
folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the per-peer shaping tab (held for next release):
* Renamed the tab to "Volume, pan and EQ for peers"; the Preferences toggle now defaults ON.
* Collapsed the two master switches (Enable EQ / Enable pan) into ONE: "Enable volume, pan and
EQ for all peers" (Alt+E). Volume now obeys it too. PeerDspChain.Build takes a single enabled
flag; Profile.EnableAllPeerShaping replaces the two bools (old ones kept for load-migration).
* Peer picker is now a CheckedListBox: ticking a peer shapes them (per-peer bypass via new
PeerShaping.Enabled, default true); the focused row is the one the controls edit. Effective
shaping = master switch AND that peer's tick. Letter-nav suppressed so keys never toggle a tick.
* Three EQ modes, renamed: "3 band simple EQ", "12 band advanced graphic EQ", and the new
"16 band parametric EQ" (PeerEqMode.Parametric16Band).
* Parametric EQ: up to 16 user bands, each a boost/cut across a start->end range (PeerShaping
.ParametricBands; ParametricToPeaking maps range -> peaking centre+Q, shared by DSP and curve).
Add band dialog (spin-or-type, numeric-only, live preview, OK/Escape); Bands list sorted
bass->treble reading "X Hz to Y Hz, plus/minus N dB"; Delete key / Delete button, multi-select.
Set peer EQ to default clears the parametric list too.
* dB now spoken as words ("plus 3 dB" / "minus 6 dB" / "flat") on the graphic sliders and the
parametric list, since NVDA users typically have punctuation off and never hear a "+".
* New unbound machine-wide global shortcut "Toggle volume, pan and EQ for all peers" (not stored
in any profile) via the hotkey controller + settings store.
* Renamed the Inputs/outputs "Set volume for all received audio" to "Master receive volume".
* Added EqCurveControl: a purely-visual EQ response graph (not focusable, invisible to NVDA).
* Full manual sweep (readme.html + regenerated MANUAL.md).
Build clean; --selftest passes. Deployed to both test folders. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full sweep of readme.html (regenerated MANUAL.md) for the held batch:
* New section 9 "Pan and EQ tab" — per-peer volume/pan/EQ, the two master switches, the peer
picker, 3-band and 12-band (31/63/80/125/250/500 Hz, 1/2/4/6/8/16 kHz) EQ, reset button,
real-time, saved per profile; shown via the "Show the Pan and EQ tab" Preferences checkbox.
Sections 9-24 renumbered to 10-25; TOC entry added.
* Recording section: the two new tickboxes (split into per-peer tracks; bypass pan/EQ for raw),
and the new date-folder / "<time> RemSound recording <machine>" / multi-track-folder naming.
* Section 4 tab list, keyboard-shortcuts (Pan and EQ tab: Alt+E/P/U/L/N/Q/M), and a Preferences
mention. Replaced a stale old-format recording-filename example.
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of Ed's test findings: the EQ mode picker still said "10 band advanced EQ" (it's 12 now);
and a single-file recording's name now carries the recorder's machine name too —
"<HH-mm-ss> RemSound recording <machine>.<ext>" — matching the split tracks. Held for release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ed's tweak: drop 3 kHz, add 80 Hz in the low end (between 63 and 125). Still 12 bands, so no
array-length change. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dialog set sourceList.TabIndex=0, so it grabbed initial focus and the user landed partway
down, past the two new toggles. Put the toggles first in tab order (and ordered the containers,
so the top row precedes the grid), and force focus to the split-tracks checkbox on show.
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ed wanted a band between 4 and 8 kHz. Advanced EQ goes from 10 to 12 bands: added 6 kHz (4-8
gap) and 3 kHz (2-4 gap) for even resolution through the presence region. PeerShaping
.AdvancedBandsDb default is now 12; MainForm.NormalizeBands grows an older 10-length array on
load so nothing breaks. (Feature unreleased, so no shipped settings to migrate.) Held for
next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues Ed found testing:
* The default recordings path was recordings\<machine>\, so date folders nested under a
machine-name folder. Dropped the per-machine subfolder — recordings now nest by date at the
top (recordings\<yyyy-MM-dd>\...); the machine name still appears in split-track file names.
* Multi-track always created the "me" (sent) track regardless of the Source setting, so a
receive-only recording wrongly produced a track of your own machine. Multi-track now follows
Source like single-file does: peer (received) tracks unless "sent only", and your own (sent)
track only when Source is "both" or "sent only".
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ed's request — an individual level fader per peer, sitting just before the pan control on the
Pan and EQ tab. New PeerShaping.Volume (0..1, default 1.0 = 100% = transparent), always applied
(no master switch — unity does nothing). It folds into PeerDspChain's L/R gain alongside pan
(gainL = panL*vol, gainR = panR*vol), so it's another per-sample multiply, zero added latency,
and multiplies with the global volume (per-peer fader -> mix -> master). Slider is 0-100%,
saved per profile, announces "Volume: N percent". Shaped recording captures it; raw (bypass)
recording doesn't. Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the multi-track feature (Ed): the split path wrote each per-peer tap straight to
that peer's file, so a peer sending on more than one lane stacked their streams sequentially
instead of summing. Playback and the single-file path already sum; now the per-peer path does
too. Each PeerTrack accumulates that peer's block(s) per render and flushes the sum once on the
block boundary (OnRecordBlockComplete = FlushPeerTracks). "Sum to listen, sum to record."
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recording Settings gains two per-profile toggles (Ed's request):
* "Split recording into separate tracks" — each recording becomes a folder with one file per
connected peer (their received audio only) plus one for your own send.
* "Bypass pan and EQ when recording" — record the RAW audio (before pan/EQ) instead of the
shaped audio you hear; applies to single and split.
Engine: a per-peer record tap in SessionPlayout hands each peer's block to the recorder RAW
(before pan/EQ) or SHAPED (after) per the bypass flag; PlayoutEngine propagates the tap to all
sessions (+ inherits on reconnect) and fires OnRecordBlockComplete each render; AudioReceiver
exposes SetPeerRecordTap / OnRecordBlockComplete. AudioRecorder now accepts an explicit path and
exposes ExtensionFor. RecordingController composes one AudioRecorder per track: multi-track =
a recorder per connected peer + a "me" recorder; single-track shaped = the existing mixed tap;
single-track raw (bypass) = sum each peer's raw block per render, flushed on the block boundary.
Naming (Ed's scheme, sortable): <recordings>/<yyyy-MM-dd>/ then, single-track, "<HH-mm-ss>
RemSound recording.<ext>"; multi-track, a folder "<HH-mm-ss> RemSound recording multi track/"
containing "<machine name> <HH-mm-ss>.<ext>" per peer (name or IP) and for your own send.
Known edge (noted): a peer using sender-side BothIndependent (two lanes) records both lanes to
one file in a split recording; single-track bypass sums per render in the Mixed path. Off by
default (both toggles unticked = today's behaviour). Builds clean; pending Ed's hands-on test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A sender reachable at more than one IP at once (e.g. LAN + Tailscale/VPN) picks its own
egress interface per packet, so its audio can arrive from a different IP than the single
address we discovered/dialled and allow-listed. The receiver then silently dropped every
Format/Audio packet (packetsRejectedNotAllowed climbing) while heartbeats — which skip the
allow-list — kept the peer showing connected: connected but silent. (Reported by
Jonathans859 building the RemSoundApple client; receiver-side, affects any multi-homed
sender incl. Windows<->Windows over a VPN.)
Discovery now remembers ALL source IPs per peer InstanceId (PeerDiscoveryService
.addressesById, expired on the same 8 s window; GetKnownAddresses). PushAllowedReceiveSenders
unions each selected peer's endpoint address with every address that peer has announced from,
so audio from any of the peer's interfaces is accepted. The SEND targets are unchanged
(still single-address) — only the accept-list widens, and only to other addresses the SAME
peer (by InstanceId) announced from, so it can't accept an unrelated machine.
Held for next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New feature (Ed's jam-mixing request): pan and EQ each peer's signal independently.
Engine (zero added latency — per-sample, applied to each peer's isolated block just
before the mix): PeerDspChain (balance pan + RBJ biquad EQ) built on the UI thread and
swapped onto SessionPlayout via a volatile reference; PlayoutEngine remembers it per
address so a reconnecting peer keeps its shaping; AudioReceiver.SetPeerDsp facade.
Model: per-profile PeerShaping dict (keyed by peer address) + EnablePan/EnableEqForPeers
master switches; machine-wide AppConfig.ShowPanEqTab. Fixed band layouts in PeerEqBands
(3-band tone control: bass/mids/treble shelves+bell; 10-band ISO graphic EQ), +/-12 dB.
UI: a "Pan and EQ" tab (before Audio profile, shown only when ShowPanEqTab is on) with
the two enables, a connected-peer picker, a pan slider (balance, keeps stereo), a
"reset EQ" button (clears both modes' bands, leaves pan), a 3/10-band mode picker and its
band sliders — all TrackBars (arrow + page-up/down), updating in real time and saved per
profile. Sliders set a friendly AccessibleName on change (pan centre/left/right %, band
dB). "Show the Pan and EQ tab" checkbox added to Preferences > General.
Everything is off by default (tab hidden, both enables off), so this is dormant for all
users until switched on. Builds clean. Pending Ed's hands-on testing before release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships the per-profile "Lock to these exact peer addresses, no matter what" toggle.
Version 4.9; About changelog + RELEASE_NOTES added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feature request from the same singer: a way to pin a profile to exact peer addresses
so RemSound never matches the other computer by its advertised name and never switches
to a different address it discovers — for a machine reachable at two addresses at once
(VPN + LAN), they want only the one they chose, and would rather the connection die than
wander. The logical end of the v4.7/v4.8 direction.
New per-profile Profile.LockPeerAddresses (default false), round-tripped through
RemSoundSettingsStore (ApplyProfile/CopyTo + Load/SaveLockPeerAddresses), mirroring the
PriorityMode pattern. New AccessibleCheckBox on the Connectivity tab ("Lock to these exact
peer addresses, no matter what — never follow names or switch", Alt+L), saved with the
profile, marks the profile dirty on change. When set, RefreshKnownPeers early-returns
before the discovered-peer merge and the address-follow, so the profile's peers stay
exactly as set (the allow-list is still pushed). Off = unchanged behaviour.
Manual: rewrote the "Connecting to one specific IP" section (which over-promised that
add-by-IP "can never drift" — the merge/follow could) into an unambiguous "Locking a
profile to one exact address" section, plus a Connectivity-tab control-table row.
Held for the next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The same singer hit a rare crash: their transmitter (COMP3) was reachable at two
addresses at once — the VPN address 10.8.0.1 they chose and that machine's wireless
192.168.3.245 — and the discovery-driven endpoint-follow ping-ponged the connection
between the two (the log shows four moves in 58 ms) right where the process died with
no shutdown line, no managed exception, no dialog: a hard crash from the receiver
audio-session teardown/rebuild churn the thrash caused.
Fix: the follow loop now never moves off an endpoint that's still answering heartbeats,
only follows once the current one has been unreachable for a sustained grace period
(6 s), only to an address that is itself answering, and never more than once per cooldown
(15 s) — so it can't thrash, and a peer reached on a working address stays put (honours
the singer's "just stay on 10.8.0.1"). New endpointUnreachableSinceUtc + lastEndpointMoveUtc
state; genuine DHCP/network moves are still followed a few seconds later.
Also: a global crash handler (Program.WriteCrashReport on AppDomain.UnhandledException +
TaskScheduler.UnobservedTaskException) writes a crash-*.txt into the logs folder, so a
future "RemSound just vanished" report leaves a stack behind. Removed a stale doc comment
left over from the v4.7 adoption removal. Manual gains a "RemSound closed unexpectedly"
troubleshooting entry. Version 4.8; About + RELEASE_NOTES updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the heartbeat "adopt a live address" feature (added v1.6). On a LAN with
more than one RemSound machine it could latch a receiver onto an unrelated sender
that happened to be pinging the audio port — then never recover to the real peer,
needing a manual restart (the singer's #15, with log). The feature guessed peer
identity from an untracked ping source with no way to verify it was the same peer;
on the stable VPN/LAN addresses RemSound is actually used with, it only ever caused
harm, since same-address reconnect already works via the continuous heartbeat.
Removed TryAdoptLiveHeartbeatAddress + IsPrivateLanAddress (MainForm) and
GetUntrackedPingSources + recentPingSources (HeartbeatService); the identity-safe
discovery-based following (by verified peer ID) stays. Manual troubleshooting entry
rewritten to match.
Also bundles the held changes since v4.6: status reads line-by-line with GB totals
and double-press-to-copy, CPU/memory in the status, the Install Scripts folder, and
the what's-new-after-failed-update fix. Version 4.7; About + RELEASE_NOTES updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Andre confirmed (via NVDA speech history) that holding the speak-status hotkey
doesn't repeat or spam — so the defensive toggle that let users disable the
double-press-to-copy was needless configurability. Removed the Preferences
checkbox and the DoublePressStatusToCopy setting; double-press-to-copy (Andre's
own idea) stays, now simply always on. There was never any debounce/anti-spam
code to remove — the only timing is the 600 ms double-tap detection window, which
is the feature itself. Manual updated to drop the toggle line.
Held for the next release (the toggle never shipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The manual intro and the repo README both carried an invented origin (a guitarist
and singer playing together, "built by a sound designer"). RemSound was actually
built to hear the audio from a powerful computer while working remotely from a
lighter one — other programs can move audio between PCs, but none did it quite the
way Ed wanted. Reworded the manual opening (readme.html) and the README audience
and "Who made this" lines to tell one true, consistent story in Ed's voice. The
music-together and podcast uses stay as the genuine secondary use cases they are.
readme.html ships to users on the next release; README/MANUAL update on push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New last status line "CPU usage 2% and memory 184 MB" — CPU as a share of the whole machine
(Task Manager style: process CPU-time delta / wall-clock / logical-core count), memory as the
working set via the same MB-to-GB formatter the totals use. Sampled on the existing once-a-second
status tick with a cached Process handle. Shows on screen and reads/copies with the rest of the
status (Ed's idea).
Held for the next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preferences General-tab checkbox "Double-press the speak-status hotkey to copy the status to the
clipboard" (Alt+C), machine-wide (AppConfig.DoublePressStatusToCopy), on by default. When off, a
double press just reads the status again. SpeakStatusLine reads the flag only when the timing already
qualifies, so a normal single press never touches the config. Manual's speak-status section now
covers the line-by-line read, GB totals, and the double-press copy + its toggle.
Held for the next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Andre's feedback on the spoken status (the issue #13 feature):
- Speak the status one line at a time instead of collapsing the lines into a run-on sentence —
this also removes the doubled "." the collapse produced (each line already ended in a period).
- Totals switch to GB once they reach a gigabyte ("4.5 GB" vs "4558.2 MB"), via a FormatDataSize
helper used by the readout itself so the on-screen and spoken figures stay consistent.
- A quick DOUBLE press of the speak-status hotkey copies the status to the clipboard (with a
"copied" announcement) so it can be shared with others.
Held for the next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RemSound is a .NET 10 app, so it can't install its own runtime — it can't start without it. A
plain .cmd/.ps1 (which run on what's already in Windows) sidesteps that: double-click
"Install Scripts\Install .NET for RemSound.cmd" and it winget-installs the .NET 10 Desktop Runtime
(Microsoft.DotNet.DesktopRuntime.10), falling back to opening Microsoft's download page if winget
is absent. Modelled on Andre Louis's accessible-sensor-readout Install_Scripts (his install .NET
Framework 4.8 because SR is Framework; ours installs the .NET 10 Desktop Runtime RemSound needs).
Bundled into the build + release zip via csproj Content + EnsureInstallScriptsPublished target.
Manual Quick-start note points users here if RemSound won't start. Held for the next release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the running-version-vs-saved-version trigger for the "what's new" popup with a
one-shot marker the updater writes ONLY on a successful update (UpdateApplier success
path). A failed/rolled-back update never writes it (and clears any stale one), so it can
no longer re-trigger what's-new — the old best-effort flag save could lose a race during
the update churn and leave the version mismatched, which was the bug.
New WhatsNewMarker seam (Write/Exists/Consume) + a SelfTest case for the consume-once
contract. MaybeShowWhatsNewAfterUpdate now shows iff the marker is present, then deletes
it; still records LastWhatsNewVersion for the import-offer's upgrade detection.
Not released yet — bundling with the connection-retry (#15) work in the next release.
No version bump, no manual change (internal fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Use Windows default audio device, follows Windows changes" entry at the top of the
received-output and send-input WASAPI lists. Resolves the current Windows default live,
re-routes automatically when the default device changes, works alongside specific devices,
and persists across launches (a follower can't go stale). Optional "untick the others?"
prompt with a remembered "don't ask again", reset via a new Options item "Reset the default
audio device prompt".
- Manual updated for the feature, plus a sweep that corrected the now-stale Options-menu
section (retired Startup-behaviour item, four->five Preferences tabs, missing entries).
About changelog and RELEASE_NOTES updated; version 4.6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pre-v4.4 upgraders are offered a one-time dialog to copy their keyboard shortcuts
from one of their profiles (still readable in the profile files) instead of being
reset. Users who already went through v4.4's reset are deliberately NOT re-offered
(gated on KeyboardShortcutsGlobalNoticeShown). New KeyboardShortcutImportDialog +
AppConfig.KeyboardShortcutsImportOffered + MainFormHotkeyController.ReloadAndReRegisterAll.
- Keyboard shortcuts dialog: new "Clear this shortcut" button; Delete inside the
capture box leaves a shortcut unassigned.
- Relabelled the three RemSound-app remote rows to "Send remote RemSound volume/..."
to distinguish them from the Windows-global ones.
- Manual (readme.html + MANUAL.md), About changelog, RELEASE_NOTES updated; version 4.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes issue #14: shortcuts were stored on each Profile, so one set on profile A
didn't apply on profile B and seemed to vanish on switch. They now live in
AppConfig (one set shared by every profile). RemSoundSettingsStore's Load*/Save*
hotkey methods re-point to AppConfig (callers unchanged); the per-profile cache
fields, profile load/save plumbing, and the HotkeySetting helper class are removed.
Profile's HotkeyRecord fields stay only for back-compat deserialization.
On upgrade, shortcuts reset to defaults (there's no single correct set to carry over
since profiles could hold different/partial sets). A one-time startup notice tells
upgraders to re-set them; fresh installs are silently marked done (nothing to reset).
Manual, About changelog and RELEASE_NOTES updated; csproj <Version> 4.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both update cue variants ("update 1.wav" / "update 2.wav") replaced with a
gentler mix that signals an incoming update without interrupting work mid-flow.
About-box note added (tidied wording); csproj <Version> bumped to 4.3.1. No code
changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New screen-reader hotkey "Speak the RemSound status information" (issue #13):
reads the status line aloud through the active screen reader via Tolk, fires from
anywhere (system-wide), unset by default. Built behind an IScreenReaderOutput seam
so a future build can swap Tolk for Prism on Windows 10+ without touching callers.
Tolk DLLs vendored under tolk/ and shipped next to the exe.
- New Logging tab in Preferences: Enable logs + Write logs now moved there, plus
opt-in startup "warn if logs folder exceeds N MB" and "delete logs older than N days",
and a "Delete all logs" button (Yes/No confirm). New LogMaintenance helper + AppConfig
settings drive it.
- Manual (readme.html + regenerated MANUAL.md), About changelog and RELEASE_NOTES
updated in plain English; csproj <Version> bumped to 4.3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- Starting straight into the tray (StartMinimised / --minimized) no longer plays the "minimise"
cue; only a genuine user minimise does (the startup path passes playCue:false)
- Restoring the window from the tray now lands focus on a real named control on the active tab so
NVDA announces it, instead of resting on the role-less QuietTabControl and surfacing silently
- Shared the focus-a-leaf-for-announcement helper between the main window and Preferences
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audio cues
- Cues for send/receive on-off, minimise/restore, checkbox tick/untick, and tab switch
- Soft keyboard clicks while typing, with a distinct passkey sound on password fields
- Per-cue "Choose sound" variant picker; "(none)" silences a cue; front-most missing-sound warning
- Send/receive cues take priority over the generic checkbox sound; programmatic ticks stay silent
Preferences
- Redesigned into four tabs (General, Audio cues, Startup behaviour, Update settings)
- Startup behaviour moved in from the Options menu
- NVDA now announces the dialog on open (focus a real named control, not the quiet tab control)
Auto-tune
- Cause-aware: tells device render-callback stalls (more buffer can't fix) apart from genuine
network/buffer starvation, so it no longer pins latency high on chunky onboard cards
- Lowering the target eases the buffer down (glide) instead of trimming it, so no clicks while tuning
Sounds layout
- Shipped defaults moved out of the per-user folder into an install-side "default sounds" folder,
so updates can refresh them; user customs are Browse-picked file paths and are left untouched
- Startup migration removes both legacy sound folders; verified from oldest (v1.0-v3.3) and v3.4 layouts
Quiet automated launches
- New --silent launch flag mutes all cue sounds and suppresses the startup dialogs (migration notice,
update check, Realtek/mic/missing-sound warnings) so test launches never disturb the user
- run-tests / build-release / SelfTest repointed to the new "default sounds" layout
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RemSound's password boxes are deliberately NOT PasswordChar-masked (a screen-reader user can't see
a mask), so the key-click hook's "is this a password field?" check (UseSystemPasswordChar /
PasswordChar) was always false and the distinct passkey.wav never played. Password fields now mark
themselves with Tag = KeyClickService.PasswordFieldTag, and the hook checks that (keeping the
masking-flag check as a fallback). Tagged both password fields - ProfilePasswordDialog (which all
password entry routes through, including the send/receive streaming gate) and
ProfilePasswordManagerDialog. So a key click + passkey now layer together on every password keystroke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Send/receive checkboxes have dedicated cue sounds (send/receive turned on/off) AND would
otherwise also fire the generic checkbox tick/untick. AccessibleCheckBox gains a SuppressCheckSound
gate; the send/receive checkboxes set it so that when their dedicated cue is on, only that cue
plays. When the dedicated cue is "(none)", the gate returns false and the generic checkbox sound
plays as normal - so the checkbox sound never overrides the purpose-built send/receive sounds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audio cues section rewritten for the redesign: the cue list is a plain list you arrow through to
hear each cue, the "Choose sound" list's "(none)" entry turns a cue off (no more tickboxes), and
a new note covers the front-most "couldn't find the sound file" warning + auto-disable. Startup
behaviour section and the Menus -> Preferences entry updated for the four-tab Preferences dialog
(General / Audio cues / Startup behaviour / Update settings) and the retired Options-menu item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a cue is switched on but its sound file can't be found (the player resolved to null - e.g.
a custom WAV was deleted, or a chosen sound is gone), RemSound now turns that cue off and tells
the user, front-most even when minimised (RestoreFromTray first), once per cue per session:
"RemSound was unable to find the <cue> sound file used when <event> happens. RemSound has set
this particular audio cue to not play for now, until a new sound file is specified."
CheckForMissingEnabledCues runs on first show (Shown) and after every cue reload (so it catches
a just-deleted custom WAV when the user closes Preferences). Per-profile cues are turned off via
the settings store, machine-wide cues via AppConfig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Preferences dialog is now a QuietTabControl with four tabs (same accessible tab approach as
the main window; Ctrl+Tab / arrows switch tabs):
- General: profiles-folder browse, accept-remote-volume, UPnP, enable logs / write logs now.
- Audio cues: the redesigned cue UI (plain cue list + "(none)" sound option) + keyboard clicks.
- Startup behaviour: Start minimised / Start with Windows / Start with a specific profile -
moved here from the standalone Options-menu dialog, wiring and persistence unchanged (AppConfig
+ the Windows auto-start registry entry).
- Update settings: startup-check, frequency, check-now, silent-install, show-what's-new.
Removed the Options-menu "Startup behaviour" item and deleted the now-unused
StartupBehaviourDialog.cs (and dropped it from the self-test's accessibility audit). The audit
still passes on the tabbed dialog with no mnemonic clashes (Alt-letters are isolated per tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-cue enable tickboxes are gone. The cue list is now a plain list of names; arrowing it
previews that cue's current sound (custom or chosen default). The sound list below gains a
"(none)" entry at the top: picking it turns the cue off (reusing each cue's existing on/off
storage - per-profile cues persist off-ness in the profile, machine-wide cues in global
settings), and picking a numbered variant turns the cue on and records that sound. All cues
default on, on the first variant; "(none)" is never the default.
Part of the larger Preferences overhaul; the 4-tab restructure, the Startup-behaviour/Update
moves, and the missing-file error are the remaining steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Part 1 - "Uncheck all inputs and outputs" button now also resets the ASIO driver to "(none)":
renamed to say so, and UncheckAllDevices sets asioDriverBox to row 0 for a full clean
WASAPI-only, nothing-selected state.
Part 2 - checkbox tick/untick sounds: every checkbox toggle anywhere in RemSound now plays a
short cue (check.wav on tick, uncheck.wav on untick) - instant feedback on which way a box
went, especially in the inputs/outputs lists. New CheckSoundService + two machine-wide cues
(CheckboxOn/Off) with the usual numbered-variant + Preferences treatment. Hooked from
AccessibleCheckBox.OnCheckedChanged and the device lists' WireCheckedListAccessibility, both
gated on the control being Focused so a genuine user toggle clicks but bulk programmatic
(un)checking (profile load, "uncheck all") stays silent. Reloaded at startup and on cue change.
Tests + manual updated; .sfk byproducts cleared.
Remaining for the overhaul (next): tabbed Preferences (General / Audio cues / Startup behaviour /
Update settings), the cue-list redesign with a "none" option replacing per-cue checkboxes, moving
Startup behaviour out of the Options menu, and a front-most "missing sound file" error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six new machine-wide cues, each with the same numbered-variant + Preferences treatment as
the others (enable tickbox, Choose default sound picker, Play/Browse):
- Send turned on / off, Receive turned on / off: fire from OnStreamingCheckboxChanged, so
they sound whether the user clicked the in-window tickbox or pressed the mute shortcut
(the hotkey flips .Checked, which routes through the same handler). Suppressed during
profile load by the existing password-gate guard, so loading a profile doesn't blast them.
- Minimise (hide) / Restore (show): fire from the tray controller's Minimize()/Restore() on a
genuine visibility transition (guarded against no-op / startup-minimise).
Enable flags + custom-WAV overrides for these six live machine-wide in AppConfig
(EnableSendOnCue.., MachineCueCustomPaths) - they're app-level feedback, not per-profile
audio - so no Profile/settings-cache plumbing. TryLoadCueSound now also honours the
machine-wide custom path. PreferencesDialog gains a MachineRow helper + the six rows.
Sounds: shipped via the existing sounds\*.wav wildcard. Fixed an obvious typo in the
supplied files ("rcieve off 1.wav" -> "recieve off 1.wav") so receive-off has both variants.
Renamed the old single-name cue WAVs to Ed's numbered-variant set; added key/passkey and the
new cue sounds.
build-release.ps1: new step deletes the SoundForge .sfk peak-file byproducts from sounds\
before packaging (they never shipped - build is *.wav only - this just keeps the tree tidy).
Tests + manual updated for the six new cues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cue sounds now ship as numbered variants ("connect 1.wav", "connect 2.wav", ...); the
count is never hard-coded so more can be added with no code change.
- CueSounds.cs: discovers a cue's "<base> <n>.wav" variants (case-insensitive) and
resolves the active default: per-profile custom WAV > machine-wide chosen variant >
first variant > silent. Wired into MainForm.TryLoadCueSound, the startup cue in
Program.cs, and PreferencesDialog.ResolveCueFilePath.
- AppConfig: DefaultCueSounds (machine-wide cueId -> chosen filename) and
EnableKeyboardClicks (on by default).
- Preferences: a "Choose default sound" listbox under the cue checklist - it lists the
selected cue's variants, arrowing it previews each sound and makes it that cue's
default. Plus a "Play keyboard clicks when typing into any edit field" checkbox.
- KeyClickService.cs: an app-wide WM_CHAR message filter + a low-latency NAudio mixer.
Typing into any edit field plays a random key click (key 1..N.wav); password fields
also play passkey.wav at the same instant. On/off live from the Preferences toggle.
Inert if the sounds are missing or the device won't open; never consumes the keystroke.
- csproj: ship every sounds\*.wav via a wildcard (variants, key clicks, passkey, future
additions) instead of stale per-file canonical names.
- Tests: resource checks (self-test + run-tests.ps1) now verify each cue has >=1 variant
and that key 1.wav / passkey.wav are present. Accessibility audit still green with the
new Preferences controls (Alt+D, Alt+K - no mnemonic clashes).
- Manual: variant picker, keyboard clicks, and the new sound-file naming documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Andre's three "bigger ideas" from RemSound-smoke-test-agent-brief.md:
- Richer diagnostics: --diagnostics now includes a live localhost audio self-check
(PCM + Opus, with packet/underrun/drop/buffer/latency counters), the most recent
session snapshot parsed from the log (codec, send/receive state, buffer, drops,
heartbeat), and a recent-warnings/errors digest from the log. BuildDiagnosticsReport
gained a runLiveAudioProbe flag so the self-test's privacy check stays fast.
- Headless accessibility audit: new --selftest step constructs the dialogs that can be
built without hardware (Startup behaviour, Recording settings, Preferences) and checks
every actionable control announces a name and that Alt-key mnemonics are unique within
a container. MainForm is out of scope (its constructor opens audio/hotkeys/sockets).
Dialogs that won't construct are skipped, not failed. Currently audits 3, no violations.
- Perf/leak sanity: new --perftest command runs several audio-loopback cycles and reports
whether handle/memory/thread counts stay bounded (handles ratcheting up cycle-on-cycle is
the leak fingerprint, given RemSound's handle-leak history). Lenient thresholds; logs the
numbers for build-to-build comparison. Wired into run-tests.ps1.
- Shared AudioLoopback helper (used by the self-test, diagnostics and perf test) so all
three exercise the identical real capture/encode/network/decode path on test port 47929.
- csproj: the four previously-unconditional cue Content items are now Exists-guarded like
the rest, so a mid-edit sounds\ folder doesn't break the dev build; the gate still
enforces the required cues before release.
Help + manual updated (--perftest, --smoke-test, --config-dir, richer --selftest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopted from Andre's RemSound-smoke-test-agent-brief.md - the gaps our pack didn't
already cover:
- --config-dir <folder>: redirect ALL user state (config, profiles, logs, cue
sounds) to an explicit folder for this process only, applied at the very start of
Program.Main before the layout migration runs. Lets a test exercise a real build
without touching the user's live settings (the brief's safety rule 1). Works with
every command. AppConfig gains SetUserDataDirectoryOverride / an override on
UserDataDirectory; CommandLine.TryGetConfigDir parses it early.
- --smoke-test / --smoketest: alias for --selftest, matching the brief's vocabulary.
- run-tests.ps1: a cold-start + clean-close smoke (brief baseline steps 3-4) -
launches the GUI minimized against an isolated --config-dir, confirms it stays up,
that it used the isolated folder (real settings untouched), and that --close shuts
it down with no orphan process. SKIPs cleanly if a RemSound instance is already
running (machine-wide single-instance lock).
Manual + --help updated for both switches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The test suite, modelled on Andre's Sensor Readout (an in-app self-test + a build
script), runnable as one step before every publish.
Part 1 - in-app multi-step self-test (SelfTest.cs), run by --selftest:
audio round-trip (PCM + Opus over localhost, dedicated test port so it never
clashes with a running instance), encryption right/wrong-password + fingerprint,
packet framing + malformed rejection, client<->server wire-format compatibility,
settings save/reload, profile save/reload (temp folder), diagnostics-report
privacy (never leaks a password), and bundled-resources present. Each step is
timed and reported PASS/FAIL/SKIP; exit 0 only if nothing failed. Replaces the
old single-shot --selftest. RunDiagnostics refactored to expose
BuildDiagnosticsReport(AppConfig) for the privacy step.
Part 2 - run-tests.ps1: builds, then checks the package (sounds, readme, native
opus, framework-dependent, dll version == csproj), the About-box changelog, the
client/server wire contract (relay magic/version/port still match RemPacket),
the CLI surface, and runs --selftest. build-release.ps1 now runs this gate first
and aborts the release if it fails.
Bug caught + fixed: the published release zip carried ZERO cue sounds (startup
sound + connect/disconnect/etc.) - MSBuild's incremental Content-copy marker
skipped sounds\ on a fresh publish. Added an AfterTargets=Publish copy in the
csproj that lands every cue WAV in the published sounds\ folder regardless of
the marker. Verified: a staging publish now contains all 9 cue WAVs.
Manual/help: --selftest description updated (readme.html + MANUAL.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Receiver: a plain (Mixed) stream now renders on an active lane when the
receiver is in two-lane (ASIO) mode, instead of being decoded into a ring
nothing reads. Fixes one-way silence ("my mic works for me but not for them").
- Receiver: drift resampler gains a buffer-depth correction term so a bloated
standing buffer eases back to the latency target over a long session.
- App: don't send audio until a peer is genuinely reachable (issue #8); status
no longer shows phantom send traffic with nobody connected.
- App: start-up cue sound (machine-wide toggle + custom path in Preferences).
- App: command-line options (CommandLine.cs) -- --devices, --selftest,
--diagnostics, --log, --close, --profile, --connect, --minimized, --version,
--help. New "Command-line options" manual section (readme.html + MANUAL.md).
- Version 3.9; About-dialog and RELEASE_NOTES updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Receiver: a Mixed (plain) session is rendered on an active lane in BothIndependent mode instead of being skipped, so a WASAPI-only sender is no longer silent to a receiver that has an ASIO driver selected. Also port the per-session buffer depth-drain (stops the receive jitter buffer bloating).
Sender: add sndAudFr meter (audio frames actually sent) to localise capture vs send.
App: startup sound cue (machine-wide, Preferences); stop sending audio when no peer is reachable (issue #8). Version 3.9.1.
Server (relay): fix updater version-compare for multi-dot tags, guard the main loop against crashes, reject spoofed BYE from a mismatched endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New profile: a File-menu item + Ctrl+N that loads a fresh blank template as a
new unsaved session via a LoadBlankTemplateNext handoff to Program.cs's relaunch
loop — reachable even when "start with a specific profile" boots past the picker
(issue #6). Offers to save the current profile first if dirty; deliberately
silent (no profile-switch cue).
Renamed the user-facing "blank template" to "New profile": the picker's synthetic
entry (now a distinct marker TYPE, collision-safe against a real profile named
"New profile"), the window title ("RemSound — New profile"), and the manual.
Fixed the password-mismatch warning flashing away: it's raised from the 1 Hz
statusTimer, which kept firing into the modal loop and rebuilt the peer lists
(SyncAllPeerLists) under the dialog, knocking it out of the foreground. Now the
tick is frozen while it's up, it's routed through ForegroundDialog, and a
re-entry guard ensures one warning that stays put. Audited: it was the only
popup raised from a recurring timer.
Docs: manual section "Connecting to one specific IP address (and only that one)"
explaining by-name vs by-fixed-IP and that a profile saves the exact address
(issue #7 — functionality already existed); About box + RELEASE_NOTES for v3.8;
MANUAL.md regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coalesce capture-engine rebuilds (CompositeCaptureBackend): a swap-triggering
source change now arms a 250ms debounce timer and re-arms on each further
change, so a flap or quick reconfiguration produces ONE rebuild to the final
state instead of a burst (Andre's 16:40 four-rebuilds-in-33s crackle). In-place
updates still apply immediately; a pending rebuild whose target flaps back is
cancelled.
Auto-tune (TickRoute) now keys off the SECOND-highest arrival-gap/render-gap
second in the lookback window instead of the single worst, so a lone ~1s
OS/driver stall no longer balloons the buffer to the 200ms cap (the 16:51
trim burst); sustained jitter still reacts at full speed. Logs both gap-max
(true peak) and gap-used (value acted on).
Mic-privacy detector widened: also catches a per-app Deny aimed at this exe
under ConsentStore\microphone\NonPackaged\<exe>, the HKLM NonPackaged gate,
and the Group-Policy/MDM force-deny (AppPrivacy LetAppsAccessMicrophone=2) —
the block shapes that silence WASAPI capture while ASIO sails past, and that
the old three-value check missed.
Forensic instrumentation so the next log proves what happened: capPeak=
(loudest pre-encode sample, per lane, on the diag line), mic-privacy verdict
logged at startup, ui: capture tick/untick events, and device-event: lines for
Windows endpoint changes.
Docs: mic-privacy + auto-tune sections updated in readme.html, MANUAL.md
regenerated, About-box changelog and RELEASE_NOTES for v3.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the generated cmd.exe + robocopy update helper — which silently
failed on some machines — with an in-app C# installer:
* Stage the new version to a per-user temp folder off the install and run
the new RemSound.exe from there, so nothing in the install is locked by
the updater itself.
* Wait for the old process to fully exit (real WaitForExit), then
back-up-and-swap files in C# with retry + rename-aside; roll the install
back to the previous version on any failure, so a failed update can never
leave a half-installed RemSound.
* Log every step to updater.log; clean up old stages and legacy batch
artefacts on launch. Removed the old BuildInstallScript batch generator.
Route the "RemSound is already running" dialog and its follow-up message
through ForegroundDialog so they surface in front from a background-relaunched
copy, matching the earlier post-update fix.
Docs: rewrite the readme update sections for the new mechanism, regenerate
MANUAL.md, refresh the About-box changelog and RELEASE_NOTES.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Recover an unplugged/replugged output sound card automatically (issue #5):
detect the dead WASAPI device and remember the receive-output selection so it
re-ticks and re-opens when the card returns.
- Adaptive per-card WASAPI buffer target sized to each card's pull chunk, held
stable so it never flits about under CPU/network load.
- Consolidate all per-user data (config, profiles, logs, sounds) into one
"user settings and logs" folder; migrate every older layout; exclude it from
the updater so custom cue sounds now survive updates.
- Mic-privacy detector: warn once when a Windows-blocked mic is switched on, or a
profile loads with one already on.
- All warning/notice dialogs now come to the foreground even when minimised.
- Apply volume + mute on profile load (were saved but not restored).
- Crash-safe (atomic) profile/config saves.
- Fix two resource leaks (push-mode capture MMDevice; UPnP DeviceFound handler).
- Remove dead code (baseline-diff machinery, dead ASIO probes, no-op stubs).
- Docs: readme.html, MANUAL.md, About-box changelog and release notes for v3.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the v3.4 freeze (dd70613) with the fixes and tuning from live testing,
plus the v3.4 documentation pass.
Audio (receiver):
- Per-device WASAPI drift correction in MultiOutputPlayout. A pull-side resampler
(mirroring SessionPlayout's proven corrector) holds each output device's buffer at
a fixed low depth, cancelling the slow clock drift that made WASAPI peers "lag
apart" over long sessions. Feed-forward clock-ratio measurement plus a gentle
depth-restoring term; the first measurement window is discarded because WASAPI
start-up priming poisons it. ASIO already self-corrected; this brings WASAPI level.
- Output device buffer requested at 5 ms (WASAPI clamps it up to the device's minimum
period, ~10 ms) instead of 15 ms, since the corrector keeps it fed — a free saving.
UI / accessibility (MainForm, Program):
- Startup notices (what's-new About box, Realtek warning) now run one at a time via a
single sequence instead of separate BeginInvokes, so they no longer stack into
nested modals that couldn't be closed. The loading splash is skipped for a
tray-bound quick switch.
- Quick profile switch keeps RemSound in the tray if it was there, and plays the
switch cue immediately on click.
- Profile-switch cue now plays on click for every switch path (recent menu, quick
switch, File > Open) and no longer on a fresh start into the first profile. It was
also previously dead on the rebuilt form (pendingProfile was nulled first).
- Realtek ASIO toggle's accessible name now reads "Enable"/"Disable" to match the
visible text, instead of "Toggle" (screen reader read the wrong word).
Docs (plain English):
- RELEASE_NOTES.md: v3.4 entry.
- About dialog: v3.4 "what's new".
- readme.html (the canonical bundled manual): quick switch, the hotkey read-outs, the
new profile-menu-open cue, Realtek auto-detect/disable, and the config-folder path.
- MANUAL.md regenerated from readme.html via sync-manual.py so the two stay in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freezes the v3.4 feature set (everything since the v3.3 public release):
- Fix the receiver handle leak: the 3-second device-refresh timer reopened the
configured ASIO driver every tick, and Realtek's ASIO driver leaks Event+Mutant
handles on every open. Cache the ASIO probe per driver so it is opened once.
- Realtek ASIO block: detect a Realtek ASIO driver, offer once to disable it, and
never touch it again if disabled; Options-menu toggle to reverse. Global config.
- Device hot-plug is event-driven (AudioDeviceChangeNotifier) instead of a 3s poll;
debounced refresh, falls back to polling if registration fails.
- Quick profile switch: new global hotkey opens an NVDA-friendly popup of all
profiles (current marked); Enter/click switches; plays a new "profile menu open"
cue with Preferences mute + custom-sound.
- Announce assigned global hotkeys on the controls/menu items they drive (NVDA reads
"press X anywhere"). File > Open already had Ctrl+O.
- Held-back changes folded in: config-folder migration, codec-column fix, Tailscale
endpoint network-prune, empty-password guard, and the handle-leak diagnostics
(ProcessSelfMeter, HandleTypeProbe).
Version bumped to 3.4.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Points users to Aryan Choudhary's community Android receiver (RemSoundAndroid) Releases page for the signed APK. Third-party companion app, TalkBack-tuned, not maintained here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Headline: all audio is now encrypted (AES-256-GCM), keyed by a per-profile
password. Mandatory — v3.3 only interoperates with v3.3+.
Encryption
- RemSoundCrypto (Core): PBKDF2 key derivation, AES-GCM encrypt/decrypt
(low-alloc, into-span), password fingerprint, light on-disk obfuscation.
- Wire: SenderLane encrypts the audio payload (PCM split across parts when the
+28 overhead crosses MTU); AudioReceiver/StreamSession decrypt via a shared
single-thread AudioDecryptor. Fingerprint piggybacks on the Format packet
(offset 36, backward-compatible) so a peer can detect a password mismatch.
- Profile.Password (scrambled), carried through BuildCurrentProfile; MainForm
derives + pushes the key/fingerprint to sender + receiver (RecomputeAudioCrypto).
- UX: ask-for-password on profile create; File -> Change this profile's password
(ProfilePasswordDialog); Options -> Profile passwords (manager); a gate that
prompts before streaming without a password; and a clear "passwords don't
match" / "peer needs to update" message driven by the fingerprint.
Cue fixes
- CuePlayer (NAudio) replaces System.Media.SoundPlayer, which silently failed
on the 96 kHz/24-bit cue WAVs (and any custom file) — cues now play reliably,
resampled to 48 kHz/16-bit. Also fixes the Preferences preview button.
- Connect/disconnect cues now audio-gated with hysteresis: connected when audio
flows OR heartbeat healthy; lost only when audio stops AND heartbeat
unreachable. Kills false disconnects and the receive-only "no cues" case.
- Honest cue logging (played / muted / not loaded).
Smaller
- Endpoint stickiness: keep the audio target pinned to the heartbeat-proven
address instead of chasing a multi-homed peer's other (unreachable) address.
- "Online/offline" label now audio+heartbeat aware, not discovery-only.
- "Show what's new after each update" preference (on by default).
Docs: About v3.3 block, RELEASE_NOTES, README (encryption as a headline),
manual section 12 "Passwords and encryption" (+ renumber), MANUAL.md regenerated.
Version 3.2.0 -> 3.3.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Update sound" cue (CueId.Update / update.wav). Plays just before an
update starts installing, on every path (manual, background-silent,
startup-silent), so a silent background update gives an audible heads-up
before RemSound closes to restart. Per-profile mute + custom-sound override
in Preferences, same infrastructure as the other cues:
* Profile.EnableUpdateCue + RemSoundSettingsStore Load/Save + round-trip
* MainForm updateSound field, TryLoadCueSound, play in InstallUpdateAsync
gated by LoadEnableUpdateCue
* PreferencesDialog "Update sound" CueRow + ResolveCueFilePath mapping
* update.wav shipped in sounds\ via csproj Content
- Single-instance "switch to the running copy" now actually brings the window
to the front. The second copy grants the running copy foreground rights via
AllowSetForegroundWindow before signalling, and lingers briefly so it can
raise itself before we exit — without this, Windows' foreground lock left
the running window only flashing in the taskbar (Ed's report).
- Docs: About box v3.2 block, RELEASE_NOTES.md, manual cue section (now seven
cues, with the update cue described), MANUAL.md regenerated.
- Version bumped 3.1.3 -> 3.2.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the chained-fault runaway Andre hit after an update (multiple copies
stacking, audio climbing to deafening, terminal kill to recover).
- Single-instance lock (SingleInstanceCoordinator + SingleInstanceDialog,
wired in Program.Main). A named mutex makes two copies impossible. A second
launch offers: switch to the running copy (default; surfaces it from the
tray via a named activation event), or force the running copy closed and
start fresh (Process.Kill, retried elevated if the target is elevated).
This is the structural fix that makes the stacking runaway impossible.
- Prompt-free update exit. InstallUpdateAsync sets updatingInProgress before
Application.Exit(); the close path's skipPrompt now honours it, so no
unsaved-changes dialog (whose default button is Cancel) can abort the
update's restart.
- Read-only persistence fix. BuildCurrentProfile now carries
currentProfileReadOnly into the saved snapshot. Previously a deliberate
save of a locked profile wrote ReadOnly=false, silently unlocking it on
disk — which re-armed the save prompt that then blocked the update.
- In-process double-install guard. updateInstallStarted stops the ~4 s
startup check and the background poll both staging an install + helper.
- Docs: About box, RELEASE_NOTES.md, readme.html + MANUAL.md ("Only one copy
of RemSound runs at a time").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Tray icon now re-registers itself (hide+re-show) whenever the meaningful
state changes — peer connect/drop, send/receive toggle, recording
start/stop — so the name a screen reader announces stays in step with
the live state. Fixes the persistent "no peers" then "1 peer" double
announcement that the old "show window then minimise again" trick used
to clear by hand. Same root cause as the v3.1.1 stuck-tooltip fix, just
exposed when the state changes a moment after the icon appears.
- Recording shows as a plain "recording" flag in the tray rather than a
live timer. A ticking timer would have either flickered the icon once a
second or left a screen reader announcing a stale time next to the live
one. Removed the now-dead FormatRecordingElapsed helper.
- Bundles the earlier Win7 updater fix (6cbde0d): a failed secure
connection is no longer mislabelled as "you're up to date".
- About box, RELEASE_NOTES.md, readme.html and MANUAL.md updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tech Singer's Windows 7 log from 2026-05-28 had this pattern:
updater: GET https://api.github.com/repos/...
updater: check failed: HttpRequestException: The SSL connection could not be established
...
updater: startup check — up to date (v3.0.1)
i.e. the SSL handshake to GitHub failed (Win7's TLS stack missing
KB3140245 / KB4474419) but the updater logged 'up to date' and the
user-facing message in CheckForUpdatesManually said the same. So a
user with a broken update check has no way to distinguish that from
genuinely having the latest version.
Fix:
* CheckForUpdateAsync now returns a discriminated UpdateCheckResult
(UpdateAvailable / UpToDate / UpdateCheckFailed) instead of the
old UpdateInfo?. Each return-null site is replaced with the
appropriate concrete type.
* The catch-all 'try { ... } catch (Exception ex) { return null; }'
becomes 'return new UpdateCheckFailed(ClassifyFailure(ex), ...)'.
ClassifyFailure walks the exception chain and maps to a coarse
FailureKind enum: SecureConnection (TLS/auth), Timeout, HttpError,
NetworkUnreachable. SecureConnection is broken out separately so
the manual-check UI can point Win7 users at the specific Microsoft
KBs that fix the issue.
* MainForm.CheckForUpdatesManually pattern-matches on the result:
UpToDate -> existing 'you're running the latest' message;
UpdateAvailable -> existing install confirmation;
UpdateCheckFailed -> NEW dialog (ShowUpdateCheckFailedDialog)
whose wording is tailored to the FailureKind. The
SecureConnection branch explicitly names KB3140245 and KB4474419
and offers the manual zip-install URL as a fallback. All branches
keep the technical detail out of the dialog and route it to the
log instead.
* Background and startup polls stay silent on UpToDate and
UpdateCheckFailed (no point nagging the user about something
they can't act on from a timer tick), but the startup-poll log
now records the failure kind and detail instead of mislabelling
the outcome as 'up to date'.
No version bump - this rides along with the next feature release
(planned v3.2 with the Reaper ReaStream integration). The bug is
silent on the affected users today, and shipping a v3.1.2 just for
the error-message improvement would mean another update cycle for
everyone for marginal benefit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: if v3.1 installed via auto-update on a profile with StartMinimised
on, the tray icon's hover tooltip got stuck at the initial "RemSound -
starting up" string. The snapshot tick was running and SetTooltip was
being called every second with the live state, but Windows shell kept
showing the original cached text on hover. The fix-by-workaround was a
hide-then-re-show cycle which forced the shell to rebuild the icon
registration with the latest NotifyIcon.Text.
Root cause: NotifyIcon.Text values set BEFORE the icon's first
NIM_ADD (i.e. while Visible=false) become the shell's "initial"
tooltip when the icon eventually appears. Subsequent NIM_MODIFY calls
from text changes DO propagate, but the shell tends to keep showing
the original text on hover - presumably a tooltip-cache eviction
quirk. In the resume-after-update-with-StartMinimised flow, the
window briefly shows then BeginInvokes a Minimize that flips
Visible=true before the snapshot timer has had a chance to fire, so
the shell registers with the stale "starting up" string.
Fix: drop the hard-coded "starting up" initial text from the
controller ctor entirely. The controller now takes a Func<string>
buildTooltip callback from MainForm and calls it in Minimize() right
before flipping Visible=true, so the shell's NIM_ADD sees current
live state instead of a stale string. The 1 Hz snapshot tick keeps
working for ongoing live updates while the icon is visible.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two Windows-specific gotchas the previous code didn't handle:
1) The 'python' / 'python3' commands on most Windows installs are
Microsoft Store execution aliases. They appear on PATH, accept
any invocation, exit with code 9009, and print a "go install
from the Store" message instead of running the script.
2) The 'py' launcher accepts --version and returns the right thing
(it knows about registered Pythons via the registry), but on
some setups it refuses to run scripts and falls through to the
Store alias too. Seen here: 'py --version' prints 3.11.9 but
'py sync-manual.py' prints the Store message and exits 9009.
The new approach runs an actual sentinel script via -c with each
candidate and only accepts the candidate when stdout matches the
expected string. User-local install paths come first because they
skip the Store-alias issue entirely. Falls through to 'py' and the
PATH commands as backstops.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new cues join the existing connect / disconnect / record-start /
record-stop set:
* profile-save cue (sounds\save.wav by default) fires in
SaveProfileTo after a successful Save or Save As. Honours the
Profile.EnableSaveCue per-profile flag.
* profile-switch cue (sounds\profile.wav by default) fires in the
MainForm Shown handler after a profile finishes loading - covers
both startup-with-profile and mid-session profile switches.
Honours the Profile.EnableProfileSwitchCue flag. Because cue
loading runs AFTER settings.ApplyProfile in the ctor (line 542),
the profile being entered determines which sound plays - not the
one being left, exactly as Ed asked.
The audio cue UI in PreferencesDialog stays as a CheckedListBox (up /
down navigates, Space toggles) with TWO action buttons below that
operate on whichever cue is selected:
* Play [cue name] (Alt+P) - previews via SoundPlayer.Play on the
resolved path (custom override if set, default in sounds\
otherwise). Independent of the tick state.
* Browse for [cue name]... (Alt+B) - opens a WAV picker. If the
user picks a file inside RemSound's own sounds\ folder, it's
treated as "use default" and the override is cleared - avoids
pinning the user to a specific shipped default that a future
release might replace. Right-click "Use default sound" reverts.
Custom cue paths AND enable flags are per-profile. Lives on
Profile.CustomCuePaths (Dictionary<string,string>) and the per-cue
EnableXxxCue bool? properties. Cache mirror in
RemSoundSettingsStore.Settings.
All default WAVs moved from the install-root flat layout into a
sounds\ subfolder (csproj Content rules updated). save.wav and
profile.wav are bundled defaults.
Tray menu rewritten (MainFormTrayController) per Ed's spec:
* Show RemSound (W) - now uses Win32 SetForegroundWindow after the
standard Activate() because WinForms Activate is blocked by the
foreground-lock when invoked from a tray-menu click, which left
screen-reader users having to Alt+Tab to reach the restored
window.
* Enable sending (S) / Enable receiving (R) - tickable, reflect
current state, TOGGLE rather than always-on.
* Profiles (P) - submenu populated from AppConfig.RecentProfiles
with the same &1..&5 mnemonics the File menu uses. Pre-populated
once at construction so WinForms recognises it as a submenu and
fires DropDownOpening - originally I relied entirely on the
open event, which the framework skipped for items with no
DropDownItems, producing the "Profiles does nothing" bug.
* Exit (X).
Tray tooltip now built dynamically from snapshot tick (1 Hz):
"RemSound - [recording for MM:SS,] N peer(s), sending (lane),
receiving (lane)". Recording timer only included while
RecordingController.IsRecording is true (added
RecordingStartedUtc accessor for the elapsed calculation). Lane is
derived from which device-list ticks are active, not just the audio-
mode setting, so a BothIndependent user with only WASAPI inputs ticked
honestly reads as "sending (WASAPI)".
Fixes:
* Initial tooltip "RemSound" produced a "RemSound RemSound" read on
NVDA because the process name and tooltip matched. Set to
"RemSound - starting up" so the duplicate disappears.
* Recent profile menu items no longer carry a "Recent profile N:"
AccessibleName prefix in either the tray submenu or the File
menu's Recent profiles - now just the profile name. Number-key
mnemonics (&1..&5) untouched.
Manual (readme.html) updated: new section 17 "Audio cue sounds"
documents all six cues, the Play/Browse buttons, the right-click
"Use default sound", and the per-profile semantics. Sections 17-21
renumbered to 18-22. New "System tray icon and its menu" subsection
inside section 4 documents the redesigned right-click menu and the
hover tooltip. MANUAL.md regenerated via sync-manual.py. About box
gets a v3.1 block at the top. RELEASE_NOTES.md fully rewritten for
v3.1.
No wire format change - v3.1 talks to other v3.0.x machines exactly
as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
People landing on the repo page were having to install RemSound just to read what
it does and how to use it. Two doc changes fix that:
1) README.md rewritten as a plain-English landing page. Drops the developer-focused
highlights / build-from-source / project-layout sections in favour of what
RemSound is, who it's for, how to install it, and a prominent link to the manual.
No jargon, no command lines, no NuGet / SDK / ASIO-protocol talk. The dev-side
information that used to live here (build commands, source layout, relay setup)
is still discoverable for anyone who wants it — the source itself is on the same
page, and the relay docs are under server/README.md.
2) MANUAL.md added at the repo root as the GitHub-rendered version of the F1 help.
Markdown derived directly from readme.html via sync-manual.py (new), so visitors
can read the manual inline on the repo page with no download. readme.html stays
exactly where it was (bundled inside RemSound, opened by F1) — it remains the
canonical source of the manual content; MANUAL.md is auto-generated from it.
The sync-manual.py script is invoked automatically from build-release.ps1 as step 0,
before any other release work. It regenerates MANUAL.md from readme.html and then
checks `git diff` on MANUAL.md — if the file changed, the release is paused with a
message asking the user to commit the updated MANUAL.md alongside the release commit.
That makes it structurally impossible to ship a release with a stale GitHub-facing
manual: forgetting to commit MANUAL.md after editing the bundled help triggers a
deliberate release-time stop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: Andre reported audio latency feeling laggier after long sessions
on his Win10 desktop receiving Opus from his laptop. His 23-hour log
showed working set climbing from 83 MB at startup to 3.5 GB at the
end, with the managed heap staying tiny (~5-7 MB) the whole time.
CPU climbed alongside (from steady-state ~7% mid-session to peaks of
~60% by the end) and audio threads ended up doing ~4x the work they
did at the start. Andre's perception of latency drift was the CPU
pressure showing up in audio scheduling, not the buffer itself
growing (bufAvg stayed roughly stable at 25-28 ms).
Root cause: Concentus.Native (introduced in v2.2 / shipped in v3.0)
returns concrete NativeOpusDecoder / NativeOpusEncoder objects that
implement IDisposable and own native libopus state. Three call sites
were taking the IOpusDecoder / IOpusEncoder interface reference and
never calling Dispose:
* StreamSession.Dispose — comment literally said "IOpusDecoder has
no Dispose; nothing else to free", which was correct for the
pure-managed Concentus.OpusDecoder pre-v2.2 but stopped being
correct the moment we added the native binding
* OpusEncoderState.Dispose — same misleading comment, same bug
* SenderLane.OnCodecChanged — overwrote the existing encoder field
without disposing the old instance on codec change
Compounding factor: Program.Main sets GCSettings.LatencyMode =
GCLatencyMode.SustainedLowLatency to keep audio scheduling smooth
(it suppresses gen2 collections). That's correct for the hot path
but it ALSO suppresses the finalizer pass that would have released
the leaked native handles as a backstop. Because the managed heap
stayed tiny, the GC never saw enough pressure to force a gen2 pass
on its own, and the native state piled up indefinitely. Multi-output
receive multiplied the per-output growth.
Fix is in two parts:
1. Call (... as IDisposable)?.Dispose() at every release point —
StreamSession.Dispose, OpusEncoderState.Dispose,
SenderLane.OnCodecChanged before overwrite, AudioRecorder's
Concentus.Oggfile-backed OpusOggFileWriter.Dispose. The
as-IDisposable cast handles both the native and the pure-managed
path transparently (managed-only IOpusDecoder isn't IDisposable;
the as-cast yields null and the null-conditional is a no-op).
2. Periodic native-memory reaper in MainForm.SnapshotLogIfDue — once
every 300 snapshot ticks (~5 min), run
GC.Collect(2, Optimized, blocking, !compacting) +
WaitForPendingFinalizers on a background Task.Run so the gen2
work doesn't hitch the UI thread. Audio threads are separate and
unaffected. Serves as belt-and-braces for any future code path we
forget to wire and for cleaning up any per-call native scratch
the underlying library might accumulate that isn't owned by a
single .NET wrapper.
Expected behaviour after fix: working set settles around 100-200 MB
on a typical receive session and holds roughly flat for as long as
the app stays running. CPU stays at its early-session baseline
across multi-hour sessions. Andre's "latency drift" symptom should
disappear.
Wire format unchanged; same codec list, same UI, same defaults.
v3.0.2 talks to other v3.0.x peers exactly as v3.0 / v3.0.1 do.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The script's ValidatePattern was '^v[0-9]+\.[0-9]+$', which rejected v3.0.1
because the v3.0 line predated any hot-fix releases. Widened to accept an
optional third component so v3.0.1 (and future patch releases) build via
the same path as v3.0 / v2.2 / etc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: ticking the "Automatically open my router for incoming connections
(UPnP)" box in Preferences could freeze the WinForms message pump until
Mono.Nat's NatUtility.StartDiscovery() returned. On Andre's setup it never
did — audio kept flowing (audio threads are independent of the UI thread)
but the window stopped repainting, the system-tray hotkey stopped
responding, and the only way out was Task Manager.
Pre-existing latent bug in the v2.1 UPnP code; we just shipped without
anyone exercising the path on a problematic network (multiple adapters /
VPN / SSDP-swallowing router).
Fix: three call sites moved off the UI thread via Task.Run -
* MainForm OnShown (startup re-enable from saved AppConfig.UpnpEnabled)
* MainForm Preferences applyUpnpEnabled callback (user ticks the box)
* MainForm power-resume handler (Refresh() after sleep/wake)
RouterPortMapper.Start() returns "immediately" only when StartDiscovery
returns quickly; on a slow network it can block synchronously for many
seconds. Same is true of Stop()'s socket teardown and Refresh()'s
teardown-then-restart sequence. All three are now safely backgrounded.
StatusChanged is unaffected - it already fires on the mapper's own thread
and the PreferencesDialog handler BeginInvokes back to the UI thread.
Live status label updates correctly during the new background discovery.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 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>