Commit Graph
5 Commits
Author SHA1 Message Date
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 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.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 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 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