using Amphora; using System; using System.IO; using System.Linq; using UnitTestSharp; using static StbVorbisSharp.StbVorbis; using static Susurrus.UnitTests.OggDecoderTests; using static Susurrus.UnitTests.TestSounds; namespace Susurrus.UnitTests { public class OggStreamTests : TestFixture { public const long ClicksFrameCount = 264600; // Chunks whose bytes can be zeroed after a stream has opened over them, as a file that went bad would be. public class DamageableChunks : IChunkedBytes { private readonly byte[] original; private byte[] current; public DamageableChunks(byte[] file) { original = file; current = file; } public long Length { get { return original.Length; } } public void ZeroFrom(int start) { current = (byte[])original.Clone(); Array.Clear(current, start, current.Length - start); } public void Repair() { current = original; } public ByteView GetChunk(long start) { return new ByteView(current, (int)start, current.Length); } } public static StatusOr Open(byte[] file, int chunkSize = 0) { if (chunkSize == 0) { return OggStream.Open(new ByteChunks(new ByteView(file))); } return OggStream.Open(new StreamChunks(new MemoryStream(file), chunkSize)); } public static OggStream OpenOrFail(byte[] file, int chunkSize = 0) { return Open(file, chunkSize).Value; } public static STBVorbisError ErrorOf(Status status) { if (status.Kind != SoundFailures.OggFailed) { return (STBVorbisError)(-1); } return SoundFailures.OggFailed.Read(in status); } public static STBVorbisError ErrorOf(StatusOr result) { return ErrorOf(result.Status); } public static byte[] ReadFrames(OggStream stream, int frames) { int bytesPerFrame = stream.ChannelCount * sizeof(short); byte[] returnMe = new byte[frames * bytesPerFrame]; int filled = 0; while (filled < returnMe.Length) { StatusOr read = stream.Read(new ByteView(returnMe, filled, returnMe.Length)); if (!read.IsOk || read.Value == 0) { break; } filled += read.Value * bytesPerFrame; } Array.Resize(ref returnMe, filled); return returnMe; } public static byte[] ExpectedFrames(byte[] file, long firstFrame, int frames) { DecodedSound sound = DecodeOgg(file).Value; int bytesPerFrame = sound.ChannelCount * sizeof(short); return BytesOf(sound.Samples[(int)firstFrame * bytesPerFrame, (int)(firstFrame + frames) * bytesPerFrame]); } public static int HeaderEnd(byte[] file) { int at = 0; while (OggPages.TryRead(file, at, file.Length, out OggPage page) && page.Granule == 0) { at += page.Size; } return at; } public static byte[] WithPageGranule(byte[] file, int pageNumber, long granule) { byte[] returnMe = (byte[])file.Clone(); int at = 0; for (int i = 0; i < pageNumber; ++i) { OggPages.TryRead(returnMe, at, returnMe.Length, out OggPage page); at += page.Size; } Array.Copy(BitConverter.GetBytes(granule), 0, returnMe, at + 6, 8); FixPageChecksum(returnMe, at); return returnMe; } public class ToPcm16Tests : TestFixture { public void Silence_IsZero() { CheckEqual((short)0, OggStream.ToPcm16(0f)); } public void AboveFullScale_ClipsToTheLargestSample() { CheckEqual(short.MaxValue, OggStream.ToPcm16(1.5f)); } public void BelowNegativeFullScale_ClipsToTheSmallestSample() { CheckEqual(short.MinValue, OggStream.ToPcm16(-1.5f)); } public void NegativeFullScale_IsTheSmallestSample() { CheckEqual(short.MinValue, OggStream.ToPcm16(-1f)); } public void APositiveFraction_TruncatesTowardZero() { CheckEqual((short)1, OggStream.ToPcm16(1.9f / 32768f)); } public void ANegativeFraction_TruncatesTowardZero() { CheckEqual((short)-1, OggStream.ToPcm16(-1.9f / 32768f)); } } public class OpenTests : TestFixture { public void AHeaderLargerThanTheFirstAttemptReads_StillOpens() { byte[] file = LoadFixture("BigHeader.ogg"); OggStream stream = OpenOrFail(file); CheckEqual(2205L, stream.FrameCount); CheckEqual(ExpectedFrames(file, 0, 2205), ReadFrames(stream, 2205)); } public void AHeaderLargerThanTheLargestAttemptReads_FailsWithInvalidSetup() { byte[] mono = LoadFixture("Mono441Hz.ogg"); int pageSize = OggPages.HeaderSize + 255 + 255 * 255; byte[] file = new byte[58 + pageSize]; Array.Copy(mono, file, 58); // The decoder never checks checksums, so the page leaves it zero. int at = 58; Array.Copy(System.Text.Encoding.ASCII.GetBytes("OggS"), 0, file, at, 4); Array.Copy(BitConverter.GetBytes(-1L), 0, file, at + 6, 8); file[at + 26] = 255; for (int segment = 0; segment < 255; ++segment) { file[at + OggPages.HeaderSize + segment] = 255; } int body = 58 + OggPages.HeaderSize + 255; file[body] = 3; Array.Copy(System.Text.Encoding.ASCII.GetBytes("vorbis"), 0, file, body + 1, 6); Array.Copy(BitConverter.GetBytes(8000000), 0, file, body + 7, 4); StatusOr opened = OggStream.Open(new ByteChunks(new ByteView(file)), headerReadSizes: new[] { 256, 1024 }); CheckEqual(STBVorbisError.VORBIS_invalid_setup, ErrorOf(opened)); } public void AFirstAttemptSmallerThanTheHeader_LetsALaterOneOpen() { byte[] file = LoadFixture("Mono441Hz.ogg"); OggStream stream = OggStream.Open(new ByteChunks(new ByteView(file)), headerReadSizes: new[] { 64, 1 << 16 }).Value; CheckEqual(4410L, stream.FrameCount); } public void ANullListOfAttempts_Throws() { CheckThrow(typeof(ArgumentNullException)); OggStream.Open(new ByteChunks(new ByteView(LoadFixture("Mono441Hz.ogg"))), headerReadSizes: null); } public void AnAttemptOfNoBytes_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); OggStream.Open(new ByteChunks(new ByteView(LoadFixture("Mono441Hz.ogg"))), headerReadSizes: new[] { 64, 0 }); } public void NoAttempts_FailsWithInvalidSetup() { StatusOr opened = OggStream.Open(new ByteChunks(new ByteView(LoadFixture("Mono441Hz.ogg"))), headerReadSizes: new int[0]); CheckEqual(STBVorbisError.VORBIS_invalid_setup, ErrorOf(opened)); } public void Mono_KeepsTheRateChannelsFormatAndLength() { OggStream stream = OpenOrFail(LoadFixture("Mono441Hz.ogg")); CheckEqual(44100, stream.SampleRate); CheckEqual(1, stream.ChannelCount); CheckEqual(SampleFormat.Pcm16, stream.Format); CheckEqual(4410L, stream.FrameCount); CheckEqual(0L, stream.Position); } public void Stereo_KeepsTheChannelsAndLength() { OggStream stream = OpenOrFail(LoadFixture("Stereo441HzLeftOnly.ogg")); CheckEqual(2, stream.ChannelCount); CheckEqual(4410L, stream.FrameCount); } public void ManyPages_TakesTheLengthFromTheLastOne() { CheckEqual(ClicksFrameCount, OpenOrFail(LoadFixture("Clicks.ogg")).FrameCount); } public void ChunksOfSevenBytes_GiveTheSameStream() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg"), chunkSize: 7); CheckEqual(ClicksFrameCount, stream.FrameCount); CheckEqual(44100, stream.SampleRate); } public void NullChunks_Throws() { CheckThrow(typeof(ArgumentNullException)); OggStream.Open(null); } public void EmptyData_FailsWithUnexpectedEof() { CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(new byte[0]))); } public void DataThatIsNotOgg_FailsWithMissingCapturePattern() { byte[] riff = System.Text.Encoding.ASCII.GetBytes("RIFF\0\0\0\0"); CheckEqual(STBVorbisError.VORBIS_missing_capture_pattern, ErrorOf(Open(riff))); } public void FileCutInsideItsFirstPage_FailsWithUnexpectedEof() { CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(Truncated(LoadFixture("Mono441Hz.ogg"), 10)))); } public void FileCutInsideItsSetupHeaders_FailsWithUnexpectedEof() { CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(Truncated(LoadFixture("Mono441Hz.ogg"), 1000)))); } public void FileCutInsideItsAudioPage_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(Truncated(file, file.Length - 100)))); } public void FileCutAtTheStartOfItsLastPage_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Clicks.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(Truncated(file, LastPageStart(file))))); } public void AByteChangedInTheSetupHeader_FailsAsDamaged() { byte[] file = LoadFixture("Mono441Hz.ogg"); OggPages.TryRead(file, 0, file.Length, out OggPage identification); file[identification.Size + 40] ^= 0x01; CheckEqual(SoundFailures.OggPageDamaged, Open(file).Status.Kind); } public void AByteChangedInTheLastPage_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); file[LastPageStart(file) + 60] ^= 0x55; CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(file))); } public void LastPageWithoutTheEndOfStreamFlag_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); int lastPage = LastPageStart(file); file[lastPage + 5] = 0; FixPageChecksum(file, lastPage); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(file))); } public void BytesAfterTheLastPage_AreIgnored() { byte[] junk = Enumerable.Range(0, 100).Select(i => (byte)(i * 37 + 11)).ToArray(); CheckEqual(4410L, OpenOrFail(LoadFixture("Mono441Hz.ogg").Concat(junk).ToArray()).FrameCount); } public void MoreBytesAfterTheLastPageThanAPageCanHold_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg").Concat(new byte[70000]).ToArray(); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(Open(file))); } public void LastPageWithNoGranule_FailsWithCantFindLastPage() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: -1); CheckEqual(STBVorbisError.VORBIS_cant_find_last_page, ErrorOf(Open(file))); } public void LastPageClaimingNoFramesOverAFileWithAudio_FailsWithInvalidStream() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: 0); CheckEqual(STBVorbisError.VORBIS_invalid_stream, ErrorOf(Open(file))); } public void FileWithNoAudioAtAll_HasNoFrames() { byte[] file = LoadFixture("Mono441Hz.ogg"); int headerEnd = HeaderEnd(file); byte[] endPage = MakePage( granule: 0, flags: OggPages.EndOfStreamFlag, lacing: new byte[0], bodySize: 0); byte[] empty = Truncated(file, headerEnd).Concat(endPage).ToArray(); OggStream stream = OpenOrFail(empty); CheckEqual(0L, stream.FrameCount); CheckEqual(0, stream.Read(new ByteView(new byte[2])).Value); } public void SampleRateAboveWhatAnIntHolds_FailsWithInvalidSetup() { byte[] file = LoadFixture("Mono441Hz.ogg"); file[40] = 0; file[41] = 0; file[42] = 0; file[43] = 0x80; CheckEqual(STBVorbisError.VORBIS_invalid_setup, ErrorOf(Open(file))); } public void ADifferentCodecInOgg_FailsTheWayTheLoaderDoes() { byte[] file = LoadFixture("OpusInOgg.ogg"); CheckEqual(OggErrorOf(DecodeOgg(file)), ErrorOf(Open(file))); } } public class ReadTests : TestFixture { public void Mono_GivesTheSamplesTheLoaderDoes() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(ExpectedFrames(file, 0, 4410), ReadFrames(OpenOrFail(file), 4410)); } public void Stereo_GivesTheSamplesTheLoaderDoes() { byte[] file = LoadFixture("Stereo441HzLeftOnly.ogg"); CheckEqual(ExpectedFrames(file, 0, 4410), ReadFrames(OpenOrFail(file), 4410)); } public void ManyPagesWithShortAndLongBlocks_GiveTheSamplesTheLoaderDoes() { byte[] file = LoadFixture("Clicks.ogg"); CheckEqual(ExpectedFrames(file, 0, (int)ClicksFrameCount), ReadFrames(OpenOrFail(file), 264600)); } public void ChunksOfSevenBytes_GiveTheSameSamples() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(ExpectedFrames(file, 0, 4410), ReadFrames(OpenOrFail(file, chunkSize: 7), 4410)); } public void ChunksOfFourKilobytes_GiveTheSameSamples() { byte[] file = LoadFixture("Clicks.ogg"); CheckEqual(ExpectedFrames(file, 0, (int)ClicksFrameCount), ReadFrames(OpenOrFail(file, chunkSize: 4096), 264600)); } public void ADamagedMiddlePage_GivesTheFramesBeforeItThenFailsAsDamaged() { byte[] file = LoadFixture("Clicks.ogg"); int damagedPage = PageStartHolding(file, 6000); file[damagedPage + 40] ^= 0x01; OggStream stream = OpenOrFail(file); byte[] frames = ReadFrames(stream, (int)ClicksFrameCount); StatusOr next = stream.Read(new ByteView(new byte[8])); CheckTrue(frames.Length > 0 && frames.Length < 2 * ClicksFrameCount); CheckEqual(SoundFailures.OggPageDamaged, next.Status.Kind); CheckTrue(next.Status.Describe().Contains($"byte {damagedPage}")); } public void ADamagedMiddlePage_GivesNoSampleFromThatPage() { byte[] file = LoadFixture("Clicks.ogg"); byte[] clean = (byte[])file.Clone(); int damagedPage = PageStartHolding(file, 6000); file[damagedPage + 40] ^= 0x01; OggStream stream = OpenOrFail(file); byte[] frames = ReadFrames(stream, (int)ClicksFrameCount); CheckEqual(ExpectedFrames(clean, 0, frames.Length / 2), frames); } public void APacketTheDecoderRejectsOnAPageWithAGoodChecksum_FailsWithTheDecodersError() { byte[] file = LoadFixture("Clicks.ogg"); int pageStart = PageStartHolding(file, 4950); file[pageStart + OggPages.HeaderSize + file[pageStart + 26]] |= 1; FixPageChecksum(file, pageStart); OggStream stream = OpenOrFail(file); ReadFrames(stream, (int)ClicksFrameCount); CheckEqual(SoundFailures.OggFailed, stream.Read(new ByteView(new byte[8])).Status.Kind); } public void BytesAfterTheEndOfStreamPage_AreNeverChecked() { byte[] file = LoadFixture("Mono441Hz.ogg"); byte[] withJunk = file.Concat(new byte[] { 1, 2, 3, 4, 5 }).ToArray(); CheckEqual(ExpectedFrames(file, 0, 4410), ReadFrames(OpenOrFail(withJunk), 4410)); } public void OneFrameAtATime_GivesTheSameSamples() { byte[] file = LoadFixture("Clicks.ogg"); OggStream stream = OpenOrFail(file); byte[] frames = new byte[2 * 3000]; for (int frame = 0; frame < 3000; ++frame) { stream.Read(new ByteView(frames, frame * 2, frame * 2 + 2)); } CheckEqual(ExpectedFrames(file, 0, 3000), frames); } public void ADestinationHoldingMoreThanIsLeft_TakesOnlyWhatIsLeft() { OggStream stream = OpenOrFail(LoadFixture("Mono441Hz.ogg")); stream.Seek(4400); byte[] destination = new byte[40]; CheckEqual(10, stream.Read(new ByteView(destination)).Value); CheckEqual((byte)0, destination[20]); } public void AtTheEnd_ReturnsZero() { OggStream stream = OpenOrFail(LoadFixture("Mono441Hz.ogg")); ReadFrames(stream, 4410); CheckEqual(0, stream.Read(new ByteView(new byte[8])).Value); } public void ADestinationOddInSize_HoldsOnlyWholeFrames() { OggStream stream = OpenOrFail(LoadFixture("Stereo441HzLeftOnly.ogg")); byte[] destination = new byte[7]; CheckEqual(1, stream.Read(new ByteView(destination)).Value); CheckEqual((byte)0, destination[4]); } public void AViewInTheMiddleOfAnArray_IsTheOnlyPartWritten() { byte[] file = LoadFixture("Mono441Hz.ogg"); OggStream stream = OpenOrFail(file); byte[] destination = new byte[8]; stream.Read(new ByteView(destination, startIndex: 2, endIndex: 6)); CheckEqual(ExpectedFrames(file, 0, 2), BytesOf(new ByteView(destination, 2, 6))); CheckEqual(0, destination[0] + destination[1] + destination[6] + destination[7]); } public void ThePosition_FollowsWhatWasRead() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg")); ReadFrames(stream, 1234); CheckEqual(1234L, stream.Position); } public void ADestinationSmallerThanAFrame_FailsAndLeavesThePosition() { OggStream stream = OpenOrFail(LoadFixture("Stereo441HzLeftOnly.ogg")); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Read(new ByteView(new byte[3])).Status.Kind); CheckEqual(0L, stream.Position); } public void AfterDispose_Fails() { OggStream stream = OpenOrFail(LoadFixture("Mono441Hz.ogg")); stream.Dispose(); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Read(new ByteView(new byte[8])).Status.Kind); } public void AFileThatEndsBeforeItsLastPageSaysItDoes_GivesWhatIsThereThenFails() { byte[] file = WithLastPageGranule(LoadFixture("Clicks.ogg"), granule: ClicksFrameCount + 40000); OggStream stream = OpenOrFail(file); byte[] destination = new byte[2 * 300000]; int first = stream.Read(new ByteView(destination)).Value; Status second = stream.Read(new ByteView(destination)).Status; CheckTrue(first > 250000); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(second)); } public void AFailureWithNothingYetCopied_RepeatsOnTheNextRead() { byte[] file = WithLastPageGranule(LoadFixture("Clicks.ogg"), granule: ClicksFrameCount + 40000); OggStream stream = OpenOrFail(file); byte[] destination = new byte[2 * 300000]; stream.Read(new ByteView(destination)); stream.Read(new ByteView(destination)); Status third = stream.Read(new ByteView(destination)).Status; CheckEqual(STBVorbisError.VORBIS_unexpected_eof, ErrorOf(third)); } } public class SeekTests : TestFixture { [IgnoreTest] private void CheckReadsMatchFrom( long frame, int frames = 3000, string name = "Clicks.ogg", int chunkSize = 0) { byte[] file = LoadFixture(name); OggStream stream = OpenOrFail(file, chunkSize); Status status = stream.Seek(frame); CheckTrue(status.IsOk); CheckEqual(frame, stream.Position); CheckEqual(ExpectedFrames(file, frame, frames), ReadFrames(stream, frames)); } public void ToTheMiddle_ReadsTheSamplesThere() { CheckReadsMatchFrom(100000); } public void ToTheStart_ReadsFromTheStart() { CheckReadsMatchFrom(0); } public void ToTheStartAfterReadingToTheEnd_ReadsFromTheStartAgain() { byte[] file = LoadFixture("Clicks.ogg"); OggStream stream = OpenOrFail(file); ReadFrames(stream, 264600); stream.Seek(0); CheckEqual(ExpectedFrames(file, 0, 3000), ReadFrames(stream, 3000)); } public void ToAFrameCloserToTheStartThanALongBlock_ReadsTheSamplesThere() { CheckReadsMatchFrom(1000); } public void ToTheFirstFrameOfTheSecondPage_ReadsTheSamplesThere() { CheckReadsMatchFrom(9024); } public void ToAFrameBeforeAnyAudioPageEnds_ReadsTheSamplesThere() { CheckReadsMatchFrom(4000, frames: 410, name: "Mono441Hz.ogg"); } public void ToAFrameWhoseCalibrationPageEndsBeforeAShortBlock_ReadsTheSamplesThere() { CheckReadsMatchFrom(60000); } public void ToAFrameWhoseCalibrationPageIsFollowedByAnotherSuchPage_ReadsTheSamplesThere() { CheckReadsMatchFrom(190000); } public void ToAFrameBeforeTheLastPage_ReadsTheSamplesThere() { CheckReadsMatchFrom(262000, frames: 2600); } public void ToTheLastFrame_ReadsThatFrame() { CheckReadsMatchFrom(ClicksFrameCount - 1, frames: 1); } public void ToTheFrameCount_SitsAtTheEnd() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg")); CheckTrue(stream.Seek(ClicksFrameCount).IsOk); CheckEqual(ClicksFrameCount, stream.Position); CheckEqual(0, stream.Read(new ByteView(new byte[8])).Value); } public void ThroughChunksOfSevenBytes_ReadsTheSamplesThere() { CheckReadsMatchFrom(100000, chunkSize: 7); } public void ThroughChunksOfFourKilobytes_ReadsTheSamplesThere() { CheckReadsMatchFrom(190000, chunkSize: 4096); } public void BackwardsAfterReading_ReadsTheSamplesThere() { byte[] file = LoadFixture("Clicks.ogg"); OggStream stream = OpenOrFail(file); ReadFrames(stream, 200000); stream.Seek(50000); CheckEqual(ExpectedFrames(file, 50000, 3000), ReadFrames(stream, 3000)); } public void ToTheSameFrameTwice_ReadsTheSameSamples() { byte[] file = LoadFixture("Clicks.ogg"); OggStream stream = OpenOrFail(file); stream.Seek(120000); ReadFrames(stream, 500); stream.Seek(120000); CheckEqual(ExpectedFrames(file, 120000, 3000), ReadFrames(stream, 3000)); } public void ThenReadingToTheEnd_GivesTheRestOfTheSound() { byte[] file = LoadFixture("Clicks.ogg"); OggStream stream = OpenOrFail(file); stream.Seek(250000); CheckEqual(ExpectedFrames(file, 250000, 14600), ReadFrames(stream, 20000)); } public void PastTheFrameCount_FailsAndLeavesThePosition() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg")); stream.Seek(1000); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(ClicksFrameCount + 1).Kind); CheckEqual(1000L, stream.Position); } public void ANegativeFrame_FailsAndLeavesThePosition() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg")); stream.Seek(1000); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(-1).Kind); CheckEqual(1000L, stream.Position); } public void AfterDispose_Fails() { OggStream stream = OpenOrFail(LoadFixture("Clicks.ogg")); stream.Dispose(); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(1000).Kind); } public void WherePagesClaimAFrameTooFarAhead_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 6, granule: 45760 + 30000); OggStream stream = OpenOrFail(file); stream.Seek(58600); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 58600, 3000), ReadFrames(stream, 3000)); } public void WherePagesClaimAFrameBeforeTheStart_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 6, granule: 100); OggStream stream = OpenOrFail(file); stream.Seek(58600); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 58600, 3000), ReadFrames(stream, 3000)); } public void WherePagesHaveNoGranule_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 18, granule: -1); OggStream stream = OpenOrFail(file); stream.Seek(150000); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 150000, 3000), ReadFrames(stream, 3000)); } public void WhereTheCalibrationPageHasNoGranule_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 12, granule: -1); OggStream stream = OpenOrFail(file); stream.Seek(100000); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 100000, 3000), ReadFrames(stream, 3000)); } public void WherePagesClaimAFrameTooFarAheadAfterTheUntrustedOne_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 9, granule: 73280 + 30000); OggStream stream = OpenOrFail(file); stream.Seek(60000); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 60000, 3000), ReadFrames(stream, 3000)); } public void WherePagesClaimAFrameBeforeTheStartAfterTheUntrustedOne_StillReadsTheSamplesThere() { byte[] file = WithPageGranule(LoadFixture("Clicks.ogg"), pageNumber: 12, granule: 100); OggStream stream = OpenOrFail(file); stream.Seek(100000); CheckEqual(ExpectedFrames(LoadFixture("Clicks.ogg"), 100000, 3000), ReadFrames(stream, 3000)); } public void AcrossMoreThanAWindowOfPaddingBetweenPages_StillReadsTheSamplesThere() { byte[] clicks = LoadFixture("Clicks.ogg"); int padAt = 10442; byte[] file = new byte[clicks.Length + 400000]; Array.Copy(clicks, 0, file, 0, padAt); Array.Copy(clicks, padAt, file, padAt + 400000, clicks.Length - padAt); OggStream stream = OpenOrFail(file); stream.Seek(60000); CheckEqual(ExpectedFrames(clicks, 60000, 3000), ReadFrames(stream, 3000)); } public static int EndOfPage(byte[] file, int pageNumber) { int returnMe = 0; for (int i = 0; i <= pageNumber; ++i) { OggPages.TryRead(file, returnMe, file.Length, out OggPage page); returnMe += page.Size; } return returnMe; } public void WhenTheFileIsZeroedAfterTheCalibrationPage_FailsAsDamaged() { byte[] file = LoadFixture("Clicks.ogg"); DamageableChunks chunks = new DamageableChunks(file); OggStream stream = OggStream.Open(chunks).Value; chunks.ZeroFrom(EndOfPage(file, pageNumber: 9)); CheckEqual(SoundFailures.OggPageDamaged, stream.Seek(60000).Kind); } public void PastADamagedPage_SeeksAndReadsTheSamplesTheLoaderDoes() { byte[] file = LoadFixture("Clicks.ogg"); byte[] clean = (byte[])file.Clone(); file[PageStartHolding(file, 6000) + 40] ^= 0x01; OggStream stream = OpenOrFail(file); CheckTrue(stream.Seek(200000).IsOk); CheckEqual(ExpectedFrames(clean, 200000, 1000), ReadFrames(stream, 1000)); } public void BackBeforeADamagedPage_FailsWhenTheReadReachesIt() { byte[] file = LoadFixture("Clicks.ogg"); file[PageStartHolding(file, 6000) + 40] ^= 0x01; OggStream stream = OpenOrFail(file); stream.Seek(200000); CheckTrue(stream.Seek(0).IsOk); ReadFrames(stream, (int)ClicksFrameCount); CheckEqual(SoundFailures.OggPageDamaged, stream.Read(new ByteView(new byte[8])).Status.Kind); } public void ASegmentCountCorruptedInAPageTheSeekDecodes_FailsTheSeekAsDamaged() { byte[] file = LoadFixture("Clicks.ogg"); file[9577] ^= 0x55; OggStream stream = OpenOrFail(file); CheckEqual(SoundFailures.OggPageDamaged, stream.Seek(200000).Kind); } public void ACapturePatternCorruptedInAPageTheSeekDecodes_FailsTheSeekAsDamaged() { byte[] file = LoadFixture("Clicks.ogg"); file[4900] ^= 0x55; OggStream stream = OpenOrFail(file); CheckEqual(SoundFailures.OggPageDamaged, stream.Seek(60000).Kind); } public void WhenTheFileHasGoneBad_FailsAndReadsFailUntilASeekWorks() { byte[] file = LoadFixture("Clicks.ogg"); DamageableChunks chunks = new DamageableChunks(file); OggStream stream = OggStream.Open(chunks).Value; chunks.ZeroFrom(4414); Status failed = stream.Seek(100000); StatusOr read = stream.Read(new ByteView(new byte[8])); CheckEqual(SoundFailures.OggFailed, failed.Kind); CheckEqual(failed, read.Status); CheckEqual(0L, stream.Position); chunks.Repair(); CheckTrue(stream.Seek(100000).IsOk); CheckEqual(ExpectedFrames(file, 100000, 1000), ReadFrames(stream, 1000)); } public void ToTheStartWhenTheHeaderHasGoneBad_FailsAndTheNextSeekRecovers() { byte[] file = LoadFixture("Clicks.ogg"); DamageableChunks chunks = new DamageableChunks(file); OggStream stream = OggStream.Open(chunks).Value; chunks.ZeroFrom(0); Status failed = stream.Seek(0); CheckEqual(SoundFailures.OggFailed, failed.Kind); CheckEqual(failed, stream.Read(new ByteView(new byte[8])).Status); chunks.Repair(); CheckTrue(stream.Seek(0).IsOk); CheckEqual(ExpectedFrames(file, 0, 1000), ReadFrames(stream, 1000)); } public void ToTheStartAfterAFailedRestart_RestartsAgainEvenWithTheHeaderStillBad() { byte[] file = LoadFixture("Clicks.ogg"); DamageableChunks chunks = new DamageableChunks(file); OggStream stream = OggStream.Open(chunks).Value; chunks.ZeroFrom(0); stream.Seek(0); CheckEqual(SoundFailures.OggFailed, stream.Seek(0).Kind); } } public class DisposeTests : TestFixture { public void CanBeCalledTwice() { OggStream stream = OpenOrFail(LoadFixture("Mono441Hz.ogg")); stream.Dispose(); stream.Dispose(); CheckEqual(SoundFailures.StreamRequestInvalid, stream.Seek(0).Kind); } public void LeavesTheChunksOpen() { MemoryStream file = new MemoryStream(LoadFixture("Mono441Hz.ogg")); OggStream stream = OggStream.Open(new StreamChunks(file, leaveOpen: true)).Value; stream.Dispose(); CheckTrue(file.CanRead); } public void AnUndisposedStream_IsFinalizedWithoutError() { OpenOrFail(LoadFixture("Mono441Hz.ogg")); GC.Collect(); GC.WaitForPendingFinalizers(); } } } }