SHA256
182 lines
6.6 KiB
C#
182 lines
6.6 KiB
C#
using System;
|
|
|
|
namespace ADOFAIRenderer.Util.Audio.Clip;
|
|
|
|
/// <summary>
|
|
/// One reader plus its resampler, producing output-rate frames of a clip as a continuous stream.
|
|
/// Consecutive <see cref="Fill"/> calls whose ranges follow each other continue the stream (reader position and resampler state);
|
|
/// any other range starts a new stream near the requested position.
|
|
///
|
|
/// Restart: the input position is moved back by a preroll and down to a multiple of in_rate / gcd(in_rate, out_rate),
|
|
/// where input frame k * (in_rate / gcd) maps exactly to output frame k * (out_rate / gcd);
|
|
/// the resampler is reset and output before the requested frame is discarded.
|
|
/// Measured with the clip harness: output is bit-identical to one continuous conversion of the whole clip.
|
|
/// Used by one thread at a time (the store's decoder thread).
|
|
/// </summary>
|
|
internal sealed class DecodeStream : IDisposable
|
|
{
|
|
/// <summary>Input frames read from the reader per resampler call.</summary>
|
|
private const int ReadBlockFrames = 4096;
|
|
|
|
private readonly PCMResampler? _resampler;
|
|
private readonly int _channels;
|
|
private readonly int _outputRate;
|
|
private readonly float[] _readBuffer;
|
|
private float[]? _discardBuffer;
|
|
private bool _flushing;
|
|
private bool _ended;
|
|
|
|
private readonly long _inputStep;
|
|
private readonly long _outputStep;
|
|
private readonly long _prerollInputFrames;
|
|
|
|
public IPCMReader Reader { get; }
|
|
|
|
/// <summary>Output frame the stream continues at, or -1 when the stream is not positioned.</summary>
|
|
public long NextOutFrame { get; private set; } = -1;
|
|
|
|
/// <summary>Value of the owner's use counter at the last <see cref="Fill"/> (for least-recently-used selection).</summary>
|
|
public long LastUse { get; set; }
|
|
|
|
/// <summary>Number of times the stream started anew (seek + resampler reset).</summary>
|
|
public int RestartCount { get; private set; }
|
|
|
|
public DecodeStream(IPCMReader reader, int outputRate)
|
|
{
|
|
Reader = reader;
|
|
_channels = reader.Channels;
|
|
_outputRate = outputRate;
|
|
_readBuffer = new float[ReadBlockFrames * _channels];
|
|
|
|
if (reader.SampleRate != outputRate)
|
|
{
|
|
_resampler = new PCMResampler(_channels, reader.SampleRate, outputRate);
|
|
var gcd = GCD(reader.SampleRate, outputRate);
|
|
_inputStep = reader.SampleRate / gcd;
|
|
_outputStep = outputRate / gcd;
|
|
|
|
// 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.
|
|
var ratio = Math.Max(1L, (reader.SampleRate + outputRate - 1L) / outputRate);
|
|
_prerollInputFrames = 4L * _resampler.FilterSize * ratio + _inputStep;
|
|
}
|
|
else
|
|
{
|
|
_inputStep = 1;
|
|
_outputStep = 1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes output frames [start, start + frames) into <paramref name="data"/> and zeros the rest of the array.
|
|
/// Frames past the end of the decoded input are zero.
|
|
/// </summary>
|
|
public unsafe void Fill(long start, int frames, float[] data)
|
|
{
|
|
var channels = _channels;
|
|
var filled = 0;
|
|
|
|
if (_resampler == null)
|
|
{
|
|
if (Reader.Position != start)
|
|
{
|
|
Reader.Seek(start);
|
|
RestartCount++;
|
|
}
|
|
while (filled < frames)
|
|
{
|
|
var request = Math.Min(ReadBlockFrames, frames - filled);
|
|
var read = Reader.Read(_readBuffer, request);
|
|
if (read <= 0) break;
|
|
Array.Copy(_readBuffer, 0, data, filled * channels, read * channels);
|
|
filled += read;
|
|
}
|
|
Array.Clear(data, filled * channels, data.Length - filled * channels);
|
|
NextOutFrame = start + frames;
|
|
return;
|
|
}
|
|
|
|
long discard = 0;
|
|
if (NextOutFrame != start)
|
|
{
|
|
// Input frame of the requested output frame, moved back by the preroll and down to a multiple of _inputStep.
|
|
var inputFrame = start * Reader.SampleRate / _outputRate - _prerollInputFrames;
|
|
var step = inputFrame > 0 ? inputFrame / _inputStep : 0;
|
|
|
|
_resampler.Reset();
|
|
Reader.Seek(step * _inputStep);
|
|
_flushing = false;
|
|
_ended = false;
|
|
discard = start - step * _outputStep;
|
|
RestartCount++;
|
|
}
|
|
|
|
fixed (float* pIn = _readBuffer)
|
|
fixed (float* pOut = data)
|
|
{
|
|
while (discard > 0 && !_ended)
|
|
{
|
|
_discardBuffer ??= new float[ReadBlockFrames * channels];
|
|
fixed (float* pDiscard = _discardBuffer)
|
|
{
|
|
var capacity = (int)Math.Min(ReadBlockFrames, discard);
|
|
discard -= Pull(pIn, pDiscard, capacity);
|
|
}
|
|
}
|
|
|
|
while (filled < frames && !_ended)
|
|
filled += Pull(pIn, pOut + (long)filled * channels, frames - filled);
|
|
}
|
|
|
|
Array.Clear(data, filled * channels, data.Length - filled * channels);
|
|
NextOutFrame = _ended ? -1 : start + frames;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes 1 ~ <paramref name="capacity"/> resampled frames to <paramref name="output"/>, reading input as needed.
|
|
/// Returns 0 and sets _ended once the input and the resampler's flushed tail are exhausted.
|
|
/// </summary>
|
|
private unsafe int Pull(float* input, float* output, int capacity)
|
|
{
|
|
var resampler = _resampler!;
|
|
if (!_flushing)
|
|
{
|
|
while (true)
|
|
{
|
|
var produced = resampler.Convert(input, 0, output, capacity);
|
|
if (produced > 0) return produced;
|
|
|
|
var read = Reader.Read(_readBuffer, ReadBlockFrames);
|
|
if (read <= 0)
|
|
{
|
|
_flushing = true;
|
|
break;
|
|
}
|
|
|
|
produced = resampler.Convert(input, read, output, capacity);
|
|
if (produced > 0) return produced;
|
|
}
|
|
}
|
|
|
|
// Input is exhausted: take the resampler's remaining tail until it returns 0.
|
|
var flushed = resampler.Flush(output, capacity);
|
|
if (flushed > 0) return flushed;
|
|
|
|
_ended = true;
|
|
return 0;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_resampler?.Dispose();
|
|
Reader.Dispose();
|
|
}
|
|
|
|
private static int GCD(int a, int b)
|
|
{
|
|
while (b != 0) (a, b) = (b, a % b);
|
|
return a;
|
|
}
|
|
}
|