using System.Text; using DeepDrftContent.Processors; namespace DeepDrftTests; /// /// Tests for WAV format handling — standard PCM, /// EXTENSIBLE-PCM, EXTENSIBLE IEEE float, padded 24-in-32, and the default-fallback paths. /// [TestFixture] public class AudioProcessorTests { private const ushort WaveFormatPcm = 0x0001; private const ushort WaveFormatExtensible = 0xFFFE; private string _testDir = string.Empty; [SetUp] public void SetUp() { _testDir = Path.Combine(Path.GetTempPath(), "AudioProcessorTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_testDir); } [TearDown] public void TearDown() { try { Directory.Delete(_testDir, recursive: true); } catch { /* Best-effort cleanup — ignore failures */ } } [Test] public async Task StandardPcm_RoundTripsUnchanged() { var path = await WriteWavAsync(BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatPcm)); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(audio!.Duration, Is.GreaterThan(0.0)); Assert.That(audio.Bitrate, Is.GreaterThan(0)); } [Test] public async Task ExtensiblePcm_NormalizesToStandardHeader() { var subFormat = SubFormatGuid(WaveFormatPcm); var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatExtensible, subFormatGuid: subFormat, validBitsPerSample: 16); var path = await WriteWavAsync(wav); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(audio!.Duration, Is.GreaterThan(0.0)); Assert.That(audio.Bitrate, Is.GreaterThan(0)); Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm), "Stored buffer must be standard PCM"); } [Test] public async Task ExtensibleIeeeFloat_AcceptedAndConverted() { // Two stereo frames of 32-bit float samples (range [-1.0, 1.0]). var samples = FloatBytes(0.5f, -0.5f, 1.0f, -1.0f); var subFormat = SubFormatGuid(0x0003); // WAVE_FORMAT_IEEE_FLOAT var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 32, audioFormat: WaveFormatExtensible, sampleData: samples, subFormatGuid: subFormat, validBitsPerSample: 32); var path = await WriteWavAsync(wav); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(ReadFmtBitsPerSample(audio!.Buffer), Is.EqualTo(16 + 8), "Float must convert to 24-bit PCM"); Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm)); // 4 float samples (4 bytes each) → 4 PCM samples (3 bytes each) = 12 data bytes after the 44-byte header. Assert.That(audio.Buffer.Length, Is.EqualTo(44 + 12)); // Verify the converted sample values: (int)(sample * 8388607.0), clamped, little-endian 3 bytes. // 0.5f → 4194303 = 0x3FFFFF → FF FF 3F // -0.5f → -4194303 = 0xFFC00001 → 24-bit LE: 01 00 C0 // 1.0f → 8388607 = 0x7FFFFF → FF FF 7F // -1.0f → -8388607 = 0xFF800001 → 24-bit LE: 01 00 80 var expectedData = new byte[] { 0xFF, 0xFF, 0x3F, 0x01, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x01, 0x00, 0x80 }; var actualData = audio.Buffer[44..]; Assert.That(actualData, Is.EqualTo(expectedData), "Float samples must be converted to 24-bit PCM correctly"); } [Test] public async Task ExtensiblePadded24in32_AcceptedAndRepacked() { // Two stereo frames; each sample is a 24-bit value stored in a 32-bit little-endian container. var samples = Padded24In32Bytes(0x123456, unchecked((int)0xFFEDCBA9), 0x000001, unchecked((int)0xFF800000)); var subFormat = SubFormatGuid(WaveFormatPcm); var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 32, audioFormat: WaveFormatExtensible, sampleData: samples, subFormatGuid: subFormat, validBitsPerSample: 24); var path = await WriteWavAsync(wav); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(ReadFmtBitsPerSample(audio!.Buffer), Is.EqualTo(24), "Padded container must repack to 24-bit"); Assert.That(ReadFmtAudioFormat(audio.Buffer), Is.EqualTo(WaveFormatPcm)); // 4 container samples (4 bytes each) → 4 PCM samples (3 bytes each) = 12 data bytes. Assert.That(audio.Buffer.Length, Is.EqualTo(44 + 12)); // Verify the repacked sample values: lowest 3 bytes of each 4-byte little-endian container. // 0x123456 → LE 4 bytes: 56 34 12 00 → keep 3: 56 34 12 // 0xFFEDCBA9 → LE 4 bytes: A9 CB ED FF → keep 3: A9 CB ED // 0x000001 → LE 4 bytes: 01 00 00 00 → keep 3: 01 00 00 // 0xFF800000 → LE 4 bytes: 00 00 80 FF → keep 3: 00 00 80 var expectedData = new byte[] { 0x56, 0x34, 0x12, 0xA9, 0xCB, 0xED, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80 }; var actualData = audio.Buffer[44..]; Assert.That(actualData, Is.EqualTo(expectedData), "Padded 24-in-32 samples must strip the padding byte correctly"); } [Test] public async Task ExtensibleUnsupportedSubFormat_FallsBackToDefaults() { var subFormat = SubFormatGuid(0x0005); // WAVE_FORMAT_DOLBY_AC3 — neither PCM nor float var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatExtensible, subFormatGuid: subFormat, validBitsPerSample: 16); var path = await WriteWavAsync(wav); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(audio!.Duration, Is.EqualTo(180.0), "Unsupported SubFormat must fall back to default metadata"); } [Test] public async Task ExtensibleFmtTooSmall_FallsBackToDefaults() { // audioFormat=EXTENSIBLE but fmt chunk declares 16 bytes — too small for the extension. var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 16, audioFormat: WaveFormatExtensible, forceFmtChunkSize: 16); var path = await WriteWavAsync(wav); var audio = await new AudioProcessor().ProcessWavFileAsync(path); Assert.That(audio, Is.Not.Null); Assert.That(audio!.Duration, Is.EqualTo(180.0), "Undersized EXTENSIBLE fmt chunk must fall back to default metadata"); } [Test] public void TryExtractPcm_FloatWav_ReturnsNull() { var subFormat = SubFormatGuid(0x0003); var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 32, audioFormat: WaveFormatExtensible, subFormatGuid: subFormat, validBitsPerSample: 32); var result = new AudioProcessor().TryExtractPcm(wav); Assert.That(result, Is.Null); } [Test] public void TryExtractPcm_Padded24in32_ReturnsNull() { var subFormat = SubFormatGuid(WaveFormatPcm); var wav = BuildMinimalWav(channels: 2, sampleRate: 44100, bitsPerSample: 32, audioFormat: WaveFormatExtensible, subFormatGuid: subFormat, validBitsPerSample: 24); var result = new AudioProcessor().TryExtractPcm(wav); Assert.That(result, Is.Null); } // -- helpers -------------------------------------------------------------------------------- /// /// Synthesises a minimal valid WAV buffer. For EXTENSIBLE (audioFormat=0xFFFE) the fmt chunk is /// 40 bytes and includes cbSize, wValidBitsPerSample, channel mask, and the SubFormat GUID. For /// standard PCM (audioFormat=1) the fmt chunk is 16 bytes. When is /// null a small block of silence sized to the block alignment is used. /// private static byte[] BuildMinimalWav( int channels, int sampleRate, int bitsPerSample, ushort audioFormat, byte[]? sampleData = null, byte[]? subFormatGuid = null, ushort validBitsPerSample = 0, uint? forceFmtChunkSize = null) { var isExtensible = audioFormat == WaveFormatExtensible; var fmtChunkSize = forceFmtChunkSize ?? (isExtensible ? 40u : 16u); var blockAlign = (ushort)(channels * (bitsPerSample / 8)); var byteRate = (uint)(sampleRate * blockAlign); var data = sampleData ?? new byte[blockAlign * 2]; using var ms = new MemoryStream(); using var w = new BinaryWriter(ms, Encoding.ASCII, leaveOpen: true); w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write((uint)(36 + fmtChunkSize - 16 + data.Length)); // riff size adjusted for fmt extension w.Write(Encoding.ASCII.GetBytes("WAVE")); w.Write(Encoding.ASCII.GetBytes("fmt ")); w.Write(fmtChunkSize); w.Write(audioFormat); w.Write((ushort)channels); w.Write((uint)sampleRate); w.Write(byteRate); w.Write(blockAlign); w.Write((ushort)bitsPerSample); // Only emit the EXTENSIBLE extension when the declared fmt size actually allows for it. A // forced-small size (fmt=16) leaves audioFormat=EXTENSIBLE but no extension, exercising the // "fmt too small" fallback. if (fmtChunkSize >= 40) { w.Write((ushort)22); // cbSize w.Write(validBitsPerSample); w.Write((uint)0); // channel mask w.Write(subFormatGuid ?? SubFormatGuid(WaveFormatPcm)); } w.Write(Encoding.ASCII.GetBytes("data")); w.Write((uint)data.Length); w.Write(data); w.Flush(); return ms.ToArray(); } /// Builds a 16-byte SubFormat GUID whose leading 2 bytes are the format tag. private static byte[] SubFormatGuid(ushort formatTag) { var guid = new byte[16]; guid[0] = (byte)(formatTag & 0xFF); guid[1] = (byte)((formatTag >> 8) & 0xFF); // Remaining 14 bytes are the fixed KSDATAFORMAT suffix; their value is irrelevant to parsing. return guid; } private static byte[] FloatBytes(params float[] samples) { var bytes = new byte[samples.Length * 4]; for (int i = 0; i < samples.Length; i++) { BitConverter.GetBytes(samples[i]).CopyTo(bytes, i * 4); } return bytes; } /// Packs each 24-bit sample value into a 32-bit little-endian container. private static byte[] Padded24In32Bytes(params int[] samples) { var bytes = new byte[samples.Length * 4]; for (int i = 0; i < samples.Length; i++) { BitConverter.GetBytes(samples[i]).CopyTo(bytes, i * 4); } return bytes; } private async Task WriteWavAsync(byte[] wav) { var path = Path.Combine(_testDir, Guid.NewGuid() + ".wav"); await File.WriteAllBytesAsync(path, wav); return path; } private static ushort ReadFmtAudioFormat(byte[] standardPcmWav) => BitConverter.ToUInt16(standardPcmWav, 20); private static ushort ReadFmtBitsPerSample(byte[] standardPcmWav) => BitConverter.ToUInt16(standardPcmWav, 34); }