using System;
namespace ADOFAIRenderer.Util.Audio.Clip;
///
/// that reproduces the game's output for mono MP3 files.
///
/// MP3Sharp (Assembly-CSharp-firstpass, MP3Sharp.Buffer16BitStereo) writes every frame in stereo layout and fills only the
/// channel 0 slot for mono streams, while MP3Stream.ChannelCount reports 1. The game creates a 1-channel clip from that data,
/// so a mono MP3 plays as [s0, 0, s1, 0, ...]: twice as long, at half speed and one octave lower
/// (confirmed in game with TestMaps/mp3_mono, 2026-10-13).
///
/// Output: 1 channel at the inner reader's sample rate. Frame 2k is inner frame k, frame 2k + 1 is 0.
/// = 2 * inner TotalFrames.
///
internal sealed class MP3MonoLayoutReader : IPCMReader
{
private readonly IPCMReader _inner;
/// Reused buffer for inner frames, grown to the largest request.
private float[] _innerBuffer = Array.Empty();
/// Mono reader of the MP3 file with the game's MP3 timing (). Owned by this reader.
public MP3MonoLayoutReader(IPCMReader inner)
{
if (inner.Channels != 1) throw new ArgumentException("Inner reader must be mono.", nameof(inner));
_inner = inner;
Position = inner.Position * 2;
}
/// Always 1.
public int Channels => 1;
/// The inner reader's sample rate (the game plays the doubled data at this rate).
public int SampleRate => _inner.SampleRate;
/// Twice the inner frame count.
public long TotalFrames => _inner.TotalFrames * 2;
///
public bool IsForwardOnly => _inner.IsForwardOnly;
///
public long Position { get; private set; }
/// Moves to ; the inner reader moves to the inner frame of the next even position.
public void Seek(long frame)
{
_inner.Seek((frame + 1) / 2);
Position = frame;
}
///
public int Read(float[] buffer, int frameCount)
{
var limit = (int)Math.Min(frameCount, TotalFrames - Position);
var written = 0;
while (written < limit)
{
// Odd position: the zero slot after an inner sample (also the first frame after a seek to an odd frame).
if ((Position & 1) == 1)
{
buffer[written++] = 0f;
Position++;
continue;
}
// Even position: read enough inner frames for the remaining output, then interleave each with a zero.
var want = (limit - written + 1) / 2;
if (_innerBuffer.Length < want) _innerBuffer = new float[want];
var got = _inner.Read(_innerBuffer, want);
if (got == 0) break;
for (var i = 0; i < got && written < limit; i++)
{
buffer[written++] = _innerBuffer[i];
Position++;
if (written >= limit) break;
buffer[written++] = 0f;
Position++;
}
}
return written;
}
/// Disposes the inner reader.
public void Dispose() => _inner.Dispose();
}