using System; namespace ADOFAIRenderer.Util.Audio.Clip; /// /// One reader plus its resampler, producing output-rate frames of a clip as a continuous stream. /// Consecutive 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). /// internal sealed class DecodeStream : IDisposable { /// Input frames read from the reader per resampler call. 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; } /// Output frame the stream continues at, or -1 when the stream is not positioned. public long NextOutFrame { get; private set; } = -1; /// Value of the owner's use counter at the last (for least-recently-used selection). public long LastUse { get; set; } /// Number of times the stream started anew (seek + resampler reset). 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; } } /// /// Writes output frames [start, start + frames) into and zeros the rest of the array. /// Frames past the end of the decoded input are zero. /// 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; } /// /// Writes 1 ~ resampled frames to , reading input as needed. /// Returns 0 and sets _ended once the input and the resampler's flushed tail are exhausted. /// 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; } }