mirror of
https://github.com/unclshura/ABStemPlayer.git
synced 2026-08-07 00:43:38 +00:00
349 lines
10 KiB
C#
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);
|
|
}
|
|
}
|