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`