Files
ADOFAIRenderer/Util/Audio/AudioSyncCalculator.cs
T
2026-10-15 18:29:22 +09:00

56 lines
2.6 KiB
C#

using System;
namespace ADOFAIRenderer.Util.Audio;
/// <summary>
/// Splits the audio sample stream into per-video-frame sample counts so that audio and video stay aligned without drift.
///
/// The frame rate is converted to an exact fraction Numerator / Denominator (<see cref="RationalUtil.Double2MaxPrecisionInt"/>).
/// Frame k receives floor((k + 1) * sampleRate * Denominator / Numerator) - floor(k * sampleRate * Denominator / Numerator) samples:
/// the remainder of each division is carried into the next frame, so the sum over any number of frames equals the exact
/// sample count of that duration rounded down (for example 44100 Hz at 60 FPS gives 735 every frame; at 144 FPS three frames of 306
/// are followed by one of 307, 306.25 on average).
/// Used by the render thread only (RenderManager.WriteAudioForFrame, once per frame sent to the encoder).
/// </summary>
internal class AudioSyncCalculator
{
/// <summary>Audio sample rate in Hz (samples per channel per second).</summary>
private readonly long _sampleRate;
/// <summary>Video frame rate as a fraction (frames per second = Numerator / Denominator).</summary>
private readonly (long Numerator, long Denominator) _fpsRational;
/// <summary>Total samples handed out since construction or <see cref="Reset"/> (kept for debugging, not read).</summary>
// ReSharper disable once NotAccessedField.Local
private long _currentSample;
/// <summary>Remainder of the last division, in units of 1 / Numerator samples (0 ~ Numerator - 1).</summary>
private long _remainder;
/// <param name="sampleRate">Audio sample rate in Hz.</param>
/// <param name="fps">Video frame rate in frames per second (the encoded frame rate).</param>
public AudioSyncCalculator(long sampleRate, double fps)
{
_sampleRate = sampleRate;
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
}
/// <summary>Restarts the sequence at frame 0 (the remainder is cleared).</summary>
public void Reset()
{
_currentSample = 0;
_remainder = 0;
}
/// <summary>Returns the sample count (frames per channel) of the next video frame and advances to the following frame.</summary>
public int GetNextFrameSamples()
{
// samplesNeeded = floor((sampleRate * Denominator + remainder) / Numerator); the new remainder carries the fraction.
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
var samplesNeeded = numerator / _fpsRational.Numerator;
_remainder = numerator % _fpsRational.Numerator;
_currentSample += samplesNeeded;
return (int)samplesNeeded;
}
}