aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Audio/Mp3.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core/Audio/Mp3.cs')
-rw-r--r--src/Godosa.Core/Audio/Mp3.cs133
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();
+}