using Amphora; using System; using System.Linq; using UnitTestSharp; using static Susurrus.UnitTests.WavDecoderTests; namespace Susurrus.UnitTests { public class StreamFeedTests : TestFixture { public const int FramesPerSecond = 1000; // A stream that does what a damaged or truncated file would, around a real one. public class FlakyStream : ISoundStream { private readonly ISoundStream inner; public FlakyStream(ISoundStream inner) { this.inner = inner; } public bool FailReads { get; set; } public bool FailSeeks { get; set; } public bool ReadNothing { get; set; } public int SampleRate => inner.SampleRate; public int ChannelCount => inner.ChannelCount; public SampleFormat Format => inner.Format; public long FrameCount => inner.FrameCount; public long Position => inner.Position; public StatusOr Read(ByteView destination) { if (FailReads) { return SoundFailures.StreamRequestInvalid.Fail("Reads fail."); } if (ReadNothing) { return 0; } return inner.Read(destination); } public Status Seek(long frame) { if (FailSeeks) { return SoundFailures.StreamRequestInvalid.Fail("Seeks fail."); } return inner.Seek(frame); } public void Dispose() { inner.Dispose(); } } // Frame i holds the sample i + 1, so a sample says which frame it came from. public static ISoundStream CountingStream(int frameCount) { byte[] samples = new byte[frameCount * 2]; for (int i = 0; i < frameCount; ++i) { samples[2 * i] = (byte)(i + 1); } byte[] file = Riff(Chunk("fmt ", FormatBody(formatTag: 1, channelCount: 1, sampleRate: FramesPerSecond, bitsPerSample: 16)), Chunk("data", samples)); return SoundStreams.Open(new ByteChunks(new ByteView(file))).Value; } public static byte[] Counts(params int[] values) { byte[] returnMe = new byte[values.Length * 2]; for (int i = 0; i < values.Length; ++i) { returnMe[2 * i] = (byte)values[i]; } return returnMe; } public static TimeSpan Frames(int count) { return TimeSpan.FromMilliseconds(count); } public static byte[] FillFrames(ISoundStream stream, Loop loop, int frames, out int filled) { byte[] returnMe = new byte[frames * 2]; filled = StreamFeed.Fill(stream, loop, new ByteView(returnMe)).Value; return returnMe; } public class BytesPerSampleTests : TestFixture { public void Pcm16_IsTwo() { CheckEqual(2, StreamFeed.BytesPerSample(SampleFormat.Pcm16)); } public void Float32_IsFour() { CheckEqual(4, StreamFeed.BytesPerSample(SampleFormat.Float32)); } } public class FrameAtTests : TestFixture { public void GivesTheFrameATimeFallsOn() { CheckEqual(3L, StreamFeed.FrameAt(CountingStream(8), Frames(3))); } public void RoundsATimeBetweenFramesToTheNearest() { ISoundStream stream = CountingStream(8); CheckEqual(3L, StreamFeed.FrameAt(stream, TimeSpan.FromMilliseconds(3.4))); CheckEqual(4L, StreamFeed.FrameAt(stream, TimeSpan.FromMilliseconds(3.6))); } public void ATimeAfterTheEnd_ClampsToTheFrameCount() { CheckEqual(8L, StreamFeed.FrameAt(CountingStream(8), Frames(20))); } } public class FillTests : TestFixture { public void NullStream_Throws() { CheckThrow(typeof(ArgumentNullException)); StreamFeed.Fill(null, Loop.None, new ByteView(new byte[4])); } public void ADestinationHoldingTheWholeSound_GetsItAndReportsTheEnd() { byte[] got = FillFrames(CountingStream(8), Loop.None, frames: 20, out int filled); CheckEqual(8, filled); CheckEqual(Counts(1, 2, 3, 4, 5, 6, 7, 8), got.Take(16).ToArray()); } public void ADestinationSmallerThanTheSound_FillsItAndLeavesTheRest() { ISoundStream stream = CountingStream(8); byte[] first = FillFrames(stream, Loop.None, frames: 5, out int firstFilled); byte[] second = FillFrames(stream, Loop.None, frames: 5, out int secondFilled); CheckEqual(5, firstFilled); CheckEqual(Counts(1, 2, 3, 4, 5), first); CheckEqual(3, secondFilled); CheckEqual(Counts(6, 7, 8), second.Take(6).ToArray()); } public void AtTheEnd_FillsNothing() { ISoundStream stream = CountingStream(8); stream.Seek(8); FillFrames(stream, Loop.None, frames: 4, out int filled); CheckEqual(0, filled); } public void ADestinationOddInSize_HoldsOnlyWholeFrames() { ISoundStream stream = CountingStream(8); byte[] destination = new byte[7]; int filled = StreamFeed.Fill(stream, Loop.None, new ByteView(destination)).Value; CheckEqual(3, filled); CheckEqual(3L, stream.Position); } public void ADestinationInTheMiddleOfAnArray_IsTheOnlyPartWritten() { ISoundStream stream = CountingStream(8); byte[] array = new byte[8]; StreamFeed.Fill(stream, Loop.None, new ByteView(array, 2, 6)); CheckEqual(new byte[] { 0, 0, 1, 0, 2, 0, 0, 0 }, array); } public void AWholeLoop_GoesBackToTheStartAtTheEnd() { byte[] got = FillFrames(CountingStream(4), Loop.Whole, frames: 10, out int filled); CheckEqual(10, filled); CheckEqual(Counts(1, 2, 3, 4, 1, 2, 3, 4, 1, 2), got); } public void ALoopOfARegion_PlaysUpToItsEndThenRepeatsIt() { Loop loop = new Loop(Frames(2), Frames(5)); byte[] got = FillFrames(CountingStream(8), loop, frames: 10, out int filled); CheckEqual(10, filled); CheckEqual(Counts(1, 2, 3, 4, 5, 3, 4, 5, 3, 4), got); } public void ALoopWithNoEnd_RepeatsFromItsStartToTheEndOfTheSound() { Loop loop = new Loop(Frames(2)); byte[] got = FillFrames(CountingStream(5), loop, frames: 8, out int filled); CheckEqual(8, filled); CheckEqual(Counts(1, 2, 3, 4, 5, 3, 4, 5), got); } public void ALoopContinuesAcrossFills() { ISoundStream stream = CountingStream(4); FillFrames(stream, Loop.Whole, frames: 3, out int firstFilled); byte[] second = FillFrames(stream, Loop.Whole, frames: 3, out int secondFilled); CheckEqual(3, firstFilled); CheckEqual(3, secondFilled); CheckEqual(Counts(4, 1, 2), second); } public void APositionPastTheEndOfTheRegion_GoesBackToTheStartOfIt() { ISoundStream stream = CountingStream(8); stream.Seek(7); Loop loop = new Loop(Frames(2), Frames(5)); byte[] got = FillFrames(stream, loop, frames: 3, out int filled); CheckEqual(3, filled); CheckEqual(Counts(3, 4, 5), got); } public void ALoopShorterThanAFrame_Fails() { Loop loop = new Loop(TimeSpan.FromTicks(1), TimeSpan.FromTicks(2)); StatusOr result = StreamFeed.Fill(CountingStream(8), loop, new ByteView(new byte[8])); CheckEqual(SoundFailures.StreamRequestInvalid, result.Status.Kind); } public void AReadThatFails_FailsTheFill() { FlakyStream stream = new FlakyStream(CountingStream(8)); stream.FailReads = true; StatusOr result = StreamFeed.Fill(stream, Loop.None, new ByteView(new byte[8])); CheckEqual(SoundFailures.StreamRequestInvalid, result.Status.Kind); } public void ASeekThatFailsAtTheEndOfALoop_FailsTheFill() { FlakyStream stream = new FlakyStream(CountingStream(4)); stream.FailSeeks = true; StatusOr result = StreamFeed.Fill(stream, Loop.Whole, new ByteView(new byte[16])); CheckEqual(SoundFailures.StreamRequestInvalid, result.Status.Kind); } public void AStreamThatRunsDryEarly_StopsEvenWhenLooping() { FlakyStream stream = new FlakyStream(CountingStream(8)); stream.ReadNothing = true; FillFrames(stream, Loop.Whole, frames: 4, out int filled); CheckEqual(0, filled); } } } }