Changes for making playback working (still doesn't).

This commit is contained in:
Alexander Shabarshov 2026-07-12 09:54:56 +01:00
parent 8216792170
commit 5010af3a62
8 changed files with 97 additions and 80 deletions

View File

@ -22,7 +22,11 @@ public class BlockingRingBuffer
while (written < srcLen) while (written < srcLen)
{ {
ct.ThrowIfCancellationRequested(); if ( ct.IsCancellationRequested )
{
Debug.WriteLine("BlockingRingBuffer: No room in the buffer to write. Timeout.");
return;
}
int remaining = srcLen - written; int remaining = srcLen - written;
@ -33,12 +37,11 @@ public class BlockingRingBuffer
? _ringWrite - _ringRead ? _ringWrite - _ringRead
: _ring.Length - _ringRead + _ringWrite; : _ring.Length - _ringRead + _ringWrite;
free = _ring.Length - used - 1; // leave 1 byte to distinguish full/empty free = _ring.Length - used - 1;
} }
if (free <= 0) if (free <= 0)
{ {
// No room → block until space becomes available
Thread.Sleep(1); Thread.Sleep(1);
continue; continue;
} }
@ -49,7 +52,6 @@ public class BlockingRingBuffer
{ {
int first = Math.Min(toWrite, _ring.Length - _ringWrite); int first = Math.Min(toWrite, _ring.Length - _ringWrite);
// Write first segment
src.Slice(written, first) src.Slice(written, first)
.CopyTo(new Span<byte>(_ring, _ringWrite, first)); .CopyTo(new Span<byte>(_ring, _ringWrite, first));
@ -58,7 +60,6 @@ public class BlockingRingBuffer
int leftover = toWrite - first; int leftover = toWrite - first;
if (leftover > 0) if (leftover > 0)
{ {
// Wrap-around segment
src.Slice(written + first, leftover) src.Slice(written + first, leftover)
.CopyTo(new Span<byte>(_ring, _ringWrite, leftover)); .CopyTo(new Span<byte>(_ring, _ringWrite, leftover));
@ -72,13 +73,19 @@ public class BlockingRingBuffer
public int WaitForOutput(CancellationToken token) public int WaitForOutput(CancellationToken token)
{ {
while (!token.IsCancellationRequested) while (true)
{ {
if (token.IsCancellationRequested)
{
Debug.WriteLine("BlockingRingBuffer: No data in the buffer to read. Timeout.");
return 0;
}
lock (_ringLock) lock (_ringLock)
{ {
var available = (_ringWrite >= _ringRead) var available = (_ringWrite >= _ringRead)
? _ringWrite - _ringRead ? _ringWrite - _ringRead
: _ring.Length - _ringRead + _ringWrite; : _ring.Length - _ringRead + _ringWrite;
if (available > 0) if (available > 0)
return available; return available;
@ -86,9 +93,6 @@ public class BlockingRingBuffer
Thread.Sleep(2); Thread.Sleep(2);
} }
Debug.WriteLine("BlockingRingBuffer: Timeout waiting for output");
return 0;
} }
public int DrainRing(Span<byte> dest, int maxBytes) public int DrainRing(Span<byte> dest, int maxBytes)

View File

@ -27,7 +27,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
_channels = channels; _channels = channels;
var bytesPerSecond = sampleRate * channels * sizeof(float); var bytesPerSecond = sampleRate * channels * sizeof(float);
_ring = new BlockingRingBuffer(1 * bytesPerSecond); _ring = new BlockingRingBuffer(10 * bytesPerSecond);
} }
public void Configure(PlaybackSpeedSettings settings) public void Configure(PlaybackSpeedSettings settings)
@ -37,25 +37,17 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
_speed = settings.Speed; _speed = settings.Speed;
if (Math.Abs(_speed - 1.0f) < 0.01f) DisposeProcess();
{ _ring.ResetRing();
DisposeProcess();
_ring.ResetRing();
}
else
{
RestartProcess();
}
} }
public Task Submit(MixedAudioBlock input) public Task Submit(MixedAudioBlock input, CancellationToken token)
{ {
// No-stretch path: enqueue block and signal semaphore // No-stretch path: enqueue block and signal semaphore
if (Math.Abs(_speed - 1.0f) < 0.01f) if (Math.Abs(_speed - 1.0f) < 0.01f)
{ {
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var b = MemoryMarshal.AsBytes(input.Buffer.Span); var b = MemoryMarshal.AsBytes(input.Buffer.Span);
_ring.WriteToOutput(b, b.Length, cts.Token); _ring.WriteToOutput(b, b.Length, token);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -72,21 +64,19 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task<TimeStretchedAudioBlock> Receive() public async Task<TimeStretchedAudioBlock> Receive(CancellationToken token)
{ {
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
int available = 0; int available = 0;
while (!cts.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
available = _ring.WaitForOutput(cts.Token); available = _ring.WaitForOutput(token);
if (available > 0) if (available > 0)
break; break;
await Task.Delay(2).ConfigureAwait(false); await Task.Delay(2).ConfigureAwait(false);
} }
if (cts.IsCancellationRequested) if (token.IsCancellationRequested)
return default; return default;
var maxFloats = available / sizeof(float); var maxFloats = available / sizeof(float);
@ -132,13 +122,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
_readerThread.Start(); _readerThread.Start();
} }
private void RestartProcess()
{
DisposeProcess();
_ring.ResetRing();
StartProcess();
}
private void ReaderLoop() private void ReaderLoop()
{ {
var buf = new byte[4096]; var buf = new byte[4096];

View File

@ -1,4 +1,6 @@
namespace AudioCore.Impl; using System.Diagnostics;
namespace AudioCore.Impl;
public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
{ {
@ -238,7 +240,7 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
var decodeTask = DecodeLoopAsync(pipeline, ct); var decodeTask = DecodeLoopAsync(pipeline, ct);
var stretchTask = StretchLoopAsync(pipeline, ct); var stretchTask = StretchLoopAsync(pipeline, ct);
await Task.WhenAny(decodeTask, stretchTask); await Task.WhenAny(decodeTask, stretchTask).ConfigureAwait(false);
// When either loop ends, stop output // When either loop ends, stop output
if (pipeline.OutputStarted) if (pipeline.OutputStarted)
@ -250,6 +252,8 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
private async Task DecodeLoopAsync(PipelineState pipeline, CancellationToken ct) private async Task DecodeLoopAsync(PipelineState pipeline, CancellationToken ct)
{ {
await Task.Yield();
try try
{ {
while (!ct.IsCancellationRequested) while (!ct.IsCancellationRequested)
@ -263,12 +267,12 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
lock (_stateLock) lock (_stateLock)
{ {
playing = _isPlaying; playing = _isPlaying;
mixerSnapshot = Mixer; mixerSnapshot = Mixer;
decodersSnapshot = pipeline.Decoders; decodersSnapshot = pipeline.Decoders;
loopStart = _loopStartFrames; loopStart = _loopStartFrames;
loopEnd = _loopEndFrames; loopEnd = _loopEndFrames;
loopEnabled = _loopRegion.IsEnabled; loopEnabled = _loopRegion.IsEnabled;
progressReporter = _progressReporter; progressReporter = _progressReporter;
} }
@ -286,7 +290,8 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
if (!decoder.TryDecodeNextBlock(out var block)) if (!decoder.TryDecodeNextBlock(out var block))
{ {
eof = true; eof = true;
foreach (var b in _stemBlocks) b.Dispose(); foreach (var b in _stemBlocks)
b.Dispose();
_stemBlocks.Clear(); _stemBlocks.Clear();
break; break;
} }
@ -307,7 +312,7 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
b.Dispose(); b.Dispose();
_stemBlocks.Clear(); _stemBlocks.Clear();
await _timeStretchEngine.Submit(mixed); await _timeStretchEngine.Submit(mixed, ct).ConfigureAwait(false);
var progress = TimeSpan.FromSeconds( var progress = TimeSpan.FromSeconds(
(double)_currentFramePosition / _outputDevice.SampleRate); (double)_currentFramePosition / _outputDevice.SampleRate);
@ -336,11 +341,12 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
private async Task StretchLoopAsync(PipelineState pipeline, CancellationToken ct) private async Task StretchLoopAsync(PipelineState pipeline, CancellationToken ct)
{ {
await Task.Yield();
try try
{ {
while (!ct.IsCancellationRequested) while (!ct.IsCancellationRequested)
{ {
var stretched = await _timeStretchEngine.Receive(); var stretched = await _timeStretchEngine.Receive(ct).ConfigureAwait(false);
if (stretched.Buffer == null) if (stretched.Buffer == null)
{ {
@ -357,7 +363,10 @@ public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable
_outputDevice.Write(stretched.Buffer.Span); _outputDevice.Write(stretched.Buffer.Span);
} }
} }
catch { } catch(Exception ex)
{
Debug.WriteLine($"StemPlaybackEngine: Error in StretchLoopAsync: {ex.Message}");
}
} }

View File

@ -101,18 +101,39 @@ public sealed class WasapiOutputDevice : IAudioOutputDevice, IDisposable
} }
private Lock _lock = new();
private void Send(byte[] bytes) private void Send(byte[] bytes)
{ {
// Wait until buffer has enough free space if (_out.PlaybackState != PlaybackState.Playing)
while (_buffer.BufferedBytes + bytes.Length > _buffer.BufferLength) return;
{
// Sleep a tiny amount to let WASAPI consume data
Thread.Sleep(2);
}
_buffer.AddSamples(bytes, 0, bytes.Length); lock (_lock)
{
int offset = 0;
while (offset < bytes.Length)
{
if (_out.PlaybackState != PlaybackState.Playing)
return;
int free = _buffer.BufferLength - _buffer.BufferedBytes;
if (free <= 0)
{
Thread.Sleep(2);
continue;
}
int toWrite = Math.Min(free, bytes.Length - offset);
_buffer.AddSamples(bytes, offset, toWrite);
offset += toWrite;
}
}
} }
public void Dispose() public void Dispose()
{ {
_out.Dispose(); _out.Dispose();

View File

@ -11,6 +11,6 @@ public interface ITimeStretchEngine
void Configure(PlaybackSpeedSettings settings); void Configure(PlaybackSpeedSettings settings);
// Streaming block processing // Streaming block processing
Task Submit(MixedAudioBlock input); Task Submit(MixedAudioBlock input, CancellationToken token);
Task<TimeStretchedAudioBlock> Receive(); Task<TimeStretchedAudioBlock> Receive(CancellationToken token);
} }

View File

@ -110,13 +110,13 @@ public sealed class StemPlaybackEngine_Tests
// no-op for tests // no-op for tests
} }
public Task Submit(MixedAudioBlock input) public Task Submit(MixedAudioBlock input, CancellationToken token)
{ {
_lastInput = input; _lastInput = input;
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<TimeStretchedAudioBlock> Receive() public Task<TimeStretchedAudioBlock> Receive(CancellationToken token)
{ {
if (_lastInput.Buffer == null) if (_lastInput.Buffer == null)
return Task.FromResult(default(TimeStretchedAudioBlock)); return Task.FromResult(default(TimeStretchedAudioBlock));

View File

@ -34,8 +34,8 @@ public sealed class TimeStretchEngine_Tests
var input = MakeBlock(5000); var input = MakeBlock(5000);
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var output = await engine.Receive(); var output = await engine.Receive(CancellationToken.None);
Assert.IsGreaterThan(0, output.Frames); Assert.IsGreaterThan(0, output.Frames);
Assert.AreEqual(2, output.Channels); Assert.AreEqual(2, output.Channels);
@ -59,12 +59,12 @@ public sealed class TimeStretchEngine_Tests
var input = MakeBlock(1000); var input = MakeBlock(1000);
for (var i = 0; i < 25; i++) for (var i = 0; i < 25; i++)
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var normalFrames = 0; var normalFrames = 0;
while (true) while (true)
{ {
using var data = await engine.Receive(); using var data = await engine.Receive(CancellationToken.None);
normalFrames += data.Frames; normalFrames += data.Frames;
if (data.Buffer == null) if (data.Buffer == null)
break; break;
@ -75,12 +75,12 @@ public sealed class TimeStretchEngine_Tests
for (var i = 0; i < 25; i++) for (var i = 0; i < 25; i++)
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var fasterFrames = 0; var fasterFrames = 0;
while (true) while (true)
{ {
using var data = await engine.Receive(); using var data = await engine.Receive(CancellationToken.None);
fasterFrames += data.Frames; fasterFrames += data.Frames;
if (data.Buffer == null) if (data.Buffer == null)
break; break;
@ -98,12 +98,12 @@ public sealed class TimeStretchEngine_Tests
var input = MakeBlock(1000); var input = MakeBlock(1000);
for (var i = 0; i < 25; i++) for (var i = 0; i < 25; i++)
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var normalFrames = 0; var normalFrames = 0;
while (true) while (true)
{ {
using var data = await engine.Receive(); using var data = await engine.Receive(CancellationToken.None);
normalFrames += data.Frames; normalFrames += data.Frames;
if (data.Buffer == null) if (data.Buffer == null)
break; break;
@ -112,12 +112,12 @@ public sealed class TimeStretchEngine_Tests
engine.Configure(new PlaybackSpeedSettings { Speed = 0.5f }); engine.Configure(new PlaybackSpeedSettings { Speed = 0.5f });
for (var i = 0; i < 25; i++) for (var i = 0; i < 25; i++)
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var slowerFrames = 0; var slowerFrames = 0;
while (true) while (true)
{ {
using var data = await engine.Receive(); using var data = await engine.Receive(CancellationToken.None);
slowerFrames += data.Frames; slowerFrames += data.Frames;
if (data.Buffer == null) if (data.Buffer == null)
break; break;
@ -135,13 +135,13 @@ public sealed class TimeStretchEngine_Tests
var input = MakeBlock(100); var input = MakeBlock(100);
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var before = await engine.Receive(); var before = await engine.Receive(CancellationToken.None);
engine.Configure(new PlaybackSpeedSettings { Speed = 0.75f }); engine.Configure(new PlaybackSpeedSettings { Speed = 0.75f });
await engine.Submit(input); await engine.Submit(input, CancellationToken.None);
var after = await engine.Receive(); var after = await engine.Receive(CancellationToken.None);
Assert.AreEqual(0, after.Frames); Assert.AreEqual(0, after.Frames);
} }

View File

@ -3,19 +3,19 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia" Version="12.0.3" /> <PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.0.3" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.3" /> <PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.3" /> <PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1"/> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="BunLabs.NAudio.Flac" Version="2.0.1" /> <PackageVersion Include="BunLabs.NAudio.Flac" Version="2.0.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.1" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.27.0" /> <PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.27.0" />
<PackageVersion Include="Microsoft.ML.OnnxRuntime.DirectML" Version="1.24.4" /> <PackageVersion Include="Microsoft.ML.OnnxRuntime.DirectML" Version="1.24.4" />
<PackageVersion Include="NAudio" Version="2.3.0" /> <PackageVersion Include="NAudio" Version="2.3.0" />
<PackageVersion Include="NAudio.Flac" Version="1.0.5702.29018" /> <PackageVersion Include="NAudio.Flac" Version="1.0.5702.29018" />
<PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="System.Text.Json" Version="10.0.5" />
<PackageVersion Include="MSTest" Version="4.0.2" /> <PackageVersion Include="MSTest" Version="4.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>