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 List(bool input) { uint defaultId = ReadUInt(SystemObject, input ? DefaultInput : DefaultOutput, ScopeGlobal); var devices = new List(); 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(handle, owns: false)?.ToString(); } finally { Marshal.FreeHGlobal(memory); } } }