using System;
using System.Buffers.Binary;
namespace Godosa.Core.Audio;
public readonly record struct PcmClip(short[] Samples, int SampleRate);
/// 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.
public static class WavReader
{
private const int FormatPcm = 1, FormatMsAdpcm = 2, FormatFloat = 3, FormatExtensible = 0xFFFE;
public static PcmClip? Read(ReadOnlySpan 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.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 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];
/// 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.
private static short[]? MsAdpcm(ReadOnlySpan data, ReadOnlySpan 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();
Span 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 frame, int channels) => (short)(channels == 1 ? frame[0] : (frame[0] + frame[1]) / 2);
private static short[]? Decode(ReadOnlySpan 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;
}
}