SHA256
Match Game MP3 Decoding Timing
This commit is contained in:
1 parent
82776fc572
commit
35606d6e58
3 files changed
+249
-6
No files matched your search
@@ -0,0 +1,73 @@
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user