using Amphora; using System; using System.IO; using UnitTestSharp; using static Susurrus.UnitTests.TestSounds; using static Susurrus.UnitTests.WavDecoderTests; namespace Susurrus.UnitTests { public class WavStreamTests : TestFixture { public static byte[] ThreeFrameFile() { return Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())); } // A chunk size of zero is the whole file in one chunk. public static StatusOr Open(byte[] file, int chunkSize = 0) { if (chunkSize == 0) { return WavStream.Open(new ByteChunks(new ByteView(file))); } return WavStream.Open(new StreamChunks(new MemoryStream(file), chunkSize)); } public class OpenTests : TestFixture { public void Pcm16Mono_KeepsTheRateChannelsFormatAndLength() { WavStream stream = Open(ThreeFrameFile()).Value; CheckEqual(44100, stream.SampleRate); CheckEqual(1, stream.ChannelCount); CheckEqual(SampleFormat.Pcm16, stream.Format); CheckEqual(3L, stream.FrameCount); CheckEqual(0L, stream.Position); } public void Float32Stereo_KeepsTheRateChannelsFormatAndLength() { WavStream stream = Open(Riff( Chunk("fmt ", FormatBody(formatTag: 3, channelCount: 2, sampleRate: 48000, bitsPerSample: 32)), Chunk("data", new byte[16]))).Value; CheckEqual(48000, stream.SampleRate); CheckEqual(2, stream.ChannelCount); CheckEqual(SampleFormat.Float32, stream.Format); CheckEqual(2L, stream.FrameCount); } public void ExtensiblePcm16_IsStreamedAsPcm16() { WavStream stream = Open(Riff( Chunk("fmt ", ExtensibleFormatBody(subformatTag: 1, channelCount: 1, sampleRate: 22050, bitsPerSample: 16, validBitsPerSample: 16, subformatSuffix: StandardSubformatSuffix)), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(SampleFormat.Pcm16, stream.Format); CheckEqual(22050, stream.SampleRate); } public void FormatChunkOfOddSize_IsSkippedWithItsPadByte() { byte[] body = Join(FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 16), new byte[1]); WavStream stream = Open(Riff(Chunk("fmt ", body), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(3L, stream.FrameCount); } public void UnknownChunkBeforeFormat_IsSkipped() { WavStream stream = Open(Riff(Chunk("LIST", new byte[10]), Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(3L, stream.FrameCount); } public void OddSizedChunk_IsSkippedWithItsPadByte() { WavStream stream = Open(Riff(Pcm16MonoFormat(), Chunk("junk", new byte[3]), Chunk("data", ThreePcm16MonoFrames()))).Value; CheckEqual(3L, stream.FrameCount); } public void ChunksAfterTheData_AreIgnored() { WavStream stream = Open(Riff(Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames()), Chunk("LIST", new byte[6]))).Value; CheckEqual(3L, stream.FrameCount); } public void EmptyDataChunk_GivesNoFrames() { CheckEqual(0L, Open(Riff(Pcm16MonoFormat(), Chunk("data", new byte[0]))).Value.FrameCount); } public void ChunksOfOneByte_GiveTheSameStream() { WavStream stream = Open(Riff(Chunk("LIST", new byte[10]), Pcm16MonoFormat(), Chunk("data", ThreePcm16MonoFrames())), chunkSize: 1).Value; CheckEqual(44100, stream.SampleRate); CheckEqual(3L, stream.FrameCount); } public void NullChunks_Throws() { CheckThrow(typeof(ArgumentNullException)); WavStream.Open(null); } public void DataShorterThanTheRiffHeader_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Ascii("RIFF")).Status.Kind); } public void DataNotStartingWithRiff_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Ascii("OggS\0\0\0\0WAVE")).Status.Kind); } public void RiffThatIsNotWave_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Ascii("RIFF\0\0\0\0AVI ")).Status.Kind); } public void Rifx_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, Open(Ascii("RIFX\0\0\0\0WAVE")).Status.Kind); } public void NoChunks_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff()).Status.Kind); } public void NoDataChunk_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(Pcm16MonoFormat())).Status.Kind); } public void BytesTooFewForAChunkHeaderAfterTheFormat_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Join(Riff(Pcm16MonoFormat()), new byte[7])).Status.Kind); } public void ChunkClaimingMoreThanTheFileHolds_IsMalformedAsHavingNoData() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(Pcm16MonoFormat(), ChunkClaiming("LIST", 0xFFFFFFFF, new byte[4]))).Status.Kind); } public void DataBeforeFormat_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(Chunk("data", ThreePcm16MonoFrames()), Pcm16MonoFormat())).Status.Kind); } public void DataChunkClaimingMoreThanTheFileHolds_SaysSo() { CheckEqual("This isn't a valid WAV file. The data chunk claims 8 bytes, but the file ends first.", Open(Riff(Pcm16MonoFormat(), ChunkClaiming("data", 8, ThreePcm16MonoFrames()))).Status.Describe()); } public void DataEndingMidFrame_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(Pcm16MonoFormat(), Chunk("data", new byte[5]))).Status.Kind); } public void FormatChunkClaimingMoreThanTheFileHolds_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(ChunkClaiming("fmt ", 16, new byte[4]))).Status.Kind); } public void FormatChunkLargerThanTheCap_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff(ChunkClaiming("fmt ", 5000, new byte[4]))).Status.Kind); } public void ZeroChannels_IsMalformed() { CheckEqual(SoundFailures.WavMalformed, Open(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 0, sampleRate: 44100, bitsPerSample: 16, blockAlign: 0)), Chunk("data", new byte[0]))).Status.Kind); } public void Pcm24_IsUnsupported() { CheckEqual(SoundFailures.WavUnsupported, Open(Riff( Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: 44100, bitsPerSample: 24)), Chunk("data", new byte[6]))).Status.Kind); } } public class ReadTests : TestFixture { public void FillsTheDestinationWithTheFramesInOrder() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[6]; StatusOr read = stream.Read(new ByteView(destination)); CheckEqual(3, read.Value); CheckEqual(ThreePcm16MonoFrames(), destination); CheckEqual(3L, stream.Position); } public void ADestinationHoldingFewerFrames_TakesThemInTurns() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[4]; CheckEqual(2, stream.Read(new ByteView(destination)).Value); CheckEqual(new byte[] { 0x01, 0x00, 0xFF, 0x7F }, destination); CheckEqual(1, stream.Read(new ByteView(destination)).Value); CheckEqual(new byte[] { 0x00, 0x80, 0xFF, 0x7F }, destination); } public void ADestinationWithRoomToSpare_TakesOnlyWhatIsLeft() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[10]; CheckEqual(3, stream.Read(new ByteView(destination)).Value); CheckEqual(new byte[] { 0x01, 0x00, 0xFF, 0x7F, 0x00, 0x80, 0, 0, 0, 0 }, destination); } public void AtTheEnd_ReturnsZero() { WavStream stream = Open(ThreeFrameFile()).Value; stream.Read(new ByteView(new byte[6])); CheckEqual(0, stream.Read(new ByteView(new byte[6])).Value); } public void ADestinationOddInSize_HoldsOnlyWholeFrames() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[5]; CheckEqual(2, stream.Read(new ByteView(destination)).Value); CheckEqual((byte)0, destination[4]); } public void AViewInTheMiddleOfAnArray_IsTheOnlyPartWritten() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[8]; stream.Read(new ByteView(destination, startIndex: 1, endIndex: 5)); CheckEqual(new byte[] { 0, 0x01, 0x00, 0xFF, 0x7F, 0, 0, 0 }, destination); } public void StereoFloatFrames_AreEightBytesEach() { byte[] samples = { 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xBF, 1, 2, 3, 4, 5, 6, 7, 8 }; WavStream stream = Open(Riff( Chunk("fmt ", FormatBody(formatTag: 3, channelCount: 2, sampleRate: 48000, bitsPerSample: 32)), Chunk("data", samples))).Value; byte[] destination = new byte[16]; CheckEqual(2, stream.Read(new ByteView(destination)).Value); CheckEqual(samples, destination); } public void FramesAcrossTinyChunks_ComeOutTheSame() { WavStream stream = Open(ThreeFrameFile(), chunkSize: 1).Value; byte[] destination = new byte[6]; CheckEqual(3, stream.Read(new ByteView(destination)).Value); CheckEqual(ThreePcm16MonoFrames(), destination); } public void ADestinationSmallerThanAFrame_FailsAndLeavesThePosition() { WavStream stream = Open(ThreeFrameFile()).Value; CheckEqual(SoundFailures.StreamRequestInvalid, stream.Read(new ByteView(new byte[1])).Status.Kind); CheckEqual(0L, stream.Position); } public void AFileThatShrankAfterOpening_IsMalformed() { MemoryStream file = new MemoryStream(ThreeFrameFile()); WavStream stream = WavStream.Open(new StreamChunks(file, leaveOpen: true)).Value; file.SetLength(file.Length - 2); CheckEqual(SoundFailures.WavMalformed, stream.Read(new ByteView(new byte[6])).Status.Kind); } public void AFailedRead_LeavesThePositionWhereItWas() { MemoryStream file = new MemoryStream(ThreeFrameFile()); WavStream stream = WavStream.Open(new StreamChunks(file, leaveOpen: true)).Value; file.SetLength(file.Length - 2); stream.Read(new ByteView(new byte[6])); CheckEqual(0L, stream.Position); } } public class SeekTests : TestFixture { public void MovesThePositionSoTheNextReadStartsThere() { WavStream stream = Open(ThreeFrameFile()).Value; byte[] destination = new byte[2]; Status status = stream.Seek(2); stream.Read(new ByteView(destination)); CheckTrue(status.IsOk); CheckEqual(new byte[] { 0x00, 0x80 }, destination); } public void BackToTheStartAfterReading_ReadsFromTheStartAgain() { WavStream stream = Open(ThreeFrameFile()).Value; stream.Read(new ByteView(new byte[6])); byte[] destination = new byte[2]; stream.Seek(0); stream.Read(new ByteView(destination)); CheckEqual(new byte[] { 0x01, 0x00 }, destination); } public void ToTheFrameCount_SitsAtTheEnd() { WavStream stream = Open(ThreeFrameFile()).Value; stream.Seek(3); CheckEqual(3L, stream.Position); CheckEqual(0, stream.Read(new ByteView(new byte[6])).Value); } public void PastTheFrameCount_FailsAndLeavesThePosition() { WavStream stream = Open(ThreeFrameFile()).Value; stream.Seek(1); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(4).Kind); CheckEqual(1L, stream.Position); } public void ANegativeFrame_FailsAndLeavesThePosition() { WavStream stream = Open(ThreeFrameFile()).Value; stream.Seek(1); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(-1).Kind); CheckEqual(1L, stream.Position); } } public class DisposeTests : TestFixture { public void LeavesTheChunksOpen() { MemoryStream file = new MemoryStream(ThreeFrameFile()); WavStream stream = WavStream.Open(new StreamChunks(file, leaveOpen: true)).Value; stream.Dispose(); CheckTrue(file.CanRead); } } } }