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 { /// Block arrays; an entry is set to null when the block is released. private readonly float[]?[] _blocks; /// Called with each released block (for example to uncount its memory in the store); may be null. private readonly Action? _onBlockReleased; private int _releasedCount; // Blocks [0, _releasedCount) are released /// public int Channels { get; } /// public int SampleRate { get; } /// public long TotalFrames { get; } /// True: blocks are released as they are read. public bool IsForwardOnly => true; /// public long Position { get; private set; } /// Frames per block (all blocks except the last). public int FramesPerBlock { get; } /// Interleaved blocks in frame order; the reader takes ownership of the arrays (the list is copied). /// Frames in every block except possibly the last (positive). /// Channel count (positive). /// Sample rate in Hz. /// Frame count; the blocks must hold at least this many frames. /// Called with each block when it is released; may be null. 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; } /// /// lies in a block that was already released. 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; } /// /// Blocks whose end the position has passed are released after each copy. 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; } /// Releases every block with an index below that is not released yet, in order. 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); } } /// Releases every remaining block. Safe to call more than once. public void Dispose() => ReleaseBlocksBefore(_blocks.Length); }