using AudioCore.Interfaces; using AudioCore.Models; using AudioCore.Impl; namespace AudioCore_Tests; [TestClass] public sealed class StemPlaybackEngine_Tests { private sealed class MockDecoder : IStemDecoder { private readonly Queue _blocks; private readonly AudioBufferPool _pool; private long _seekPosition; public StemTrack Stem { get; } public MockDecoder(int framesPerBlock, int blocks, AudioBufferPool pool) { _pool = pool; Stem = new StemTrack { Channels = 2, SampleRate = 44100, Duration = TimeSpan.FromSeconds(10), FilePath = "x" }; _blocks = new Queue(); long pos = 0; for (var i = 0; i < blocks; i++) { var buf = _pool.Rent(framesPerBlock * Stem.Channels); buf.Length = framesPerBlock * Stem.Channels; _blocks.Enqueue(new AudioBlock(buf, Stem.SampleRate, Stem.Channels, pos)); pos += framesPerBlock; } } public bool TryDecodeNextBlock(out AudioBlock block) { if (_blocks.Count == 0) { block = default; return false; } block = _blocks.Dequeue(); return true; } public void Seek(long samplePosition) { _seekPosition = samplePosition; } public void Reset() { _seekPosition = 0; } public void Dispose() { } } private sealed class MockDecoderFactory : IStemDecoderFactory { private readonly AudioBufferPool _pool; private readonly int _frames; private readonly int _blocks; public MockDecoderFactory(AudioBufferPool pool, int frames, int blocks) { _pool = pool; _frames = frames; _blocks = blocks; } public IStemDecoder Create(StemTrack stem) { return new MockDecoder(_frames, _blocks, _pool); } } private sealed class MockMixer(AudioBufferPool _pool) : IAudioMixer { public MixedAudioBlock Mix(IReadOnlyList stemBlocks, MixerSettings settings) { var first = stemBlocks[0]; var buf = _pool.Rent(first.Length); Array.Copy(first.Buffer.Samples, buf.Samples, first.Length); return new MixedAudioBlock( buf, first.Frames, first.Channels, first.SampleRate, first.Position); } } private sealed class MockTimeStretch : ITimeStretchEngine { private MixedAudioBlock _lastInput; public void Configure(PlaybackSpeedSettings settings) { // no-op for tests } public Task Submit(MixedAudioBlock input, CancellationToken token) { _lastInput = input; return Task.CompletedTask; } public Task Receive(CancellationToken token) { if (_lastInput.Buffer == null) return Task.FromResult(default(TimeStretchedAudioBlock)); var block = new TimeStretchedAudioBlock( _lastInput.Buffer, _lastInput.Frames, _lastInput.Channels, _lastInput.SampleRate); _lastInput = default; return Task.FromResult(block); } } private sealed class MockOutput : IAudioOutputDevice { public int SampleRate => 44100; public int Channels => 2; public int WriteCount { get; private set; } public int LastWriteSamples { get; private set; } public bool Started { get; private set; } public void Start() { Started = true; } public void Stop() { Started = false; } public void Write(ReadOnlySpan samples) { WriteCount++; LastWriteSamples = samples.Length; } } private PlaybackSession CreateSession(int stems) { var stemList = new List(); var mixList = new List(); for (var i = 0; i < stems; i++) { stemList.Add(new StemTrack { Channels = 2, SampleRate = 44100, Duration = TimeSpan.FromSeconds(10), FilePath = "x" }); mixList.Add(new StemMixSettings { Enabled = true, GainDb = 0, Pan = 0 }); } return new PlaybackSession { StemSet = new StemSet { OriginalFilePath = "x", Stems = stemList }, Mixer = new MixerSettings { Stems = mixList }, Loop = new LoopRegion { IsEnabled = false }, Speed = new PlaybackSpeedSettings { Speed = 1.0f } }; } private class DummyProgressReporter : IProgressReporter { public Task ReportProgress(TimeSpan value, CancellationToken ct) => Task.CompletedTask; } [TestMethod] public async Task LoadSession_InitializesDecoders() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 5); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(3); await engine.LoadSessionAsync(session, new DummyProgressReporter()); Assert.IsFalse(output.Started); } //[TestMethod] //public async Task PlayAsync_StartsOutputDevice() //{ // var pool = new AudioBufferPool(); // var decoderFactory = new MockDecoderFactory(pool, 1024, 5); // var output = new MockOutput(); // var mixer = new MockMixer(pool); // var stretch = new MockTimeStretch(); // var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); // var session = CreateSession(2); // await engine.LoadSessionAsync(session, new DummyProgressReporter()); // await engine.PlayAsync(); // Assert.IsTrue(output.Started); //} [TestMethod] public async Task PauseAsync_StopsOutputDevice() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 5); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(2); await engine.LoadSessionAsync(session, new DummyProgressReporter()); await engine.PlayAsync(); await engine.PauseAsync(); Assert.IsFalse(output.Started); } [TestMethod] public async Task RenderLoop_WritesAudioBlocks() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 3); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(1); await engine.LoadSessionAsync(session, new DummyProgressReporter()); await engine.PlayAsync(); await Task.Delay(50); await engine.StopAsync(); Assert.IsGreaterThan(0, output.WriteCount); Assert.AreEqual(1024 * 2, output.LastWriteSamples); } [TestMethod] public async Task SeekAsync_MovesDecoders() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 5); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(1); await engine.LoadSessionAsync(session, new DummyProgressReporter()); await engine.SeekAsync(TimeSpan.FromSeconds(2)); Assert.IsFalse(output.Started); } [TestMethod] public async Task LoopRegion_SeeksBackOnBoundary() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 5); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(1); session.Loop = new LoopRegion { IsEnabled = true, Start = TimeSpan.FromSeconds(0), End = TimeSpan.FromSeconds(1) }; await engine.LoadSessionAsync(session, new DummyProgressReporter()); await engine.PlayAsync(); await Task.Delay(50); await engine.StopAsync(); Assert.IsGreaterThan(0, output.WriteCount); } [TestMethod] public async Task SpeedChange_ReconfiguresTimeStretch() { var pool = new AudioBufferPool(); var decoderFactory = new MockDecoderFactory(pool, 1024, 5); var output = new MockOutput(); var mixer = new MockMixer(pool); var stretch = new MockTimeStretch(); var engine = new StemPlaybackEngine(decoderFactory, output, mixer, stretch); var session = CreateSession(1); await engine.LoadSessionAsync(session, new DummyProgressReporter()); engine.CurrentSession!.Speed.Speed = 1.5f; Assert.IsFalse(output.Started); } }