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

+23
View File
@@ -18,18 +18,34 @@ internal sealed class DecodeStream : IDisposable
/// <summary>Input frames read from the reader per resampler call.</summary>
private const int ReadBlockFrames = 4096;
/// <summary>Resampler from the reader's rate to the output rate; null when the rates are equal (samples are copied).</summary>
private readonly PCMResampler? _resampler;
private readonly int _channels;
private readonly int _outputRate;
/// <summary>Input block of <see cref="ReadBlockFrames"/> frames, interleaved.</summary>
private readonly float[] _readBuffer;
/// <summary>Scratch output for frames discarded after a restart; allocated on first use.</summary>
private float[]? _discardBuffer;
/// <summary>True once the reader returned no more input and the resampler is being drained with Flush.</summary>
private bool _flushing;
/// <summary>True once the input and the resampler tail are exhausted; the stream must restart before producing more.</summary>
private bool _ended;
/// <summary>Input frames per alignment step: input rate / gcd (1 without resampling).</summary>
private readonly long _inputStep;
/// <summary>Output frames per alignment step: output rate / gcd (1 without resampling).</summary>
private readonly long _outputStep;
/// <summary>Input frames fed before the requested position on a restart, so the filter history matches a continuous run.</summary>
private readonly long _prerollInputFrames;
/// <summary>Reader of this stream (owned; disposed with the stream).</summary>
public IPCMReader Reader { get; }
/// <summary>Output frame the stream continues at, or -1 when the stream is not positioned.</summary>
@@ -41,6 +57,8 @@ internal sealed class DecodeStream : IDisposable
/// <summary>Number of times the stream started anew (seek + resampler reset).</summary>
public int RestartCount { get; private set; }
/// <param name="reader">Reader of the clip; ownership passes to the stream.</param>
/// <param name="outputRate">Output sample rate in Hz.</param>
public DecodeStream(IPCMReader reader, int outputRate)
{
Reader = reader;
@@ -58,6 +76,7 @@ internal sealed class DecodeStream : IDisposable
// History fed before the first kept output frame when a stream starts mid-clip.
// The filter spans about filter_size input frames per output frame at ratios up to 1:1
// and filter_size * (input rate / output rate) input frames when downsampling.
// The preroll is four times that span plus one alignment step (the restart position is rounded down to a step).
var ratio = Math.Max(1L, (reader.SampleRate + outputRate - 1L) / outputRate);
_prerollInputFrames = 4L * _resampler.FilterSize * ratio + _inputStep;
}
@@ -77,6 +96,7 @@ internal sealed class DecodeStream : IDisposable
var channels = _channels;
var filled = 0;
// Same rate: output frame = input frame; seek only when the reader is not already at the start.
if (_resampler == null)
{
if (Reader.Position != start)
@@ -115,6 +135,7 @@ internal sealed class DecodeStream : IDisposable
fixed (float* pIn = _readBuffer)
fixed (float* pOut = data)
{
// Produce and drop the output frames between the aligned restart position and the requested start.
while (discard > 0 && !_ended)
{
_discardBuffer ??= new float[ReadBlockFrames * channels];
@@ -167,12 +188,14 @@ internal sealed class DecodeStream : IDisposable
return 0;
}
/// <summary>Releases the resampler and the reader.</summary>
public void Dispose()
{
_resampler?.Dispose();
Reader.Dispose();
}
/// <summary>Greatest common divisor of two positive integers (Euclid).</summary>
private static int GCD(int a, int b)
{
while (b != 0) (a, b) = (b, a % b);