fix(audio): the track starts with the session, not with the first write

Third measurement, and it names the start-up defect the creator heard as "started off
wrong, then corrected" (ty-20261112-1222-748-0000-2.mp4):

  wall clock (audio start -> audio stop)  12.614 s
  video: 754 frames                       12.588 s   correct
  audio: 585 packets                      12.477 s   137 ms short
  pipe connected                          160 ms after the session opened
  first 150 ms of the audio track         digital silence (the mic delay line's head)

The rate is fixed — 12.5 s of show, no accumulating drift — but starting the clock at
connect made the audio stream begin 160 ms after the video stream, so every word sat 160 ms
late and the track came out that much shorter. The silence at the head is the 175 ms mic
delay doing its job, not a fault.

Now the clock runs from session open. Time that passes before ffmpeg connects stays owed,
and it is paid the moment the pipe connects — out of the capture ring, so it is the audio
that was missed rather than padding. Debt larger than one 200 ms chunk continues to be paid
over the following ticks.

674 pass, 0 warnings.
This commit is contained in:
2026-11-12 12:27:34 -08:00
parent 102cb96b28
commit 751d9b199c
2 changed files with 13 additions and 7 deletions
+8 -4
View File
@@ -402,7 +402,12 @@ public sealed class AudioMixer : IDisposable
// any scheduler error into a permanent A/V rate error (measured 2026-11-12: 2.1% fast, 224 ms
// of drift in ten seconds); asking the clock how much is owed and producing exactly that keeps
// the track the same length as the show.
Stopwatch? clock = null;
// The clock runs from the moment the session opens, NOT from the first successful write. Starting
// it at connect made the track begin that much later than the picture: measured 2026-11-12, the
// writes began 160 ms after ffmpeg started reading and the audio stream came out 137 ms shorter
// than the video, so every word sat 160 ms late. Owed time is paid out of the capture ring when
// the pipe connects, which is the audio that was missed — real audio, not padding.
var clock = Stopwatch.StartNew();
long framesProduced = 0;
var maxChunkFrames = micChunk.Length;
@@ -411,15 +416,14 @@ public sealed class AudioMixer : IDisposable
var nextTick = DateTime.UtcNow + _mixInterval;
try
{
// The clock starts when ffmpeg is actually reading. Before that there is nowhere for
// audio to go, and time spent waiting must not become a shorter track.
// Nowhere for audio to go until ffmpeg is reading: wait without producing, so the debt
// stays owed and is paid from the ring the moment the pipe connects.
if (!pipe.IsConnected)
{
await Task.Delay(1, cancellationToken).ConfigureAwait(false);
continue;
}
clock ??= Stopwatch.StartNew();
var frames = AudioClock.FramesDue(
clock.Elapsed.TotalSeconds, framesProduced, OutputSampleRate, maxChunkFrames);
if (frames <= 0)
+5 -3
View File
@@ -308,9 +308,11 @@ 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.
**`Advance` counts a frame only when its write reached the pipe**, and the clock runs from session
open, not from the first successful write. Both come from measurement: 27 start-up writes were being
counted while dropped (**226 ms short**), and starting the clock at connect instead made the track
begin 160 ms after the picture (**137 ms short**, every word late). The owed time is paid out of the
capture ring when the pipe connects — that is the audio that was missed, not padding.
- **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`