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