using System;
using System.IO;
using NLayer;
namespace Godosa.Core.Audio;
/// Whole MP3 files decoded at once (NLayer), downmixed to mono at the file's rate like
/// : for positioned sounds and loop emitters. Undecodable → null.
public static class Mp3Reader
{
/// Length in seconds from the frame headers (NLayer's scan, no decoding: ~1/60 of 's cost),
/// so a caller can time a sound and decode it elsewhere. Undecodable → null.
public static double? Seconds(byte[] file)
{
try
{
using var mp3 = new MpegFile(new MemoryStream(file, writable: false));
var samples = mp3.Length / (sizeof(float) * Math.Max(1, mp3.Channels));
return samples > 0 && mp3.SampleRate > 0 ? samples / (double)mp3.SampleRate : null;
}
catch (Exception e) when (e is InvalidDataException or IOException or InvalidOperationException
or ArgumentException or IndexOutOfRangeException or NullReferenceException)
{
return null;
}
}
public static PcmClip? Read(byte[] file)
{
try
{
using var mp3 = new MpegFile(new MemoryStream(file, writable: false));
var channels = Math.Max(1, mp3.Channels);
var samples = new System.Collections.Generic.List();
var block = new float[4096 * channels];
int read;
while ((read = mp3.ReadSamples(block, 0, block.Length)) > 0)
samples.AddRange(block.AsSpan(0, read - read % channels).ToArray());
return samples.Count == 0 ? null : new PcmClip(Pcm.Downmix([.. samples], channels), mp3.SampleRate);
}
catch (Exception e) when (e is InvalidDataException or IOException or InvalidOperationException
or ArgumentException or IndexOutOfRangeException or NullReferenceException)
{
return null;
}
}
}
/// An MP3 as a stereo stream (NLayer decode on the audio thread; lifted from an earlier game of the same author): mono is duplicated,
/// other sample rates are resampled linearly to ; a once-only stream goes silent and reports
/// . For music and speech.
public sealed class Mp3Stream : IStereoStream, IDisposable
{
private readonly MpegFile _file;
private readonly bool _loop;
private readonly int _channels;
private readonly double _step;
private float[] _buffer = [];
private int _available, _read;
private double _position;
private volatile bool _finished;
public Mp3Stream(byte[] data, bool loop)
{
_file = new MpegFile(new MemoryStream(data, writable: false));
_loop = loop;
_channels = Math.Max(1, _file.Channels);
_step = _file.SampleRate / (double)Pcm.SampleRate;
}
public bool Finished => _finished;
public void Render(float[] interleaved, int frames)
{
for (var f = 0; f < frames; f++)
{
if (!Frame(out var left, out var right))
{
Array.Clear(interleaved, 2 * f, 2 * (frames - f));
return;
}
interleaved[2 * f] = left;
interleaved[2 * f + 1] = right;
_position += _step;
}
}
private bool Frame(out float left, out float right)
{
left = right = 0f;
var index = (int)_position;
while (index >= _available)
{
if (!Fill())
return false;
index = (int)_position;
}
var next = Math.Min(index + 1, _available - 1);
var t = (float)(_position - index);
left = Lerp(_buffer[index * _channels], _buffer[next * _channels], t);
right = _channels > 1 ? Lerp(_buffer[index * _channels + 1], _buffer[next * _channels + 1], t) : left;
return true;
}
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
private bool Fill()
{
if (_finished)
return false;
_position -= _available;
if (_buffer.Length == 0)
_buffer = new float[4096 * _channels];
var read = _file.ReadSamples(_buffer, 0, _buffer.Length);
if (read <= 0 && _loop && _read > 0)
{
_file.Position = 0;
_read = 0;
read = _file.ReadSamples(_buffer, 0, _buffer.Length);
}
if (read <= 0)
{
_finished = true;
_available = 0;
return false;
}
_read += read;
_available = read / _channels;
return true;
}
public void Dispose() => _file.Dispose();
}