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);
}