114 lines
5.2 KiB
C#
114 lines
5.2 KiB
C#
using System.Runtime.InteropServices;
|
|
using Foundation;
|
|
using ObjCRuntime;
|
|
using VoiceCat.Audio;
|
|
|
|
namespace VoiceCat.Mac;
|
|
|
|
internal static class CoreAudioDevices
|
|
{
|
|
private const string CoreAudio = "/System/Library/Frameworks/CoreAudio.framework/CoreAudio";
|
|
private const uint SystemObject = 1;
|
|
private const uint Devices = 0x64657623; // 'dev#'
|
|
private const uint DefaultInput = 0x64496e20; // 'dIn '
|
|
private const uint DefaultOutput = 0x644f7574; // 'dOut'
|
|
private const uint StreamConfiguration = 0x736c6179; // 'slay'
|
|
private const uint ObjectName = 0x6c6e616d; // 'lnam'
|
|
private const uint ScopeGlobal = 0x676c6f62; // 'glob'
|
|
private const uint ScopeInput = 0x696e7074; // 'inpt'
|
|
private const uint ScopeOutput = 0x6f757470; // 'outp'
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct PropertyAddress(uint selector, uint scope)
|
|
{
|
|
public uint Selector = selector;
|
|
public uint Scope = scope;
|
|
public uint Element;
|
|
}
|
|
|
|
[DllImport(CoreAudio)]
|
|
private static extern int AudioObjectGetPropertyDataSize(uint objectId, ref PropertyAddress address,
|
|
uint qualifierDataSize, nint qualifierData, out uint dataSize);
|
|
|
|
[DllImport(CoreAudio)]
|
|
private static extern int AudioObjectGetPropertyData(uint objectId, ref PropertyAddress address,
|
|
uint qualifierDataSize, nint qualifierData, ref uint dataSize, nint data);
|
|
|
|
internal static IReadOnlyList<AudioDeviceInfo> List(bool input)
|
|
{
|
|
uint defaultId = ReadUInt(SystemObject, input ? DefaultInput : DefaultOutput, ScopeGlobal);
|
|
var devices = new List<AudioDeviceInfo>();
|
|
foreach (uint id in ReadUIntArray(SystemObject, Devices, ScopeGlobal))
|
|
{
|
|
if (ChannelCount(id, input ? ScopeInput : ScopeOutput) == 0) continue;
|
|
string name = ReadString(id, ObjectName, ScopeGlobal) ?? $"Core Audio device {id}";
|
|
devices.Add(new(id.ToString(System.Globalization.CultureInfo.InvariantCulture), name, id == defaultId));
|
|
}
|
|
return devices.OrderByDescending(device => device.IsDefault).ThenBy(device => device.Name, StringComparer.CurrentCultureIgnoreCase).ToArray();
|
|
}
|
|
|
|
internal static uint ParseId(string? value)
|
|
{
|
|
if (!uint.TryParse(value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out uint id) || id == 0)
|
|
throw new ArgumentException("The selected Core Audio device is no longer available.", nameof(value));
|
|
return id;
|
|
}
|
|
|
|
private static uint ReadUInt(uint objectId, uint selector, uint scope)
|
|
{
|
|
var address = new PropertyAddress(selector, scope);
|
|
uint size = sizeof(uint);
|
|
nint memory = Marshal.AllocHGlobal(sizeof(uint));
|
|
try { return AudioObjectGetPropertyData(objectId, ref address, 0, 0, ref size, memory) == 0 ? unchecked((uint)Marshal.ReadInt32(memory)) : 0; }
|
|
finally { Marshal.FreeHGlobal(memory); }
|
|
}
|
|
|
|
private static uint[] ReadUIntArray(uint objectId, uint selector, uint scope)
|
|
{
|
|
var address = new PropertyAddress(selector, scope);
|
|
if (AudioObjectGetPropertyDataSize(objectId, ref address, 0, 0, out uint size) != 0 || size < sizeof(uint)) return [];
|
|
nint memory = Marshal.AllocHGlobal(checked((int)size));
|
|
try
|
|
{
|
|
if (AudioObjectGetPropertyData(objectId, ref address, 0, 0, ref size, memory) != 0) return [];
|
|
var result = new uint[size / sizeof(uint)];
|
|
for (int i = 0; i < result.Length; i++) result[i] = unchecked((uint)Marshal.ReadInt32(memory, i * sizeof(uint)));
|
|
return result;
|
|
}
|
|
finally { Marshal.FreeHGlobal(memory); }
|
|
}
|
|
|
|
private static int ChannelCount(uint deviceId, uint scope)
|
|
{
|
|
var address = new PropertyAddress(StreamConfiguration, scope);
|
|
if (AudioObjectGetPropertyDataSize(deviceId, ref address, 0, 0, out uint size) != 0 || size < 8) return 0;
|
|
nint memory = Marshal.AllocHGlobal(checked((int)size));
|
|
try
|
|
{
|
|
if (AudioObjectGetPropertyData(deviceId, ref address, 0, 0, ref size, memory) != 0) return 0;
|
|
int count = Marshal.ReadInt32(memory);
|
|
int first = IntPtr.Size == 8 ? 8 : 4;
|
|
int stride = IntPtr.Size == 8 ? 16 : 12;
|
|
int channels = 0;
|
|
for (int i = 0; i < count && first + i * stride + sizeof(uint) <= size; i++)
|
|
channels += Marshal.ReadInt32(memory, first + i * stride);
|
|
return channels;
|
|
}
|
|
finally { Marshal.FreeHGlobal(memory); }
|
|
}
|
|
|
|
private static string? ReadString(uint objectId, uint selector, uint scope)
|
|
{
|
|
var address = new PropertyAddress(selector, scope);
|
|
uint size = checked((uint)IntPtr.Size);
|
|
nint memory = Marshal.AllocHGlobal(IntPtr.Size);
|
|
try
|
|
{
|
|
if (AudioObjectGetPropertyData(objectId, ref address, 0, 0, ref size, memory) != 0) return null;
|
|
nint handle = Marshal.ReadIntPtr(memory);
|
|
return handle == 0 ? null : Runtime.GetNSObject<NSString>(handle, owns: false)?.ToString();
|
|
}
|
|
finally { Marshal.FreeHGlobal(memory); }
|
|
}
|
|
}
|