using System; using System.Collections.Generic; namespace ADOFAIRenderer.Util.Audio.Clip; /// /// Forward-only over PCM held in memory as a list of blocks. /// Block b holds frames [b * FramesPerBlock, (b + 1) * FramesPerBlock) interleaved; the last block may be shorter. /// Each block is released (passed to the release callback and dropped) as soon as the read position passes its end, /// and all remaining blocks are released on . /// internal sealed class MemoryPCMReader : IPCMReader { private readonly float[]?[] _blocks; private readonly Action? _onBlockReleased; private int _releasedCount; // Blocks [0, _releasedCount) are released public int Channels { get; } public int SampleRate { get; } public long TotalFrames { get; } public bool IsForwardOnly => true; public long Position { get; private set; } /// Frames per block (all blocks except the last). public int FramesPerBlock { get; } public MemoryPCMReader(IReadOnlyList blocks, int framesPerBlock, int channels, int sampleRate, long totalFrames, Action? onBlockReleased) { if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels)); if (framesPerBlock <= 0) throw new ArgumentOutOfRangeException(nameof(framesPerBlock)); if (totalFrames > (long)blocks.Count * framesPerBlock) throw new ArgumentException("Blocks hold fewer frames than totalFrames.", nameof(blocks)); _blocks = new float[blocks.Count][]; for (var i = 0; i < blocks.Count; i++) _blocks[i] = blocks[i]; _onBlockReleased = onBlockReleased; FramesPerBlock = framesPerBlock; Channels = channels; SampleRate = sampleRate; TotalFrames = totalFrames; } public void Seek(long frame) { if (frame < 0 || frame > TotalFrames) throw new ArgumentOutOfRangeException(nameof(frame)); if (frame < (long)_releasedCount * FramesPerBlock) throw new InvalidOperationException($"Frame {frame} was already released by the forward-only reader."); Position = frame; } public int Read(float[] buffer, int frameCount) { var total = (int)Math.Min(frameCount, TotalFrames - Position); if (total <= 0) return 0; var written = 0; while (written < total) { var blockIndex = (int)(Position / FramesPerBlock); var block = _blocks[blockIndex] ?? throw new InvalidOperationException($"Block {blockIndex} was already released."); var offset = (int)(Position - (long)blockIndex * FramesPerBlock); var count = Math.Min(total - written, FramesPerBlock - offset); Array.Copy(block, offset * Channels, buffer, written * Channels, count * Channels); written += count; Position += count; ReleaseBlocksBefore((int)(Position / FramesPerBlock)); } return written; } private void ReleaseBlocksBefore(int blockIndex) { while (_releasedCount < blockIndex && _releasedCount < _blocks.Length) { var block = _blocks[_releasedCount]; _blocks[_releasedCount] = null; _releasedCount++; if (block != null) _onBlockReleased?.Invoke(block); } } public void Dispose() => ReleaseBlocksBefore(_blocks.Length); }