using System.Diagnostics; using System.Threading; using NAudio.Wave; namespace AudioCore.Impl; public sealed class StemPlaybackEngine : IStemPlaybackEngine, IDisposable { private sealed class PipelineState : IDisposable { public IStemDecoder[] Decoders = Array.Empty(); public bool OutputStarted; public CancellationTokenSource? Cts; public Task? RenderTask; public void Dispose() { try { Cts?.Cancel(); } catch { } try { Cts?.Dispose(); } catch { } foreach (var d in Decoders) { try { d.Dispose(); } catch { } } Decoders = Array.Empty(); } } private readonly IStemDecoderFactory _stemDecoderFactory; private readonly IAudioOutputDevice _outputDevice; private readonly IAudioMixer _audioMixer; private readonly ITimeStretchEngine _timeStretchEngine; private readonly Lock _stateLock = new(); private PlaybackSession? _session; private MixerSettings? Mixer => _session?.Mixer; private LoopRegion _loopRegion = new(); private long _decodedFramePosition; private long _loopStartFrames; private long _loopEndFrames; private long _outputFramesWritten; private float _currentSpeed = 1.0f; private bool IsPlaying => _outputDevice.State == PlaybackState.Playing; private IProgressReporter? _progressReporter; private PipelineState? _pipeline; private long _pendingSeekFrames; public StemPlaybackEngine( IStemDecoderFactory stemDecoderFactory, IAudioOutputDevice outputDevice, IAudioMixer audioMixer, ITimeStretchEngine timeStretchEngine) { _stemDecoderFactory = stemDecoderFactory; _outputDevice = outputDevice; _audioMixer = audioMixer; _timeStretchEngine = timeStretchEngine; } public PlaybackSession? CurrentSession { get { lock (_stateLock) return _session; } } public async Task LoadSessionAsync(PlaybackSession session, IProgressReporter progress) { await StopAsync().ConfigureAwait(false); await _timeStretchEngine.Configure(session.Speed, CancellationToken.None).ConfigureAwait(false); lock (_stateLock) { _session = session; _progressReporter = progress; _currentSpeed = session.Speed.Speed; _loopRegion = session.Loop; if (_loopRegion.IsEnabled) { _loopStartFrames = TimeToFrames(_loopRegion.Start); _loopEndFrames = TimeToFrames(_loopRegion.End); } else { _loopStartFrames = 0; _loopEndFrames = 0; } _pendingSeekFrames = 0; _decodedFramePosition = 0; _outputFramesWritten = 0; } } public Task PlayAsync() { lock (_stateLock) { if (IsPlaying || _session is null) return Task.CompletedTask; if (_pipeline is not null) { return Task.CompletedTask; } _pipeline = new PipelineState { Decoders = _session.StemSet.Stems .Select(stem => _stemDecoderFactory.Create(stem)) .ToArray(), Cts = new CancellationTokenSource() }; foreach (var d in _pipeline.Decoders) { d.Reset(); d.Seek(_pendingSeekFrames); } _decodedFramePosition = _pendingSeekFrames; _outputFramesWritten = (long)(_decodedFramePosition / Math.Max(_currentSpeed, 0.0001f)); _pipeline.RenderTask = Task.Run(() => RenderLoopAsync(_pipeline, _pipeline.Cts.Token)); } return Task.CompletedTask; } public Task PauseAsync() { lock (_stateLock) { if (!IsPlaying) return Task.CompletedTask; _outputDevice.Pause(); if (_pipeline is not null && _pipeline.OutputStarted) _pipeline.OutputStarted = false; } return Task.CompletedTask; } public async Task StopAsync() { PipelineState? pipelineToDispose; lock (_stateLock) { if (!IsPlaying && _pipeline is null) return; _decodedFramePosition = 0; _pendingSeekFrames = 0; _outputFramesWritten = 0; pipelineToDispose = _pipeline; _pipeline = null; } if (pipelineToDispose is not null) { try { pipelineToDispose.Cts?.Cancel(); } catch { } var task = pipelineToDispose.RenderTask; if (task is not null && task.Id != Task.CurrentId) { try { await task.ConfigureAwait(false); } catch (OperationCanceledException) { } } pipelineToDispose.Dispose(); } _outputDevice.Stop(); } public Task SeekAsync(TimeSpan position) { var frameIndex = TimeToFrames(position); lock (_stateLock) { _pendingSeekFrames = frameIndex; if (_pipeline is not null) { foreach (var d in _pipeline.Decoders) d.Seek(frameIndex); _decodedFramePosition = frameIndex; _outputFramesWritten = (long)(_decodedFramePosition / Math.Max(_currentSpeed, 0.0001f)); } else { _decodedFramePosition = frameIndex; _outputFramesWritten = (long)(_decodedFramePosition / Math.Max(_currentSpeed, 0.0001f)); } } return Task.CompletedTask; } public async Task UpdatePlaybackSpeedAsync(PlaybackSpeedSettings settings) { lock (_stateLock) { _currentSpeed = settings.Speed; _outputFramesWritten = (long)(_decodedFramePosition / Math.Max(_currentSpeed, 0.0001f)); } Debug.Assert(_pipeline?.Cts != null); await _timeStretchEngine.Configure(settings, _pipeline!.Cts!.Token).ConfigureAwait(false); } public Task UpdateMixerAsync(MixerSettings settings) { lock (_stateLock) { if (_session is not null) _session.Mixer = settings; } return Task.CompletedTask; } public void SetLoop(TimeSpan start, TimeSpan end) { lock (_stateLock) { _loopRegion = new LoopRegion { IsEnabled = true, Start = start, End = end }; _loopStartFrames = TimeToFrames(start); _loopEndFrames = TimeToFrames(end); } } public void ClearLoop() { lock (_stateLock) { _loopRegion = new LoopRegion { IsEnabled = false, Start = TimeSpan.Zero, End = TimeSpan.Zero }; _loopStartFrames = 0; _loopEndFrames = 0; } } private bool _decodeCompleted; private async Task RenderLoopAsync(PipelineState pipeline, CancellationToken token) { if (!pipeline.OutputStarted) { _outputDevice.Start(); pipeline.OutputStarted = true; } _decodeCompleted = false; var decodeTask = DecodeLoopAsync(pipeline, token); var stretchTask = StretchLoopAsync(pipeline, token); // Wait for BOTH to finish naturally await Task.WhenAll(decodeTask, stretchTask).ConfigureAwait(false); if (pipeline.OutputStarted) { _outputDevice.Stop(); pipeline.OutputStarted = false; } } private async Task DecodeLoopAsync(PipelineState pipeline, CancellationToken token) { await Task.Yield(); var stemBlocks = new List(6); try { while (!token.IsCancellationRequested) { bool playing; MixerSettings? mixerSnapshot; IStemDecoder[] decodersSnapshot; long loopStart, loopEnd; bool loopEnabled; lock (_stateLock) { playing = IsPlaying; mixerSnapshot = Mixer; decodersSnapshot = pipeline.Decoders; loopStart = _loopStartFrames; loopEnd = _loopEndFrames; loopEnabled = _loopRegion.IsEnabled; } if (!playing || mixerSnapshot is null || decodersSnapshot.Length == 0) { await Task.Delay(5, token).ConfigureAwait(false); continue; } if (!await ReadStemsAsync(stemBlocks, decodersSnapshot, token).ConfigureAwait(false)) { DisposeStems(stemBlocks); break; } var mixed = _audioMixer.Mix(stemBlocks, mixerSnapshot); DisposeStems(stemBlocks); await _timeStretchEngine.IsReadyToAccept(token).ConfigureAwait(false); await _timeStretchEngine.Submit(mixed, token).ConfigureAwait(false); var nextPosition = mixed.SamplePosition + mixed.Frames; if (loopEnabled && loopEnd > loopStart && nextPosition >= loopEnd) { lock (_stateLock) _decodedFramePosition = loopEnd; break; } lock (_stateLock) _decodedFramePosition = nextPosition; } } catch { } finally { _decodeCompleted = true; } } private static void DisposeStems(List stemBlocks) { foreach (var b in stemBlocks) b.Dispose(); stemBlocks.Clear(); } private static async Task ReadStemsAsync( List stemBlocks, IStemDecoder[] decodersSnapshot, CancellationToken ct) { DisposeStems(stemBlocks); foreach (var decoder in decodersSnapshot) { var block = await decoder.DecodeNextBlockAsync(ct).ConfigureAwait(false); if (block is null) { DisposeStems(stemBlocks); return false; } stemBlocks.Add(block.Value); } return true; } private async Task StretchLoopAsync(PipelineState pipeline, CancellationToken token) { await Task.Yield(); try { var gotFirstBlock = false; while (!token.IsCancellationRequested) { var stretched = await _timeStretchEngine.Receive(token).ConfigureAwait(false); if (stretched.Buffer == null) { if (_decodeCompleted && gotFirstBlock) break; // fully drained await Task.Delay(1, token).ConfigureAwait(false); continue; } await _outputDevice.IsReadyToAccept(token).ConfigureAwait(false); _outputDevice.Write(stretched.Buffer.Span); gotFirstBlock = true; lock (_stateLock) { _outputFramesWritten += stretched.Frames; } try { long sourceFrames; lock (_stateLock) { sourceFrames = (long)(_outputFramesWritten * _currentSpeed); } double progress; lock (_stateLock) { var total = _session?.StemSet.TotalFrames ?? 1L; progress = (double)sourceFrames / Math.Max(total, 1L); } if (_progressReporter != null) await _progressReporter.ReportProgress(progress).ConfigureAwait(false); } catch { } try { stretched.Dispose(); } catch { } } } catch (OperationCanceledException) { } catch (Exception ex) { Debug.WriteLine($"StemPlaybackEngine: Error in PlaybackLoopAsync: {ex.Message}"); try { pipeline.Cts?.Cancel(); } catch { } } } private long TimeToFrames(TimeSpan time) { return (long)(time.TotalSeconds * _outputDevice.SampleRate); } public void Dispose() { _ = StopAsync(); if (_pipeline is not null) { try { _pipeline.Dispose(); } catch { } _pipeline = null; } } }