SHA256
Add Chunked Audio Clip Store
This commit is contained in:
1 parent
4e556a4632
commit
0a12c12ff5
7 files changed
+1412
No files matched your search
@@ -0,0 +1,181 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user