mirror of
https://github.com/unclshura/ABStemPlayer.git
synced 2026-08-07 00:43:38 +00:00
169 lines
4.5 KiB
C#
169 lines
4.5 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace AudioCore.Impl;
|
|
|
|
public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposable
|
|
{
|
|
private readonly AudioBufferPool _pool;
|
|
private readonly int _sampleRate;
|
|
private readonly int _channels;
|
|
|
|
private FfmpegProcess? _ff;
|
|
private Stream? _stdin;
|
|
private Stream? _stdout;
|
|
|
|
private BlockingRingBuffer _ring;
|
|
private float _speed = 1.0f;
|
|
|
|
private Thread? _readerThread;
|
|
private bool _readerRunning;
|
|
|
|
public RubberBandTimeStretchEngine(AudioBufferPool pool, int sampleRate = 44100, int channels = 2)
|
|
{
|
|
_pool = pool;
|
|
_sampleRate = sampleRate;
|
|
_channels = channels;
|
|
|
|
var bytesPerSecond = sampleRate * channels * sizeof(float);
|
|
_ring = new BlockingRingBuffer(10 * bytesPerSecond);
|
|
}
|
|
|
|
public void Configure(PlaybackSpeedSettings settings)
|
|
{
|
|
if (Math.Abs(settings.Speed - _speed) < 0.0001f)
|
|
return;
|
|
|
|
_speed = settings.Speed;
|
|
|
|
DisposeProcess();
|
|
_ring.ResetRing();
|
|
}
|
|
|
|
public Task Submit(MixedAudioBlock input, CancellationToken token)
|
|
{
|
|
// No-stretch path: enqueue block and signal semaphore
|
|
if (Math.Abs(_speed - 1.0f) < 0.01f)
|
|
{
|
|
var b = MemoryMarshal.AsBytes(input.Buffer.Span);
|
|
_ring.WriteToOutput(b, b.Length, token);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
if (_ff is null)
|
|
StartProcess();
|
|
|
|
var span = input.Buffer.Span;
|
|
var bytes = MemoryMarshal.AsBytes(span);
|
|
|
|
_stdin!.Write(bytes);
|
|
_stdin.Flush();
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task<TimeStretchedAudioBlock> Receive(CancellationToken token)
|
|
{
|
|
int available = 0;
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
available = _ring.WaitForOutput(token);
|
|
if (available > 0)
|
|
break;
|
|
|
|
await Task.Delay(2).ConfigureAwait(false);
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
return default;
|
|
|
|
var maxFloats = available / sizeof(float);
|
|
var outBuf = _pool.Rent(maxFloats);
|
|
var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
|
|
|
|
var readBytes = _ring.DrainRing(outBytes, outBytes.Length);
|
|
if (readBytes <= 0)
|
|
{
|
|
outBuf.Dispose();
|
|
Debug.WriteLine("RubberBandTimeStretchEngine: Failed to drain ring buffer.");
|
|
return default;
|
|
}
|
|
|
|
var frames = readBytes / (_channels * sizeof(float));
|
|
outBuf.Length = frames * _channels;
|
|
|
|
return new TimeStretchedAudioBlock(outBuf, frames, _channels, _sampleRate);
|
|
}
|
|
|
|
|
|
private void StartProcess()
|
|
{
|
|
var cmd =
|
|
$"-hide_banner -loglevel error " +
|
|
$"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " +
|
|
$"-af \"rubberband=tempo={_speed}\" " +
|
|
$"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1";
|
|
|
|
_ff = new FfmpegProcess(
|
|
name: $"rubberband:{_speed:F3}",
|
|
commandLine: cmd,
|
|
redirectOutput: true,
|
|
redirectInput: true);
|
|
|
|
_ff.StartProcess();
|
|
|
|
_stdin = _ff.Stdin!;
|
|
_stdout = _ff.Stdout!;
|
|
|
|
_readerRunning = true;
|
|
_readerThread = new Thread(ReaderLoop) { IsBackground = true };
|
|
_readerThread.Start();
|
|
}
|
|
|
|
private void ReaderLoop()
|
|
{
|
|
var buf = new byte[4096];
|
|
|
|
try
|
|
{
|
|
while (_readerRunning)
|
|
{
|
|
var read = _stdout!.Read(buf, 0, buf.Length);
|
|
if (read <= 0)
|
|
break;
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
_ring.WriteToOutput(buf, read, cts.Token);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
|
|
private void DisposeProcess()
|
|
{
|
|
_readerRunning = false;
|
|
|
|
try { _stdout?.Close(); } catch { }
|
|
try { _stdin?.Close(); } catch { }
|
|
try { _ff?.Dispose(); } catch { }
|
|
|
|
if (_readerThread != null)
|
|
{
|
|
try { _readerThread.Join(500); } catch { }
|
|
_readerThread = null;
|
|
}
|
|
|
|
_ff = null;
|
|
_stdin = null;
|
|
_stdout = null;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeProcess();
|
|
}
|
|
}
|