Files
meeting-assistant/MeetingAssistant.Tests/MicrophoneAudioSourceTests.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

110 lines
4.0 KiB
C#

using MeetingAssistant.Recording;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class MicrophoneAudioSourceTests
{
[Fact]
public async Task CaptureMovesToNewlyResolvedMicrophoneWhenCurrentCaptureFails()
{
var captureSources = new SequenceMicrophoneCaptureSourceFactory(
new FailingAfterChunkAudioSource(Pcm16(1_000)),
new ActiveAudioSource(Pcm16(2_000)));
var source = new MicrophoneAudioSource(
captureSources,
NullLogger<MicrophoneAudioSource>.Instance,
TimeSpan.Zero);
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await using var chunks = source
.CaptureAsync(new MeetingAssistantOptions(), cancellation.Token)
.GetAsyncEnumerator(cancellation.Token);
Assert.True(await chunks.MoveNextAsync());
Assert.Equal(1_000, BitConverter.ToInt16(chunks.Current.Pcm));
Assert.True(await chunks.MoveNextAsync());
Assert.Equal(2_000, BitConverter.ToInt16(chunks.Current.Pcm));
Assert.Equal(2, captureSources.CaptureCreationCount);
await cancellation.CancelAsync();
}
[Fact]
public async Task CaptureWaitsForMicrophoneToBecomeAvailable()
{
var captureSources = new InitiallyUnavailableMicrophoneCaptureSourceFactory(
new ActiveAudioSource(Pcm16(3_000)));
var source = new MicrophoneAudioSource(
captureSources,
NullLogger<MicrophoneAudioSource>.Instance,
TimeSpan.Zero);
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await using var chunks = source
.CaptureAsync(new MeetingAssistantOptions(), cancellation.Token)
.GetAsyncEnumerator(cancellation.Token);
Assert.True(await chunks.MoveNextAsync());
Assert.Equal(3_000, BitConverter.ToInt16(chunks.Current.Pcm));
Assert.Equal(2, captureSources.CaptureCreationCount);
await cancellation.CancelAsync();
}
private static byte[] Pcm16(short sample)
{
return BitConverter.GetBytes(sample);
}
private sealed class SequenceMicrophoneCaptureSourceFactory(params IMeetingAudioSource[] sources)
: IMicrophoneCaptureSourceFactory
{
private readonly Queue<IMeetingAudioSource> sources = new(sources);
public int CaptureCreationCount { get; private set; }
public IMeetingAudioSource CreateCapture(MeetingAssistantOptions options)
{
CaptureCreationCount++;
return sources.Dequeue();
}
}
private sealed class InitiallyUnavailableMicrophoneCaptureSourceFactory(IMeetingAudioSource availableSource)
: IMicrophoneCaptureSourceFactory
{
public int CaptureCreationCount { get; private set; }
public IMeetingAudioSource CreateCapture(MeetingAssistantOptions options)
{
CaptureCreationCount++;
if (CaptureCreationCount == 1)
{
throw new InvalidOperationException("No microphone is currently available.");
}
return availableSource;
}
}
private sealed class FailingAfterChunkAudioSource(byte[] pcm) : IMeetingAudioSource
{
public async IAsyncEnumerable<AudioChunk> CaptureAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Yield();
yield return new AudioChunk(pcm, 16000, 1);
throw new InvalidOperationException("The active microphone was disconnected.");
}
}
private sealed class ActiveAudioSource(byte[] pcm) : IMeetingAudioSource
{
public async IAsyncEnumerable<AudioChunk> CaptureAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return new AudioChunk(pcm, 16000, 1);
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
}
}