fix(audio): a sync offset is saved the moment it changes
Both offsets rode the debounced layout save, so a value set seconds before the app closed was lost without a word. That is what happened to the -130 ms offset on 2026-11-12: 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. Setters now write the row immediately (two tiny rows), keeping ScheduleSave for the rest of the layout. AudioSyncSettingsTests gains the regression: set both offsets on a view model and read them back through a fresh store with no debounce flush. MyMistakes records the general shape — "a setting that didn't stick" is almost always ScheduleSave — with the check list and the two related rules: offsets never ship with a non-zero default, and scripts/avsync.py only resolves ~50 ms, so below that the creator's judgement wins. 677 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
// 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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user