using System; namespace ytLive.Services.Audio; /// /// TASK 22: a fixed end-to-end audio delay applied to the interleaved stereo /// mix in . This is OBS's documented fix for the /// "audio is ahead of video" lip-sync case: delay the audio by a few /// milliseconds so it lands on the video. It is a pure delay line — input in, /// the same audio N ms later out — so it is trivially testable and carries no /// state outside its own ring. /// /// Positive offsets only (0..500 ms). Advancing audio ( /// would need a video-side delay and is out of scope for the audio layer. /// Changing the delay flushes the line, so a live slider change clicks rather /// than smearing. Safe to call from any thread — the mixer owns it. /// public sealed class AudioSyncDelay { private readonly int _sampleRate; private float[] _buffer = Array.Empty(); private int _delaySamples; private int _writePos; public AudioSyncDelay(int sampleRate) { _sampleRate = Math.Max(1, sampleRate); Configure(0); } /// Sets the delay in whole milliseconds (clamped to 0..500 and /// flushed). public void Configure(int delayMs) { _delaySamples = Frames(delayMs); _buffer = new float[(_delaySamples + _sampleRate) * 2 + 1024]; _writePos = 0; } /// Delays an interleaved stereo chunk by the configured offset. /// The returned length equals 's; the head is /// silence until the line fills. public void Process(ReadOnlySpan input, Span output) { if (output.Length < input.Length) throw new ArgumentException("output must be at least as long as input", nameof(output)); var len = input.Length; if (_delaySamples == 0) { input.CopyTo(output); return; } var line = _buffer; var lineLen = line.Length; for (var i = 0; i < len; i++) { line[_writePos] = input[i]; _writePos++; if (_writePos == lineLen) _writePos = 0; } var readStart = (_writePos - _delaySamples - len + lineLen * 2) % lineLen; for (var i = 0; i < len; i++) { var idx = readStart + i; if (idx >= lineLen) idx -= lineLen; output[i] = line[idx]; } } private int Frames(int delayMs) { var clamped = Math.Clamp(delayMs, 0, 500); // Interleaved stereo: each frame is two samples (L, R), so the delay // in buffer positions is twice the frame count. return (int)Math.Round(_sampleRate / 1000.0 * clamped) * 2; } }