using System;
using System.Buffers.Binary;
namespace AcDream.Core.Audio;
///
/// Decodes the AC Wave dat (`0x0A000000..0x0A00FFFF`) into playable
/// PCM data.
///
///
/// On-disk format (r05 §2.1 + ACE.DatLoader.FileTypes.Wave + ACViewer's
/// Wave.cs:32-72):
///
/// int32 headerSize
/// int32 dataSize
/// byte[headerSize] Header // raw WAVEFORMATEX — NO RIFF wrapper
/// byte[dataSize] Data // raw sample bytes
///
///
///
///
/// Header byte 0 (low byte of WAVEFORMATEX.wFormatTag):
///
/// - 0x01 linear PCM — feeds straight into OpenAL.
/// - 0x55 MPEG Layer 3 — retail decoded via winmm ACM.
/// - 0x02 Microsoft ADPCM — retail decoded via winmm ACM.
///
///
///
///
/// Our decoder currently handles the PCM case (the vast majority of AC's
/// ~3500 waves). MP3 / ADPCM decoding is best handled by a managed
/// decoder added later (NAudio / MediaToolkit / custom). Unsupported
/// formats return null and the caller logs + skips.
///
///
public static class WaveDecoder
{
///
/// Format tag from the first 2 bytes of the WAVEFORMATEX header.
/// Retail and ACE map identically; we use these exact values for logs.
///
public enum WaveFormatTag : ushort
{
Unknown = 0x0000,
Pcm = 0x0001, // Microsoft PCM
Adpcm = 0x0002, // Microsoft ADPCM (rare in AC)
Mp3 = 0x0055, // MPEGLAYER3 (common for music-ish cues)
}
///
/// Parse a Wave dat record into a PCM
/// container. Returns null when the source format is compressed
/// (MP3 / ADPCM) and we don't yet have a decoder wired up, OR when the
/// header is malformed.
///
/// Raw WAVEFORMATEX bytes (at least 14 bytes).
/// Raw payload bytes following the header.
public static WaveData? Decode(byte[] header, byte[] data)
{
ArgumentNullException.ThrowIfNull(header);
ArgumentNullException.ThrowIfNull(data);
if (header.Length < 14) return null;
// WAVEFORMATEX fields (little-endian):
// wFormatTag u16 @ 0
// nChannels u16 @ 2
// nSamplesPerSec u32 @ 4
// nAvgBytesPerSec u32 @ 8
// nBlockAlign u16 @12
// wBitsPerSample u16 @14 (present if headerSize >= 16)
// cbSize u16 @16 (optional extra-data length)
var h = header.AsSpan();
ushort fmtTag = BinaryPrimitives.ReadUInt16LittleEndian(h);
ushort channels = BinaryPrimitives.ReadUInt16LittleEndian(h.Slice(2));
uint sampleRate = BinaryPrimitives.ReadUInt32LittleEndian(h.Slice(4));
ushort bitsPer = header.Length >= 16
? BinaryPrimitives.ReadUInt16LittleEndian(h.Slice(14))
: (ushort)16;
if (channels == 0 || sampleRate == 0)
return null;
// Only PCM is natively supported here. Compressed paths require a
// secondary decoder (MP3/ADPCM); caller logs + skips when null.
if ((WaveFormatTag)fmtTag != WaveFormatTag.Pcm)
return null;
// For PCM the Data array IS the sample buffer (no framing needed).
double duration = bitsPer > 0 && channels > 0
? data.Length * 8.0 / (double)(sampleRate * channels * bitsPer)
: 0.0;
return new WaveData
{
ChannelCount = channels,
SampleRate = (int)sampleRate,
BitsPerSample = bitsPer == 0 ? 16 : bitsPer,
PcmBytes = data,
Duration = TimeSpan.FromSeconds(duration),
};
}
///
/// Peek only the format tag byte. Useful for logging which formats
/// failed to decode without allocating a .
///
public static WaveFormatTag PeekFormat(byte[] header)
{
if (header is null || header.Length < 2) return WaveFormatTag.Unknown;
ushort fmtTag = BinaryPrimitives.ReadUInt16LittleEndian(header);
return (WaveFormatTag)fmtTag;
}
}