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.
44 lines
2.3 KiB
C#
44 lines
2.3 KiB
C#
using System;
|
|
|
|
namespace ytLive.Services.Audio;
|
|
|
|
/// <summary>
|
|
/// How many frames the mix must produce <i>now</i>, derived from the monotonic clock rather than from a
|
|
/// tick count.
|
|
///
|
|
/// <para><b>Why this exists (2026-11-12):</b> the live loop used to write a fixed 480 frames per
|
|
/// iteration and trust that every iteration was 10 ms. It is not: `Task.Delay` plus processing lands
|
|
/// near 9.8 ms under a raised timer resolution, and 102 ticks per second where 100 are expected is
|
|
/// 102% of an audio second per wall second. FFmpeg stamps audio by sample count, so the track comes out
|
|
/// longer than the show and the sound slides later against a correct picture — measured on a 9.9 s
|
|
/// recording: video 9.888 s (right), audio 10.112 s (+2.1%, 224 ms).</para>
|
|
///
|
|
/// <para>The video side never had this bug because the pump paces off a stopwatch. This is the audio
|
|
/// equivalent: ask the clock what should exist, produce the difference, pad on underrun. Pure, so the
|
|
/// drift case is a test rather than a listening test.</para>
|
|
/// </summary>
|
|
public static class AudioClock
|
|
{
|
|
/// <summary>
|
|
/// Frames still owed at <paramref name="elapsedSeconds"/>: expected-minus-produced, clamped to
|
|
/// 0..<paramref name="maxFrames"/>. Zero means "not due yet"; the cap keeps a stalled loop from
|
|
/// dumping a huge burst in one write (the deficit stays owed and is paid off over the next ticks).
|
|
/// </summary>
|
|
public static int FramesDue(double elapsedSeconds, long framesProduced, int sampleRate, int maxFrames)
|
|
{
|
|
if (elapsedSeconds <= 0 || sampleRate <= 0 || maxFrames <= 0) return 0;
|
|
var expected = (long)Math.Round(elapsedSeconds * sampleRate);
|
|
var debt = expected - framesProduced;
|
|
return (int)Math.Clamp(debt, 0L, maxFrames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advance the produced count for frames that actually reached the encoder. A write that went
|
|
/// nowhere (ffmpeg had not connected yet) must stay owed: counting it would shorten the track by
|
|
/// that gap — measured 2026-11-12 on the first clock-driven recording, where 27 start-up drops made
|
|
/// the audio 226 ms <i>short</i> after the same bug had made it 224 ms long.
|
|
/// </summary>
|
|
public static long Advance(long framesProduced, int frames, bool wrote)
|
|
=> framesProduced + (wrote ? frames : 0);
|
|
}
|