Document Audio Pipeline

This commit is contained in:
copecone committed 2026-10-15 16:17:12 +09:00
1 parent 7bb47e4974
commit 19d0e258ac
22 files changed
+634 -12

No files matched your search

+56
View File
@@ -31,12 +31,20 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
/// </summary>
private const double SeekMarginSeconds = 0.25;
// FFmpeg objects owned by this reader, freed by Dispose (null after).
private AVFormatContext* _format;
private AVCodecContext* _codec;
/// <summary>Sample format converter: decoder format to interleaved float, same rate and layout.</summary>
private SwrContext* _swr;
private AVPacket* _packet;
private AVFrame* _frame;
/// <summary>Index of the decoded audio stream in the container; every other stream is discarded by the demuxer.</summary>
private readonly int _streamIndex;
/// <summary>Time base of the stream's timestamps (seconds per tick).</summary>
private readonly AVRational _timeBase;
private readonly long _ptsOrigin; // Stream timestamp of output frame _leadFrames
private readonly long _leadFrames; // Silent frames placed before the first decoded frame (MP3 header frame, see class summary)
@@ -49,14 +57,27 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
private long _nextDecodedFrame; // Frame index of the next decoded frame when counting sequentially
private bool _syncToTimestamp; // True until the first frame after open/seek sets _nextDecodedFrame from its timestamp
// _flushSent: the null packet (drain request) was sent at the end of the file.
// _endOfStream: the decoder returned EOF, or nothing more after the drain request.
private bool _flushSent;
private bool _endOfStream;
/// <summary>File path the reader was opened with.</summary>
public string Path { get; }
/// <inheritdoc/>
public int Channels { get; }
/// <inheritdoc/>
public int SampleRate { get; }
/// <inheritdoc/>
public long TotalFrames { get; }
/// <summary>False: the reader seeks within the file.</summary>
public bool IsForwardOnly => false;
/// <inheritdoc/>
public long Position { get; private set; }
/// <summary>True when the stream is MP3 and is decoded with the game's MP3 timing (see class summary).</summary>
@@ -65,6 +86,12 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
/// <summary>Number of demuxer seeks performed (for diagnostics).</summary>
public int SeekCount { get; private set; }
/// <summary>
/// Opens <paramref name="path"/>, selects the best audio stream and opens its decoder. For MP3 files the whole file is
/// scanned once to count frames (see the class summary).
/// </summary>
/// <exception cref="InvalidOperationException">The file cannot be opened, has no decodable audio stream, has no duration
/// (non-MP3), or an FFmpeg setup call fails. Everything allocated so far is freed.</exception>
public FFmpegPCMReader(string path)
{
Path = path;
@@ -181,6 +208,11 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
throw new InvalidOperationException($"Failed to rewind '{Path}': {ErrorString(seek)}");
}
/// <summary>
/// Frame count from the stream duration, or from the container duration (AV_TIME_BASE units, microseconds) when the stream
/// has none.
/// </summary>
/// <exception cref="InvalidOperationException">Neither duration is known.</exception>
private long EstimateTotalFrames(AVStream* stream)
{
if (stream->duration != ffmpeg.AV_NOPTS_VALUE && stream->duration > 0)
@@ -190,6 +222,13 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
throw new InvalidOperationException($"Audio file '{Path}' has no duration.");
}
/// <summary>
/// Moves to <paramref name="frame"/> (0 ~ <see cref="TotalFrames"/>). A target within the pending block or up to the seek margin
/// after it is reached by decoding forward; otherwise the demuxer seeks backward (AVSEEK_FLAG_BACKWARD) to target - margin
/// (or to the stream start if that fails) and the decoder is flushed. Decoded samples before the target are discarded by Read.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="frame"/> is outside 0 ~ TotalFrames.</exception>
/// <exception cref="InvalidOperationException">The demuxer cannot seek.</exception>
public void Seek(long frame)
{
if (frame < 0 || frame > TotalFrames) throw new ArgumentOutOfRangeException(nameof(frame));
@@ -205,6 +244,7 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
return;
}
// Target timestamp in stream ticks; the lead (silent) frames have no packets, so they are subtracted first.
var targetTs = _ptsOrigin + ffmpeg.av_rescale_q(Math.Max(0, frame - _leadFrames - marginFrames), new AVRational { num = 1, den = SampleRate }, _timeBase);
var ret = ffmpeg.av_seek_frame(_format, _streamIndex, targetTs, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (ret < 0)
@@ -222,6 +262,12 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
Position = frame;
}
/// <inheritdoc/>
/// <remarks>
/// Frames before the first decoded frame (MP3 lead silence) and gaps between decoded timestamps are returned as zeros.
/// Returns fewer frames than requested when decoding ends before <see cref="TotalFrames"/>.
/// </remarks>
/// <exception cref="InvalidOperationException">Reading, decoding or sample conversion fails.</exception>
public int Read(float[] buffer, int frameCount)
{
var limit = (int)Math.Min(frameCount, TotalFrames - Position);
@@ -264,6 +310,10 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
}
/// <summary>Decodes the next audio frame into the pending buffer. Returns false at the end of the stream.</summary>
/// <remarks>
/// Send/receive loop: receive a frame; on EAGAIN read and send the next packet of the stream; at the end of the file send a
/// null packet to drain the decoder, then receive until EOF.
/// </remarks>
private bool DecodeNextFrame()
{
while (!_endOfStream)
@@ -315,6 +365,10 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
return false;
}
/// <summary>
/// Converts the decoded frame to interleaved float into the pending buffer and assigns its frame position: from its timestamp
/// for the first frame after open/seek (the current position when it has none), sequential afterwards.
/// </summary>
private void StoreFrame()
{
var samples = _frame->nb_samples;
@@ -347,6 +401,7 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
_nextDecodedFrame += samples;
}
/// <summary>Frees every FFmpeg object and closes the file. Safe to call more than once and on a partly constructed reader.</summary>
public void Dispose()
{
if (_frame != null)
@@ -381,6 +436,7 @@ internal sealed unsafe class FFmpegPCMReader : IPCMReader
}
}
/// <summary>FFmpeg error text for <paramref name="error"/> (av_strerror) followed by the numeric code.</summary>
private static string ErrorString(int error)
{
var buffer = stackalloc byte[1024];