using System; namespace ytLive.Services.Audio; /// /// How many frames the mix must produce now, derived from the monotonic clock rather than from a /// tick count. /// /// Why this exists (2026-11-12): 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). /// /// 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. /// public static class AudioClock { /// /// Frames still owed at : expected-minus-produced, clamped to /// 0... 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). /// 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); } /// /// 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 short after the same bug had made it 224 ms long. /// public static long Advance(long framesProduced, int frames, bool wrote) => framesProduced + (wrote ? frames : 0); }