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
+12 -2
View File
@@ -2599,6 +2599,8 @@ public sealed class MainForm : Form
// Appearance-tab changes (tab order, show pan/EQ tab, show discovered/remembered lists) apply now.
ApplyMainTabLayout();
RefreshConnectivityListVisibility();
var appearanceCfg = AppConfig.Load();
logFile.Event($"appearance applied: theme={appearanceCfg.ThemeMode}, tabs=[{string.Join(", ", mainTabControl.TabPages.Cast<TabPage>().Select(t => t.Text))}], discovered-list={appearanceCfg.ShowDiscoveredPeers}, remembered-list={appearanceCfg.ShowRememberedPeers}");
// The Preferences dialog includes per-cue Browse buttons that can change custom
// WAV paths in AppConfig.CustomCuePaths. Reload the cached SoundPlayer instances
// here unconditionally — cheap, only six small files, and guarantees the next
@@ -3260,7 +3262,7 @@ public sealed class MainForm : Form
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(12), ColumnCount = 1, RowCount = 9, AutoScroll = true };
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
enableAllPeerShapingBox.CheckedChanged += (_, _) => { if (!loadingPanEqControls) { MarkProfileDirty(); ApplyAllPeerShaping(); } };
enableAllPeerShapingBox.CheckedChanged += (_, _) => { if (!loadingPanEqControls) { logFile.Event($"shaping: master switch {(enableAllPeerShapingBox.Checked ? "on" : "off")} ({peerShaping.Count} peer(s) with saved shaping)"); MarkProfileDirty(); ApplyAllPeerShaping(); } };
panEqPeerList.SelectedIndexChanged += (_, _) => OnPanEqPeerSelected();
panEqPeerList.ItemCheck += OnPeerShapeToggled;
// First-letter navigation on some Windows configs can accidentally toggle the checkbox; do the
@@ -3520,7 +3522,9 @@ public sealed class MainForm : Form
private void OnEqModeChanged()
{
if (loadingPanEqControls || selectedShapingKey is null) return;
GetOrCreateShaping(selectedShapingKey).EqMode = ModeForIndex(eqModeList.SelectedIndex);
var mode = ModeForIndex(eqModeList.SelectedIndex);
GetOrCreateShaping(selectedShapingKey).EqMode = mode;
logFile.Event($"shaping: EQ mode → {mode} for {selectedShapingKey}");
RebuildEqBandSliders();
ApplyPeerShaping(selectedShapingKey);
UpdateEqCurve();
@@ -3744,6 +3748,7 @@ public sealed class MainForm : Form
if (dlg.ShowDialog(this) == DialogResult.OK)
{
s.ParametricBands.Add(dlg.Result);
logFile.Event($"shaping: added parametric band {dlg.Result.StartHz:0}-{dlg.Result.EndHz:0} Hz {dlg.Result.GainDb:0.#} dB for {selectedShapingKey} ({s.ParametricBands.Count} band(s))");
RefreshParametricBandList();
MarkProfileDirty();
}
@@ -3759,6 +3764,7 @@ public sealed class MainForm : Form
int firstIdx = parametricBandList.SelectedIndex;
var toRemove = parametricBandList.SelectedItems.Cast<ParametricBandItem>().Select(x => x.Band).ToList();
foreach (var band in toRemove) s.ParametricBands.Remove(band);
logFile.Event($"shaping: deleted {toRemove.Count} parametric band(s) for {selectedShapingKey} ({s.ParametricBands.Count} left)");
RefreshParametricBandList();
// Put focus on whatever now occupies the first removed slot so NVDA announces it.
if (parametricBandList.Items.Count > 0)
@@ -4482,6 +4488,9 @@ public sealed class MainForm : Form
/// refresh every place a peer name shows. A blank/cleared name removes the entry entirely.</summary>
private void ApplyFriendlyName(string identityKey, string machineName, string? address, string? name)
{
logFile.Event(string.IsNullOrWhiteSpace(name)
? $"named peer: cleared name for {identityKey}"
: $"named peer: {identityKey} → \"{name.Trim()}\"");
if (string.IsNullOrWhiteSpace(name))
{
namedPeers.Remove(identityKey);
@@ -4597,6 +4606,7 @@ public sealed class MainForm : Form
void DeleteSelected()
{
if (list.SelectedItem is not NamedPeerItem it) return;
logFile.Event($"named peer: deleted \"{it.Peer.FriendlyName}\" ({it.Key})");
namedPeers.Remove(it.Key);
SaveNamedPeers();
lastPanEqPeerSignature = "";