Public Access
126 lines
4.0 KiB
C#
126 lines
4.0 KiB
C#
using System.Threading.Channels;
|
|
using NAudio.CoreAudioApi;
|
|
using NAudio.Wave;
|
|
|
|
namespace MeetingAssistant.Recording;
|
|
|
|
public sealed class MicrophoneAudioSource : IMeetingAudioSource
|
|
{
|
|
private readonly ILogger<MicrophoneAudioSource> logger;
|
|
|
|
public MicrophoneAudioSource(ILogger<MicrophoneAudioSource> logger)
|
|
{
|
|
this.logger = logger;
|
|
}
|
|
|
|
public IAsyncEnumerable<AudioChunk> CaptureAsync(CancellationToken cancellationToken)
|
|
{
|
|
return CaptureAsync(new MeetingAssistantOptions(), cancellationToken);
|
|
}
|
|
|
|
public IAsyncEnumerable<AudioChunk> CaptureAsync(
|
|
MeetingAssistantOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return CaptureAsync(new WasapiCapture(), options, cancellationToken);
|
|
}
|
|
|
|
private IAsyncEnumerable<AudioChunk> 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<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 MicrophoneAudioSource.CaptureWith(capture, "system", logger, cancellationToken);
|
|
}
|
|
}
|