Files
LlamaCasty/HANDOFF.md
T
gramps 11a7af2dc0 fix: clear pre-live capture ring backlog at StartLive (2.2s A/V sync lag)
AudioMixer.Start() begins mic/loopback capture at app startup to feed the
level meters, so both 2s ring buffers fill with pre-live audio. StartLive()
drained from the oldest tail sample, putting every recorded event ~2.2s late
in the audio track (confirmed by clap analysis + cross-correlation on two
takes: +2.11 to +2.22s).

Fix: AudioMixer.StartLive() now runs _micBuffer.Clear() + _loopbackBuffer.Clear()
immediately after the pipe starts, before the drain task runs. Recording now
begins at go-live; the <=10ms in-flight chunk evicted by Clear() is imperceptible.

Regression test: StartLive_DiscardsPreLiveBacklog_SoFirstAudioIsCurrent
saturates the loopback ring with stale 0.8 pre-live audio, then asserts the
wire carries fresh post-live 0.2 (max < 0.3).

Reference (external, per derivative-work rule): OBS 'Audio mixer' keeps its
buffers fed continuously and syncs the stream start timestamp at record time
rather than replaying pre-live capture; a go-live flush of the capture buffer
is the accepted pattern for live tools restarting a stream.

Docs: HANDOFF.md (fix shipped), MyMistakes.md (A/V sync measurement recipe).
2026-09-14 11:24:12 -07:00

143 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.