diff --git a/MyMistakes.md b/MyMistakes.md index 2a785f0..1378bfb 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -35,6 +35,37 @@ max 200. ## 🔬 Recipes registry +### A SETTING THAT "DIDN'T STICK" — CHECK THE DEBOUNCE FIRST (RECIPE, 2026-11-12) + +**Symptom:** a value is set in the UI, behaves correctly for one session, and is gone after a +restart. It looks like a wiring bug in the control; it is almost always `ScheduleSave()`. + +**Cause:** `MainViewModel.ScheduleSave()` restarts a debounce timer that later calls `Save()`, which +writes the whole layout. Anything changed in the last few seconds before the app closes is lost — +and none of it is logged, so the next session reads a value the creator never chose. + +**Quick fix:** for anything small and decisive (a sync offset, a toggle the creator just clicked), +call the `_layoutStore.Save…()` method **in the setter itself**, before `ScheduleSave()`. Two rows +cost nothing; a lost setting costs a whole reproduce-and-test cycle. Live example: +`MainViewModel.Audio.cs` → `AudioSyncOffsetMs` / `MicSyncOffsetMs`. + +**Check list when a remembered setting misbehaves:** grep the property for `ScheduleSave`, confirm a +direct save exists, then reproduce with a *restart* (not just a new session) before debugging deeper. + +**Bit us:** the lip-sync offset. `Audio.SyncOffsetMs` was set to −130, lost on close, and two clips +were measured against a setting that was not in force — the numbers disagreed by ~65 ms and the +disagreement was blamed on the audio path. + +### A SYNC OFFSET IS NEVER A PRODUCT DEFAULT (2026-11-12) + +**Mistake:** `MicSyncOffsetMs` shipped at **175 ms** because that suited one C920 on one desk. It +shifted every creator's voice to correct one machine and hid whether a desync was the app's fault. + +**Rule:** offsets ship at 0 and are opt-in. `AudioSyncSettingsTests` pins it. And the tool that +measures them, `scripts/avsync.py`, varies **±35 ms between clips** on identical settings — it can +answer "is this 100 ms out?" but never "is this 20 ms out?". Below ~50 ms the creator's judgement is +the better instrument, so stop tuning rather than chasing the tool's noise. + ### A LOOSE FILE IN `Assets/` IS NOT AN ASSET — THE CSProj LIST IS (RECIPE, 2026-11-04) **Problem:** four untracked `Assets/llamacasty-*` exports sat in the tree for days, reported in diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs index 40591d0..7f1e688 100644 --- a/ViewModels/MainViewModel.Audio.cs +++ b/ViewModels/MainViewModel.Audio.cs @@ -243,6 +243,9 @@ public partial class MainViewModel var clamped = Math.Clamp(value, -500, 500); if (!SetProperty(ref _audioSyncOffsetMs, clamped)) return; OnPropertyChanged(nameof(SyncOffsetLabel)); + // Written at once, not through the debounced layout save: a sync offset set seconds before + // closing the app used to be lost, and losing it costs a whole record-and-measure cycle. + if (_layoutStore is { } store) store.SaveAudioSyncOffsetMs(clamped); ScheduleSave(); } } @@ -264,6 +267,7 @@ public partial class MainViewModel var clamped = Math.Clamp(value, 0, 500); if (!SetProperty(ref _micSyncOffsetMs, clamped)) return; OnPropertyChanged(nameof(MicSyncOffsetLabel)); + if (_layoutStore is { } store) store.SaveMicSyncOffsetMs(clamped); // immediate, see above ScheduleSave(); } } diff --git a/ytLive.Tests/AudioSyncSettingsTests.cs b/ytLive.Tests/AudioSyncSettingsTests.cs index 6b32936..3c96d24 100644 --- a/ytLive.Tests/AudioSyncSettingsTests.cs +++ b/ytLive.Tests/AudioSyncSettingsTests.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Services; +using ytLive.ViewModels; namespace ytLive.Tests; @@ -11,6 +12,8 @@ namespace ytLive.Tests; /// not arrive with audio already shifted — a non-zero default would move every creator's voice to fix /// one machine and would hide whether the fault is theirs or ours. /// +// The view model below touches the static LayoutPathOverride, so it shares the layout collection. +[Collection("MainViewModelLayoutPath")] public sealed class AudioSyncSettingsTests : IDisposable { private readonly string _db = Path.Combine(Path.GetTempPath(), $"ytLlive-syncsettings-{Guid.NewGuid():N}.db"); @@ -44,4 +47,29 @@ public sealed class AudioSyncSettingsTests : IDisposable Assert.Equal(240, reopened.LoadMicSyncOffsetMs()); Assert.Equal(-130, reopened.LoadAudioSyncOffsetMs()); } + + /// + /// A slider move is persisted as it happens. It used to ride the debounced layout save, so a value + /// set seconds before closing the app was lost — which cost a record-and-measure cycle on 2026-11-12 + /// when a −130 ms offset vanished between two clips. + /// + [Fact] + public void A_slider_move_survives_without_any_debounce_flush() + { + MainViewModel.LayoutPathOverride = _db; + try + { + var vm = new MainViewModel(); + vm.AudioSyncOffsetMs = -130; + vm.MicSyncOffsetMs = 240; + + using var store = new LayoutStore(_db); + Assert.Equal(-130, store.LoadAudioSyncOffsetMs()); + Assert.Equal(240, store.LoadMicSyncOffsetMs()); + } + finally + { + MainViewModel.LayoutPathOverride = null; + } + } }