using Amphora; using System; using System.IO; using System.Linq; using UnitTestSharp; using static StbVorbisSharp.StbVorbis; using static Susurrus.UnitTests.TestSounds; namespace Susurrus.UnitTests { public class OggDecoderTests : TestFixture { public const int QuarterPeriod = 25; public const int HalfPeriod = 50; public const int ThreeQuarterPeriod = 75; public const int PeakSample = 16384; public const int LastFrameSample = 8780; public const int Tolerance = 1000; public static byte[] Truncated(byte[] file, int length) { byte[] returnMe = new byte[length]; Array.Copy(file, returnMe, length); return returnMe; } public static byte[] WithLastPageGranule(byte[] file, long granule) { byte[] returnMe = (byte[])file.Clone(); int lastPage = LastPageStart(returnMe); Array.Copy(BitConverter.GetBytes(granule), 0, returnMe, lastPage + 6, 8); FixPageChecksum(returnMe, lastPage); return returnMe; } public class DecodeBytesTests : TestFixture { public void DefaultView_Throws() { CheckThrow(typeof(ArgumentException)); OggDecoder.Decode(default(ByteView)); } public void ViewOfALargerArray_DecodesOnlyTheWindow() { byte[] file = LoadFixture("Mono441Hz.ogg"); byte[] padded = new byte[file.Length + 500]; Array.Copy(file, 0, padded, 200, file.Length); DecodedSound sound = OggDecoder.Decode(new ByteView(padded, startIndex: 200, endIndex: 200 + file.Length)).Value; CheckEqual(4410, sound.FrameCount); } public void Mono_ReportsTheFormatRateAndChannels() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckEqual(SampleFormat.Pcm16, sound.Format); CheckEqual(44100, sound.SampleRate); CheckEqual(1, sound.ChannelCount); } public void Mono_HasEveryFrameTheLastPageCounts() { CheckEqual(4410, DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value.FrameCount); } public void Mono_LastsAsLongAsTheLastPageSays() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckEqual(TimeSpan.FromMilliseconds(100), sound.Duration); } public void Mono_PeaksAtAQuarterPeriod() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckTrue(Math.Abs(SampleAt(sound, QuarterPeriod, channel: 0) - PeakSample) < Tolerance); } public void Mono_CrossesZeroAtAHalfPeriod() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckTrue(Math.Abs(SampleAt(sound, HalfPeriod, channel: 0)) < Tolerance); } public void Mono_TroughsAtThreeQuartersOfAPeriod() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckTrue(Math.Abs(SampleAt(sound, ThreeQuarterPeriod, channel: 0) + PeakSample) < Tolerance); } public void Mono_EndsPartwayThroughACycleWithItsLastSampleKept() { DecodedSound sound = DecodeOgg(LoadFixture("Mono441Hz.ogg")).Value; CheckTrue(Math.Abs(SampleAt(sound, frame: 4409, channel: 0) - LastFrameSample) < Tolerance); } public void Stereo_HasTwoChannelsAndEveryFrame() { DecodedSound sound = DecodeOgg(LoadFixture("Stereo441HzLeftOnly.ogg")).Value; CheckEqual(2, sound.ChannelCount); CheckEqual(4410, sound.FrameCount); } public void Stereo_PutsTheSineInTheLeftChannel() { DecodedSound sound = DecodeOgg(LoadFixture("Stereo441HzLeftOnly.ogg")).Value; CheckTrue(Math.Abs(SampleAt(sound, QuarterPeriod, channel: 0) - PeakSample) < Tolerance); CheckTrue(Math.Abs(SampleAt(sound, ThreeQuarterPeriod, channel: 0) + PeakSample) < Tolerance); } public void Stereo_LeavesTheRightChannelSilent() { DecodedSound sound = DecodeOgg(LoadFixture("Stereo441HzLeftOnly.ogg")).Value; int loudestRightSample = 0; for (int frame = 0; frame < sound.FrameCount; ++frame) { loudestRightSample = Math.Max(loudestRightSample, Math.Abs((int)SampleAt(sound, frame, 1))); } CheckEqual(0, loudestRightSample); } public void EmptyData_FailsWithUnexpectedEof() { CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(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, OggErrorOf(DecodeOgg(riff))); } public void FileCutInsideItsFirstPage_FailsWithInvalidFirstPage() { CheckEqual(STBVorbisError.VORBIS_invalid_first_page, OggErrorOf(DecodeOgg(Truncated(LoadFixture("Mono441Hz.ogg"), 10)))); } public void FileCutInsideItsSetupHeaders_FailsWithInvalidSetup() { CheckEqual(STBVorbisError.VORBIS_invalid_setup, OggErrorOf(DecodeOgg(Truncated(LoadFixture("Mono441Hz.ogg"), 1000)))); } public void FileCutInsideItsAudioPage_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(Truncated(file, file.Length - 100)))); } public void FileMissingOnlyItsLastByte_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(Truncated(file, file.Length - 1)))); } public void FileCutAtTheStartOfItsLastPage_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(Truncated(file, LastPageStart(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, OggErrorOf(DecodeOgg(file))); } public void BytesAfterTheLastPage_AreIgnored() { byte[] file = LoadFixture("Mono441Hz.ogg"); byte[] junk = Enumerable.Range(0, 100).Select(i => (byte)(i * 37 + 11)).ToArray(); CheckEqual(4410, DecodeOgg(file.Concat(junk).ToArray()).Value.FrameCount); } public void MoreBytesAfterTheLastPageThanAPageCanHold_FailsWithUnexpectedEof() { byte[] file = LoadFixture("Mono441Hz.ogg").Concat(new byte[70000]).ToArray(); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(file))); } public void LastPageWithNoGranule_FailsWithCantFindLastPage() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: -1); CheckEqual(STBVorbisError.VORBIS_cant_find_last_page, OggErrorOf(DecodeOgg(file))); } public void LastPageClaimingNoFrames_FailsWithInvalidStream() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: 0); CheckEqual(STBVorbisError.VORBIS_invalid_stream, OggErrorOf(DecodeOgg(file))); } public void SampleRateAboveWhatAnIntHolds_FailsWithInvalidSetup() { byte[] file = LoadFixture("Mono441Hz.ogg"); file[40] = 0; file[41] = 0; file[42] = 0; file[43] = 0x80; FixPageChecksum(file, 0); CheckEqual(STBVorbisError.VORBIS_invalid_setup, OggErrorOf(DecodeOgg(file))); } public void AudioByteChangedInTheLastPage_FailsAsDamaged() { byte[] file = LoadFixture("Mono441Hz.ogg"); file[LastPageStart(file) + 60] ^= 0x55; CheckEqual(SoundFailures.OggPageDamaged, KindOf(DecodeOgg(file))); } public void AudioByteChangedInAMiddlePage_FailsAsDamaged() { byte[] file = LoadFixture("Clicks.ogg"); file[PageStartHolding(file, 6000) + 40] ^= 0x01; CheckEqual(SoundFailures.OggPageDamaged, KindOf(DecodeOgg(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, KindOf(DecodeOgg(file))); } public void ADamagedPage_SaysWhereItStarts() { byte[] file = LoadFixture("Clicks.ogg"); int pageStart = PageStartHolding(file, 6000); file[pageStart + 40] ^= 0x01; CheckTrue(DecodeOgg(file).Status.Describe().Contains($"byte {pageStart}")); } public void LastPageClaimingMoreFramesThanTheStreamHolds_FailsWithUnexpectedEof() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: 6000); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(DecodeOgg(file))); } public void LastPageClaimingFewerFramesThanTheStreamHolds_FailsWithInvalidStream() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: 3000); CheckEqual(STBVorbisError.VORBIS_invalid_stream, OggErrorOf(DecodeOgg(file))); } public void LastPageClaimingMoreFramesThanAnArrayCanHold_FailsAsTooLarge() { byte[] file = WithLastPageGranule(LoadFixture("Mono441Hz.ogg"), granule: int.MaxValue); CheckEqual(SoundFailures.TooLarge, KindOf(DecodeOgg(file))); } public void OggHoldingOpus_FailsWithInvalidFirstPage() { CheckEqual(STBVorbisError.VORBIS_invalid_first_page, OggErrorOf(DecodeOgg(LoadFixture("OpusInOgg.ogg")))); } public void Failure_DescribesTheDecodersError() { CheckEqual("The file ends before the audio does.", DecodeOgg(new byte[0]).Status.Describe()); } } public class DecodeStreamTests : TestFixture { public void NullStream_Throws() { CheckThrow(typeof(ArgumentNullException)); OggDecoder.Decode((Stream)null); } public void SeekableStream_DecodesTheSameSamplesAsTheBytes() { DecodedSound fromBytes = DecodeOgg(LoadFixture("Stereo441HzLeftOnly.ogg")).Value; using (Stream stream = OpenFixture("Stereo441HzLeftOnly.ogg")) { CheckTrue(BytesOf(fromBytes.Samples).SequenceEqual( BytesOf(OggDecoder.Decode(stream).Value.Samples))); } } public void ForwardOnlyStream_DecodesTheSameSamplesAsTheBytes() { byte[] file = LoadFixture("Stereo441HzLeftOnly.ogg"); DecodedSound fromBytes = DecodeOgg(file).Value; DecodedSound fromStream = OggDecoder.Decode(new TrickleStream(file, bytesPerRead: 100)).Value; CheckTrue(BytesOf(fromBytes.Samples).SequenceEqual(BytesOf(fromStream.Samples))); } public void StreamStartingMidWay_DecodesFromItsPosition() { byte[] file = LoadFixture("Mono441Hz.ogg"); MemoryStream stream = new MemoryStream(new byte[] { 1 }.Concat(file).ToArray()); stream.Position = 1; CheckEqual(4410, OggDecoder.Decode(stream).Value.FrameCount); } public void ForwardOnlyStreamEndingEarly_FailsWithTheDecodersError() { byte[] file = LoadFixture("Mono441Hz.ogg"); CheckEqual(STBVorbisError.VORBIS_unexpected_eof, OggErrorOf(OggDecoder.Decode(new TrickleStream(Truncated(file, file.Length - 100), 500)))); } public void StreamLongerThanAnArrayCanHold_FailsAsTooLarge() { ClaimedLengthStream stream = new ClaimedLengthStream(new byte[10], claimedLength: 1L << 31); CheckEqual(SoundFailures.TooLarge, KindOf(OggDecoder.Decode(stream))); } } } }