Commit Graph
96 Commits
Author SHA1 Message Date
EdnunpandClaude Fable 5 9e247112cb Release v3.7: device-change smoothing, auto-tune spike rejection, wider mic detector, forensic logging
Coalesce capture-engine rebuilds (CompositeCaptureBackend): a swap-triggering
source change now arms a 250ms debounce timer and re-arms on each further
change, so a flap or quick reconfiguration produces ONE rebuild to the final
state instead of a burst (Andre's 16:40 four-rebuilds-in-33s crackle). In-place
updates still apply immediately; a pending rebuild whose target flaps back is
cancelled.

Auto-tune (TickRoute) now keys off the SECOND-highest arrival-gap/render-gap
second in the lookback window instead of the single worst, so a lone ~1s
OS/driver stall no longer balloons the buffer to the 200ms cap (the 16:51
trim burst); sustained jitter still reacts at full speed. Logs both gap-max
(true peak) and gap-used (value acted on).

Mic-privacy detector widened: also catches a per-app Deny aimed at this exe
under ConsentStore\microphone\NonPackaged\<exe>, the HKLM NonPackaged gate,
and the Group-Policy/MDM force-deny (AppPrivacy LetAppsAccessMicrophone=2) —
the block shapes that silence WASAPI capture while ASIO sails past, and that
the old three-value check missed.

Forensic instrumentation so the next log proves what happened: capPeak=
(loudest pre-encode sample, per lane, on the diag line), mic-privacy verdict
logged at startup, ui: capture tick/untick events, and device-event: lines for
Windows endpoint changes.

Docs: mic-privacy + auto-tune sections updated in readme.html, MANUAL.md
regenerated, About-box changelog and RELEASE_NOTES for v3.7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:44:55 +01:00
EdnunpandClaude Opus 4.8 55bdcde0af Release v3.6: rebuild the self-updater (in-app, rollback-safe); warnings to front
Replace the generated cmd.exe + robocopy update helper — which silently
failed on some machines — with an in-app C# installer:
  * Stage the new version to a per-user temp folder off the install and run
    the new RemSound.exe from there, so nothing in the install is locked by
    the updater itself.
  * Wait for the old process to fully exit (real WaitForExit), then
    back-up-and-swap files in C# with retry + rename-aside; roll the install
    back to the previous version on any failure, so a failed update can never
    leave a half-installed RemSound.
  * Log every step to updater.log; clean up old stages and legacy batch
    artefacts on launch. Removed the old BuildInstallScript batch generator.

Route the "RemSound is already running" dialog and its follow-up message
through ForegroundDialog so they surface in front from a background-relaunched
copy, matching the earlier post-update fix.

Docs: rewrite the readme update sections for the new mechanism, regenerate
MANUAL.md, refresh the About-box changelog and RELEASE_NOTES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:37:19 +01:00
EdnunpandClaude Opus 4.8 4dbe9a47a0 Release v3.5: USB-card recovery, per-card adaptive buffer, one user folder, audit fixes
- Recover an unplugged/replugged output sound card automatically (issue #5):
  detect the dead WASAPI device and remember the receive-output selection so it
  re-ticks and re-opens when the card returns.
- Adaptive per-card WASAPI buffer target sized to each card's pull chunk, held
  stable so it never flits about under CPU/network load.
- Consolidate all per-user data (config, profiles, logs, sounds) into one
  "user settings and logs" folder; migrate every older layout; exclude it from
  the updater so custom cue sounds now survive updates.
- Mic-privacy detector: warn once when a Windows-blocked mic is switched on, or a
  profile loads with one already on.
- All warning/notice dialogs now come to the foreground even when minimised.
- Apply volume + mute on profile load (were saved but not restored).
- Crash-safe (atomic) profile/config saves.
- Fix two resource leaks (push-mode capture MMDevice; UPnP DeviceFound handler).
- Remove dead code (baseline-diff machinery, dead ASIO probes, no-op stubs).
- Docs: readme.html, MANUAL.md, About-box changelog and release notes for v3.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:34:17 +01:00
EdnunpandClaude Opus 4.8 946e6f4be4 v3.4 refinements: WASAPI drift correction, startup-dialog sequencing, quick-switch polish, docs
Builds on the v3.4 freeze (dd70613) with the fixes and tuning from live testing,
plus the v3.4 documentation pass.

Audio (receiver):
- Per-device WASAPI drift correction in MultiOutputPlayout. A pull-side resampler
  (mirroring SessionPlayout's proven corrector) holds each output device's buffer at
  a fixed low depth, cancelling the slow clock drift that made WASAPI peers "lag
  apart" over long sessions. Feed-forward clock-ratio measurement plus a gentle
  depth-restoring term; the first measurement window is discarded because WASAPI
  start-up priming poisons it. ASIO already self-corrected; this brings WASAPI level.
- Output device buffer requested at 5 ms (WASAPI clamps it up to the device's minimum
  period, ~10 ms) instead of 15 ms, since the corrector keeps it fed — a free saving.

UI / accessibility (MainForm, Program):
- Startup notices (what's-new About box, Realtek warning) now run one at a time via a
  single sequence instead of separate BeginInvokes, so they no longer stack into
  nested modals that couldn't be closed. The loading splash is skipped for a
  tray-bound quick switch.
- Quick profile switch keeps RemSound in the tray if it was there, and plays the
  switch cue immediately on click.
- Profile-switch cue now plays on click for every switch path (recent menu, quick
  switch, File > Open) and no longer on a fresh start into the first profile. It was
  also previously dead on the rebuilt form (pendingProfile was nulled first).
- Realtek ASIO toggle's accessible name now reads "Enable"/"Disable" to match the
  visible text, instead of "Toggle" (screen reader read the wrong word).

Docs (plain English):
- RELEASE_NOTES.md: v3.4 entry.
- About dialog: v3.4 "what's new".
- readme.html (the canonical bundled manual): quick switch, the hotkey read-outs, the
  new profile-menu-open cue, Realtek auto-detect/disable, and the config-folder path.
- MANUAL.md regenerated from readme.html via sync-manual.py so the two stay in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:31:32 +01:00
EdnunpandClaude Opus 4.8 dd70613017 v3.4: handle-leak fix, Realtek ASIO block, device notifications, quick profile switch, hotkey announcements
Freezes the v3.4 feature set (everything since the v3.3 public release):

- Fix the receiver handle leak: the 3-second device-refresh timer reopened the
  configured ASIO driver every tick, and Realtek's ASIO driver leaks Event+Mutant
  handles on every open. Cache the ASIO probe per driver so it is opened once.
- Realtek ASIO block: detect a Realtek ASIO driver, offer once to disable it, and
  never touch it again if disabled; Options-menu toggle to reverse. Global config.
- Device hot-plug is event-driven (AudioDeviceChangeNotifier) instead of a 3s poll;
  debounced refresh, falls back to polling if registration fails.
- Quick profile switch: new global hotkey opens an NVDA-friendly popup of all
  profiles (current marked); Enter/click switches; plays a new "profile menu open"
  cue with Preferences mute + custom-sound.
- Announce assigned global hotkeys on the controls/menu items they drive (NVDA reads
  "press X anywhere"). File > Open already had Ctrl+O.
- Held-back changes folded in: config-folder migration, codec-column fix, Tailscale
  endpoint network-prune, empty-password guard, and the handle-leak diagnostics
  (ProcessSelfMeter, HandleTypeProbe).

Version bumped to 3.4.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:12:11 +01:00
EdnunpandClaude Opus 4.8 fc451ba3e3 Add Android receiver link to README front page
Points users to Aryan Choudhary's community Android receiver (RemSoundAndroid) Releases page for the signed APK. Third-party companion app, TalkBack-tuned, not maintained here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 05:08:50 +01:00
EdnunpandClaude Opus 4.8 959720f54d Release v3.3: end-to-end encrypted audio, plus cue and reliability fixes
Headline: all audio is now encrypted (AES-256-GCM), keyed by a per-profile
password. Mandatory — v3.3 only interoperates with v3.3+.

Encryption
- RemSoundCrypto (Core): PBKDF2 key derivation, AES-GCM encrypt/decrypt
  (low-alloc, into-span), password fingerprint, light on-disk obfuscation.
- Wire: SenderLane encrypts the audio payload (PCM split across parts when the
  +28 overhead crosses MTU); AudioReceiver/StreamSession decrypt via a shared
  single-thread AudioDecryptor. Fingerprint piggybacks on the Format packet
  (offset 36, backward-compatible) so a peer can detect a password mismatch.
- Profile.Password (scrambled), carried through BuildCurrentProfile; MainForm
  derives + pushes the key/fingerprint to sender + receiver (RecomputeAudioCrypto).
- UX: ask-for-password on profile create; File -> Change this profile's password
  (ProfilePasswordDialog); Options -> Profile passwords (manager); a gate that
  prompts before streaming without a password; and a clear "passwords don't
  match" / "peer needs to update" message driven by the fingerprint.

Cue fixes
- CuePlayer (NAudio) replaces System.Media.SoundPlayer, which silently failed
  on the 96 kHz/24-bit cue WAVs (and any custom file) — cues now play reliably,
  resampled to 48 kHz/16-bit. Also fixes the Preferences preview button.
- Connect/disconnect cues now audio-gated with hysteresis: connected when audio
  flows OR heartbeat healthy; lost only when audio stops AND heartbeat
  unreachable. Kills false disconnects and the receive-only "no cues" case.
- Honest cue logging (played / muted / not loaded).

Smaller
- Endpoint stickiness: keep the audio target pinned to the heartbeat-proven
  address instead of chasing a multi-homed peer's other (unreachable) address.
- "Online/offline" label now audio+heartbeat aware, not discovery-only.
- "Show what's new after each update" preference (on by default).

Docs: About v3.3 block, RELEASE_NOTES, README (encryption as a headline),
manual section 12 "Passwords and encryption" (+ renumber), MANUAL.md regenerated.
Version 3.2.0 -> 3.3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:57:32 +01:00
EdnunpandClaude Opus 4.8 cdcac859c4 Release v3.2: update sound cue + single-instance bring-to-front fix
- New "Update sound" cue (CueId.Update / update.wav). Plays just before an
  update starts installing, on every path (manual, background-silent,
  startup-silent), so a silent background update gives an audible heads-up
  before RemSound closes to restart. Per-profile mute + custom-sound override
  in Preferences, same infrastructure as the other cues:
    * Profile.EnableUpdateCue + RemSoundSettingsStore Load/Save + round-trip
    * MainForm updateSound field, TryLoadCueSound, play in InstallUpdateAsync
      gated by LoadEnableUpdateCue
    * PreferencesDialog "Update sound" CueRow + ResolveCueFilePath mapping
    * update.wav shipped in sounds\ via csproj Content
- Single-instance "switch to the running copy" now actually brings the window
  to the front. The second copy grants the running copy foreground rights via
  AllowSetForegroundWindow before signalling, and lingers briefly so it can
  raise itself before we exit — without this, Windows' foreground lock left
  the running window only flashing in the taskbar (Ed's report).
- Docs: About box v3.2 block, RELEASE_NOTES.md, manual cue section (now seven
  cues, with the update cue described), MANUAL.md regenerated.
- Version bumped 3.1.3 -> 3.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:58:25 +01:00
EdnunpandClaude Opus 4.8 9ab0a1a20a Bump to v3.1.3: single-instance lock + update/read-only reliability fixes
Fixes the chained-fault runaway Andre hit after an update (multiple copies
stacking, audio climbing to deafening, terminal kill to recover).

- Single-instance lock (SingleInstanceCoordinator + SingleInstanceDialog,
  wired in Program.Main). A named mutex makes two copies impossible. A second
  launch offers: switch to the running copy (default; surfaces it from the
  tray via a named activation event), or force the running copy closed and
  start fresh (Process.Kill, retried elevated if the target is elevated).
  This is the structural fix that makes the stacking runaway impossible.
- Prompt-free update exit. InstallUpdateAsync sets updatingInProgress before
  Application.Exit(); the close path's skipPrompt now honours it, so no
  unsaved-changes dialog (whose default button is Cancel) can abort the
  update's restart.
- Read-only persistence fix. BuildCurrentProfile now carries
  currentProfileReadOnly into the saved snapshot. Previously a deliberate
  save of a locked profile wrote ReadOnly=false, silently unlocking it on
  disk — which re-armed the save prompt that then blocked the update.
- In-process double-install guard. updateInstallStarted stops the ~4 s
  startup check and the background poll both staging an install + helper.
- Docs: About box, RELEASE_NOTES.md, readme.html + MANUAL.md ("Only one copy
  of RemSound runs at a time").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:33:11 +01:00
EdnunpandClaude Opus 4.8 baf50baf73 Bump to v3.1.2: tray icon re-stamps on state change, recording shows as a flag
- Tray icon now re-registers itself (hide+re-show) whenever the meaningful
  state changes — peer connect/drop, send/receive toggle, recording
  start/stop — so the name a screen reader announces stays in step with
  the live state. Fixes the persistent "no peers" then "1 peer" double
  announcement that the old "show window then minimise again" trick used
  to clear by hand. Same root cause as the v3.1.1 stuck-tooltip fix, just
  exposed when the state changes a moment after the icon appears.
- Recording shows as a plain "recording" flag in the tray rather than a
  live timer. A ticking timer would have either flickered the icon once a
  second or left a screen reader announcing a stale time next to the live
  one. Removed the now-dead FormatRecordingElapsed helper.
- Bundles the earlier Win7 updater fix (6cbde0d): a failed secure
  connection is no longer mislabelled as "you're up to date".
- About box, RELEASE_NOTES.md, readme.html and MANUAL.md updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 23:34:44 +01:00
EdnunpandClaude Opus 4.7 6cbde0da12 Updater: distinguish 'no newer version' from 'check failed' on all three paths
Tech Singer's Windows 7 log from 2026-05-28 had this pattern:

  updater: GET https://api.github.com/repos/...
  updater: check failed: HttpRequestException: The SSL connection could not be established
  ...
  updater: startup check — up to date (v3.0.1)

i.e. the SSL handshake to GitHub failed (Win7's TLS stack missing
KB3140245 / KB4474419) but the updater logged 'up to date' and the
user-facing message in CheckForUpdatesManually said the same. So a
user with a broken update check has no way to distinguish that from
genuinely having the latest version.

Fix:
  * CheckForUpdateAsync now returns a discriminated UpdateCheckResult
    (UpdateAvailable / UpToDate / UpdateCheckFailed) instead of the
    old UpdateInfo?. Each return-null site is replaced with the
    appropriate concrete type.
  * The catch-all 'try { ... } catch (Exception ex) { return null; }'
    becomes 'return new UpdateCheckFailed(ClassifyFailure(ex), ...)'.
    ClassifyFailure walks the exception chain and maps to a coarse
    FailureKind enum: SecureConnection (TLS/auth), Timeout, HttpError,
    NetworkUnreachable. SecureConnection is broken out separately so
    the manual-check UI can point Win7 users at the specific Microsoft
    KBs that fix the issue.
  * MainForm.CheckForUpdatesManually pattern-matches on the result:
    UpToDate -> existing 'you're running the latest' message;
    UpdateAvailable -> existing install confirmation;
    UpdateCheckFailed -> NEW dialog (ShowUpdateCheckFailedDialog)
    whose wording is tailored to the FailureKind. The
    SecureConnection branch explicitly names KB3140245 and KB4474419
    and offers the manual zip-install URL as a fallback. All branches
    keep the technical detail out of the dialog and route it to the
    log instead.
  * Background and startup polls stay silent on UpToDate and
    UpdateCheckFailed (no point nagging the user about something
    they can't act on from a timer tick), but the startup-poll log
    now records the failure kind and detail instead of mislabelling
    the outcome as 'up to date'.

No version bump - this rides along with the next feature release
(planned v3.2 with the Reaper ReaStream integration). The bug is
silent on the affected users today, and shipping a v3.1.2 just for
the error-message improvement would mean another update cycle for
everyone for marginal benefit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:42:26 +01:00
EdnunpandClaude Opus 4.7 aa099eb555 Bump to v3.1.1: tray tooltip 'starting up' could get stuck after first launch
Bug: if v3.1 installed via auto-update on a profile with StartMinimised
on, the tray icon's hover tooltip got stuck at the initial "RemSound -
starting up" string. The snapshot tick was running and SetTooltip was
being called every second with the live state, but Windows shell kept
showing the original cached text on hover. The fix-by-workaround was a
hide-then-re-show cycle which forced the shell to rebuild the icon
registration with the latest NotifyIcon.Text.

Root cause: NotifyIcon.Text values set BEFORE the icon's first
NIM_ADD (i.e. while Visible=false) become the shell's "initial"
tooltip when the icon eventually appears. Subsequent NIM_MODIFY calls
from text changes DO propagate, but the shell tends to keep showing
the original text on hover - presumably a tooltip-cache eviction
quirk. In the resume-after-update-with-StartMinimised flow, the
window briefly shows then BeginInvokes a Minimize that flips
Visible=true before the snapshot timer has had a chance to fire, so
the shell registers with the stale "starting up" string.

Fix: drop the hard-coded "starting up" initial text from the
controller ctor entirely. The controller now takes a Func<string>
buildTooltip callback from MainForm and calls it in Minimize() right
before flipping Visible=true, so the shell's NIM_ADD sees current
live state instead of a stale string. The 1 Hz snapshot tick keeps
working for ongoing live updates while the icon is visible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:43:06 +01:00
EdnunpandClaude Opus 4.7 752c12b579 build-release.ps1: more robust Python detection
Two Windows-specific gotchas the previous code didn't handle:

1) The 'python' / 'python3' commands on most Windows installs are
   Microsoft Store execution aliases. They appear on PATH, accept
   any invocation, exit with code 9009, and print a "go install
   from the Store" message instead of running the script.

2) The 'py' launcher accepts --version and returns the right thing
   (it knows about registered Pythons via the registry), but on
   some setups it refuses to run scripts and falls through to the
   Store alias too. Seen here: 'py --version' prints 3.11.9 but
   'py sync-manual.py' prints the Store message and exits 9009.

The new approach runs an actual sentinel script via -c with each
candidate and only accepts the candidate when stdout matches the
expected string. User-local install paths come first because they
skip the Store-alias issue entirely. Falls through to 'py' and the
PATH commands as backstops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:28:04 +01:00
EdnunpandClaude Opus 4.7 70c2669d70 Bump to v3.1.0: new audio cues (save, profile-switch), per-profile custom cue paths, redesigned tray menu
Two new cues join the existing connect / disconnect / record-start /
record-stop set:
  * profile-save cue (sounds\save.wav by default) fires in
    SaveProfileTo after a successful Save or Save As. Honours the
    Profile.EnableSaveCue per-profile flag.
  * profile-switch cue (sounds\profile.wav by default) fires in the
    MainForm Shown handler after a profile finishes loading - covers
    both startup-with-profile and mid-session profile switches.
    Honours the Profile.EnableProfileSwitchCue flag. Because cue
    loading runs AFTER settings.ApplyProfile in the ctor (line 542),
    the profile being entered determines which sound plays - not the
    one being left, exactly as Ed asked.

The audio cue UI in PreferencesDialog stays as a CheckedListBox (up /
down navigates, Space toggles) with TWO action buttons below that
operate on whichever cue is selected:
  * Play [cue name] (Alt+P) - previews via SoundPlayer.Play on the
    resolved path (custom override if set, default in sounds\
    otherwise). Independent of the tick state.
  * Browse for [cue name]... (Alt+B) - opens a WAV picker. If the
    user picks a file inside RemSound's own sounds\ folder, it's
    treated as "use default" and the override is cleared - avoids
    pinning the user to a specific shipped default that a future
    release might replace. Right-click "Use default sound" reverts.

Custom cue paths AND enable flags are per-profile. Lives on
Profile.CustomCuePaths (Dictionary<string,string>) and the per-cue
EnableXxxCue bool? properties. Cache mirror in
RemSoundSettingsStore.Settings.

All default WAVs moved from the install-root flat layout into a
sounds\ subfolder (csproj Content rules updated). save.wav and
profile.wav are bundled defaults.

Tray menu rewritten (MainFormTrayController) per Ed's spec:
  * Show RemSound (W) - now uses Win32 SetForegroundWindow after the
    standard Activate() because WinForms Activate is blocked by the
    foreground-lock when invoked from a tray-menu click, which left
    screen-reader users having to Alt+Tab to reach the restored
    window.
  * Enable sending (S) / Enable receiving (R) - tickable, reflect
    current state, TOGGLE rather than always-on.
  * Profiles (P) - submenu populated from AppConfig.RecentProfiles
    with the same &1..&5 mnemonics the File menu uses. Pre-populated
    once at construction so WinForms recognises it as a submenu and
    fires DropDownOpening - originally I relied entirely on the
    open event, which the framework skipped for items with no
    DropDownItems, producing the "Profiles does nothing" bug.
  * Exit (X).

Tray tooltip now built dynamically from snapshot tick (1 Hz):
"RemSound - [recording for MM:SS,] N peer(s), sending (lane),
receiving (lane)". Recording timer only included while
RecordingController.IsRecording is true (added
RecordingStartedUtc accessor for the elapsed calculation). Lane is
derived from which device-list ticks are active, not just the audio-
mode setting, so a BothIndependent user with only WASAPI inputs ticked
honestly reads as "sending (WASAPI)".

Fixes:
  * Initial tooltip "RemSound" produced a "RemSound RemSound" read on
    NVDA because the process name and tooltip matched. Set to
    "RemSound - starting up" so the duplicate disappears.
  * Recent profile menu items no longer carry a "Recent profile N:"
    AccessibleName prefix in either the tray submenu or the File
    menu's Recent profiles - now just the profile name. Number-key
    mnemonics (&1..&5) untouched.

Manual (readme.html) updated: new section 17 "Audio cue sounds"
documents all six cues, the Play/Browse buttons, the right-click
"Use default sound", and the per-profile semantics. Sections 17-21
renumbered to 18-22. New "System tray icon and its menu" subsection
inside section 4 documents the redesigned right-click menu and the
hover tooltip. MANUAL.md regenerated via sync-manual.py. About box
gets a v3.1 block at the top. RELEASE_NOTES.md fully rewritten for
v3.1.

No wire format change - v3.1 talks to other v3.0.x machines exactly
as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:24:34 +01:00
EdnunpandClaude Opus 4.7 0189b12668 Docs: rewrite README.md for users, add MANUAL.md, sync from readme.html on every release
People landing on the repo page were having to install RemSound just to read what
it does and how to use it. Two doc changes fix that:

1) README.md rewritten as a plain-English landing page. Drops the developer-focused
   highlights / build-from-source / project-layout sections in favour of what
   RemSound is, who it's for, how to install it, and a prominent link to the manual.
   No jargon, no command lines, no NuGet / SDK / ASIO-protocol talk. The dev-side
   information that used to live here (build commands, source layout, relay setup)
   is still discoverable for anyone who wants it — the source itself is on the same
   page, and the relay docs are under server/README.md.

2) MANUAL.md added at the repo root as the GitHub-rendered version of the F1 help.
   Markdown derived directly from readme.html via sync-manual.py (new), so visitors
   can read the manual inline on the repo page with no download. readme.html stays
   exactly where it was (bundled inside RemSound, opened by F1) — it remains the
   canonical source of the manual content; MANUAL.md is auto-generated from it.

The sync-manual.py script is invoked automatically from build-release.ps1 as step 0,
before any other release work. It regenerates MANUAL.md from readme.html and then
checks `git diff` on MANUAL.md — if the file changed, the release is paused with a
message asking the user to commit the updated MANUAL.md alongside the release commit.
That makes it structurally impossible to ship a release with a stale GitHub-facing
manual: forgetting to commit MANUAL.md after editing the bundled help triggers a
deliberate release-time stop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:48:35 +01:00
EdnunpandClaude Opus 4.7 8aa8d0c3bd Bump to v3.0.2: fix slow native-memory leak on the receive side
Bug: Andre reported audio latency feeling laggier after long sessions
on his Win10 desktop receiving Opus from his laptop. His 23-hour log
showed working set climbing from 83 MB at startup to 3.5 GB at the
end, with the managed heap staying tiny (~5-7 MB) the whole time.
CPU climbed alongside (from steady-state ~7% mid-session to peaks of
~60% by the end) and audio threads ended up doing ~4x the work they
did at the start. Andre's perception of latency drift was the CPU
pressure showing up in audio scheduling, not the buffer itself
growing (bufAvg stayed roughly stable at 25-28 ms).

Root cause: Concentus.Native (introduced in v2.2 / shipped in v3.0)
returns concrete NativeOpusDecoder / NativeOpusEncoder objects that
implement IDisposable and own native libopus state. Three call sites
were taking the IOpusDecoder / IOpusEncoder interface reference and
never calling Dispose:

  * StreamSession.Dispose — comment literally said "IOpusDecoder has
    no Dispose; nothing else to free", which was correct for the
    pure-managed Concentus.OpusDecoder pre-v2.2 but stopped being
    correct the moment we added the native binding
  * OpusEncoderState.Dispose — same misleading comment, same bug
  * SenderLane.OnCodecChanged — overwrote the existing encoder field
    without disposing the old instance on codec change

Compounding factor: Program.Main sets GCSettings.LatencyMode =
GCLatencyMode.SustainedLowLatency to keep audio scheduling smooth
(it suppresses gen2 collections). That's correct for the hot path
but it ALSO suppresses the finalizer pass that would have released
the leaked native handles as a backstop. Because the managed heap
stayed tiny, the GC never saw enough pressure to force a gen2 pass
on its own, and the native state piled up indefinitely. Multi-output
receive multiplied the per-output growth.

Fix is in two parts:

1. Call (... as IDisposable)?.Dispose() at every release point —
   StreamSession.Dispose, OpusEncoderState.Dispose,
   SenderLane.OnCodecChanged before overwrite, AudioRecorder's
   Concentus.Oggfile-backed OpusOggFileWriter.Dispose. The
   as-IDisposable cast handles both the native and the pure-managed
   path transparently (managed-only IOpusDecoder isn't IDisposable;
   the as-cast yields null and the null-conditional is a no-op).

2. Periodic native-memory reaper in MainForm.SnapshotLogIfDue — once
   every 300 snapshot ticks (~5 min), run
   GC.Collect(2, Optimized, blocking, !compacting) +
   WaitForPendingFinalizers on a background Task.Run so the gen2
   work doesn't hitch the UI thread. Audio threads are separate and
   unaffected. Serves as belt-and-braces for any future code path we
   forget to wire and for cleaning up any per-call native scratch
   the underlying library might accumulate that isn't owned by a
   single .NET wrapper.

Expected behaviour after fix: working set settles around 100-200 MB
on a typical receive session and holds roughly flat for as long as
the app stays running. CPU stays at its early-session baseline
across multi-hour sessions. Andre's "latency drift" symptom should
disappear.

Wire format unchanged; same codec list, same UI, same defaults.
v3.0.2 talks to other v3.0.x peers exactly as v3.0 / v3.0.1 do.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:16:51 +01:00
EdnunpandClaude Opus 4.7 d904bafe73 build-release.ps1: accept three-part hot-fix tags (e.g. v3.0.1)
The script's ValidatePattern was '^v[0-9]+\.[0-9]+$', which rejected v3.0.1
because the v3.0 line predated any hot-fix releases. Widened to accept an
optional third component so v3.0.1 (and future patch releases) build via
the same path as v3.0 / v2.2 / etc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:42:13 +01:00
EdnunpandClaude Opus 4.7 5737550453 Bump to v3.0.1: move UPnP discovery off the UI thread (fixes hang on Andre's network)
Bug: ticking the "Automatically open my router for incoming connections
(UPnP)" box in Preferences could freeze the WinForms message pump until
Mono.Nat's NatUtility.StartDiscovery() returned. On Andre's setup it never
did — audio kept flowing (audio threads are independent of the UI thread)
but the window stopped repainting, the system-tray hotkey stopped
responding, and the only way out was Task Manager.

Pre-existing latent bug in the v2.1 UPnP code; we just shipped without
anyone exercising the path on a problematic network (multiple adapters /
VPN / SSDP-swallowing router).

Fix: three call sites moved off the UI thread via Task.Run -
  * MainForm OnShown (startup re-enable from saved AppConfig.UpnpEnabled)
  * MainForm Preferences applyUpnpEnabled callback (user ticks the box)
  * MainForm power-resume handler (Refresh() after sleep/wake)
RouterPortMapper.Start() returns "immediately" only when StartDiscovery
returns quickly; on a slow network it can block synchronously for many
seconds. Same is true of Stop()'s socket teardown and Refresh()'s
teardown-then-restart sequence. All three are now safely backgrounded.

StatusChanged is unaffected - it already fires on the mapper's own thread
and the PreferencesDialog handler BeginInvokes back to the UI thread.
Live status label updates correctly during the new background discovery.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:41:20 +01:00
EdnunpandClaude Opus 4.7 af0e7c3fff Bump to v3.0.0: Opus live latency, codec rename, profile-resume after update, save-through-lock
* New "Opus, live latency" codec mode: 2.5 ms frames (120 samples/ch at 48 kHz)
  via the float-input encode path. End-to-end codec delay drops to ~5 ms (vs
  ~12.5 ms at standard 10 ms Opus). Test on LAN: 400 pps/lane, zero missed /
  reordered / duplicate packets, ~15 ms one-way saved end-to-end.

* Wire-format change: AudioFormatInfo.FrameDurationMilliseconds renamed to
  FrameSamplesPerChannel (int sample-count at announced sample rate). Removes
  the lossy 48000*ms/1000 conversion that couldn't represent 2.5 ms. v3 <-> v3
  exact; v3 <-> v2 still passes audio (Opus decoder is self-describing from
  packet TOC) but v2 side over-sizes its buffer wildly. v2.x profiles auto-
  migrate via <120 sentinel rule in RemSoundSettingsStore (anything below 120
  is treated as legacy ms and multiplied by 48). Profile JSON key kept as
  OpusFrameMilliseconds via [JsonPropertyName] so old profile files still load.

* Codec dropdown rebuilt with use-case names: "PCM 48K 24 bit - uncompressed",
  "Opus, broadcast quality - loss tolerant", "Opus, live latency - for jamming
  and monitoring". Middle 10 ms option retired; saved 480-sample profiles
  collapse to broadcast quality (safer-side default).

* Profile auto-resume after self-update: RemSoundUpdater writes a one-shot
  _resume-after-update.txt sentinel containing the active profile title before
  exit; Program.Main reads + deletes it on next start and silently loads that
  profile, skipping the picker. Helper batch's robocopy /XF excludes the
  sentinel and the failure-branch cleans it up if the install aborts. Falls
  through to normal startup behaviour (StartWithProfileTitle or picker) if the
  sentinel is missing, empty, or names a profile that no longer exists.

* Read-only profile saves now go through on explicit Ctrl+S / File -> Save
  with a one-time TaskDialog warning ("Save anyway" / "Cancel" + Do-not-show-
  again). Lock continues to suppress the automatic unsaved-changes prompt on
  close / profile switch (its main job). AppConfig.SaveOnReadOnlyMessageSuppressed
  renamed to SaveOnReadOnlyWarningSuppressed; v2.x suppression flag is silently
  discarded since the behaviour changed and the user needs to see the warning
  once on each machine.

* Manual (readme.html) updated: codec table rewritten with the three new
  choices and corrected bandwidth figures, send-rate description updated, new
  sections "The same profile picks up automatically after an update" and
  "Saving on purpose while a profile is locked".

* Subsumes the never-separately-released v2.2 work: native Opus encoder
  (~97% less per-second memory churn on Opus send path via Concentus.Native),
  efficiency tidy-ups (item 4 ASIO probe rate, item 6 WaitHandle, item 7
  snapshot cache, items 14/16 heartbeat + discovery), legacy cleanup
  (items 30/34/35/36: KeepAlive infrastructure, drift drop/repeat/accumulator
  fields, fan-out cache stat). New diagnostic columns cpu/memMB/wsMB/
  allocKBps/captureMs/sendMs/recvMs/renderMs gated on Enable-logs.

About dialog updated with v3.0 block at top; v2.2 block retained for the
subsumed work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:31:55 +01:00
Ednunp 6d6d6897e4 Bump to v2.2.0: Opus native binding, efficiency tidy-up, diag self-meter
Single biggest change: added the Concentus.Native NuGet package. Concentus
2.0+ auto-detects native libopus at runtime and routes encode calls
through it; encoder state lives on the C side and is reused across calls
rather than `new`ing ~15 working buffers per call (Concentus issue #22,
open since 2018). Measured on the desktop test at 15:36:55 — Opus 10 ms
allocation rate dropped from 4,625 KB/s to 108 KB/s, a 97.7% reduction.
Process CPU dropped from 4.7% to 1.6% in the same config. Audio is bit-
for-bit identical (it's literally the same encoder, just better
packaged). `OpusEncoderState.cs` itself unchanged on the call site.

Diagnostic / measurement layer (gated on Enable-logs, zero cost when off):
* ProcessSelfMeter: CPU%, managed heap MB, working set MB, allocation
  rate per second, GC counts per generation
* Per-thread work-time counters: captureMs / sendMs / recvMs / renderMs
  expressed as milliseconds of CPU consumed by each audio thread per
  second
* Inter-packet arrival gap measured at the user-space UDP socket
  (rxNetGapMs) — pinpoints whether arrival jitter is in the network or
  our own dispatch path

Small efficiency wins (each one was small but cumulative):
* deviceRefreshTimer interval 1s -> 3s (item 4)
* WaitHandle array allocations eliminated in MixingEngine.MixLoop and
  MultiOutputPlayout.ProduceLoop (item 6)
* MultiOutputPlayout caches its output-buffer snapshot and only rebuilds
  on SetOutputDevices, instead of rebuilding every 10 ms (item 7)
* HeartbeatService reuses an outbound ping byte[] instead of allocating
  per send (item 14)
* PeerDiscoveryService caches broadcast addresses and invalidates on
  Windows' NetworkChange event instead of walking all NICs every 1.5 s
  (item 16)

Legacy / dead-code removal:
* KeepAlive packet's implementation (struct, enums, writer, reader, size
  constant) — all dead since HeartbeatService landed 2026-05-06. Kept
  the RemPacketType.KeepAlive enum value and silent-drop dispatch for
  wire compat with any pre-2026-05-06 build still in the wild (item 30)
* driftDropFramesTotal / driftRepeatFramesTotal fields and accessors —
  Phase-2 splice corrector relics, never incremented since Phase-4
  resampler design landed; backed five always-zero diag log columns
  (items 34 + 35)
* DriftAccumulator (always returned 0) — same shape, removed alongside
  the driftAcc= column (item 35)
* TakeMaxFanOutCacheBytes / Ms + fanCacheMs column — FanOutSource was
  retired in May (item 36)

Project documentation:
* RemSoundefficiency.md added as the canonical record of the efficiency
  analysis, every item's status, and the measured wins from this round
* Honest item-by-item review of the original 50-item list — several
  items I had sized optimistically in the original analysis turned out
  to be already-done (item 20), already-optimal (item 22), or below
  the meter floor (items 9, 15, 17, 25). Recorded so future passes
  don't re-investigate.

Wire format and audio pipeline unchanged from v1.5 onward — v1.5 through
v2.2 peers interoperate.
2026-05-23 15:56:03 +01:00
Ednunp 79b28b6c02 Bump to v2.1.0: UPnP, read-only profile lock, sleep/hibernate audio fix
Headline features:
* Automatic router port opening (UPnP / NAT-PMP / PCP). Opt-in via
  Preferences; surfaces external address + carrier-grade NAT detection.
* Lock profile (read-only). New File-menu tick that makes a profile
  load-only — session changes don't persist, no save prompt on close.
  Unblocks unattended shutdowns (NVDA gone, remote dropped, hibernate)
  where the existing save prompt could deadlock.
* Check for updates on startup (default on) + brief countdown notice
  before silent updates install, so a launch-time update doesn't make
  the app silently vanish.
* "Cue sounds" -> "Audio cue sounds" label clarification.

Bug fixes:
* No sound after the computer wakes from sleep. PowerResumeHandler
  rebuilds the audio backend automatically on resume; brief
  "Reconnecting to audio driver" splash during the rebuild.
* Receiver audio silent after waking from hibernate. RefreshAudioDeviceLists
  now treats a transient ASIO probe failure (returns -1/-1 because the
  driver is mid-teardown / mid-reinit) as "retry next tick" instead of
  clearing the user's tick selection.

Diagnostic-only changes (gated on the existing Enable-logs checkbox,
zero cost when off):
* AudioStepProbe split into cross-buffer vs within-buffer maxes so log
  inspection can tell a real-content sharp transient apart from a
  pipeline-boundary glitch. Plumbed through every probe owner.
* New rxNetGapMs + gc0/gc1/gc2 delta columns in the diag log to split
  receive-side jitter into network-layer vs managed-runtime causes.

Files touched: RELEASE_NOTES.md + readme.html + 24 source files across
RemSound.Core / RemSound.Sender / RemSound.Receiver / RemSound.App.
Three new app files: PowerResumeHandler, RouterPortMapper,
UpdateInstallNoticeDialog.

Wire format and audio pipeline unchanged from v1.5 onward — v1.5
through v2.1 peers interoperate.
2026-05-22 23:06:07 +01:00
EdnunpandClaude Opus 4.7 00cb4deef1 Bump to v2.0.0: ASIO startup splash so launch no longer looks frozen
Opening an ASIO driver takes 1-3 seconds, synchronously, during MainForm
construction — confirmed in logs as a ~2.8s dead gap on an ASIO-profile
launch (a WASAPI-only launch is ~131ms for the same stretch). During that
gap the main window is blank / "Not Responding" and looks hung.

New AsioLoadingSplash shows a small "Loading audio driver, please wait..."
window on its OWN dedicated STA thread with its own message loop, so it
stays painted while the main thread is blocked opening the driver.
Program.cs starts it before new MainForm() and dismisses it after.

Deliberately, the ASIO driver open stays on the main UI thread — that
STA/message-pump thread is what ASIO/COM drivers are most compatible with,
and moving the open off it risks breaking drivers that can't be tested.
Only the cosmetic splash moved to a side thread; no ASIO/driver code is
touched. Splash shows only for profiles with an ASIO driver selected;
WASAPI-only profiles build fast and get no splash.

Version bumped to 2.0.0 to mark the milestone.

No wire-format or audio-pipeline changes — v1.5 through v2.0 interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:23:56 +01:00
EdnunpandClaude Opus 4.7 4344e3439d Bump to v1.9.0: fix the robocopy trailing-backslash bug — auto-updater finally works
The install helper's robocopy line was:
  robocopy "{stagingRoot}" "{installDir}" ...
installDir is AppContext.BaseDirectory, which always ends in a directory
separator, so the destination argument was a quoted path ending in a
backslash: "D:\...\publish\". Windows command-line parsing reads the \"
as an escaped quote, so robocopy never received a valid destination,
rejected the command line, and exited 16 (usage error, nothing copied)
instantly. The auto-updater has never worked in any release because of
this — v1.0-v1.2 failed silently, v1.3+ detected the failure and wrote
update-failed.txt but never fixed the robocopy line.

Fix: BuildInstallScript now strips trailing separators —
stagingArg = stagingRoot.TrimEnd('\','/'), installArg likewise — and
the robocopy line uses the trimmed forms. Verified by running the
corrected robocopy against real staged files: exit 3 (success), files
copied.

The broken helper is baked into every shipped build including v1.8, and
the helper is generated by the running version — so v1.8 and earlier
cannot auto-install v1.9. v1.9 must be installed by hand once; from v1.9
onward the updater works.

No wire-format or audio-pipeline changes — v1.5 through v1.9 interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:49:44 +01:00
EdnunpandClaude Opus 4.7 e6d47e229d Bump to v1.8.0: updater hardening + plain-language manual rewrite
Updater (RemSoundUpdater.cs), all from Andre's feedback:
* CheckForUpdateAsync now requests /releases?per_page=100 instead of the
  default 30-item page, so a burst of server-vX.Y relay releases can't
  push the newest client release off page 1.
* The install helper's robocopy now excludes remsound.config.json and the
  logs / profiles / recordings folders — an update replaces app files
  only and can never overwrite the user's own config or data.
* On a successful update the helper now also deletes _update-helper.log
  and any stale update-failed.txt (the _update folder was already
  removed), leaving a tidy install folder. The failure branch still keeps
  them all for diagnosis.
* update-failed.txt rewritten as plain user-facing instructions: numbered
  steps, no brand names, names the real _update folder, no robocopy
  jargon. The exit code now goes to _update-helper.log only.

Manual (readme.html): rewritten in plain language — developer jargon
removed or explained in everyday terms — and a second pass removed the
keystroke-navigation choreography and screen-reader narration from the
prose. The Keyboard shortcuts section and all shortcut reference are
kept intact.

No wire-format or audio-pipeline changes — v1.5 through v1.8 interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:07:05 +01:00
EdnunpandClaude Opus 4.7 6b1ae2d017 Bump to v1.7.0: self-updater release-filtering fix
The client and the relay server are published from the same GitHub repo;
the server's releases use "server-" prefixed tags. RemSoundUpdater hit
/releases/latest, which is repo-wide — when a server release was newest,
the updater fed "server-v2.3" to ParseTag (-> a bogus 0.0.3) and concluded
"up to date", silently skipping real client updates.

CheckForUpdateAsync now lists /releases and picks the highest-versioned
release whose tag is a RemSound client tag (new IsClientReleaseTag: after
an optional leading "v", first char must be a digit). Drafts and
pre-releases are skipped. The server-side updater already filters to
"server-" tags, so client + server coexist in one repo cleanly.

Also rewrites build-release.ps1 with a data-safety check: it publishes to
a fresh staging folder and aborts the release if any logs/, profiles/,
recordings/ folder, .log file or remsound.config.json is present in the
staged output or the finished zip — preventing a repeat of the v1.5/v1.6
zips that shipped with developer logs and profiles.

No wire-format or audio-pipeline changes — v1.5/v1.6/v1.7 interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:05:40 +01:00
EdnunpandClaude Opus 4.7 918ca6cac0 Bump to v1.6.0: peer-address recovery, reconnect crash, long-run memory/CPU leak
Three reliability fixes. Wire format and audio pipeline unchanged from
v1.4 / v1.5 — all interoperate.

Peer address recovery:
* When a tracked peer goes Unreachable (its resolved address — often a
  stale DNS / Pi-hole record, or a peer that rebooted onto a new IP) but
  the same peer is still heartbeat-pinging us from a different address,
  RemSound now adopts the live address instead of transmitting to a dead
  one. HeartbeatService records untracked ping sources; MainForm's
  TryAdoptLiveHeartbeatAddress (1 Hz) re-points the sender, heartbeat
  tracking and receiver allow-list. Conservative: fires only on the
  unambiguous one-unreachable-and-one-source case, private-range (RFC1918)
  addresses only so a relay can't hijack the sender, 10 s cooldown.

Reconnect crash:
* Fixed IndexOutOfRangeException in MainForm.SyncConnectedList. A churny
  peer-list rebuild (peer reboot) left SelectedIndex pointing past the
  rebuilt item array; the 1 Hz status timer read SelectedItem and crashed
  the app. New SafeSelectedItem bounds-checks the index; applied to all
  three timer-driven sync methods. The status tick is also wrapped in
  try/catch so a transient WinForms hiccup logs instead of crashing.

Long-run memory / CPU leak:
* A receiver left running for hours grew to gigabytes and climbing CPU.
  Decoder sessions orphaned by peer reconnects were not reaped — every
  reconnect mints a fresh (endpoint, streamId) key, and PruneIdleSessions
  silently skipped sessions whose PlayoutEngine lookup missed. Rewrote it
  to reap on each session's own LastWriteUtc (no cross-dictionary lookup),
  added a hard MaxLiveSessions cap as a backstop, and a "stream sessions
  live: N" diagnostic line. Bounds both memory and render-thread CPU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:32:22 +01:00
Ednunp 9ceff8bcc1 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:31 +01:00
Ednunp 217b051c29 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:30 +01:00
Ednunp f872ebf4f6 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:29 +01:00
Ednunp 10df575ef7 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:27 +01:00
Ednunp 6ef9419aed server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:26 +01:00
Ednunp e42ff53234 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:24 +01:00
Ednunp f0a674fba4 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:23 +01:00
Ednunp 98a3ddeff9 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:21 +01:00
Ednunp 4cd0fec049 server: update to v2.3 — dual-protocol relay (v1 pair + v2 lobby) + auto-updater with EXIT trap fix 2026-05-15 17:24:20 +01:00
EdnunpandClaude Opus 4.7 4915a9afe1 Bump to v1.5.0: cross-lane routing fix, recording double-tap fix, menu reorg
Bug fixes:
* BothIndependent recording was double-tapped — when both WASAPI and
  ASIO outputs were ticked, recordings came out garbled and ~2x the
  expected duration. AudioRecorder now uses four per-lane rings
  (sent-wasapi, sent-asio, recv-wasapi, recv-asio) plus a writer-
  thread mix step that drains min(wasapi, asio) frames and sum-mixes
  with the soft-tanh limiter. Tap signatures gained a RenderRoute
  parameter; Mixed maps to the wasapi slot.
* A peer announcing on the WASAPI lane was inaudible when the
  receiver only had an ASIO output ticked (and vice versa). The
  session opened, samples flowed into the SessionPlayout ring, but
  ReadForRoute(AsioLane) skipped any session whose Route was
  WasapiLane so nothing drained the ring. PlayoutEngine now tracks
  per-lane "is this lane backed by a device" via volatile bools
  settable through SetLaneActive(RenderRoute, bool), called from
  CompositeRenderBackend.SetOutputDevices whenever the device split
  changes. ReadForRoute admits orphan sessions when the other lane
  is inactive.

Menu reorganisation:
* New Options menu (Alt+O) holds Recording settings (Alt+S), Keyboard
  shortcuts (Alt+K, Ctrl+K), Startup behaviour (Alt+T), Preferences
  (Alt+P, Ctrl+P). Pre-v1.5 these were scattered across File menu,
  Record menu, and inside the Preferences dialog itself.
* Record menu mnemonic moved from Alt+O to Alt+K, rendered as
  "Record (Alt+K)" so the chord is visible despite K not being a
  letter in "Record". Alt+R is taken by Receive audio.
* File menu — new Recent profiles submenu (Alt+F, R) listing the
  five most-recently-opened profiles. Press 1..5 inside the submenu
  to jump to a slot. Missing files are skipped from the menu but
  kept in storage.
* Rename current profile moves to Alt+M (was R), Minimise to tray
  moves to Alt+N (was M).
* Lock to audio clock was Alt+K, now Alt+D.

UX additions:
* Ctrl+O = Open profile (matches the menu chord).
* New global hotkey: Start / Stop recording. Pickable from Options
  -> Keyboard shortcuts. Unbound by default. Works system-wide.

Wire format and audio pipeline unchanged from v1.4 — v1.4 and v1.5
peers interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:20:01 +01:00
EdnunpandClaude Opus 4.7 b96ba82378 Bump to v1.4.0: recording-settings dialog cleanup, menu mnemonics
Ships the dialog reorganisation and mnemonic adjustments from
commit 45b709d. No wire-format or audio-pipeline changes — v1.3 and
v1.4 peers interoperate.

About dialog gets a v1.4 release-notes block; csproj Version
property bumped 1.3.0 → 1.4.0; RELEASE_NOTES.md rewritten with the
v1.4 highlights and a one-paragraph upgrading-from-v1.3 note (clean
auto-update via Help → Check for updates is now expected to work on
Dropbox-installed copies since v1.3's helper hardening took effect).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 13:44:15 +01:00
EdnunpandClaude Opus 4.7 45b709d49d Recording-settings dialog rework + menu mnemonic clean-up
Recording settings dialog
=========================

Promote what were row attributes inside the format-attributes table
into their own dedicated listboxes. Pre-rework each format's
attribute list contained a row per (quality knob × channel mode)
combination — doubling the row count for no real benefit. WAV had
6 rows, MP3 / OGG-Opus had 8, FLAC had 4. Channel mode is now its
own listbox; per-format attribute lists shrink to 3 (WAV), 4
(MP3 / OGG-Opus), 2 (FLAC).

FLAC compression level was previously a hidden RecordingSettings
field locked at the libFLAC default of 5. Now exposed as its own
conditional listbox with all 9 levels (0..8). Visible only when the
selected file format is FLAC; the column collapses to 0% width
otherwise so the dialog reads cleanly as 4 columns for non-FLAC
formats and 5 for FLAC.

Mnemonics:
  * Source         Alt+S    (unchanged)
  * File format    Alt+F    (unchanged)
  * Attributes     Alt+A    (unchanged)
  * Compression    Alt+L    (new, FLAC-only visible)
  * Channels       Alt+C    (new)
  * OK             Alt+O    (unchanged)
  * Cancel         Alt+N    (was Alt+C — moved to free C for Channels)

Record menu mnemonics
=====================

Item mnemonics inside the Record menu reorganised so the keystroke
inside the menu matches the same letter used for Ctrl+R outside it,
and Settings reads naturally:

  * Start recording / Stop recording   Alt+R  (was Alt+S)
  * Recording settings                 Alt+S  (was Alt+T)
  * Open recordings folder             Alt+O  (unchanged)
  * Change recordings folder           Alt+C  (unchanged)

UpdateStartStopRecordingMenuLabel keeps the underline on a literal
R in both states ("Sta&rt recording" → "Stop &recording") so the
keystroke does the same job regardless of which label is active.

No version bump — the running version stays at 1.3.0. These changes
will be bundled with the next public release alongside whatever else
is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 13:40:42 +01:00
EdnunpandClaude Opus 4.7 d927b34c9c Release notes: add manual-install steps for stuck pre-v1.3 users
Anyone on v1.0 / v1.1 / v1.2 in a Dropbox-synced folder will hit the
auto-updater bug v1.3 fixes — but the fix lives in the running
version's helper, so their installed binary can't use it. Adds a
prominent "Already on an older version and inside a Dropbox folder?
Read this first" section at the top of the release notes with the
manual-install steps, and renames the existing install steps to
"Install (clean machine)" for clarity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 13:14:15 +01:00
EdnunpandClaude Opus 4.7 9a5c46eecf Bump to v1.3.0: harden updater helper for Dropbox-installed copies
v1.2's self-updater silently failed when the install folder lived
inside a Dropbox sync path. Dropbox held write locks on the existing
RemSound.exe / DLLs during the brief window between the parent exiting
and the helper script copying the new files in. The helper's robocopy
(/R:5 /W:1) gave up after 5 seconds, and the helper then
unconditionally relaunched the OLD binary — so the user saw the same
version they started with after pressing "Yes" on the install prompt,
with no visible error.

Helper script (BuildInstallScript) changes:

* Robocopy retries bumped to /R:60 /W:1 — up to 60 seconds per file.
  Dropbox lock release happens reliably within that window in
  practice.

* Robocopy exit code is captured and checked. Codes >= 8 are real
  failures. On a failure the helper writes update-failed.txt to the
  install folder with the cause + recovery steps, leaves the staging
  folder intact, and does NOT relaunch the old binary. Earlier
  versions silently relaunched the unmodified old binary, hiding the
  failure.

* Helper appends a step-by-step trace to _update-helper.log (in the
  install folder), with robocopy's own output included via /LOG+:.

* update-failed.txt, _update-helper.log, and _apply-update.cmd are
  added to the /XF exclusion list so the helper's own state files
  don't get copied to themselves on a repeat update run.

DownloadAndStageInstallAsync also clears any stale update-failed.txt
at the start of every new attempt, so a successful run leaves the
install folder clean.

readme.html "If install fails" section expanded with the new
update-failed.txt marker file behaviour and the _update-helper.log
location.

Wire format, audio pipeline, and recording feature unchanged from
v1.2 — this is updater-machinery-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 13:07:36 +01:00
EdnunpandClaude Opus 4.7 c59e413c1f Bump to v1.2.0: recording, sound cues, drift compensation, diagnostics
New user-facing features:

* Recording. Dedicated Record menu (Alt+O — moved from Alt+R to
  avoid clashing with the Receive audio checkbox), Start/Stop on
  Ctrl+R, settings dialog, per-profile source / format / bit-depth /
  channel-mode / folder. Three source modes (received only, sent
  only, both). Files are crash-resilient — a process crash
  mid-recording leaves a playable file containing everything up to
  the last header refresh (~5 seconds).

* Four output formats, all functional:
    - WAV: 16/24-bit PCM or 32-bit float, custom writer with
      periodic RIFF re-patching.
    - MP3: LAME 128–320 kbps CBR (via NAudio.Lame).
    - OGG-Opus: 96–256 kbps VBR (via Concentus.Oggfile, reusing the
      Concentus encoder from the wire path).
    - FLAC: 16/24-bit lossless (via CUETools.Codecs.FLAKE — pure
      managed, no native DLL).

* Recording start/stop sound cues. record start.wav and
  record stop.wav play around the recording transition. Played via
  System.Media.SoundPlayer to the default Windows output, separate
  from the recording pipeline so a normal recording does not contain
  the cue.

* Per-cue Preferences. The old single "Mute connect/disconnect
  sounds" checkbox is replaced by a CheckedListBox: Connect /
  Disconnect / Recording start / Recording stop. Old profiles with
  the legacy MuteConnectionCues=true are honoured on first load via
  a migration path in the new Load* helpers.

* Receiver-side drift compensation switched from discrete
  single-frame splices to a continuous WdlResampler at a smoothed
  rate ratio. SessionPlayout.cs rewrite.

Diagnostics (only active with Enable logs ticked):

* Per-stage discontinuity probes — sender raw capture (per backend,
  PushModeWasapi + Asio both wired), sender pre-encode (now per
  lane in BothIndependent, fixing a cross-stream artefact), receiver
  post-decode, post-ring, post-resampler.

* Wire-level packet sequence tracking on each PCM stream — in-order
  / missed / reordered / duplicated counts in the diag log.

* Clipped-sample delta in the diag log.

* New AudioStepProbe in RemSound.Core with per-channel scan helper.

UI changes:

* Record menu uses Alt+O (Rec&ord). Inside the menu, item mnemonics
  unchanged (S / T / O / C).

* Auto-tune interval combo label is mode-aware: "Auto-tune latency
  interval" in classic modes, "Auto-tune interval — WASAPI and ASIO"
  in BothIndependent. The combo's Enabled state now follows EITHER
  lane's auto-tune checkbox (was only the WASAPI one — bug).

Wire format and audio pipeline unchanged from v1.1 — v1.1 and v1.2
peers interoperate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 12:40:01 +01:00
EdnunpandClaude Opus 4.7 a0fe8070ed Bump version to 1.1.0 and update About dialog release notes
Adds a v1.1 block above the existing v1.0 block in the About dialog,
covering the priority-mode feature, always-on network packet priority,
larger UDP kernel buffers, MMCSS Critical bump, and the logging-toggle
bug fix. Version property in the .csproj bumped from 1.0.0 to 1.1.0 so
the About dialog heading and the self-updater's CurrentVersion both
report 1.1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:24:14 +01:00
EdnunpandClaude Opus 4.7 768875cebb Add priority mode + always-on network packet priority
Priority mode (per-profile toggle, Audio profile tab, Alt+U):
- Bundled PROCESS_POWER_THROTTLING_STATE with EXECUTION_SPEED and
  IGNORE_TIMER_RESOLUTION flags (keeps CPU at full clock + 1 ms quantum
  pinned regardless of OS idle decisions)
- PowerSetRequest(EXECUTION_REQUIRED) + SetThreadExecutionState so the
  system can't decide we're idle and downclock
- timeBeginPeriod(1) for 1 ms scheduler quantum
- ProcessPriorityClass.High, ProcessMemoryPriority normal, working-set
  minimum locked via SetProcessWorkingSetSizeEx(HARDWS_MIN_ENABLE)
- Fully reversed on toggle-off and on form close
- Off by default (battery cost on laptops); explicit opt-in per profile

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:21:58 +01:00
Ednunp 146e76c6e6 Force LF line endings on server/ (bash/Python/systemd files) 2026-05-13 17:06:07 +01:00
Ednunp a6542e38ef Add Pi relay server bundle (server/) 2026-05-13 17:05:41 +01:00
Ednunp 17259438c6 Initial commit: RemSound v1.0 2026-05-13 15:15:27 +01:00