using System.Threading.Channels; using NAudio.Wave; namespace MeetingAssistant.Workflow; public interface IWorkflowRulesEditorSamplePlaybackQueue { Task QueueAsync(int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default); } public sealed class WorkflowRulesEditorSamplePlaybackQueue : IWorkflowRulesEditorSamplePlaybackQueue, IDisposable { private readonly Channel channel = Channel.CreateUnbounded(); private readonly ILogger logger; private readonly CancellationTokenSource cancellation = new(); private readonly Task playbackTask; public WorkflowRulesEditorSamplePlaybackQueue(ILogger logger) { this.logger = logger; playbackTask = Task.Run(PlayQueuedSamplesAsync); } public async Task QueueAsync( int sampleId, byte[] wavBytes, CancellationToken cancellationToken = default) { if (wavBytes.Length == 0) { return $"Sample {sampleId} is empty and was not queued."; } await channel.Writer.WriteAsync(new QueuedSample(sampleId, wavBytes), cancellationToken); return $"Queued sample {sampleId} for playback."; } public void Dispose() { cancellation.Cancel(); channel.Writer.TryComplete(); try { playbackTask.Wait(TimeSpan.FromSeconds(2)); } catch { } cancellation.Dispose(); } private async Task PlayQueuedSamplesAsync() { try { await foreach (var sample in channel.Reader.ReadAllAsync(cancellation.Token)) { try { await PlayAsync(sample, cancellation.Token); } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogWarning( exception, "Could not play queued speaker identity sample {SampleId}", sample.SampleId); } } } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { } } private static async Task PlayAsync(QueuedSample sample, CancellationToken cancellationToken) { await using var stream = new MemoryStream(sample.WavBytes, writable: false); using var reader = new WaveFileReader(stream); using var output = new WaveOutEvent(); var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); output.PlaybackStopped += (_, _) => completed.TrySetResult(); output.Init(reader); output.Play(); await using var registration = cancellationToken.Register(() => { output.Stop(); completed.TrySetCanceled(cancellationToken); }); await completed.Task; } private sealed record QueuedSample(int SampleId, byte[] WavBytes); }