using System.Threading.Channels; using NAudio.CoreAudioApi; using NAudio.Wave; namespace MeetingAssistant.Recording; internal sealed class NaudioCaptureAudioSource : IMeetingAudioSource { private readonly IWaveIn capture; private readonly string sourceName; private readonly ILogger logger; public NaudioCaptureAudioSource( IWaveIn capture, string sourceName, ILogger logger) { this.capture = capture; this.sourceName = sourceName; this.logger = logger; } public IAsyncEnumerable CaptureAsync(CancellationToken cancellationToken) { return CaptureWith(capture, sourceName, logger, cancellationToken); } private static async IAsyncEnumerable CaptureWith( IWaveIn capture, string sourceName, ILogger logger, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { var channel = Channel.CreateUnbounded(); var chunks = 0; capture.DataAvailable += (_, args) => { if (args.BytesRecorded <= 0) { return; } var buffer = new byte[args.BytesRecorded]; Buffer.BlockCopy(args.Buffer, 0, buffer, 0, args.BytesRecorded); if (Interlocked.Increment(ref chunks) == 1) { logger.LogInformation( "Audio capture source {SourceName} produced first chunk: {ByteCount} bytes at {SampleRate} Hz/{Channels} channel(s)", sourceName, args.BytesRecorded, capture.WaveFormat.SampleRate, capture.WaveFormat.Channels); } channel.Writer.TryWrite(new AudioChunk(buffer, capture.WaveFormat.SampleRate, capture.WaveFormat.Channels)); }; capture.RecordingStopped += (_, args) => { if (args.Exception is not null) { channel.Writer.TryComplete(args.Exception); return; } channel.Writer.TryComplete(); }; try { using var registration = cancellationToken.Register(capture.StopRecording); logger.LogInformation( "Starting audio capture source {SourceName} at {SampleRate} Hz/{Channels} channel(s)", sourceName, capture.WaveFormat.SampleRate, capture.WaveFormat.Channels); capture.StartRecording(); await foreach (var chunk in channel.Reader.ReadAllAsync(cancellationToken)) { yield return chunk; } } finally { capture.Dispose(); } } } public sealed class SystemAudioSource : IMeetingAudioSource { private readonly ILogger logger; public SystemAudioSource(ILogger logger) { this.logger = logger; } public IAsyncEnumerable CaptureAsync(CancellationToken cancellationToken) { return CaptureAsync(new MeetingAssistantOptions(), cancellationToken); } public IAsyncEnumerable CaptureAsync( MeetingAssistantOptions options, CancellationToken cancellationToken) { var capture = new WasapiLoopbackCapture { WaveFormat = new WaveFormat(options.Recording.SampleRate, 16, options.Recording.Channels) }; return new NaudioCaptureAudioSource(capture, "system", logger).CaptureAsync(cancellationToken); } }