Files
meeting-assistant/MeetingAssistant/Recording/NaudioCaptureSource.cs
T
codex b9547ae4c4
PR and Push Build/Test / build-and-test (push) Successful in 12m29s
fix: recover from microphone disconnects
2026-08-03 15:57:30 +02:00

117 lines
3.6 KiB
C#

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<AudioChunk> CaptureAsync(CancellationToken cancellationToken)
{
return CaptureWith(capture, sourceName, logger, cancellationToken);
}
private static async IAsyncEnumerable<AudioChunk> CaptureWith(
IWaveIn capture,
string sourceName,
ILogger logger,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<AudioChunk>();
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<SystemAudioSource> logger;
public SystemAudioSource(ILogger<SystemAudioSource> logger)
{
this.logger = logger;
}
public IAsyncEnumerable<AudioChunk> CaptureAsync(CancellationToken cancellationToken)
{
return CaptureAsync(new MeetingAssistantOptions(), cancellationToken);
}
public IAsyncEnumerable<AudioChunk> 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);
}
}