aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Audio/WavReader.cs
blob: f5871095a7c742737f4a8e4f0146a37bc2daaaf4 (plain)
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System;
using System.Buffers.Binary;

namespace Godosa.Core.Audio;

public readonly record struct PcmClip(short[] Samples, int SampleRate);

/// <summary>WAV files for sound overrides (audio spec §3): 8/16/24-bit PCM, 32-bit float or
/// Microsoft ADPCM (4-bit, format 2), plain or extensible header, any channel count, downmixed
/// to mono (OpenAL only spatialises mono). Anything else → null.</summary>
public static class WavReader
{
    private const int FormatPcm = 1, FormatMsAdpcm = 2, FormatFloat = 3, FormatExtensible = 0xFFFE;

    public static PcmClip? Read(ReadOnlySpan<byte> file)
    {
        if (file.Length < 12 || !file[..4].SequenceEqual("RIFF"u8) || !file[8..12].SequenceEqual("WAVE"u8)) return null;
        int pos = 12, format = 0, channels = 0, rate = 0, bits = 0, blockAlign = 0, factFrames = -1;
        var fmt = ReadOnlySpan<byte>.Empty;
        while (pos + 8 <= file.Length)
        {
            var id = file.Slice(pos, 4);
            int size = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(pos + 4, 4));
            int body = pos + 8;
            if (size < 0 || body > file.Length) return null;
            if (id.SequenceEqual("fmt "u8) && size >= 16 && body + 16 <= file.Length)
            {
                format = BinaryPrimitives.ReadUInt16LittleEndian(file.Slice(body, 2));
                channels = BinaryPrimitives.ReadUInt16LittleEndian(file.Slice(body + 2, 2));
                rate = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(body + 4, 4));
                blockAlign = BinaryPrimitives.ReadUInt16LittleEndian(file.Slice(body + 12, 2));
                bits = BinaryPrimitives.ReadUInt16LittleEndian(file.Slice(body + 14, 2));
                fmt = file.Slice(body, Math.Min(size, file.Length - body));
                if (format == FormatExtensible && size >= 26 && body + 26 <= file.Length)
                    format = BinaryPrimitives.ReadUInt16LittleEndian(file.Slice(body + 24, 2));
            }
            else if (id.SequenceEqual("fact"u8) && size >= 4 && body + 4 <= file.Length)
                factFrames = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(body, 4));
            else if (id.SequenceEqual("data"u8))
            {
                if (channels < 1 || rate < 1) return null;
                var data = file.Slice(body, Math.Min(size, file.Length - body));
                // fact (sample count) usually follows fmt, but may come after data: look ahead for it.
                if (factFrames < 0 && FindFact(file, body + size + (size & 1)) is { } later) factFrames = later;
                var pcm = format == FormatMsAdpcm ? MsAdpcm(data, fmt, channels, blockAlign) : Decode(data, format, channels, bits);
                if (pcm != null && factFrames >= 0 && factFrames < pcm.Length) pcm = pcm[..factFrames];
                return pcm is null ? null : new PcmClip(pcm, rate);
            }
            pos = body + size + (size & 1);
        }
        return null;
    }

    private static int? FindFact(ReadOnlySpan<byte> file, int pos)
    {
        while (pos + 8 <= file.Length)
        {
            int size = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(pos + 4, 4));
            if (size < 0) return null;
            if (file.Slice(pos, 4).SequenceEqual("fact"u8) && size >= 4 && pos + 12 <= file.Length)
                return BinaryPrimitives.ReadInt32LittleEndian(file.Slice(pos + 8, 4));
            pos += 8 + size + (size & 1);
        }
        return null;
    }

    private static readonly int[] AdpcmAdaptation = [230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230];

    /// <summary>Microsoft ADPCM: per block and channel a predictor index, delta, and two history samples (output
    /// oldest first), then 4-bit codes, high nibble first, channels interleaved per nibble. Coefficient pairs come from
    /// the fmt extension (cbSize, samples per block, count, pairs). A trailing partial block decodes what it holds.</summary>
    private static short[]? MsAdpcm(ReadOnlySpan<byte> data, ReadOnlySpan<byte> fmt, int channels, int blockAlign)
    {
        if (channels > 2 || fmt.Length < 22 || blockAlign < 7 * channels) return null;
        int count = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(20, 2));
        if (count < 1 || fmt.Length < 22 + 4 * count) return null;
        var coef = new (int C1, int C2)[count];
        for (int i = 0; i < count; i++)
            coef[i] = (BinaryPrimitives.ReadInt16LittleEndian(fmt.Slice(22 + 4 * i, 2)), BinaryPrimitives.ReadInt16LittleEndian(fmt.Slice(24 + 4 * i, 2)));
        var mono = new System.Collections.Generic.List<short>();
        Span<int> c1 = stackalloc int[2], c2 = stackalloc int[2], delta = stackalloc int[2], s1 = stackalloc int[2], s2 = stackalloc int[2],
            frame = stackalloc int[2];
        for (int start = 0; start + 7 * channels <= data.Length; start += blockAlign)
        {
            var block = data.Slice(start, Math.Min(blockAlign, data.Length - start));
            int p = 0;
            for (int c = 0; c < channels; c++)
            {
                int index = block[p++];
                if (index >= count) return null;
                (c1[c], c2[c]) = coef[index];
            }
            for (int c = 0; c < channels; c++, p += 2) delta[c] = BinaryPrimitives.ReadInt16LittleEndian(block.Slice(p, 2));
            for (int c = 0; c < channels; c++, p += 2) s1[c] = BinaryPrimitives.ReadInt16LittleEndian(block.Slice(p, 2));
            for (int c = 0; c < channels; c++, p += 2) s2[c] = BinaryPrimitives.ReadInt16LittleEndian(block.Slice(p, 2));
            mono.Add(Mix(s2, channels));
            mono.Add(Mix(s1, channels));
            int ch = 0;
            for (; p < block.Length; p++)
                for (int shift = 4; shift >= 0; shift -= 4)
                {
                    int code = (block[p] >> shift) & 0xF;
                    int signed = code >= 8 ? code - 16 : code;
                    int predicted = ((s1[ch] * c1[ch]) + (s2[ch] * c2[ch])) >> 8;
                    int sample = Math.Clamp(predicted + signed * delta[ch], short.MinValue, short.MaxValue);
                    (s2[ch], s1[ch]) = (s1[ch], sample);
                    delta[ch] = Math.Max(16, (AdpcmAdaptation[code] * delta[ch]) >> 8);
                    frame[ch] = sample;
                    if (++ch == channels)
                    {
                        mono.Add(Mix(frame, channels));
                        ch = 0;
                    }
                }
        }
        return [.. mono];
    }

    private static short Mix(ReadOnlySpan<int> frame, int channels) => (short)(channels == 1 ? frame[0] : (frame[0] + frame[1]) / 2);

    private static short[]? Decode(ReadOnlySpan<byte> data, int format, int channels, int bits)
    {
        int bytes = bits / 8;
        bool supported = (format == FormatPcm && bits is 8 or 16 or 24) || (format == FormatFloat && bits == 32);
        if (!supported) return null;
        int frames = data.Length / (bytes * channels);
        var pcm = new short[frames];
        for (int f = 0; f < frames; f++)
        {
            if (bits == 8)
            {
                int sum8 = 0;
                for (int c = 0; c < channels; c++) sum8 += (data[f * channels + c] - 128) << 8;   // unsigned, 128 = 0
                pcm[f] = (short)(sum8 / channels);
                continue;
            }
            if (bits == 16)
            {
                int sum = 0;
                for (int c = 0; c < channels; c++)
                    sum += BinaryPrimitives.ReadInt16LittleEndian(data.Slice((f * channels + c) * 2, 2));
                pcm[f] = (short)(sum / channels);
                continue;
            }
            float mix = 0f;
            for (int c = 0; c < channels; c++)
            {
                var s = data.Slice((f * channels + c) * bytes, bytes);
                mix += bits == 24
                    ? ((s[0] | s[1] << 8 | (sbyte)s[2] << 16) / 8388608f)
                    : BinaryPrimitives.ReadSingleLittleEndian(s);
            }
            pcm[f] = Pcm.ToShort(mix / channels);
        }
        return pcm;
    }
}