Files

112 lines
5.9 KiB
C#
Raw Permalink Normal View History

using System.Runtime.InteropServices;
using VoiceCat.Audio;
using static VoiceCat.App.Audio.InputDeviceCapture;
namespace VoiceCat.App.Audio;
public sealed class WasapiAudioBackend : IAudioDeviceBackend
{
public IReadOnlyList<AudioDeviceInfo> Enumerate(bool input) => InputDeviceEnumerator.List(input).Select(d => new AudioDeviceInfo(d.Id, d.Name, d.IsDefault)).ToArray();
public IAudioCapture OpenCapture(string? deviceId, bool loopback, CapturePcmHandler handler)
{
var capture = new InputDeviceCapture(NormalizeDeviceId(deviceId), loopback);
capture.PcmFrameReady += (pcm, _, channels) => handler(pcm, channels);
if (!capture.Start()) { capture.Dispose(); throw new InvalidOperationException("WASAPI capture could not start."); }
return new Capture(capture);
}
public IAudioPlayback OpenPlayback(string? deviceId = null) => new Playback(deviceId);
// The old miniaudio ABI persisted its raw device union as hex. Its Windows member
// is a null-terminated UTF-16 WASAPI endpoint id; preserve saved selections.
internal static string? NormalizeDeviceId(string? id)
{
if (string.IsNullOrEmpty(id)) return null;
if (id.StartsWith('{')) return id;
try
{
byte[] bytes = Convert.FromHexString(id);
string decoded = System.Text.Encoding.Unicode.GetString(bytes).Split('\0')[0];
return decoded.StartsWith('{') ? decoded : null;
}
catch (FormatException) { return id; }
}
private sealed class Capture(InputDeviceCapture capture) : IAudioCapture { public void Dispose() => capture.Dispose(); }
private sealed class Playback : IAudioPlayback
{
private readonly string? deviceId;
private readonly AdaptivePcmBuffer pcm = new(2);
private readonly ManualResetEventSlim initialized = new(false);
private readonly Thread thread;
private volatile bool running = true;
private bool ready;
private int disposed;
internal Playback(string? deviceId)
{
this.deviceId = deviceId;
thread = new Thread(Work) { IsBackground = true, Name = "VoiceCat WASAPI playback" };
thread.Start();
if (!initialized.Wait(5000) || !ready) { Dispose(); throw new InvalidOperationException("WASAPI playback could not start."); }
}
public int BufferMilliseconds { get => pcm.BufferMilliseconds; set => pcm.BufferMilliseconds = value; }
public void Write(ReadOnlySpan<short> stereoPcm) => pcm.TryWrite(stereoPcm);
private unsafe void Work()
{
IMMDeviceEnumerator? enumerator = null; IMMDevice? device = null; IAudioClient? client = null; IAudioRenderClient? render = null;
using var bufferReady = new AutoResetEvent(false);
try
{
enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;
int result = deviceId is null ? enumerator.GetDefaultAudioEndpoint(0, 0, out device) : enumerator.GetDevice(deviceId, out device);
if (result < 0 || device is null) return;
Guid iid = typeof(IAudioClient).GUID;
if (device.Activate(ref iid, 0x17, 0, out object audio) < 0) return;
client = (IAudioClient)audio;
WaveFormat format = new() { Format = 1, Channels = 2, Samples = 48000, Bytes = 192000, Align = 4, Bits = 16 };
if (client.Initialize(0, 0x00040000u | 0x80000000u | 0x08000000u, 600000, 0, (nint)(&format), 0) < 0) return;
if (client.GetBufferSize(out uint capacity) < 0 || client.SetEventHandle(bufferReady.SafeWaitHandle.DangerousGetHandle()) < 0) return;
iid = typeof(IAudioRenderClient).GUID;
if (client.GetService(ref iid, out object output) < 0) return;
render = (IAudioRenderClient)output;
if (client.Start() < 0) return;
ready = true; initialized.Set();
while (running)
{
bufferReady.WaitOne(20); // Scheduling wait is outside the buffer-fill cycle.
if (!running || client.GetCurrentPadding(out uint padding) < 0) break;
uint frames = capacity - Math.Min(capacity, padding);
if (frames == 0) continue;
if (render.GetBuffer(frames, out nint buffer) < 0) break;
var destination = new Span<short>((void*)buffer, checked((int)frames * 2));
int count = pcm.Read(destination); destination[count..].Clear();
if (render.ReleaseBuffer(frames, 0) < 0) break;
}
}
catch { ready = false; }
finally
{
initialized.Set();
try { client?.Stop(); } catch { }
if (render is not null) Marshal.ReleaseComObject(render);
if (client is not null) Marshal.ReleaseComObject(client);
if (device is not null) Marshal.ReleaseComObject(device);
if (enumerator is not null) Marshal.ReleaseComObject(enumerator);
}
}
public void Dispose()
{
if (Interlocked.Exchange(ref disposed, 1) != 0) return;
running = false;
if (!thread.Join(5000)) throw new TimeoutException("WASAPI playback did not stop.");
initialized.Dispose();
}
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat { internal ushort Format, Channels; internal uint Samples, Bytes; internal ushort Align, Bits, Extra; }
[ComImport, Guid("F294ACFC-3146-4483-A7BF-ADDCA7C260E2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioRenderClient
{
[PreserveSig] int GetBuffer(uint frames, out nint data);
[PreserveSig] int ReleaseBuffer(uint frames, uint flags);
}
}