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>
This commit is contained in:
Ednunp
2026-07-08 18:19:01 +01:00
co-authored by Claude Opus 4.8
parent fcc3cdc793
commit aeca24ae17
10 changed files with 193 additions and 31 deletions
+20 -11
View File
@@ -204,16 +204,21 @@ internal sealed class RecordingController
t.Len = span.Length;
}
// Audio thread. Once per render, flush each peer's summed block to their file and reset.
private void FlushPeerTracks()
// Audio thread. Once per render, flush each peer's summed block to their file. Every peer track gets
// EXACTLY one render block per callback: a peer that produced nothing this block — because it went
// quiet and its session was dropped, or it disconnected — is padded with silence to the same length.
// That keeps every track sample-locked to the single render clock, so the tracks can never drift
// apart from each other however long the recording runs, and they all end up the same length.
private void FlushPeerTracks(int floats)
{
var tracks = peerTracks;
if (tracks is null) return;
if (tracks is null || floats <= 0) return;
foreach (var t in tracks.Values)
{
if (t.Len <= 0) continue;
t.Recorder.WriteReceived(t.Accum.AsMemory(0, t.Len), RenderRoute.Mixed);
Array.Clear(t.Accum, 0, t.Len);
if (t.Accum.Length < floats) { var a = t.Accum; Array.Resize(ref a, floats); t.Accum = a; }
for (int i = t.Len; i < floats; i++) t.Accum[i] = 0f; // pad an empty/short block with silence
t.Recorder.WriteReceived(t.Accum.AsMemory(0, floats), RenderRoute.Mixed);
Array.Clear(t.Accum, 0, floats);
t.Len = 0;
}
}
@@ -227,13 +232,17 @@ internal sealed class RecordingController
rawMixLen = span.Length;
}
// Audio thread. Flush the summed raw mix for this render to the single recorder, then reset.
private void FlushRawMix()
// Audio thread. Flush the summed raw mix for this render to the single recorder, padding a silent
// block so the file stays aligned to real time even through total silence, then reset.
private void FlushRawMix(int floats)
{
var rec = active;
int n = rawMixLen;
if (rec is not null && n > 0) rec.WriteReceived(rawMixAccum.AsMemory(0, n), RenderRoute.Mixed);
if (n > 0) { Array.Clear(rawMixAccum, 0, n); rawMixLen = 0; }
if (rec is null || floats <= 0) return;
if (rawMixAccum.Length < floats) { var a = rawMixAccum; Array.Resize(ref a, floats); rawMixAccum = a; }
for (int i = rawMixLen; i < floats; i++) rawMixAccum[i] = 0f;
rec.WriteReceived(rawMixAccum.AsMemory(0, floats), RenderRoute.Mixed);
Array.Clear(rawMixAccum, 0, floats);
rawMixLen = 0;
}
private void StopRecorder(AudioRecorder? r)