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