SHA256
74 lines
2.6 KiB
C#
74 lines
2.6 KiB
C#
using System;
|
|
|
|
namespace ADOFAIRenderer.Util.Audio.Clip;
|
|
|
|
/// <summary>
|
|
/// <see cref="IPCMReader"/> 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.
|
|
/// <see cref="TotalFrames"/> = 2 * inner TotalFrames.
|
|
/// </summary>
|
|
internal sealed class MP3MonoLayoutReader : IPCMReader
|
|
{
|
|
private readonly IPCMReader _inner;
|
|
private float[] _innerBuffer = Array.Empty<float>();
|
|
|
|
/// <param name="inner">Mono reader of the MP3 file with the game's MP3 timing (<see cref="FFmpegPCMReader.IsMP3"/>). Owned by this reader.</param>
|
|
public MP3MonoLayoutReader(IPCMReader inner)
|
|
{
|
|
if (inner.Channels != 1) throw new ArgumentException("Inner reader must be mono.", nameof(inner));
|
|
_inner = inner;
|
|
Position = inner.Position * 2;
|
|
}
|
|
|
|
public int Channels => 1;
|
|
public int SampleRate => _inner.SampleRate;
|
|
public long TotalFrames => _inner.TotalFrames * 2;
|
|
public bool IsForwardOnly => _inner.IsForwardOnly;
|
|
public long Position { get; private set; }
|
|
|
|
/// <summary>Moves to <paramref name="frame"/>; the inner reader moves to the inner frame of the next even position.</summary>
|
|
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)
|
|
{
|
|
if ((Position & 1) == 1)
|
|
{
|
|
buffer[written++] = 0f;
|
|
Position++;
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public void Dispose() => _inner.Dispose();
|
|
}
|