using System.Threading.Channels; using NAudio.CoreAudioApi; using NAudio.Wave; namespace MeetingAssistant.Recording; public sealed class MicrophoneAudioSource : IMeetingAudioSource { private readonly ILogger logger; public MicrophoneAudioSource(ILogger logger) { this.logger = logger; } public IAsyncEnumerable CaptureAsync(CancellationToken cancellationToken) { return CaptureAsync(new MeetingAssistantOptions(), cancellationToken); } public IAsyncEnumerable CaptureAsync( MeetingAssistantOptions options, CancellationToken cancellationToken) { return CaptureAsync(new WasapiCapture(), options, cancellationToken); } private IAsyncEnumerable CaptureAsync( IWaveIn capture, MeetingAssistantOptions options, CancellationToken cancellationToken) { capture.WaveFormat = new WaveFormat(options.Recording.SampleRate, 16, options.Recording.Channels); return CaptureWith(capture, "microphone", logger, cancellationToken); } internal 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 MicrophoneAudioSource.CaptureWith(capture, "system", logger, cancellationToken); } }