Commit Graph
100 Commits
Author SHA1 Message Date
EdnunpandClaude Opus 4.8 4d49d5554e Issue #23: instant session-kick self-heal — re-attach BEFORE the first note, no polling
Ed: ~1s was still too slow; the capture must hear the boot audio from the instant
it starts — and no hot check-check-check loop.

Event-driven instant path: hook the Windows session-created notification on the
default render device (the same AudioSessionStartWatcher that catches a per-app
send from its very start). An app setting up an audio session fires it BEFORE its
first sound plays — at the boot lock screen that's LogonUI / the Windows tune /
NVDA arriving. New session + this capture has never heard audio -> re-open right
then, so the re-attached capture is listening from the first note. Zero polling.

- CRITICAL filter: our own silence keepalive also creates a session on that device
  — reacting to our own pid would re-open in an endless loop. Filtered.
- Re-open is now capture-only (sender stop -> rebuild specs -> start): network
  presence stays up, peers see no discovery blip. Shared by both self-heal paths
  (instant session-kick + the 500ms meter watchdog, which stays as the backstop in
  case the session notification doesn't cross sessions pre-login) and rate-limited
  + capped in ONE place so the two paths can't stack re-opens.
- Callback hops to the thread pool — never tears down audio objects from inside an
  audio notification.
- Watcher recreated per apply (re-points at the current default device), disposed
  on suspend/dispose.

Gate: 39/39.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:27:35 +01:00
EdnunpandClaude Opus 4.8 950a61cbf3 Issue #23: deaf-capture detector via endpoint meter — react in ~1s, not 15s
The 15s silence-pulse trigger was far too slow (Ed: the boot tune would be over
before the first check) and silence alone was always a weak signal — a quiet
machine and a deaf capture look identical from inside the stream.

New detector: read the endpoint's OWN output meter (IAudioMeterInformation
.MasterPeakValue) every watch tick, independently of our capture stream, and
compare it with what the capture is hearing. Device audibly playing (meter >=
0.003) while the capture has heard only silence since it opened = the capture is
provably DEAF -> re-open it immediately so it re-attaches to the live audio graph.

- Service loop tick 1000ms -> 500ms; deafness threshold is TIME-based (450ms of
  continuous divergence) so reaction lands ~1s after the first audible sound —
  fast enough that the boot tune itself comes through — and a fast test cadence
  can't trip it (a healthy capture hears real sound within ~200ms).
- Zero churn risk: a quiet machine reads quiet on BOTH sides, so healthy captures
  never re-open (the old design would have re-opened 3x on any quiet stint).
- Frozen callbacks (2s+) still re-open regardless of loudness.
- Ladder: max 3 re-opens per stint, 2s spacing, ends at the first real audio
  heard; refilled on Resume and power resume.
- Meter readers swapped per (re)apply, disposed on suspend; per-device catch
  absorbs a disposed/invalidated endpoint mid-read.
- 15s pulse is now purely diagnostic and logs capPeak + meterPeak maxima with an
  explicit "(DEVICE AUDIBLE BUT CAPTURE SILENT)" flag.
- Decision core (ShouldReopenCapture) pure + pinned by updated self-test.

Gate: 39/39.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:21:54 +01:00
EdnunpandClaude Opus 4.8 c72169c7f5 Issue #23: boot lock-screen silent-capture self-heal (re-open ladder)
Corrected diagnosis (Ed: the machine's OWN speakers audibly play the Windows tune
and NVDA at the boot lock screen — the endpoint is NOT silent): a loopback capture
attached in the first seconds of boot can land on an audio-engine mix the
logon-session audio path was never wired into. Callbacks flow (fed by our own
silence keepalive) but carry none of the audio that is audibly playing, and Windows
fires no device event about it — the previously-shipped device-change reopen never
triggers. Signing in re-plumbs the session audio into the graph, which is why sound
starts instantly with zero change on our side; a capture opened after sign-out
(graph fully live) works at the lock screen, matching Jonathan's reports exactly.

Fix: while sending, the 15s capture pulse now drives a self-heal — if the capture
has heard only silence since it opened (pre-encode peak < 0.001 on every pulse), or
its callbacks freeze, tear down and re-open the capture so it re-attaches to the
live graph. Capped at 3 attempts per sending stint; the first real audio ends the
ladder so a quiet-but-healthy capture is never churned. Ladder refills on Resume()
and on power resume (wake re-plumbs the graph like boot). Every re-open is logged
with the attempt count, so Jonathan's next log shows either "re-open recovered
audio" (fixed) or three silent re-opens (deeper Windows routing issue, and we know
exactly where we stand).

Decision core (ShouldReopenSilentCapture) is pure and pinned by a new self-test.
Gate: 39/39.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:08:21 +01:00
EdnunpandClaude Opus 4.8 1003bbfdf2 Per-app send fixes + apps-mode UI rework (no send-all) + truly-global remembered lists
Per-app capture (the "foobar alone = no sound" report):
- ProcessLoopbackCapture: take ActivateAudioInterfaceAsync's out operation as a raw
  IntPtr (released after completion) instead of a typed interface. The eager RCW cast
  of the not-yet-realised operation object threw InvalidCastException / E_NOINTERFACE
  and killed EVERY specific-app capture at start.
- CompositeCaptureBackend: single IsPushEligible authority shared by Start,
  UpdateSources and the coalesced rebuild. The update paths were missing the
  ProcessLoopback exclusion, so switching whole-device -> one app kept the push
  backend and fed it "proc:<pid>" (GetDevice ArgumentException).
- PushModeWasapiBackend: loud backstop rejecting process-loopback specs.
- Self-test: lifecycle test now FAILS on an activation error (it previously passed
  green with the feature completely dead) + pure routing-rule checks.

Apps-mode UI (Ed 2026-07-16): the "Send all applications" checkbox is GONE from the
main window - applications mode always means picking specific apps; whole-system
audio is devices mode's job. Active list = running apps + any ticked app that is not
running ("(not running)" so it can be unticked); Remembered list = global address
book minus whatever is ticked. In-place list reconcile (no Clear+rebuild) kills the
NVDA double-read of the toggled row. Profile.SendAllApplications stays for the
SERVICE (deliberate divergence - a headless lock-screen sender wants system audio).

Remembered lists now genuinely machine-wide (AppConfig-backed): the settings store is
an intra-process cache, so remembered applications were forgotten on every exit and
remembered peers were per-profile in practice. Both books moved to AppConfig; legacy
per-profile peers are unioned in on profile load; profile save snapshots the global
book back for old-build compat. Cross-instance persistence pinned by self-test.

