Second measurement of the same recording family (ty-20261112-1133-701-0000-2.mp4):
wall clock (audio start -> audio stop) 6.477 s
video: 386 frames 6.455 s correct
audio: 294 packets 6.252 s 226 ms SHORT
Same magnitude as the original bug, opposite sign — the clock fixed the tick-rate error and
exposed the next one underneath. Audio started 49 ms before the pump, ffmpeg connected ~80 ms
after, and every chunk written in that window was dropped (dropped writes 27). The loop still
counted those frames as produced, so the track lost exactly that much time.
Now: the clock does not start until the pipe is connected, and AudioClock.Advance counts a
frame only when its write reached the pipe. A dropped write stays owed and is paid off on the
next tick, so a start-up gap costs content (silence, which there was anyway) instead of
length.
Two measurements, two different signs, one lesson: the audio track's LENGTH has to be the
clock's, and anything that removes samples without removing time — a fast tick, a dropped
write — shows up as drift no static offset can fix.
674 tests (one notification-timing flake under full-suite load; passes alone), 0 warnings.
Measured on the creator's recording (ty-20261112-1118-423-0000-2.mp4), which drifts
out of sync over ten seconds:
wall clock (pump start -> audio stop) 9.875 s
video: 594 frames, uniform 1/60 9.888 s correct
audio: 485,376 samples @ 48 kHz 10.112 s +2.1%, 224 ms long
So it was not VFR and not wall-clock PTS: the video is CFR and matches the clock (the
only PTS anomaly in the file is one 38 ms gap from two lost frames). The audio track is
what was too long, and a static offset cannot fix a rate error — which is why the opening
looked aligned (the 175 ms mic delay cancels the camera's constant pipeline latency) and
the end did not.
Cause: LiveLoopAsync wrote a fixed 480 frames per iteration and trusted that each
iteration was 10 ms. Under a raised timer resolution they land near 9.8 ms — 102 ticks
per second where 100 are expected, i.e. 102% of an audio second per wall second. FFmpeg
stamps audio by sample count, so every second of show pushed the sound ~2% later against
a correct picture. Video never had the bug because the pump paces off a stopwatch.
Fix: Services/Audio/AudioClock asks the clock how many frames are owed
(expected - produced, clamped to one 200 ms chunk) and the loop produces exactly that,
padding on underrun. A stalled loop pays its debt over the following ticks instead of
dumping one huge write. FillAndMix takes a frame count, and the master limiter/meter take
spans, so a partial chunk is processed without touching stale buffer tail.
674 tests (one log-rotation flake under full-suite load while the app itself is running
and writing startup.log; passes alone), 0 warnings. Ear check pending.