# HANDOFF — 2026-09-14 (AUDIO−VIDEO SYNC FIX SHIPPED: StartLive now clears the pre-live ring backlog) ## Branch / Commit State `main` HEAD = `5ba4d70`. Ahead of origin by **27 commits**. Working tree **dirty**: the sync fix is implemented + tests green below, HANDOFF/MyMistakes updated — NOT yet committed. **NOT pushing** — the prior no-push ruling (transparency + audio-silence) is clear on the first gate; the second was the sync issue, whose fix is now in the tree but the user has not greenlit a push. On-board next: verify a take, then decide. Heads-up for any reader: the previous HANDOFF (a62a283 era) described webcam/audio/ truncation fixes as **uncommitted** — that file was stale on arrival. They were actually already committed: - `724af14` fix: audio silence (idempotent `AudioSyncDelay.Configure`), webcam gray block (cbW/cbH default), truncated videos (SceneGraph split/static bake docs + FramePump probe) - `5ba4d70` docs: audio-sync offset measured ~367ms w/ 300 offset → "set SyncOffsetMs=0" Build was green (0 warnings) and 295 tests passing at the prior session end; no code changed this session (analysis only). ## ✅ SHIPPED — pre-live capture ring backlog fix (2026-09-14) **Root cause:** `AudioMixer.Start()` begins mic and loopback capture at **app startup** for the level meters. The 2-second ring buffers (`_micBuffer` = 96k floats = 2.0s mono; `_loopbackBuffer` = 192k floats = 2.0s stereo) fill continuously with pre-live audio. `StartLive` → `LiveLoopAsync` drained from the ring's tail — the **oldest** sample — so every recorded event landed ~2.0s late in the audio track (fixed lag, scaled with time-since-app-launch, capped at ring depth). **Fix:** `AudioMixer.StartLive()` now runs `_micBuffer.Clear(); _loopbackBuffer.Clear();` immediately after the pipe starts, before the drain task runs — the in-flight ≤10ms chunk loss is imperceptible and correct ("recording begins at go-live"). **Regression test (the ONE integration test for this change):** `StartLive_DiscardsPreLiveBacklog_SoFirstAudioIsCurrent` in `ytLive.Tests/AudioPipelineTests.cs` — saturates the loopback ring with stale 0.8 (simulated pre-live meters), StartLive, then emits fresh 0.2; asserts the wire carries ~0.2 (max < 0.3), failing loudly if the stale 0.8 backlog survived the clear. **Test collateral:** the 3 existing pipe-emit tests pre-filled the rings BEFORE StartLive (relying on the bug as a reservoir). Their emits now land right after StartLive (post-clear, pre-connect — the pipe drops pre-connect writes, so this keeps the ring primed for the client) with the 2026-09-14 comments updated. All passed. Verified: `AudioPipelineTests` 26/26 green. Room-native build 0 warnings. Full gate pending (verify.sh clean build + full suite + scope check). ### Read/Write semantics confirmed (AudioRingBuffer.cs) Thread-safe via `_sync`: Write evicts oldest when full; Read returns from `tail = (_head - _count + C) % C` — when full `tail == _head`, so every drained sample is one lap behind the last write. `Clear()` is locked; it is the right seam. **Root cause (confirmed two independent takes):** `AudioMixer.Start()` begins mic and loopback capture at **app startup** for the level meters. The 2-second ring buffers (`_micBuffer` = 96k floats = 2.0s mono; `_loopbackBuffer` = 192k floats = 2.0s stereo) fill continuously with pre-live audio. `StartLive` → `LiveLoopAsync` drains from the ring's tail — the **oldest** sample, which is the moment-of-go-live sample minus the full ring depth — so every recorded event appears ~2.0s late in the audio track. The lag is **fixed** for the entire recording and scales linearly with "how long the app was open before you hit record", capped at the 2.0s ring depth (plus the configured sync offset plus ~0.1s encoder pipeline). **Read/Write semantics confirmed** in `AudioRingBuffer.cs` (thread-safe via `_sync`): Write evicts oldest samples when full (`_count == _buffer.Length`); Read returns from `tail = (_head - _count + C) % C` — when full, `tail == _head`, meaning every drained sample is exactly one lap behind what was last written. `Clear()` exists and is locked; it is the right seam. **Only one Clear call exists** in the capture path: `RestartMic()` (AudioMixer.cs:132) clears `_micBuffer` on device swap. **Neither buffer is cleared at `StartLive`** — this is the bug. **Taking:** the lag also explains the earlier 13:58 take that measured only ~367ms with 300ms offset active: the app had just been restarted for that take, so the rings were only partially pre-filled (lag = min(2.0s, time-since-app-launch)). Two takes hours later (rings fully saturated) reproduce the full ~2.0s backlog consistently. --- **Take 1** (ty-20260912-1923-0000-2.mp4, talking + 3 claps): - Audio 17.25s, video 17.05s (1023 frames), no frame drops — NOT truncated. - Per-clap offsets: +2.16 / +2.18 / +2.15s (audio later). Cross-correlation peak: **+133 frames (+2.217s)**, sharp and unique. Fixed offset, no drift. - DB: `Audio.SyncOffsetMs = 300` (not 0 as the 5ba4d70 commit message claimed — the write never actually landed). **Take 2** (ty-20260914-1104-0000-2.mp4, clean clap test): - Audio 22.31s, video 22.10s (1326 frames), no frame drops. - Audio clap peak at **14.04s**; video motion peak at **11.93s** (yavg 2.75 vs background ~0.1–0.5). Offset: **+2.11s** — confirms the same mechanism. - Tiny pre-clap blips at 0.2/1.5/4.5/13.5s are faint artifacts (< 100 RMS); the 11617-RMS clap is unambiguous. **Theory of the "cut off at the end":** audio events land ~2s late; when the video ends the audio track is still playing the final 2s of real-time content, giving the impression of a cut. The audio track's slightly longer duration (17.25 vs 17.05; 22.31 vs 22.10) is the tail of that delayed signal — real audio outlives the pump-stopped video. **Theories RULED OUT by data:** - ❌ Frame truncation / slow full-render — video is real-time; frame counts match wall-clock (FramePump log: 301/300 per 5s, dropped 0, avg render 14ms). - ❌ Named-pipe-connect drop — would *shorten* audio (events earlier), opposite sign. Pipe connected near-instantly in both takes. - ❌ AudioSyncDelay alone — capped at 0..500ms; 300ms is a subset, not the whole. - ❌ Audio drift/resampler rate — offset is constant, not growing. ## Open threads - **Audio-silence verification** — FIXED code (idempotent `Configure`, committed `724af14`); the 19:23 take had real peakMix. Final user confirmation still pending. - **Audio sync offset DB value** — `Audio.SyncOffsetMs = 300` (the "set to 0" write in 5ba4d70 never actually landed). Fix for the ~2s ring backlog is now SHIPPED (local, uncommitted). Next: re-record the clap pattern, run the cross-correlation recipe → expect residual ≈ 400ms (300ms configured sync + ~100ms pipeline); then optionally zero the slider to hit ~100ms. - **Truncation with DYNAMIC scenes** — proven mechanism + probe in place; static scenes full-length. 19:23 take confirms dynamic scenes hold ~60fps (no truncation) but with 30ms stall cadence. - **Webcam MJPG missing**, **web capture ~10–14Hz**, **layer SortOrder not persisting** — queued, unchanged. ## Landmines - testhost shares startup.log — filter by time. - `cmd.exe /c "taskkill /F /IM ytLive.exe"` (WSL form 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/ffmpeg.exe` with Windows paths. - **`Audio.SyncOffsetMs` currently = 300 in the DB** (contradicts the "set to 0" line in 5ba4d70; the write likely never happened). Non-zero offsets still depend on the idempotent `Configure` guard to avoid the silence bug. - `MyMistakes.md` → Recipes registry now has the **audio/video sync measurement recipe** (claps + cross-correlation) — grep before re-deriving. ## Next step **Verify the shipped fix:** re-record with the same clap pattern and run the cross-correlation recipe from `MyMistakes.md` → expect offset ≈ 400ms (300ms configured sync + ~100ms pipeline) with a `SyncOffsetMs=300` DB value, or ~100ms with the slider zeroed. Then commit this session's work (StartLive clear + regression test + docs) and decide with the user whether the no-push ruling is lifted.