Service (issue #23): 15s capture pulse (callbacks/bytes/pre-encode peak/frames sent)
while sending - distinguishes "endpoint mix is genuinely silent at the lock screen"
from a pipeline fault, which callbacks alone cannot.

Gate: 38/38.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:50:38 +01:00
EdnunpandClaude Opus 4.8 92ad77ff52 Service: wire sender diagnostics into the service log + capture-health watch (issue #23)
Issue #23's log proved the service can be perfectly healthy on the network
(discoverable, heartbeats, peers armed) while its WASAPI loopback capture is
open but STARVED — zero buffers ever delivered on the boot lock screen, so
peers connect and hear nothing. And the log was blind below 'streaming N
sources' because the service never wired the audio engine's diagnostics in.

- sender.Diagnostic now feeds the service log: capture opens/failures, backend
  switches, the silence keepalive result, composite mode. The keepalive already
  runs on every loopback device (SilentRenderKeepAlive via MixingEngine); on the
  next boot repro the log will show whether it started or was refused in
  session 0 pre-login — the deciding fact.
- New capture-health watch on the existing 1s tick: logs 'first capture
  callback received' once audio genuinely flows, and an explicit 'ZERO audio
  callbacks after 10s' line naming the starved-loopback fault instead of the
  log just going quiet.

Gate 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:56:39 +01:00
EdnunpandClaude Opus 4.8 18b079ba3a Send-app UI: 'Send all applications' is now the first row of the list
Per Ed: instead of a separate 'Send all applications' checkbox that hides the
list, that toggle is now the FIRST ROW of the active-applications list. Ticked =
send the whole system and the individual apps collapse away; untick it and the
running apps appear beneath it (the remembered list shows too). Backed by the
now-hidden sendAllApplicationsCheckbox as state; a reserved sentinel row name
routes the first-row toggle to it. One scrollable control instead of a checkbox
plus a list. Coverage audit + profile round-trip still clean; gate 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:00:17 +01:00
EdnunpandClaude Opus 4.8 3103bac791 ASIO: park the driver on switch-away instead of closing it (Ed's fix)
The 'set ASIO to none => app dies' crash is the native close inside the Audient
driver. Per Ed: don't tear it down, just stop using it. EnsurePersistentAsioLocked
now, when switching to WasapiOnly/none, keeps the persistent ASIO instance OPEN
but PARKED — rewires its callback to a no-op and drops it to zero active pairs
(UpdateSources([]), which explicitly never closes the driver). No native close =
no crash. It's reused instantly if ASIO is turned back on (also dodges the
close+reopen hang), and only truly closes on a driver change or app exit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:55:33 +01:00
EdnunpandClaude Opus 4.8 0c67897448 ASIO close: step-by-step logging + drain before release (Audient crash)
The 'set ASIO driver to none => app dies completely' crash is a NATIVE crash
inside the driver's own Stop/Dispose (Audient), which bypasses every managed
try/catch and leaves no crash file. Two changes to pin it down and reduce it:
- Log each close step (unhook / stop / release / released) so the next crash's
  log ends right after the native call that died, naming it exactly.
- Sleep 60ms after unhooking the callback, before Stop/Dispose, so the native
  close isn't racing an in-flight ASIO buffer callback (a common crash trigger).

Diagnostic + mitigation; the underlying native driver crash needs the Audient
hardware to confirm. Gate 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:50:21 +01:00
EdnunpandClaude Opus 4.8 d9c78a5956 Two app lists in the main window: Currently active + Remembered
In WASAPI 'applications' send mode you now see two checkbox lists, mirroring the
peers lists:
- Currently active applications (Alt+8) — apps running right now.
- Remembered applications (Alt+9) — the global remembered-apps address book,
  including apps that aren't running (shown '(not running)').

Both drive ONE shared send set (selectedSendApps): tick an app in either list
and it's marked to send; the other list re-renders to match. Tick a remembered
app that's closed and, the instant it goes live, it appears ticked in the active
list AND capture begins from its start (the session-start watcher). Unticking in
either removes it. selectedSendApps is the source of truth (= per-profile
SelectedSendApplications on save); both lists render their checkboxes from it.

Layout: inserted the remembered list at row 11, shifted the send-input rows down.
Reconcile now rebuilds both lists; capture change-detection and profile
round-trip unchanged. Coverage audit now 42 controls, mnemonics + tab order
clean. Gate 37/37. Live two-list sync + capture wants on-machine testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:29:11 +01:00
EdnunpandClaude Opus 4.8 e95fc36926 Remembered lists are global; add Clear buttons in Preferences
Remembered peers were already machine-wide for the main app; remembered apps are
now global too (a shared address book across all profiles), so it doesn't confuse
the user with a different list per profile.

- RemSoundSettingsStore.Load/SaveRememberedApplications (global, NOT written into
  profile files) mirrors remembered peers. Removed the per-profile
  Profile.RememberedSendApplications. Ticking an app in any profile adds it to the
  global list (RememberCheckedApps); the per-profile SelectedSendApplications
  (the active/ticked subset) is unchanged.
- Preferences -> General now has two buttons at the end, under a 'Remembered
  lists (shared across all profiles)' header: 'Clear remembered peers list...'
  and 'Clear remembered applications list...', each with a Yes/No confirmation.
  Clearing peers refreshes the main window; clearing apps empties the global list.
- Self-test 'Remembered applications list is global + clearable' (round-trip,
  case-insensitive dedupe, clear); the dialog accessibility audit covers the two
  new buttons (names + mnemonics, no clash). Gate 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 22:54:55 +01:00
EdnunpandClaude Opus 4.8 1e5030b08f Catch a per-app send from its very start (instant capture-on-open)
Fixes the bug where a ticked app that launched AFTER the profile loaded was
never captured: the 3s reconcile only redrew the list and only ran while you
were looking at it, so it never restarted the audio capture.

Now:
- AudioSessionStartWatcher (RemSound.Sender): hooks the Windows audio-session-
  created notification on the default render device, firing the instant an app
  opens an audio session (i.e. about to make its first sound). Re-points on a
  default-device change. Best-effort; falls back to the poll.
- RefreshSendAppCapture: re-applies the send sources whenever the ticked apps'
  running process ids change, driven by both the session watcher (instant) and
  the reconcile poll (backstop) — and the poll now runs regardless of which tab
  is showing, so a remembered app is caught even when you're not on the list.
- Profile.RememberedSendApplications added (persisted; the two-list UI that
  uses it lands next).
- Self-test 'Send-app capture change-detection': the pure signature is stable
  while nothing changes and flips when a ticked app opens/closes. Gate 36/36.

Live behaviour (real apps + the session notification) needs on-machine testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:54:27 +01:00
EdnunpandClaude Opus 4.8 5129aa2a39 Always-on service events log + show failure reasons on screen
So that if the service breaks on a machine where nobody enabled logging first
(e.g. a Win7 tester), we still learn WHY:

- New always-on ServiceStore.AppendServiceEvent -> service-events.log in
  ProgramData, independent of any logging toggle (like the update log).
- Service menu status-query failure now shows the actual reason IN the status
  line (a screen reader reads it) AND records it to the events log, instead of a
  bare 'status unavailable' whose reason only went to the off-by-default app log.
- ServiceAction (install/uninstall/start/stop) records request + outcome to the
  events log.
- ServiceEntry (the elevated child) logs each verb's start, and CATCHES a
  System.ServiceProcess load failure to record its reason before rethrowing (so
  the crash file still gets the full stack) — this is the likely Win7 failure.
- 'View service log' falls back to the events log when no runtime diagnostic log
  exists, so there's always something to view after any service interaction.

Recap of coverage: hard crashes -> crash-*.txt (always, no setup). Graceful
service failures -> now the events log + on-screen reason (no setup). Gate 35/35.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:08:49 +01:00
EdnunpandClaude Opus 4.8 4d1815e071 Show the Service menu on all Windows (let Win7 try the service)
Removes the Windows-10+ version gate on the Service menu so a Win7 user can
actually attempt Install/Configure and we can see whether the service works
there. Still launch-safe: building the menu references no service type (inlined
const verbs + method-group handlers), so System.ServiceProcess is not loaded at
window construction on any OS — verified by the 'main window builds without
loading the service assembly' self-test and an empirical 0-modules launch check.
The assembly loads only when the menu is opened (status query) or an action runs,
both try/catch-wrapped, so on Win7 it degrades to 'status unavailable' rather
than crashing. Re-gate with IsWindowsVersionAtLeast(10,0) if Win7 can't run it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:59:11 +01:00
EdnunpandClaude Opus 4.8 5fa88aba5c Service: reachability-gated sending (issues #8/#15), matching the app
Checked the service's connection handling against the networking issues that
shaped the main app. The service reused the low-level protocol components
(discovery, heartbeat, listener, sender), so it inherits their behaviour — but
it was MISSING the app's higher-level connection management from
MainForm.RefreshAudioReceivers: it called SetReceivers once with ALL configured
peers and blind-sent forever, even into dead addresses. That's exactly issue #8
(streaming into a peer that's gone) and it ignored issue #15 (retry/recover).

Fix: the service now streams ONLY to peers the heartbeat can reach, drops any
that stay unreachable past a 30s grace window, and re-arms them the moment they
recover — the same logic (and 30s threshold) as the app. Runs on the service's
existing 1s poll tick (no new timer, no background pile-up). Send-only, so no
"actively receiving" carve-out.

- ServiceNetworkPresence.PeerHealthSnapshot() exposes the heartbeat health.
- ServiceSendHost.ComputeArmedEndpoints (pure) + RefreshSendArming, wired into
  RunLoopCore.
- Self-test "Service reachability-gated sending": reachable armed, long-
  unreachable dropped, grace-window kept, no-data arms all. Gate 35/35.

Coverage of the other networking issues: multi-homed LAN+VPN (#18) is a
receiver-side allow-list fix — N/A to a send-only service, and its sender-side
support (announcing on all interfaces) is inherited from PeerDiscoveryService.
Forced/locked IPs (#17/#7): the service resolves peers literally and never
follows names, so it's inherently "locked" (what #17 asked for). Device
recovery (#5): already built. NOT built: discovery-based name-following (the
app can chase a peer whose IP changes); the service stays on its configured
addresses by design — flagged for Ed to decide if the service needs it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 08:33:54 +01:00
EdnunpandClaude Opus 4.8 c482857314 Fix the actual Win7 launch crash (issue #22): empty send-mode list
The real cause — from the tester's crash stack, not my earlier (wrong)
System.ServiceProcess theory: ApplySendModeVisibility() is called early in the
MainForm constructor (via ApplyAsioMode), BEFORE the Input/Output tab populates
the send-mode ListBox. On Windows 7 process-loopback is unsupported, so it took
the `!supported` branch and set SelectedIndex on an EMPTY list, throwing
ArgumentOutOfRangeException ("value ('0') must be less than '0'") and crashing
at launch. On Windows 10/11 that branch is skipped (process-loopback IS
supported), which hid it from the gate and from every Win10/11 test.

.NET 10 genuinely runs on the tester's Win7 SP1 box (CoreLib 10.0.826 in the
crash) — so the earlier assembly-load theory was wrong; those Win7 changes were
addressing a non-problem. This is the fault.

Fix: guard the SelectedIndex set with `sendModeList.Items.Count > 0`. When the
list is empty there's nothing to reset (it's created selecting Devices, and this
runs again once the tab is built).

Test: "Main window builds where process-loopback is unsupported (issue #22)" —
forces ProcessLoopbackCapture.IsSupported = false (new ForceSupportedForTest
seam) and constructs the headless MainForm, so a Win10/11 box exercises the Win7
path. VERIFIED it reproduces: with the guard reverted the test fails with the
exact tester exception; with the guard it passes. Gate 34/34.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:18:30 +01:00
EdnunpandClaude Opus 4.8 0567ea22c3 Restore Win7 launch safety: version-gate the Service menu, don't probe
The capability PROBE (added earlier for feature-detect) constructed a
ServiceController during window construction, which loaded System.ServiceProcess
on EVERY launch. On Win11 that's harmless, but on Win7 it meant the app was
attempting that risky load at startup, surviving only on a catch — too fragile
for a guarantee. Verified empirically: a normal launch was loading
System.ServiceProcess.ServiceController.dll again.

Fix: decide Service-menu visibility with a Windows-VERSION check
(OperatingSystem.IsWindowsVersionAtLeast(10,0)) instead of a runtime probe. A
version check touches no service type, so the service assembly is NOT referenced
at launch on any OS. On Win7/8 the menu is hidden and that assembly is never
loaded; on Win10+ it loads only later, if the user actually opens the Service
menu (already wrapped in try/catch). Feature-detecting on Win7 and keeping Win7
launch-safe are mutually exclusive (you can't test the load without doing it),
so we choose guaranteed launch safety.

- Deleted ServiceCapability (the probe) — now unused.
- Replaced its self-test with "Main window builds without loading the service
  assembly (Win7-safe)": constructing the main window must not load
  System.ServiceProcess. Verified empirically too: normal launch now loads 0
  service modules (was 1).

Gate 33/33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:03:17 +01:00
EdnunpandClaude Opus 4.8 541f66d379 Make the service a reachable network peer (discoverable + connectable)
The send-only service could never be found or connected to — it only pushed
audio blindly to fixed peer addresses, with no beacon and nothing listening.
So a phone could neither discover it nor dial it. This gives the service a real
network presence built from the SAME components the interactive app uses, wired
the same way, so its discovery / heartbeat / NAT-pinhole / relay behaviour is
identical to the app's — which is what makes it "one identity" (both announce
under the machine name and pair through the relay the same way). Works LAN and,
inheriting the app's relay path, across the internet.

New ServiceNetworkPresence (reuses PeerDiscoveryService + AudioReceiver listener
+ HeartbeatService, wired to the host's AudioSender):
- Discoverable: announces send-only under the machine name (LAN broadcast +
  unicast to the configured peers for across-the-internet).
- Reachable: binds the well-known audio-port listener; PLAYBACK stays OFF
  (send-only never plays received audio — the listener only carries
  heartbeat/pairing).
- Pairable: heartbeat pings the peers (opens the NAT pinhole, drives relay
  pairing); replies route back on the listener (LAN) or the sender socket
  (relay).

Integrated into ServiceSendHost: comes up alongside the sender while streaming,
and — critically — tears ALL the way down to a shell on Suspend (stop
announcing, unbind the port, stop the heartbeat) so the service and the
interactive app never both hold the network. A brief dropout on that handover
is accepted (Ed's call); only one owns the network at a time.

Tests: "Service network presence" (Start binds the listener + comes up; Stop
unbinds to a shell; re-startable). "Service send host" now also asserts the
presence comes up with streaming and drops to a shell on Suspend. Gate 33/33.

NOTE: the live discover/connect/relay path can only be proven by the tester's
phone — the headless tests prove the lifecycle and teardown, not the internet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:52:38 +01:00
EdnunpandClaude Opus 4.8 91f5fc74c0 Add "View service log" (diagnostic activity log), distinct from update log
Tester couldn't see anything under "View service update log" -- that item only
shows the SELF-UPDATE log (written when the service updates itself to a new
version), so with no update it's empty. What he actually wanted is the service's
ACTIVITY log, which records why the service is or isn't sending
("streaming N sources to M peers", "profile has no WASAPI send sources",
"no reachable peers"), but there was no way to open it.

- New Service menu item "View service log" opens the newest diagnostic log in
  ProgramData\RemSound\service\logs. If none exists it explains that service
  logging must be enabled first (Configure service profile -> Logging), so the
  path to getting a log is discoverable.
- ServiceStore.LogsDirectory + NewestLogFile() to locate it (same absolute path
  for the SYSTEM service and the interactive user).
- Clarified the update-log "nothing yet" message to point at the new item.

This also unblocks diagnosing the pre-login send issue: enable service logging,
reproduce, then View service log to see the exact reason.

New self-test "Service log discovery": folder resolves under the service dir,
newest .log is chosen, empty case handled. Gate 32/32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:18:18 +01:00
EdnunpandClaude Opus 4.8 e3e57affb4 Fix Service menu shortcut: Alt+J, not Alt+S (Send owns Alt+S)
Alt+S didn't open the Service menu because the always-visible "Send my audio"
checkbox already owns Alt+S, and a visible control beats a top-level menu for
the same Alt key. Every letter in "Service" (S/e/r/v/i/c) collides with a
control (Send / Receive / Volume / Connected peers / EQ / ...), so the menu
now uses Alt+J -- unused anywhere in the main window, so it opens reliably from
every tab. Same approach the Record menu already uses ("Record (Alt+&K)").
Kept Send on Alt+S (frequent control) rather than moving it.

New self-test "Menu shortcuts don't clash with controls": builds the main
window and asserts no top-level menu mnemonic collides with any control
mnemonic. The existing coverage audit couldn't catch this -- menu items are
ToolStripItems, not Controls, so its control walk never saw them. Confirms the
other four menus (File/Record/Options/Help) were already clean. Gate 31/31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:17:39 +01:00
EdnunpandClaude Opus 4.8 2dad00c42f Offer the service on Win7+ by feature-detecting, not version-gating
Replaces the hardcoded "Windows 10 or newer" gate on the Service menu with a
runtime capability probe (ServiceCapability). It tries to load the .NET
service machinery (System.ServiceProcess) and offers the Service menu wherever
that succeeds -- including Windows 7 if the service layer loads there, which is
what Ed asked for ("install on Win7 or above"). If the assembly can't load on
an older/unsupported Windows, the probe catches it and the menu is simply
hidden, so such a machine can never be crashed by it.

Safety by construction: the reference to the service types lives only in the
NoInlining Probe method, so the assembly load is triggered by the CALL from
IsAvailable (inside its try/catch) and is catchable -- not during JIT of the
caller, which would be fatal. The startup path stays service-type-free via
ServiceEntry, so this probe runs only at window construction, never at launch.

Probe uses the parameterless ServiceController ctor: it forces the assembly to
load but touches no service and no SCM. (First cut read .ServiceName on a
made-up name, which actually queries the SCM and threw "service not found" --
the self-test caught that it was hiding the menu on Windows 11 too.)

New self-test "Service capability probe": available + cached + never throws on
the Win10/11 gate runner. Gate 30/30.

Note: this makes the service INSTALLABLE on Win7 wherever the layer loads; it
does not prove the service RUNS there (Session-0 capture on an unsupported
runtime) -- only a real test on the Win7 box can confirm that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:55:16 +01:00
EdnunpandClaude Opus 4.8 9392cc1fcc Fix Win7 launch crash: keep service types off the startup path
Reported: RemSound no longer launches at all on Windows 7 since the
send-only service was added. Cause: RemSoundService derives from
ServiceBase (System.ServiceProcess), and Program.Main called
RemSoundService.RunAsService() directly in its body. The runtime resolves
every type a method names when it JIT-compiles that method -- so the moment
Main was compiled, at the very start of every launch and before any argument
was read, it force-loaded System.ServiceProcess. That assembly won't load on
Windows 7 under the .NET 10 runtime, so Main failed to compile and the app
died with no window. net10 has always been the target, so this was a pure
regression from the service work, not a framework change.

Fix:
- Move the whole service-verb dispatch into a separate ServiceEntry class.
  Program.Main now only calls it (a) after a cheap check that uses only
  inlined const verb strings, and (b) only when a service verb is actually
  present. A normal launch never JIT-compiles anything that names a service
  type, so System.ServiceProcess is never loaded. Verified empirically:
  a normal launch loads 107 modules, none of them System.ServiceProcess.
- Gate the Service menu to Windows 10+ (OperatingSystem.IsWindowsVersionAtLeast),
  mirroring how the "capture individual apps" feature is gated. On Win7/8 the
  menu isn't shown and no service code is reachable. Made the status-query
  handler defensive too, so a query failure can never crash the menu.

New self-test "Service verb gate": normal launches (no args, --silent,
--profile, --connect, --minimized, --config-dir) are never treated as a
service invocation; all five service verbs are recognised case-insensitively;
and deciding a normal launch loads no service assembly. Gate 29/29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:41:56 +01:00
EdnunpandClaude Opus 4.8 26903950f7 Add "auto save non-read only profiles" preference (silent)
New global setting in Preferences -> General, right after "Browse for
profiles folder": a list "Auto-save non-read-only profiles" with Never /
Every 2 / 5 / 10 / 15 / 20 / 30 minutes. When enabled, RemSound periodically
saves the current profile, but ONLY if it is a real saved profile, is NOT
read-only, and has unsaved changes -- and it saves SILENTLY (no save cue,
no confirmation), so it never interrupts the user.

- AppConfig.AutoSaveNonReadOnlyMinutes (machine-wide, 0 = Never = default).
- SaveProfileTo gains a playCue flag; auto-save passes playCue: false.
- MainForm autoSaveTimer + ApplyAutoSaveTimer + ShouldAutoSave guard,
  re-applied live when the setting changes in Preferences.
- New self-test "Auto-save non-read-only profiles": options list, AppConfig
  persistence, the guard (read-only / blank / unchanged all skipped), and the
  timer turning on/off. Gate 28/28.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:32:17 +01:00
EdnunpandClaude Opus 4.8 71fb065233 Add make-tester-zip.ps1 for clean portable copies to hand to testers
Local checkpoint - NOT for public release.

Produces a zip that mirrors the publish folder (every current code fix, program files,
pdb, all runtimes, sounds, readme, Tolk, install scripts) with NONE of the developer's
personal state (no profiles/config/logs/recordings). Same safety approach as
build-release.ps1: publishes into a fresh never-run folder, then scans both the staged
files AND the finished zip and aborts if any personal data is present. Not a public
release (no tag/notes/GitHub) - a private portable copy for external testers.

Runs the build-and-test gate first by default (don't ship testers a broken build);
-SkipGate to bypass, -Output to pick the path (defaults to D:\Dropbox\remsound.zip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:27:08 +01:00
EdnunpandClaude Opus 4.8 4b2ecca960 Service: always-on update log + "View service update log" menu item
Local checkpoint - NOT for public release. Ed wants logging for the service updating.

- New ALWAYS-ON update log at ProgramData\RemSound\service\update.log (not gated on the
  service-logging toggle - updates are rare but important). It records the full sequence:
    * "update detected: newer RemSound.exe (5.3) found, running 5.2 - restarting"
    * the restarter's own "stopping" / "service started" (or "START FAILED - <reason>")
    * "update complete: now running version 5.3"
  The restart PowerShell writes its stop/start outcome itself, so the part that runs AFTER
  the old service process is gone - and any failed start - is still captured. A pending
  marker set at detection and consumed on the next start closes the loop (its absence flags
  a stuck update).
- Service menu gains "View service update log" to open it (friendly message if none yet).

Update log + pending-marker round-trip covered by the isolation self-test. Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:59:40 +01:00
EdnunpandClaude Opus 4.8 d5767050f2 Service: show the running version in the Service menu (so a self-update is visible)
Local checkpoint - NOT for public release. Answers "how will we know if the service
updated itself?"

- The service records its version + start time on every OnStart into ServiceStore
  (ProgramData). A self-update restart therefore bumps the version and refreshes the
  start time.
- The Service menu status line now shows it: "Service: installed, running — version 5.3,
  running since 2 min ago". So you can see the running version at a glance, and a recent
  start with a bumped version is the self-update landing.
- Also logged on start ("service: OnStart, version X") for the service log.

Status round-trip covered by the isolation self-test. Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:51:59 +01:00
EdnunpandClaude Opus 4.8 56dbd0b80f Service: pick up app updates by self-restarting onto the new binary
Local checkpoint - NOT for public release. Ed: an update should tear down the service,
update it, and restart it. It didn't - the updater has no service awareness.

Current (unchanged, verified correct): the auto-updater renames the install files aside
and copies the new ones in, which a running service TOLERATES (no failed swap). The old
version keeps streaming; the new files sit in place.

New: the service now adopts the update itself. Because it runs as SYSTEM (which has the
rights the non-elevated updater lacks), a 45s timer notices when a strictly-newer
RemSound.exe has landed next to it and restarts itself onto the new binary (detached
PowerShell Stop-Service+Start-Service). Loop-safe: only fires on a strictly-newer on-disk
version; any uncertainty (file mid-swap, unparseable version) means don't restart, and
after the restart on-disk == running so it never re-triggers.

Test "Service registration args" now also covers the version-comparison logic (newer =>
restart; same/older/missing => no restart). Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:38:52 +01:00
EdnunpandClaude Opus 4.8 941e0df99f Service: log install/uninstall/start/stop/configure to the RemSound log
Local checkpoint - NOT for public release. ServiceAction and ConfigureServiceProfile
now write to the main app's log (gated on RemSound logging), so turning on logs in
RemSound captures the install request + result code and profile saves. (Windows also
records the install itself in the System Event Log, Event 7045, always.) Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:30:55 +01:00
EdnunpandClaude Opus 4.8 92ed477ddf Service: harden for unattended running (auto-restart, findable logs, resume)
Local checkpoint - NOT for public release. A "what else does a service need" pass.

- AUTO-RESTART ON CRASH: DoInstall now sets sc failure actions (restart 5s/10s/then 60s,
  reset daily). Without this a crashed service stays dead until reboot - fatal for an
  always-on streamer.
- FINDABLE LOGS: --run-service redirects the service's data dir to the machine-wide
  ProgramData\RemSound\service location, so its log sits next to its profile instead of
  buried in the SYSTEM account's AppData.
- POWER RESUME: the service handles OnPowerEvent and re-opens capture on wake (audio
  devices re-initialise after sleep; the device-change watcher usually catches it, but a
  resume doesn't always fire an endpoint change, so we re-open explicitly).
- Start/stop already auto-log to the Windows Event Log via ServiceBase.

Test: "Service registration args" now also checks the audio-service dependency and the
auto-restart failure args. Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:43:33 +01:00
EdnunpandClaude Opus 4.8 4f5265d8b0 Service: isolate the profile in ProgramData, out of all normal profile paths
Local checkpoint - NOT for public release. Ed: the service profile must never be
reachable except through the Service menu.

Also fixes a real bug: the service runs as SYSTEM, whose per-user data folder is NOT the
interactive user's - so a profile saved in the user's profiles folder (or AppConfig, both
per-user) was invisible to the service. It would have idled, never streaming.

- New RemSound.Core.ServiceStore: the service profile + its settings (logging) live in a
  MACHINE-WIDE ProgramData\RemSound\service location - same absolute path for the user
  (config dialog) and SYSTEM (service). Moved ServiceProfileName/ServiceLoggingEnabled off
  AppConfig (per-user) onto this store.
- ServiceSendHost.FromConfig + RemSoundService now read ServiceStore; ConfigureServiceProfile
  saves there (and migrates + deletes any profile left in the old user-folder location).
- Because it's no longer in the user's profiles folder, it can't appear in the startup
  picker, File->Open, Recent profiles, or the password manager (all of which read the user
  ProfileStore); the reserved-title filter in ListProfileTitles stays as belt-and-braces.
- Password button renamed "Set service profile password".
- New self-test "Service profile isolation": store is under ProgramData, the reserved title
  is filtered from the listing, and it round-trips through the machine-wide store.

Gate 27/27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:23:10 +01:00
EdnunpandClaude Opus 4.8 f0b35b2b8c Tests: remove the opt-in real-service-lifecycle test
Local checkpoint - NOT for public release. Ed will test the real install/service and
the sound by hand, so the elevated (admin-only) install/start/stop/uninstall self-test
isn't worth keeping. Removed it and its IsAdministrator helper. The headless service
tests stay (parity, app-yield, send host, registration args) - those run in the gate and
guard the service against drifting from the main app. Gate 26/26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:51:47 +01:00
EdnunpandClaude Opus 4.8 4ee9cf6907 Tests: opt-in real Windows-service lifecycle (install/start/stop/uninstall)
Local checkpoint - NOT for public release.

New self-test "Service real lifecycle" drives the ACTUAL SCM end to end: install ->
report installed -> start -> report running -> stop -> report stopped -> uninstall ->
report gone. Needs admin, so it's OPT-IN via REMSOUND_TEST_SERVICE=1 and skips cleanly
in the normal unprivileged gate. It clears any leftover registration first (DoUninstall
stops a running copy, so a stray from an aborted run can't fail it) and always removes
the service afterwards, even on failure - so it never leaves a service pointing at a
throwaway exe. Run elevated: set REMSOUND_TEST_SERVICE=1 then RemSound.exe --selftest.

The app-yield takeover is already covered headlessly by "Service send host (stream +
yield)"; a full real end-to-end (installed service goes quiet when the app opens) needs
a live peer to observe and stays a manual check. Gate 26 passed, 1 skipped of 27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:41:26 +01:00
EdnunpandClaude Opus 4.8 ce45489b30 Tests: guard the service peer-port default matches the main app
Local checkpoint - NOT for public release. Adds a check that a peer with no explicit
port resolves to RemPacket.DefaultPeerDialPort (not the local audio port), guarding the
third audit divergence. Gate 26/26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:33:27 +01:00
EdnunpandClaude Opus 4.8 78a9aa6572 Service: fix divergences from the main app's send path
Local checkpoint - NOT for public release. Audited ServiceSendHost against MainForm's
send path (Ed: make the service reuse the same code, be just as stable). Three real
divergences found and fixed:

1. ENCRYPTION FINGERPRINT (critical): the host set sender.AudioKey but NOT
   sender.AudioFingerprint. The main app (RecomputeAudioCrypto) sets both, and the peer
   verifies the fingerprint before accepting a stream - so the service's encrypted audio
   would have been REJECTED at the far end. Now derives and sets both from the password.
2. OPUS FRAME: the main app applies EffectiveOpusFrameSamples (the "Small" send rate
   halves the Opus frame); the host passed the raw frame, so it would encode differently
   than the main app for the same profile. Now reuses MainForm.EffectiveOpusFrameSamples
   (made internal - same code, not a copy).
3. PEER PORT: send target fell back to the profile's LOCAL AudioPort; the correct default
   is RemPacket.DefaultPeerDialPort (what the main app's manual-peer path uses). Same value
   today but the right constant.

Reviewed and OK: sender defaults to WasapiOnly (no SetAudioMode needed); BuildSendSpecs
matches ApplySendSources for explicit-device profiles; default-device changes are covered
by the device-change watcher; direct-send (no relay/StartReceiving) is the intended v1 scope.

New self-test "Service sender parity" asserts key + fingerprint + effective Opus frame
match the main app. Gate 26/26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:13:48 +01:00
EdnunpandClaude Opus 4.8 4fb490b2d5 Service: use the main app's event-driven device watcher, not a poll
Local checkpoint - NOT for public release. Ed: don't add continuous background checks
(they've piled up before) - reuse the disconnect/reconnect mechanism the main app uses.

Replaced the 5s packet-flow health poll with AudioDeviceChangeNotifier - the SAME
event-driven watcher the main window uses. It fires only when a device is added/removed/
changes state or the default changes (nothing polls, nothing accumulates - it's a single
registered COM callback, disposed with the host). While the service intends to send, that
event re-opens capture, covering: the audio stack finishing coming up at boot, a device
plugged/unplugged, and the audio service restarting. Debounced (a hot-plug fires a burst).

Also dropped the per-tick Resume retry: now one start attempt per app-absence, then the
device watcher drives any re-open. The only remaining periodic thing is the tiny 1s
presence-token check for the app-yield (a mutex probe - allocates nothing, accumulates
nothing). Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:04:26 +01:00
EdnunpandClaude Opus 4.8 a7742fe2ec Service: self-heal if capture isn't ready (or a device drops) at start
Local checkpoint - NOT for public release. Answers "what if it looks for sound
devices before their services are up?"

Two layers now:
- depend= Audiosrv/AudioEndpointBuilder (prior commit) makes Windows start the service
  only once the audio services are running.
- Self-heal in the run loop: while it should be sending, if no packets have flowed for
  ~5s then no capture is actually running (endpoints not fully ready at boot, a device
  dropped, or the audio service restarted) - it re-opens the capture. And while NOT
  sending it already re-tries every second, so a slow-to-appear audio stack or a device
  that returns later is picked up automatically.

So even if the service races ahead of the endpoints being fully enumerated, it keeps
retrying/re-opening until audio actually flows, rather than sitting silent. Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:52:16 +01:00
EdnunpandClaude Opus 4.8 91b3f0c1c1 Service: menu before Options + start-after-audio dependency
Local checkpoint - NOT for public release.

- Service menu now sits before Options in the menu bar (File / Record / Service /
  Options / Help), per Ed.
- The service is registered with depend= Audiosrv/AudioEndpointBuilder, so Windows
  starts it the instant the audio services are ready at boot (before login) - the
  earliest point WASAPI capture can find any audio. It CANNOT start before the audio
  services (there'd be no endpoints to capture, and no sound exists before audio is up),
  so this is the earliest useful start. DoInstall now uses the single BuildCreateArgs
  source of truth.

Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:34:34 +01:00
EdnunpandClaude Opus 4.8 004e01306b Service dialog: Connectivity tab now mirrors the main window
Local checkpoint - NOT for public release. Ed: the connectivity box was totally
different from the main window's - I'd rolled a bespoke listbox + textbox + buttons
instead of copying the real tab.

Now built like the main window's Connectivity tab:
- Theme.SectionHeader("Peers") header.
- Peers list is a wired CheckedListBox ("Peers to send to", Alt+C) with the shared
  CheckedListAccessibility so it announces state like every other list; a ticked peer
  is one the service sends to, Delete removes it.
- "Add peer by IP (Alt+A)" button uses the SAME ManualPeerPrompt the main window uses,
  not an inline textbox.
- Set password button kept.

Checked peers persist as SelectedConnectedPeers (the send-to set); every listed peer is
kept as RememberedPeers. Gate 25/25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:30:37 +01:00
EdnunpandClaude Opus 4.8 37f28d6c91 Service config dialog: rebuilt to match the main window exactly
Local checkpoint - NOT for public release. Ed flagged the dialog as sloppy - lists
not announcing, wrong tab order, leftover Alt+4/5/6, "Standard/Tight" instead of the
real labels, and not looking like the main window.

- Screen-reader parity: new CheckedListAccessibility.Wire (factored from the main
  window's WireCheckedListAccessibility) drives every checked list - announces
  "checked, <item>. Item N of M. Press Space to toggle." on focus and arrow, plus
  first-letter nav that never toggles. This was the core miss (lists not announcing).
- Layout parity: uses the house FormLayoutRows rows + status labels, QuietTabControl,
  AccessibleCheckBox, MnemonicLabel, and the app icon - so it reads/looks like a real tab.
- Tab order now mirrors the main window: Connectivity, then Audio send, then Audio profile.
- Alt keys renumbered for the dialog (were lifted verbatim from the I/O tab): send tab
  1-5, connectivity 1-2, profile keeps the main window's C/P/D.
- Exact main-window labels copied verbatim: codec ("PCM 48K 24 bit - uncompressed", etc.),
  packet size ("Standard (5 ms PCM, 10/20 ms Opus)" / "Small (2.5 ms ...)"), and the
  lock-to-audio-clock text + accessible description.

Gate 25/25 (dialog passes the accessibility audit: names + unique mnemonics + tab order).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:21:48 +01:00
EdnunpandClaude Opus 4.8 9a7bd6c9e8 Release scripts: notes-freshness check + gate the test deploy
Local checkpoint - NOT for public release.

- build-release.ps1: abort if RELEASE_NOTES.md is missing or doesn't mention the tag
  (it leads with "# RemSound <tag>"), so a stale notes file from the previous release
  can't ship with the wrong content via `gh release create --notes-file`.
- deploy-test.ps1: a binary deploy now runs the build-and-test gate (run-tests.ps1)
  first and refuses to deploy a build that didn't pass - so a test build Ed picks up
  has always passed the suite. Sound-only refreshes skip it (no code change). New
  -SkipGate switch overrides when the gate was just run. Verified end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:06:34 +01:00
EdnunpandClaude Opus 4.8 2a8c608653 Release scripts: bulletproof test-deploy data preservation + tag/version gate
Local checkpoint - NOT for public release.

Two gaps found reviewing the publish/release scripts:

- deploy-test.ps1: the binary sync (publish -> D:\Dropbox\remsound) used /E with no /MIR,
  so it never DELETES user data - but it could OVERWRITE it if publish\ ever accumulated a
  "user settings and logs" folder. Added /XD/'user settings and logs','recordings','logs',
  'profiles','config' + /XF 'global config.json','remsound.config.json' so the binary sync
  is physically incapable of touching Ed's profiles/logs/config in either direction. Test
  deploys keep ALL of his data current, by construction.

- build-release.ps1: never checked the -Tag against the csproj <Version>. A mismatch ships
  RemSound-<tag>.zip containing a different version's binary, which the in-app updater (it
  downloads by tag name) reads as a perpetual "update available". Now aborts up front if
  tag != csproj <Version> - also catches a forgotten version bump.

The intended split was already correct: test deploy keeps everything; build-release
publishes to a fresh folder, strips pdb, and scans staged files AND the zip for any
profiles/logs/config/recordings, aborting if found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:56:34 +01:00
EdnunpandClaude Opus 4.8 a52f08c188 Tests: functional profile round-trip through the real main-window controls
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>
2026-07-12 19:16:08 +01:00
EdnunpandClaude Opus 4.8 499e9644d7 Tests: split-track recording coverage (per-peer + own send)
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>
2026-07-12 19:02:02 +01:00
EdnunpandClaude Opus 4.8 c8fed26115 Tests: headless main-window coverage (all tabs + controls) + fix Alt+L clash
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>
2026-07-12 18:59:09 +01:00
EdnunpandClaude Opus 4.8 fd981211c0 Tests: recording churn + env-gated soak, leak-accurate handle checks
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>
2026-07-12 16:09:04 +01:00
EdnunpandClaude Opus 4.8 47e4159cf9 Tests: broaden dialog accessibility audit + tab-order cycle check
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>
2026-07-12 16:03:24 +01:00
EdnunpandClaude Opus 4.8 b258bec9a2 Tests: recording engine coverage (all formats + source gate + mono)
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>
2026-07-12 15:58:46 +01:00
EdnunpandClaude Opus 4.8 6e84f491e8 Service: manual section + menu-list sweep
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>
2026-07-12 15:53:54 +01:00
EdnunpandClaude Opus 4.8 f4551517e5 Service: config dialog + Service menu
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>
2026-07-12 15:27:37 +01:00
EdnunpandClaude Opus 4.8 5dbbe4dd71 Service: --run-service host + registration CLI (install/uninstall/start/stop)
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>
2026-07-12 15:19:01 +01:00
EdnunpandClaude Opus 4.8 e648bee531 Lock-screen service: spike + headless send host + app-yield coordination
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>
2026-07-12 14:59:31 +01:00
EdnunpandClaude Opus 4.8 8f61eb800d Add lifecycle-churn soak test for runtime transitions
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>
2026-07-12 11:31:50 +01:00
EdnunpandClaude Opus 4.8 ffeba4ad2b Fix hard crash when toggling ASIO while sending a specific app
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>
2026-07-12 11:23:18 +01:00
EdnunpandClaude Opus 4.8 f8a42f2806 Per-app send: UI on the Input/Output tab, saved per profile
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>
2026-07-12 10:53:58 +01:00
EdnunpandClaude Opus 4.8 8a7c4ddf2b Multi-output fan-out, offline-marker fix, #19, and per-app send engine core
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>
2026-07-12 05:58:33 +01:00
EdnunpandClaude Opus 4.8 2fb9274a95 v5.2: stability and polish — deep-audit bug fixes + install-flow fixes
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>
2026-07-09 20:54:53 +01:00
EdnunpandClaude Opus 4.8 7038fef67c v5.1: install RemSound as a proper Windows app, plus smaller sounds
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>
2026-07-09 15:45:21 +01:00
EdnunpandClaude Opus 4.8 a92928d357 Docs: add the iOS (TestFlight beta) companion app alongside the Android one
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>
2026-07-09 11:18:02 +01:00
EdnunpandClaude Opus 4.8 849a0b4c73 v5 manual: deep audit sweep, About-box changelog, version bump
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>
2026-07-08 18:22:59 +01:00
EdnunpandClaude Opus 4.8 aeca24ae17 v5 pre-release: multi-track drift fix, more self-tests, logging, CLI, version bump
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>
2026-07-08 18:19:01 +01:00
EdnunpandClaude Opus 4.8 fcc3cdc793 Ctrl+1..9 to jump to a tab by its current position (main window + Preferences)
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>
2026-07-08 14:31:22 +01:00
EdnunpandClaude Opus 4.8 597b002f01 Clearer wording for the tab-order list: "Tab order, press the move up and move down buttons to reorder"
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>
2026-07-08 13:31:47 +01:00
EdnunpandClaude Opus 4.8 78684f29f1 New Preferences "Appearance" tab: theme + show-pan/EQ moved in, tab reordering, peer-list toggles
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>
2026-07-08 12:56:06 +01:00
EdnunpandClaude Opus 4.8 9c8e50667b Actually fix Connectivity tab order — set TabIndex on the list WRAPPERS, not the lists
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>
2026-07-08 12:04:57 +01:00
EdnunpandClaude Opus 4.8 9d54d4c9e9 Connectivity tab order: Add-by-IP after the lists (per Ed's revised order)
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>
2026-07-08 11:57:03 +01:00
EdnunpandClaude Opus 4.8 5e974f7064 Fix Connectivity tab order (SetTabOrder was overriding layout) and announce parametric gain
* 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>
2026-07-08 11:49:04 +01:00
EdnunpandClaude Opus 4.8 e35f2f318a Testing-feedback fixes: tab order, label wording, parametric editing, recording default
* 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>
2026-07-08 11:36:02 +01:00
EdnunpandClaude Opus 4.8 90a1ab3257 Visual polish pass: modern font, light/dark theme, app icon, accent, health cue
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>
2026-07-08 08:47:26 +01:00
EdnunpandClaude Opus 4.8 3c449d78b3 Named-peers registry + Options → Manage named peers dialog
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>
2026-07-08 08:20:31 +01:00
EdnunpandClaude Opus 4.8 289bb2b15c F2 renames the highlighted connected peer (Windows Explorer idiom); manual note
Held for next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:08:47 +01:00
EdnunpandClaude Opus 4.8 f35f6c3c28 Peer rename + details box on Connectivity tab; Add band dialog fixes
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>
2026-07-07 23:56:42 +01:00
EdnunpandClaude Opus 4.8 033edd776f Volume/pan/EQ tab overhaul: one master switch, peer checklist, 16-band parametric EQ
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>
2026-07-07 23:21:18 +01:00
EdnunpandClaude Opus 4.8 eaae76d015 Manual: document Pan and EQ, multi-track/raw recording, new naming, and the tab
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>
2026-07-07 20:43:23 +01:00
EdnunpandClaude Opus 4.8 57847ff358 Pan/EQ: label the advanced mode "12 band"; single recordings include the machine name
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>
2026-07-07 20:36:17 +01:00
EdnunpandClaude Opus 4.8 c138fdf39c Pan/EQ: 12-band advanced EQ — swap 3 kHz for 80 Hz
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>
2026-07-07 20:21:39 +01:00
EdnunpandClaude Opus 4.8 bbfa76945a Recording settings: open focused on the first checkbox, not the option columns
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>
2026-07-07 20:20:31 +01:00
EdnunpandClaude Opus 4.8 d0d14ab95c Pan/EQ: 12-band advanced EQ (add 3 kHz and 6 kHz)
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>
2026-07-07 20:16:21 +01:00
EdnunpandClaude Opus 4.8 fc4ad3bb92 Recording fixes: date folders on top, and multi-track honours the Source setting
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>
2026-07-07 20:10:39 +01:00
EdnunpandClaude Opus 4.8 6b1e65f418 Pan/EQ: add a per-peer volume slider (before pan)
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>
2026-07-06 10:45:52 +01:00
EdnunpandClaude Opus 4.8 efa9435da7 Recording: sum each peer's streams per track (record how it sounds)
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>
2026-07-06 10:39:09 +01:00
EdnunpandClaude Opus 4.8 de55230048 Multi-track + shaped/raw recording (per-peer split tracks) — held for next release
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>
2026-07-06 10:05:00 +01:00
EdnunpandClaude Opus 4.8 703e6a022d Fix #18: accept audio from all of a multi-homed sender's source IPs
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>
2026-07-04 14:26:42 +01:00
EdnunpandClaude Opus 4.8 1c1bf5a5cb Per-peer pan + EQ ("Pan and EQ" tab) — off by default, held for next release
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>
2026-07-04 13:54:30 +01:00
EdnunpandClaude Opus 4.8 d7f6d9fd9d Release v4.9: lock a profile to fixed peer addresses (#17)
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>
2026-06-28 17:04:22 +01:00
EdnunpandClaude Opus 4.8 c7d422e3ba Add per-profile "Lock to exact peer addresses" setting (#17, held for next release)
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>
2026-06-28 15:31:50 +01:00
EdnunpandClaude Opus 4.8 0df2a576fc Release v4.8: fix rare crash from endpoint thrash on a dual-address peer (#16)
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>
2026-06-27 22:57:49 +01:00
EdnunpandClaude Opus 4.8 64d5cb5a86 Release v4.7: keep trying your configured peer after a reboot (#15)
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>
2026-06-24 22:41:43 +01:00
EdnunpandClaude Opus 4.8 843be9b7fb Speak-status: drop the double-press-copy Preferences toggle
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>
2026-06-24 18:55:03 +01:00
EdnunpandClaude Opus 4.8 6c36369806 Docs: correct RemSound's origin story (true remote-audio-monitoring use)
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>
2026-06-24 16:04:31 +01:00
EdnunpandClaude Opus 4.8 771a358a87 Status box: show RemSound's own CPU and memory usage (held for next release)
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>
2026-06-24 15:22:40 +01:00
EdnunpandClaude Opus 4.8 be32a060e7 Add "double-press to copy status" toggle + document the spoken-status changes (held for next release)
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>
2026-06-24 14:47:02 +01:00
EdnunpandClaude Opus 4.8 0fe9637356 Status speech: read line-by-line, MB->GB on totals, double-press to copy (held for next release)
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>
2026-06-24 14:31:03 +01:00
EdnunpandClaude Opus 4.8 f9b3ed9e67 Add Install Scripts (winget .NET 10 install) for the no-runtime case (held for next release)
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>
2026-06-24 14:24:20 +01:00
EdnunpandClaude Opus 4.8 e710834458 Fix what's-new re-appearing after a failed update (held for next release)
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>
2026-06-23 17:16:54 +01:00
EdnunpandClaude Opus 4.8 36431d6d39 Release v4.6: follow the Windows default audio device (output + input)
- 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>
2026-06-23 11:41:58 +01:00
EdnunpandClaude Opus 4.8 962f35ab80 Release v4.5: import shortcuts on pre-4.4 upgrade + clearer shortcuts dialog
- 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>
2026-06-22 06:36:00 +01:00
EdnunpandClaude Opus 4.8 8a1bc0c813 Release v4.4: keyboard shortcuts are now machine-wide, not per-profile
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>
2026-06-19 23:11:53 +01:00
EdnunpandClaude Opus 4.8 32be3426e7 Release v4.3.1: quieter default update sound
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>
2026-06-19 14:46:46 +01:00
EdnunpandClaude Opus 4.8 fd5c31740a Release v4.3: speak status line (Tolk) + Logging tab with housekeeping
- 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>
2026-06-19 13:09:06 +01:00
EdnunpandClaude Opus 4.8 034a7de632 Release v4.2: fix connect-freeze (#10) and new-profile minimize (#12); smoother WASAPI timing
- Connect no longer freezes: PushDiscoveryUnicastHints resolved remembered
  hostnames with synchronous Dns.GetHostAddresses on the UI thread, blocking the
  whole window for the DNS timeout on an unresolvable name. A screen-reader user
  experiences that as the entire machine locking up. Resolution now runs off the
  UI thread. Same class of bug as the v3.0.1 UPnP-on-the-UI-thread hang. (#10)

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:21:04 +01:00
EdnunpandClaude Opus 4.8 5abbeefe29 v4.1: two screen-reader fixes — silent start-in-tray, announce on restore
- 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>
2026-06-13 15:59:59 +01:00