ABStemPlayer/AudioCore_Tests/StemPlaybackEngine_Tests.cs

349 lines
10 KiB
C#

using AudioCore.Impl;
using AudioCore.Interfaces;
using AudioCore.Models;
using NAudio.Wave;
namespace AudioCore_Tests;
[TestClass]
public sealed class StemPlaybackEngine_Tests
{
private sealed class MockDecoder : IStemDecoder
{
private readonly Queue<AudioBlock> _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<AudioBlock>();
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 Task<AudioBlock?> DecodeNextBlockAsync(CancellationToken token)
{
if (_blocks.Count == 0)
return Task.FromResult<AudioBlock?>(null);
return Task.FromResult<AudioBlock?>(_blocks.Dequeue());
}
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<TimeStretchedAudioBlock> stemBlocks, MixerSettings settings)
{
var first = stemBlocks[0];
// Correct: use buffer length
var buf = _pool.Rent(first.Buffer.Length);
Array.Copy(first.Buffer.Samples, buf.Samples, first.Buffer.Length);
return new MixedAudioBlock(
buf,
first.Frames,
first.Channels,
first.SampleRate,
first.Position);
}
}
private sealed class MockTimeStretch : ITimeStretchEngine
{
private IReadOnlyList<AudioBlock>? _lastInput;
public Task Configure(PlaybackSpeedSettings settings, CancellationToken token)
=> Task.CompletedTask;
public Task IsReadyToAcceptStems(CancellationToken token)
=> Task.CompletedTask;
public Task SubmitStems(IReadOnlyList<AudioBlock> stemBlocks, CancellationToken token)
{
_lastInput = stemBlocks;
return Task.CompletedTask;
}
public Task<TimeStretchedAudioBlock[]> ReceiveStems(CancellationToken token)
{
if (_lastInput == null || _lastInput.Count == 0)
return Task.FromResult(Array.Empty<TimeStretchedAudioBlock>());
var result = new TimeStretchedAudioBlock[_lastInput.Count];
for (int i = 0; i < _lastInput.Count; i++)
{
var src = _lastInput[i];
result[i] = new TimeStretchedAudioBlock(
src.Buffer,
src.Frames,
src.Channels,
src.SampleRate,
src.Position);
}
_lastInput = null;
return Task.FromResult(result);
}
}
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 Task IsReadyToAccept(CancellationToken token) => Task.CompletedTask;
public void Start() => Started = true;
public void Stop() => Started = false;
public void Pause() => Started = false;
public PlaybackState State => Started ? PlaybackState.Playing : PlaybackState.Stopped;
public void Write(ReadOnlySpan<float> samples)
{
WriteCount++;
LastWriteSamples = samples.Length;
}
}
private PlaybackSession CreateSession(int stems)
{
var stemList = new List<StemTrack>();
var mixList = new List<StemMixSettings>();
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<double>
{
public Task ReportProgress(double 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 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]
[TestCategory("ProductionBugSuspected")]
//[Ignore("ProductionBugSuspected")]
public async Task RenderLoop_WritesAudioBlocks()
{
var pool = new AudioBufferPool();
var decoderFactory = new MockDecoderFactory(pool, 44100, 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(500);
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]
[TestCategory("ProductionBugSuspected")]
[Ignore("ProductionBugSuspected")]
public async Task LoopRegion_SeeksBackOnBoundary()
{
var pool = new AudioBufferPool();
var decoderFactory = new MockDecoderFactory(pool, 44100, 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(TimeSpan.FromSeconds(6));
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);
}
}