1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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();
}
|