diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Audio/Mp3.cs | |
| download | godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip | |
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core/Audio/Mp3.cs')
| -rw-r--r-- | src/Godosa.Core/Audio/Mp3.cs | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/src/Godosa.Core/Audio/Mp3.cs b/src/Godosa.Core/Audio/Mp3.cs new file mode 100644 index 0000000..8ca1b57 --- /dev/null +++ b/src/Godosa.Core/Audio/Mp3.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using NLayer; + +namespace Godosa.Core.Audio; + +/// <summary>Whole MP3 files decoded at once (NLayer), downmixed to mono at the file's rate like +/// <see cref="WavReader"/>: for positioned sounds and loop emitters. Undecodable → null.</summary> +public static class Mp3Reader +{ + /// <summary>Length in seconds from the frame headers (NLayer's scan, no decoding: ~1/60 of <see cref="Read"/>'s cost), + /// so a caller can time a sound and decode it elsewhere. Undecodable → null.</summary> + 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<float>(); + 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; + } + } +} + +/// <summary>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 <see cref="Pcm.SampleRate"/>; a once-only stream goes silent and reports +/// <see cref="Finished"/>. For music and speech.</summary> +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(); +} |
