using Amphora; using System; using System.IO; using System.Linq; using System.Text; using UnitTestSharp; using static Susurrus.UnitTests.TestSounds; namespace Susurrus.UnitTests { public class WavDecoderTests : TestFixture { public static readonly byte[] StandardSubformatSuffix = { 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71, }; public static byte[] Join(params byte[][] parts) { return parts.SelectMany(part => part).ToArray(); } public static byte[] Ascii(string text) { return Encoding.ASCII.GetBytes(text); } public static byte[] LittleEndian(long value, int byteCount) { byte[] returnMe = new byte[byteCount]; for (int i = 0; i < byteCount; ++i) { returnMe[i] = (byte)(value >> (8 * i)); } return returnMe; } public static byte[] ChunkClaiming(string id, uint claimedSize, byte[] body) { return Join(Ascii(id), LittleEndian(claimedSize, 4), body); } public static byte[] Chunk(string id, byte[] body) { byte[] padding = new byte[body.Length % 2]; return ChunkClaiming(id, (uint)body.Length, Join(body, padding)); } public static byte[] Riff(params byte[][] chunks) { byte[] body = Join(chunks); return Join(Ascii("RIFF"), LittleEndian(body.Length + 4, 4), Ascii("WAVE"), body); } public static byte[] FormatBody(ushort formatTag, ushort channelCount, uint sampleRate, ushort bitsPerSample, int blockAlign = -1) { if (blockAlign < 0) { blockAlign = channelCount * bitsPerSample / 8; } return Join(LittleEndian(formatTag, 2), LittleEndian(channelCount, 2), LittleEndian(sampleRate, 4), LittleEndian(sampleRate * (uint)blockAlign, 4), LittleEndian(blockAlign, 2), LittleEndian(bitsPerSample, 2)); } public static byte[] ExtensibleFormatBody(ushort subformatTag, ushort channelCount, uint sampleRate, ushort bitsPerSample, ushort validBitsPerSample, byte[] subformatSuffix) { int blockAlign = channelCount * bitsPerSample / 8; return Join(LittleEndian(0xFFFE, 2), LittleEndian(channelCount, 2), LittleEndian(sampleRate, 4), LittleEndian(sampleRate * (uint)blockAlign, 4), LittleEndian(blockAlign, 2), LittleEndian(bitsPerSample, 2), LittleEndian(22, 2), LittleEndian(validBitsPerSample, 2), LittleEndian(0, 4), LittleEndian(subformatTag, 2), subformatSuffix); } public static byte[] Pcm16MonoFormat() { return Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16)); } public static byte[] ThreePcm16MonoFrames() { return new byte[] { 0x01, 0x00, 0xFF, 0x7F, 0x00, 0x80 }; } public class DecodeBytesTests : TestFixture { public void Pcm16Mono_KeepsTheSampleBytesRateAndChannels() { DecodedSound sound = DecodeWav(Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); CheckEqual(44100, sound.SampleRate); CheckEqual(1, sound.ChannelCount); CheckEqual(SampleFormat.Pcm16, sound.Format); } public void Float32Stereo_KeepsTheSampleBytesRateAndChannels() { byte[] samples = { 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xBF }; DecodedSound sound = DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 3, channelCount: 2, sampleRate: 48000, bitsPerSample: 32)), Chunk("data", samples))).Value; CheckEqual(samples, BytesOf(sound.Samples)); CheckEqual(48000, sound.SampleRate); CheckEqual(2, sound.ChannelCount); CheckEqual(SampleFormat.Float32, sound.Format); } public void ExtensiblePcm16_IsReadAsPcm16() { DecodedSound sound = DecodeWav(Riff( Chunk("fmt ", ExtensibleFormatBody(subformatTag: 1, channelCount: 1, sampleRate: 22050, bitsPerSample: 16, validBitsPerSample: 16, subformatSuffix: StandardSubformatSuffix)), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(SampleFormat.Pcm16, sound.Format); CheckEqual(22050, sound.SampleRate); CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); } public void ExtensibleFloat32_IsReadAsFloat32() { DecodedSound sound = DecodeWav(Riff( Chunk("fmt ", ExtensibleFormatBody(subformatTag: 3, channelCount: 2, sampleRate: 48000, bitsPerSample: 32, validBitsPerSample: 32, subformatSuffix: StandardSubformatSuffix)), Chunk("data", new byte[16]))).Value; CheckEqual(SampleFormat.Float32, sound.Format); CheckEqual(2, sound.ChannelCount); CheckEqual(2, sound.FrameCount); } public void FormatChunkWithExtraBytes_IsAccepted() { byte[] body = Join(FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16), new byte[2]); DecodedSound sound = DecodeWav(Riff(Chunk("fmt ", body), Chunk("data", new byte[4]))).Value; CheckEqual(2, sound.FrameCount); } public void FormatChunkOfOddSize_IsSkippedWithItsPadByte() { byte[] body = Join(FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16), new byte[1]); DecodedSound sound = DecodeWav(Riff(Chunk("fmt ", body), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); } public void UnknownChunkBeforeFormat_IsSkipped() { DecodedSound sound = DecodeWav(Riff(Chunk("LIST", new byte[10]), Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); } public void OddSizedChunk_IsSkippedWithItsPadByte() { DecodedSound sound = DecodeWav(Riff(Pcm16MonoFormat(), Chunk("junk", new byte[3]), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); } public void ChunksAfterTheData_AreIgnored() { DecodedSound sound = DecodeWav(Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames()), Chunk("LIST", new byte[6]))).Value; CheckEqual(3, sound.FrameCount); } public void TheRiffSize_IsIgnored() { byte[] encoded = Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())); Array.Copy(LittleEndian(0, 4), 0, encoded, 4, 4); CheckEqual(3, DecodeWav(encoded).Value.FrameCount); } public void EmptyDataChunk_GivesNoFrames() { CheckEqual(0, DecodeWav(Riff(Pcm16MonoFormat(), Chunk("data", new byte[0]))).Value.FrameCount); } public void DefaultView_Throws() { CheckThrow(typeof(ArgumentException)); WavDecoder.Decode(default(ByteView)); } public void ViewOfALargerArray_DecodesOnlyTheWindow() { byte[] file = Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())); byte[] padded = new byte[file.Length + 40]; Array.Copy(file, 0, padded, 10, file.Length); DecodedSound sound = WavDecoder.Decode(new ByteView(padded, startIndex: 10, endIndex: 10 + file.Length)).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); } public void DataShorterThanTheRiffHeader_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Ascii("RIFF")))); } public void DataNotStartingWithRiff_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Ascii("OggS\0\0\0\0WAVE")))); } public void RiffThatIsNotWave_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Ascii("RIFF\0\0\0\0AVI ")))); } public void Rifx_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Ascii("RIFX\0\0\0\0WAVE")))); } public void Rifx_SaysWhatItIs() { CheckEqual("This WAV file can't be loaded. It is RIFX, and only plain RIFF is supported.", DecodeWav(Ascii("RIFX\0\0\0\0WAVE")).Status.Describe()); } public void Rf64_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Ascii("RF64\0\0\0\0WAVE")))); } public void NoChunks_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff()))); } public void NoDataChunk_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff(Pcm16MonoFormat())))); } public void BytesTooFewForAChunkHeaderAfterTheFormat_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Join(Riff(Pcm16MonoFormat()), new byte[7])))); } public void ChunkClaimingMoreThanTheFileHolds_IsMalformedAsHavingNoData() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav( Riff(Pcm16MonoFormat(), ChunkClaiming("LIST", 0xFFFFFFFF, new byte[4]))))); } public void DataBeforeFormat_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav( Riff(Chunk("data", ThreePcm16MonoFrames()), Pcm16MonoFormat())))); } public void DataChunkClaimingMoreThanTheFileHolds_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav( Riff(Pcm16MonoFormat(), ChunkClaiming("data", 8, ThreePcm16MonoFrames()))))); } public void DataChunkClaimingMoreThanTheFileHolds_SaysSo() { CheckEqual("This isn't a valid WAV file. The data chunk claims 8 bytes, but the file ends first.", DecodeWav( Riff(Pcm16MonoFormat(), ChunkClaiming("data", 8, ThreePcm16MonoFrames()))).Status.Describe()); } public void DataChunkClaimingMoreThanTwoGigabytes_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav( Riff(Pcm16MonoFormat(), ChunkClaiming("data", 0x80000000, new byte[0]))))); } public void DataEndingMidFrame_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff(Pcm16MonoFormat(), Chunk("data", new byte[5]))))); } public void FormatChunkClaimingMoreThanTheFileHolds_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff(ChunkClaiming("fmt ", 16, new byte[10]))))); } public void FormatChunkLargerThanTheCap_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff(ChunkClaiming("fmt ", 5000, new byte[5000]))))); } public void FormatChunkSmallerThanSixteenBytes_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff(Chunk("fmt ", new byte[14]), Chunk("data", new byte[4]))))); } public void ExtensibleFormatChunkSmallerThanFortyBytes_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 0xFFFE, channelCount: 1, sampleRate: 44100, bitsPerSample: 16)), Chunk("data", new byte[4]))))); } public void ZeroChannels_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 0, sampleRate: 44100, bitsPerSample: 16)), Chunk("data", new byte[4]))))); } public void ZeroSampleRate_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 0, bitsPerSample: 16)), Chunk("data", new byte[4]))))); } public void SampleRateAboveIntMax_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 0x80000000, bitsPerSample: 16)), Chunk("data", new byte[4]))))); } public void BlockAlignDisagreeingWithChannelsAndBits_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 2, sampleRate: 44100, bitsPerSample: 16, blockAlign: 2)), Chunk("data", new byte[4]))))); } public void Pcm8_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 8)), Chunk("data", new byte[4]))))); } public void Pcm24_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 24)), Chunk("data", new byte[6]))))); } public void Pcm32_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 32)), Chunk("data", new byte[4]))))); } public void Float64_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 3, channelCount: 1, sampleRate: 44100, bitsPerSample: 64)), Chunk("data", new byte[8]))))); } public void MsAdpcm_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", FormatBody(formatTag: 2, channelCount: 1, sampleRate: 44100, bitsPerSample: 4, blockAlign: 1)), Chunk("data", new byte[4]))))); } public void ExtensibleWithANonStandardSubformat_IsUnsupported() { byte[] suffix = (byte[])StandardSubformatSuffix.Clone(); suffix[13] = 0x70; CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", ExtensibleFormatBody(subformatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16, validBitsPerSample: 16, subformatSuffix: suffix)), Chunk("data", new byte[4]))))); } public void ExtensibleWithFewerValidBitsThanTheSampleHolds_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, KindOf(DecodeWav(Riff( Chunk("fmt ", ExtensibleFormatBody(subformatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16, validBitsPerSample: 12, subformatSuffix: StandardSubformatSuffix)), Chunk("data", new byte[4]))))); } } public class DecodeStreamTests : TestFixture { public void NullStream_Throws() { CheckThrow(typeof(ArgumentNullException)); WavDecoder.Decode((Stream)null); } public void SeekableStreamStartingMidWay_DecodesFromItsPosition() { byte[] file = Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())); MemoryStream stream = new MemoryStream(Join(new byte[5], file)); stream.Position = 5; CheckEqual(ThreePcm16MonoFrames(), BytesOf(WavDecoder.Decode(stream).Value.Samples)); } public void ForwardOnlyStreamGivingOneByteAtATime_DecodesTheSameSound() { byte[] file = Riff(Chunk("LIST", new byte[5]), Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())); DecodedSound sound = WavDecoder.Decode(new TrickleStream(file, bytesPerRead: 1)).Value; CheckEqual(ThreePcm16MonoFrames(), BytesOf(sound.Samples)); CheckEqual(44100, sound.SampleRate); } public void ForwardOnlyStreamWithAnOddSizedFormatChunk_SkipsItsPadByte() { byte[] body = Join(FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16), new byte[1]); byte[] file = Riff(Chunk("fmt ", body), Chunk("data", ThreePcm16MonoFrames())); CheckEqual(ThreePcm16MonoFrames(), BytesOf(WavDecoder.Decode(new TrickleStream(file, 3)).Value.Samples)); } public void ForwardOnlyStreamHoldingMoreThanTheFirstGrowthStep_KeepsEverySampleByte() { byte[] samples = new byte[70000]; for (int i = 0; i < samples.Length; ++i) { samples[i] = (byte)(i * 7); } byte[] file = Riff(Pcm16MonoFormat(), Chunk("data", samples)); DecodedSound sound = WavDecoder.Decode(new TrickleStream(file, bytesPerRead: 4096)).Value; CheckEqual(samples.Length, sound.Samples.Length); CheckTrue(samples.SequenceEqual(BytesOf(sound.Samples))); } public void ForwardOnlyStreamEndingInsideTheData_IsMalformed() { byte[] file = Riff(Pcm16MonoFormat(), ChunkClaiming("data", 100000, new byte[70000])); CheckEqual(SoundFailures.WavMalformed, KindOf(WavDecoder.Decode(new TrickleStream(file, 4096)))); } public void ForwardOnlyStreamEndingInsideTheFormat_IsMalformed() { byte[] file = Riff(ChunkClaiming("fmt ", 16, new byte[10])); CheckEqual(SoundFailures.WavMalformed, KindOf(WavDecoder.Decode(new TrickleStream(file, 1)))); } public void ForwardOnlyStreamWithNoDataChunk_IsMalformed() { byte[] file = Riff(Pcm16MonoFormat(), Chunk("LIST", new byte[9])); CheckEqual(SoundFailures.WavMalformed, KindOf(WavDecoder.Decode(new TrickleStream(file, 2)))); } } } }