From 751d9b199ca2e8b1a712061d4728cbc6a5bc9bad Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 12 Nov 2026 12:27:34 -0800 Subject: [PATCH] fix(audio): the track starts with the session, not with the first write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Services/Audio/AudioMixer.cs | 12 ++++++++---- docs/ai/output.md | 8 +++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Services/Audio/AudioMixer.cs b/Services/Audio/AudioMixer.cs index e773b22..dd0b6e3 100644 --- a/Services/Audio/AudioMixer.cs +++ b/Services/Audio/AudioMixer.cs @@ -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) diff --git a/docs/ai/output.md b/docs/ai/output.md index ff3c444..3aa90d4 100644 --- a/docs/ai/output.md +++ b/docs/ai/output.md @@ -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`