SHA256
Document Audio Pipeline
This commit is contained in:
1 parent
7bb47e4974
commit
19d0e258ac
22 files changed
+634
-12
No files matched your search
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user