fix(audio): only count audio that actually reached the encoder

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.
This commit is contained in:
2026-11-12 11:39:20 -08:00
parent 47f409f0e4
commit 102cb96b28
5 changed files with 42 additions and 9 deletions
+3
View File
@@ -308,6 +308,9 @@ devices, no timers).
the lips across ten seconds and no static delay could fix it. Buffers size to the largest chunk the
planner may return (`OutputSampleRate / 5`), and a stalled loop pays its debt over the following ticks
rather than dumping one huge write. Video never had this bug — the pump paces off a stopwatch.
**The clock also starts only once ffmpeg is connected, and `Advance` counts a frame only when its write
reached the pipe**: the first clock-driven recording came out **226 ms short** because 27 start-up
writes were dropped before the encoder connected, which is the same desync wearing the opposite sign.
- **TRAX — free background music (TASK 8):** `MusicPlayer` = NAudio `MediaFoundationReader`
(mp3/wav/m4a) → `VolumeWaveProvider16` at the hardcoded **0.20** bed (no slider) → `WaveOutEvent` on
the default device, **looping on any clean natural end** (`PlaybackStopped` with `e.Exception == null`