Files
LlamaCasty/HANDOFF.md
T
gramps b22d08eca6 feat: signed A/V sync offset (−500..+500), negative advances by eating live stream head (OBS eat-head semantics)
Positive offsets still delay the whole mix via the delay line (lip-sync fix);
negative offsets now ARM once at StartLive and drop |N| ms off the pipe's write
head so audio events land earlier when audio runs BEHIND video. Slider relabeled
AUDIO SYNC, Min −500, locked while live/recording (IsEditMode). LayoutStore and
VM clamp to −500..500.

OBS reference for eat-the-head negative sync: https://obsproject.com/kb/obs-studio/buffering-time (negative sync values pull audio earlier by discarding buffered player audio).

Test: StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead (6x0.9 head
must be eaten before 0.2 bed reaches the wire).
2026-09-14 12:30:44 -07:00

5.1 KiB
Raw Blame History

HANDOFF — 2026-09-14 (SIGNED AUDIO SYNC ONLINE: −500..+500, negative advances by eating the stream head; slider locked while live/recording)

Branch / Commit State

main HEAD = 11a7af2 (pre-live ring-backlog fix, committed, pushed NOT authorized). Working tree dirty with the signed-sync work below — NOT yet committed. NOT pushing (no-push ruling still in effect; the last two fixes — audio silence + ring backlog — are committed locally and the user has not greenlit a push).

DB now: Audio.SyncOffsetMs = 0 (confirmed via sqlite3 this session — the creator slid the sync to 0 and the write finally stuck; sound is correct at 0). The 0.54s residual in the 1128 take ≈ 300ms injected offset (leftover DB value) + ~240ms natural (partly webcam clap-quantization ±40–85ms, partly measurement).

✅ SHIPPED (this dirty tree) — signed audio sync, −500..+500

What: the audio-sync control is now a SIGNED offset. Positive = delay the mix (audio runs AHEAD of video — existing AudioSyncDelay behavior, unchanged and live-reactive). Negative = advance the audio (audio runs BEHIND video): OBS's "eat the head of the buffer" fix — the mixer drops the first |N| ms of the written stream at the pipe, re-anchoring the audio stream so every event lands |N| ms EARLIER relative to video.

Mechanism (AudioMixer): StartLive arms _advanceSamplesRemaining = |N| ms → samples at go-live (a negative offset can only eat the HEAD of the stream; it is armed once, not live-reactive). LiveLoopAsync skips min(budget, mixBuffer.Length) samples off each write head while the budget lasts — the pipe writer accepts a partial chunk via AsMemory(writeFrom). Positive path untouched (delay line still re-reads the Func every tick).

UI/plumbing:

  • MainViewModel.Audio.cs — clamp Math.Clamp(value, -500, 500), doc updated.
  • LayoutStore.Settings.cs — LoadAudioSyncOffsetMs/SaveAudioSyncOffsetMs clamp −500..500.
  • PreviewPane.xaml — label SYNC → "AUDIO SYNC", Minimum="-500", tooltip explains both directions (calibrate with a clap: clap late → negative; early → positive), and IsEnabled="{Binding IsEditMode}" — the slider locks during live AND recording (gun safety, same property IsRecording/StreamStatus already raise PropertyChanged for).
  • AudioSyncDelay unchanged (still clamps negative→0 internally; header doc updated to point at the mixer for the advance side).

Regression test (the ONE integration test for this change): StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead in AudioPipelineTests.cs — −40 ms advance (8-tick budget at 5ms interval), emits 6×0.9 fresh right after StartLive (≤ budget, so the head MUST be eaten) then a long 0.2 bed; asserts the wire max ≈ 0.2 (< 0.3). Fails loudly if −N no longer drops the head (0.9 leaks).

User directives (this session):

  • Signed −500..+500 with positive=delay / negative=advance, relabel "AUDIO SYNC", default 0.
  • Lock the sync control when live or recording (done — IsEditMode).
  • NOTE ONLY, no fix: the live recording is completely different from the "recording results" shown in Chat view (recorded in bugs.md — do not rediscover as a surprise).
  • Before 1.0: write a detailed USER-DOC tutorial on the audio-sync feature (see TASKS.md note; add it to the gold-pass/1.0 checklist). docs/ currently holds only the README image.

Take verification so far (ring-backlog fix)

Take ty-20260914-1128-0000-2.mp4 (1920×1080@60fps, 356 frames / 5.93s): audio clap RMS peak 3.260s; video diff-frame 163 → 2.717s; offset ≈ +0.54s — collapsed from +2.11/2.22s; the remaining ~300ms was the baked-in SyncOffsetMs=300 (now 0). Both streams start_time=0 — not an avoid_negative_ts artifact.

Open threads

  • Verify signed sync on device: re-run the clap take with a NEGATIVE offset to confirm the advance direction end-to-end (the regression test proves the mixer; a take proves the file).
  • Audio-silence verification — fixed code (724af14) confirmed; creator heard real audio.
  • Webcam MJPG missing / ~10–14Hz, layer SortOrder, truncation-with-dynamic-scenes — queued.
  • Sync control tutorial in user docs — REQUIRED before 1.0 (creator directive).

Landmines

  • testhost shares startup.log — filter by time.
  • cmd.exe /c "taskkill /F /IM ytLive.exe" (WSL double-slashes mangle) before rebuilds.
  • Build/tests: Windows dotnet host (/mnt/c/Program Files/dotnet/dotnet.exe). 0 warnings.
  • ffmpeg/ffprobe: /mnt/c/Program Files/Krita (x64)/bin/ with Windows paths.
  • MyMistakes.md has the audio/video sync measurement recipe (claps + cross-correlation) — grep it before re-deriving.
  • sqlite3 lives at /home/gramps/android-sdk/platform-tools/sqlite3 (WSL) for the DB at /mnt/c/Users/gramp/AppData/Roaming/ytLlive/ytLlive.db.

Next step

Run the verify.sh gate (clean build, 0 warnings, full suite, scope check) on the dirty tree, then commit the signed-sync work unit (no push). Optionally a device clap take with a negative offset to validate the advance on the file.