Public Access
Archive completed meeting assistant changes
PR and Push Build/Test / build-and-test (push) Successful in 9m19s
PR and Push Build/Test / build-and-test (push) Successful in 9m19s
This commit is contained in:
@@ -66,32 +66,22 @@ public sealed class FileOfflineTranscriptionBacklog : IOfflineTranscriptionBackl
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task CompleteAsync(string id, CancellationToken cancellationToken)
|
||||
public Task CompleteAsync(OfflineTranscriptionBacklogItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var folder = GetBacklogFolder(options);
|
||||
var path = GetItemPath(folder, id);
|
||||
var path = GetItemPath(folder, item.Id);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OfflineTranscriptionBacklogItem? item = null;
|
||||
try
|
||||
{
|
||||
item = JsonSerializer.Deserialize<OfflineTranscriptionBacklogItem>(
|
||||
await File.ReadAllTextAsync(path, cancellationToken),
|
||||
JsonOptions);
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not read completed offline transcription backlog item {BacklogItemPath}", path);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
File.Delete(path);
|
||||
if (item is not null && File.Exists(item.AudioPath))
|
||||
if (File.Exists(item.AudioPath))
|
||||
{
|
||||
DeleteCompletedAudio(item.AudioPath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string GetBacklogFolder(MeetingAssistantOptions options)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public interface IMicrophoneDeviceProvider
|
||||
{
|
||||
IReadOnlyList<MicrophoneDevice> GetAvailableMicrophones();
|
||||
|
||||
MicrophoneDeviceSnapshot GetMicrophoneSnapshot(MeetingAssistantOptions options);
|
||||
|
||||
IWaveIn CreateCapture(MeetingAssistantOptions options);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
private readonly ILaunchProfileOptionsProvider? launchProfiles;
|
||||
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
|
||||
private readonly IMeetingScreenshotService screenshotService;
|
||||
private readonly IMeetingNoteImageOcrService meetingNoteImageOcrService;
|
||||
private readonly IMeetingRunArtifactCleaner artifactCleaner;
|
||||
private readonly IMeetingInactivityPromptService inactivityPromptService;
|
||||
private readonly IMeetingInactivityClock inactivityClock;
|
||||
@@ -58,6 +59,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
ILaunchProfileOptionsProvider? launchProfiles = null,
|
||||
IMeetingWorkflowEngine? meetingWorkflowEngine = null,
|
||||
IMeetingScreenshotService? screenshotService = null,
|
||||
IMeetingNoteImageOcrService? meetingNoteImageOcrService = null,
|
||||
IMeetingRunArtifactCleaner? artifactCleaner = null,
|
||||
IMeetingInactivityPromptService? inactivityPromptService = null,
|
||||
IMeetingInactivityClock? inactivityClock = null,
|
||||
@@ -78,6 +80,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
this.launchProfiles = launchProfiles;
|
||||
this.meetingWorkflowEngine = meetingWorkflowEngine ?? NoopMeetingWorkflowEngine.Instance;
|
||||
this.screenshotService = screenshotService ?? NoopMeetingScreenshotService.Instance;
|
||||
this.meetingNoteImageOcrService = meetingNoteImageOcrService ?? NoopMeetingNoteImageOcrService.Instance;
|
||||
this.artifactCleaner = artifactCleaner ?? new MeetingRunArtifactCleaner();
|
||||
this.inactivityPromptService = inactivityPromptService ?? new NoopMeetingInactivityPromptService();
|
||||
this.inactivityClock = inactivityClock ?? new SystemMeetingInactivityClock();
|
||||
@@ -142,12 +145,28 @@ public sealed class MeetingRecordingCoordinator
|
||||
|
||||
public async Task<RecordingStatus> StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await StartAsync(null, cancellationToken);
|
||||
return await StartAsync(null, null, suppressMetadataLookup: false, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RecordingStatus> StartFromPromptAsync(
|
||||
MeetingMetadata? metadata,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await StartAsync(null, metadata, suppressMetadataLookup: true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RecordingStatus> StartAsync(
|
||||
string? launchProfileName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await StartAsync(launchProfileName, null, suppressMetadataLookup: false, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<RecordingStatus> StartAsync(
|
||||
string? launchProfileName,
|
||||
MeetingMetadata? suppliedMetadata,
|
||||
bool suppressMetadataLookup,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await gate.WaitAsync(cancellationToken);
|
||||
try
|
||||
@@ -164,14 +183,16 @@ public sealed class MeetingRecordingCoordinator
|
||||
var recordedAudio = await recordedAudioStore.CreateSessionAsync(runOptions, cancellationToken);
|
||||
var assistantContextPath = GetAssistantContextPath(startedAt, runOptions);
|
||||
var summaryPath = GetSummaryPath(startedAt, runOptions);
|
||||
var meetingNote = MeetingNoteTemplate.Create(
|
||||
title: $"Meeting {startedAt:yyyy-MM-dd HH:mm}",
|
||||
startTime: startedAt,
|
||||
attendees: [],
|
||||
transcriptPath: currentSession.TranscriptPath,
|
||||
assistantContextPath: assistantContextPath,
|
||||
summaryPath: summaryPath);
|
||||
|
||||
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
||||
MeetingNoteTemplate.Create(
|
||||
title: $"Meeting {startedAt:yyyy-MM-dd HH:mm}",
|
||||
startTime: startedAt,
|
||||
attendees: [],
|
||||
transcriptPath: currentSession.TranscriptPath,
|
||||
assistantContextPath: assistantContextPath,
|
||||
summaryPath: summaryPath),
|
||||
meetingNote,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
currentArtifacts = new MeetingSessionArtifacts(
|
||||
@@ -179,12 +200,32 @@ public sealed class MeetingRecordingCoordinator
|
||||
currentSession.TranscriptPath,
|
||||
assistantContextPath,
|
||||
summaryPath);
|
||||
await meetingArtifactStore.CreateAssistantContextAsync(currentArtifacts, currentMeetingNote, "", null, cancellationToken);
|
||||
await meetingArtifactStore.CreateAssistantContextAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
"",
|
||||
null,
|
||||
cancellationToken);
|
||||
await meetingWorkflowEngine.RunAsync(
|
||||
MeetingWorkflowEvent.Created(currentArtifacts),
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
currentMeetingNote = await meetingNoteStore.ReadAsync(currentMeetingNote.Path, cancellationToken);
|
||||
if (suppliedMetadata is not null)
|
||||
{
|
||||
await ApplyMeetingMetadataAsync(currentMeetingNote, suppliedMetadata, runOptions, cancellationToken);
|
||||
currentMeetingNote = await meetingNoteStore.SaveAsync(
|
||||
currentMeetingNote,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
await meetingArtifactStore.UpdateAssistantContextMetadataAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
suppliedMetadata.Agenda,
|
||||
suppliedMetadata.ScheduledEnd,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await meetingArtifactStore.UpdateAssistantContextMeetingAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
@@ -226,9 +267,20 @@ public sealed class MeetingRecordingCoordinator
|
||||
}
|
||||
|
||||
currentRun = run;
|
||||
_ = Task.Run(
|
||||
() => ApplyMeetingMetadataWhenAvailableAsync(startedAt, run),
|
||||
CancellationToken.None);
|
||||
if (suppliedMetadata is null && !suppressMetadataLookup)
|
||||
{
|
||||
_ = Task.Run(
|
||||
() => ApplyMeetingMetadataWhenAvailableAsync(startedAt, run),
|
||||
CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await TransitionMeetingAsync(
|
||||
run,
|
||||
AssistantContextState.Transcribing,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("Meeting recording started");
|
||||
|
||||
return CurrentStatus;
|
||||
@@ -445,7 +497,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
"System",
|
||||
$"Transcription profile changed to {targetProfile.Name}.");
|
||||
$"Transcription profile changed to {targetProfile.Name}. Speaker recognition and identities reset.");
|
||||
run.AddLiveSegment(marker);
|
||||
await AppendTranscriptSegmentAsync(run, marker, cancellationToken);
|
||||
|
||||
@@ -642,14 +694,23 @@ public sealed class MeetingRecordingCoordinator
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var formatted = TranscriptLineFormatter.Format(segment);
|
||||
var lineReference = segment.ReplacesMarkerId is { } replacesMarkerId &&
|
||||
run.TryTakeTranscriptMarker(replacesMarkerId, out var markerReference)
|
||||
? await ReplaceTranscriptMarkerAsync(run, markerReference, formatted, cancellationToken)
|
||||
: segment.Kind == TranscriptionSegmentKind.Marker &&
|
||||
segment.MarkerId is { } existingMarkerId &&
|
||||
run.TryTakeTranscriptMarker(existingMarkerId, out var existingMarkerReference)
|
||||
? await ReplaceTranscriptMarkerAsync(run, existingMarkerReference, formatted, cancellationToken)
|
||||
: await transcriptStore.AppendLineAsync(run.Session, formatted.Line, cancellationToken);
|
||||
TranscriptLineReference lineReference;
|
||||
if (segment.ReplacesMarkerId is { } replacesMarkerId &&
|
||||
run.TryTakeTranscriptMarker(replacesMarkerId, out var markerReference))
|
||||
{
|
||||
lineReference = await ReplaceTranscriptMarkerAsync(run, markerReference, formatted, cancellationToken);
|
||||
}
|
||||
else if (segment.Kind == TranscriptionSegmentKind.Marker &&
|
||||
segment.MarkerId is { } existingMarkerId &&
|
||||
run.TryTakeTranscriptMarker(existingMarkerId, out var existingMarkerReference))
|
||||
{
|
||||
lineReference = await ReplaceTranscriptMarkerAsync(run, existingMarkerReference, formatted, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
lineReference = await transcriptStore.AppendLineAsync(run.Session, formatted.Line, cancellationToken);
|
||||
}
|
||||
|
||||
if (segment.Kind == TranscriptionSegmentKind.Marker && segment.MarkerId is { } markerId)
|
||||
{
|
||||
run.SetTranscriptMarker(markerId, lineReference);
|
||||
@@ -1505,6 +1566,10 @@ public sealed class MeetingRecordingCoordinator
|
||||
|
||||
try
|
||||
{
|
||||
await meetingNoteImageOcrService.ProcessMeetingNoteImageEmbedsAsync(
|
||||
run.Artifacts,
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
await screenshotService.WaitForPendingOcrAsync(
|
||||
run.Artifacts,
|
||||
run.Options.Screenshots.Ocr.GetEffectiveTimeout(),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed record MicrophoneDevice(
|
||||
string Id,
|
||||
string Name);
|
||||
|
||||
public sealed record MicrophoneDeviceSnapshot(
|
||||
IReadOnlyList<MicrophoneDevice> Available,
|
||||
MicrophoneDevice? Current);
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed class MicrophoneDeviceSelection
|
||||
{
|
||||
private readonly object sync = new();
|
||||
private string? selectedDeviceId;
|
||||
|
||||
public string? SelectedDeviceId
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
return selectedDeviceId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Select(string deviceId)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
selectedDeviceId = string.IsNullOrWhiteSpace(deviceId)
|
||||
? null
|
||||
: deviceId.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
public MicrophoneDevice? Resolve(
|
||||
string? configuredDeviceId,
|
||||
MicrophoneDevice? defaultDevice,
|
||||
IReadOnlyList<MicrophoneDevice> availableDevices)
|
||||
{
|
||||
var selected = SelectedDeviceId;
|
||||
return FindById(availableDevices, selected) ??
|
||||
FindById(availableDevices, configuredDeviceId) ??
|
||||
defaultDevice;
|
||||
}
|
||||
|
||||
private static MicrophoneDevice? FindById(
|
||||
IReadOnlyList<MicrophoneDevice> devices,
|
||||
string? id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return devices.FirstOrDefault(device => string.Equals(
|
||||
device.Id,
|
||||
id.Trim(),
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,14 @@ namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed class MicrophoneAudioSource : IMeetingAudioSource
|
||||
{
|
||||
private readonly IMicrophoneDeviceProvider microphones;
|
||||
private readonly ILogger<MicrophoneAudioSource> logger;
|
||||
|
||||
public MicrophoneAudioSource(ILogger<MicrophoneAudioSource> logger)
|
||||
public MicrophoneAudioSource(
|
||||
IMicrophoneDeviceProvider microphones,
|
||||
ILogger<MicrophoneAudioSource> logger)
|
||||
{
|
||||
this.microphones = microphones;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@@ -22,7 +26,7 @@ public sealed class MicrophoneAudioSource : IMeetingAudioSource
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return CaptureAsync(new WasapiCapture(), options, cancellationToken);
|
||||
return CaptureAsync(microphones.CreateCapture(options), options, cancellationToken);
|
||||
}
|
||||
|
||||
private IAsyncEnumerable<AudioChunk> CaptureAsync(
|
||||
|
||||
@@ -6,7 +6,7 @@ public interface IOfflineTranscriptionBacklog
|
||||
|
||||
Task<IReadOnlyList<OfflineTranscriptionBacklogItem>> ListAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task CompleteAsync(string id, CancellationToken cancellationToken);
|
||||
Task CompleteAsync(OfflineTranscriptionBacklogItem item, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record OfflineTranscriptionBacklogItem(
|
||||
@@ -38,7 +38,7 @@ public sealed class NoopOfflineTranscriptionBacklog : IOfflineTranscriptionBackl
|
||||
return Task.FromResult<IReadOnlyList<OfflineTranscriptionBacklogItem>>([]);
|
||||
}
|
||||
|
||||
public Task CompleteAsync(string id, CancellationToken cancellationToken)
|
||||
public Task CompleteAsync(OfflineTranscriptionBacklogItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ using MeetingAssistant.Summary;
|
||||
using MeetingAssistant.Transcription;
|
||||
using MeetingAssistant.Workflow;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed class OfflineTranscriptionBacklogProcessor
|
||||
{
|
||||
private const int MaxChunkDurationMilliseconds = 1000;
|
||||
|
||||
private readonly IOfflineTranscriptionBacklog backlog;
|
||||
private readonly ISpeechRecognitionPipelineFactory pipelineFactory;
|
||||
private readonly ITranscriptStore transcriptStore;
|
||||
@@ -72,7 +69,7 @@ public sealed class OfflineTranscriptionBacklogProcessor
|
||||
try
|
||||
{
|
||||
await ProcessAsync(item, cancellationToken);
|
||||
await backlog.CompleteAsync(item.Id, cancellationToken);
|
||||
await backlog.CompleteAsync(item, cancellationToken);
|
||||
logger.LogInformation(
|
||||
"Completed offline transcription backlog item {BacklogItemId} for recording {RecordingPath}",
|
||||
item.Id,
|
||||
@@ -215,29 +212,9 @@ public sealed class OfflineTranscriptionBacklogProcessor
|
||||
ISpeechRecognitionPipeline pipeline,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var reader = new WaveFileReader(audioPath);
|
||||
var bytesPerSecond = reader.WaveFormat.AverageBytesPerSecond;
|
||||
var chunkSize = Math.Max(
|
||||
reader.WaveFormat.BlockAlign,
|
||||
bytesPerSecond * MaxChunkDurationMilliseconds / 1000);
|
||||
chunkSize -= chunkSize % reader.WaveFormat.BlockAlign;
|
||||
|
||||
var buffer = new byte[chunkSize];
|
||||
while (true)
|
||||
await foreach (var chunk in PcmWavAudioChunkReader.ReadChunksAsync(audioPath, cancellationToken: cancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var read = await reader.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await pipeline.WriteAsync(
|
||||
new AudioChunk(
|
||||
buffer[..read],
|
||||
reader.WaveFormat.SampleRate,
|
||||
reader.WaveFormat.Channels),
|
||||
cancellationToken);
|
||||
await pipeline.WriteAsync(chunk, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public static class PcmWavAudioChunkReader
|
||||
{
|
||||
private const int MaxChunkDurationMilliseconds = 1000;
|
||||
|
||||
public static async IAsyncEnumerable<AudioChunk> ReadChunksAsync(
|
||||
string path,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reader = new WaveFileReader(path);
|
||||
try
|
||||
{
|
||||
var format = reader.WaveFormat;
|
||||
if (format.Encoding != WaveFormatEncoding.Pcm || format.BitsPerSample != 16)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Only 16-bit PCM WAV files are supported. Found {format.Encoding} with {format.BitsPerSample} bits per sample.");
|
||||
}
|
||||
|
||||
var chunkSize = Math.Max(
|
||||
format.BlockAlign,
|
||||
format.AverageBytesPerSecond * MaxChunkDurationMilliseconds / 1000);
|
||||
chunkSize -= chunkSize % format.BlockAlign;
|
||||
|
||||
var buffer = new byte[chunkSize];
|
||||
int read;
|
||||
while ((read = await reader.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0)
|
||||
{
|
||||
yield return new AudioChunk(buffer[..read], format.SampleRate, format.Channels);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
reader.Dispose();
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
// NAudio can throw while disposing reader streams from async iterators after all chunks were read.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using NAudio.CoreAudioApi;
|
||||
using NAudio.Wave;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
|
||||
public sealed class WindowsMicrophoneDeviceProvider : IMicrophoneDeviceProvider
|
||||
{
|
||||
private readonly MicrophoneDeviceSelection selection;
|
||||
private readonly ILogger<WindowsMicrophoneDeviceProvider> logger;
|
||||
|
||||
public WindowsMicrophoneDeviceProvider(
|
||||
MicrophoneDeviceSelection selection,
|
||||
ILogger<WindowsMicrophoneDeviceProvider> logger)
|
||||
{
|
||||
this.selection = selection;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MicrophoneDevice> GetAvailableMicrophones()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var enumerator = new MMDeviceEnumerator();
|
||||
return enumerator
|
||||
.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active)
|
||||
.Select(ToMicrophoneDevice)
|
||||
.OrderBy(device => device.Name, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not enumerate microphone capture endpoints");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public MicrophoneDeviceSnapshot GetMicrophoneSnapshot(MeetingAssistantOptions options)
|
||||
{
|
||||
var devices = GetAvailableMicrophones();
|
||||
return new MicrophoneDeviceSnapshot(
|
||||
devices,
|
||||
selection.Resolve(options.Recording.MicrophoneDeviceId, GetDefaultMicrophone(), devices));
|
||||
}
|
||||
|
||||
public IWaveIn CreateCapture(MeetingAssistantOptions options)
|
||||
{
|
||||
var current = GetMicrophoneSnapshot(options).Current;
|
||||
if (current is null)
|
||||
{
|
||||
logger.LogInformation("Starting microphone capture from Windows default capture endpoint");
|
||||
return new WasapiCapture();
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Starting microphone capture from {MicrophoneName} ({MicrophoneDeviceId})",
|
||||
current.Name,
|
||||
current.Id);
|
||||
using var enumerator = new MMDeviceEnumerator();
|
||||
return new WasapiCapture(enumerator.GetDevice(current.Id));
|
||||
}
|
||||
|
||||
private static MicrophoneDevice? GetDefaultMicrophone()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var enumerator = new MMDeviceEnumerator();
|
||||
return ToMicrophoneDevice(enumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static MicrophoneDevice ToMicrophoneDevice(MMDevice device)
|
||||
{
|
||||
return new MicrophoneDevice(device.ID, device.FriendlyName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user