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; } }