Port managed client audio and Windows application
This commit is contained in:
@@ -16,7 +16,7 @@ public sealed record InputDeviceInfo(string Id, string Name, bool IsDefault)
|
||||
/// the aux device is opened client-side and needs a WASAPI id, not a miniaudio one.</summary>
|
||||
public static class InputDeviceEnumerator
|
||||
{
|
||||
public static IReadOnlyList<InputDeviceInfo> List()
|
||||
public static IReadOnlyList<InputDeviceInfo> List(bool input = true)
|
||||
{
|
||||
var result = new List<InputDeviceInfo>();
|
||||
InputDeviceCapture.IMMDeviceEnumerator? enumerator = null;
|
||||
@@ -29,7 +29,7 @@ public static class InputDeviceEnumerator
|
||||
Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;
|
||||
|
||||
// Resolve the default capture endpoint id so the picker can flag it.
|
||||
if (enumerator.GetDefaultAudioEndpoint(1 /*eCapture*/, 0 /*eConsole*/,
|
||||
if (enumerator.GetDefaultAudioEndpoint(input ? 1 : 0, 0 /*eConsole*/,
|
||||
out var defDev) == 0 && defDev != null)
|
||||
{
|
||||
try { if (defDev.GetId(out string id) == 0) defaultId = id; }
|
||||
@@ -37,7 +37,7 @@ public static class InputDeviceEnumerator
|
||||
}
|
||||
|
||||
// DEVICE_STATE_ACTIVE = 0x1 — only currently-usable endpoints.
|
||||
if (enumerator.EnumAudioEndpoints(1 /*eCapture*/, 0x1, out collectionPtr) != 0
|
||||
if (enumerator.EnumAudioEndpoints(input ? 1 : 0, 0x1, out collectionPtr) != 0
|
||||
|| collectionPtr == IntPtr.Zero)
|
||||
return result;
|
||||
|
||||
@@ -107,6 +107,7 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
private const int FrameSamples = 960; // 20 ms
|
||||
|
||||
private readonly string? _deviceId; // null = system default capture endpoint
|
||||
private readonly bool _loopback;
|
||||
|
||||
private IAudioClient? _audioClient;
|
||||
private IAudioCaptureClient? _captureClient;
|
||||
@@ -123,8 +124,9 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
// Init-done signal: Set() by the capture thread after activation completes.
|
||||
private readonly ManualResetEventSlim _initDone = new(false);
|
||||
private bool _initOk;
|
||||
private int _disposed;
|
||||
|
||||
public InputDeviceCapture(string? deviceId) => _deviceId = deviceId;
|
||||
public InputDeviceCapture(string? deviceId, bool loopback = false) { _deviceId = deviceId; _loopback = loopback; }
|
||||
|
||||
/// <summary>Starts capture. Blocks until WASAPI activation completes (typically <100 ms).
|
||||
/// Returns false if the device cannot be opened.</summary>
|
||||
@@ -148,15 +150,13 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
{
|
||||
_running = false;
|
||||
_bufferEvent?.Set();
|
||||
_captureThread?.Join(500);
|
||||
try { _audioClient?.Stop(); } catch { /* device already gone */ }
|
||||
if (_captureThread is not null && !_captureThread.Join(5000)) throw new TimeoutException("Capture did not stop.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
|
||||
Stop();
|
||||
if (_captureClient != null) { Marshal.ReleaseComObject(_captureClient); _captureClient = null; }
|
||||
if (_audioClient != null) { Marshal.ReleaseComObject(_audioClient); _audioClient = null; }
|
||||
_bufferEvent?.Dispose();
|
||||
_initDone.Dispose();
|
||||
}
|
||||
@@ -165,10 +165,19 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
|
||||
private void CaptureThreadProc()
|
||||
{
|
||||
_initOk = ActivateAndStart();
|
||||
_initDone.Set();
|
||||
if (!_initOk) return;
|
||||
CaptureLoop();
|
||||
try
|
||||
{
|
||||
_initOk = ActivateAndStart();
|
||||
_initDone.Set();
|
||||
if (_initOk && _running) CaptureLoop();
|
||||
}
|
||||
catch { _initOk = false; _initDone.Set(); }
|
||||
finally
|
||||
{
|
||||
try { _audioClient?.Stop(); } catch { }
|
||||
if (_captureClient != null) { Marshal.ReleaseComObject(_captureClient); _captureClient = null; }
|
||||
if (_audioClient != null) { Marshal.ReleaseComObject(_audioClient); _audioClient = null; }
|
||||
}
|
||||
}
|
||||
|
||||
private bool ActivateAndStart()
|
||||
@@ -192,7 +201,7 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;
|
||||
|
||||
int hr = _deviceId is null
|
||||
? enumerator.GetDefaultAudioEndpoint(1 /*eCapture*/, 0 /*eConsole*/, out device)
|
||||
? enumerator.GetDefaultAudioEndpoint(_loopback ? 0 : 1, 0 /*eConsole*/, out device)
|
||||
: enumerator.GetDevice(_deviceId, out device);
|
||||
if (hr != 0 || device == null) return false;
|
||||
|
||||
@@ -220,7 +229,7 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
// AUDCLNT_STREAMFLAGS_EVENTCALLBACK = 0x00040000
|
||||
// AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM = 0x80000000
|
||||
// AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY = 0x08000000
|
||||
const uint streamFlags = 0x00040000u | 0x80000000u | 0x08000000u;
|
||||
uint streamFlags = 0x00040000u | 0x80000000u | 0x08000000u | (_loopback ? 0x00020000u : 0u);
|
||||
|
||||
foreach (int ch in new[] { 2, 1 })
|
||||
{
|
||||
@@ -327,9 +336,7 @@ public sealed class InputDeviceCapture : IDisposable
|
||||
|
||||
private void FlushFrame()
|
||||
{
|
||||
var copy = new short[_accumBuf.Length];
|
||||
_accumBuf.AsSpan().CopyTo(copy);
|
||||
PcmFrameReady?.Invoke(copy, FrameSamples, _channels);
|
||||
PcmFrameReady?.Invoke(_accumBuf, FrameSamples, _channels); // Borrowed until callback returns.
|
||||
_accumCount = 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user