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