using System.Diagnostics; using System.Runtime.InteropServices; namespace VoiceCat.App.Audio; // ── Scope types (mirror macOS ScreenAudioScope / ScreenAudioSelection) ──────── public abstract record AppAudioScope; public sealed record EntireDesktop : AppAudioScope; public sealed record OnlyApps(IReadOnlyList Pids, IReadOnlyList Names) : AppAudioScope; public sealed record AllExceptApps(IReadOnlyList Pids, IReadOnlyList Names) : AppAudioScope; public sealed record AudioAppInfo(int Pid, string DisplayName); // ── Enumerator ───────────────────────────────────────────────────────────────── public static class AudioSessionEnumerator { // Returns all user-facing apps: visible-window processes (primary, like macOS // SCShareableContent.current) plus any background audio-session-only processes // (e.g. Spotify in mini-player). Excludes VoiceCat itself and system processes. public static IReadOnlyList GetAudioApps() { var seen = new HashSet(); var result = new List(); int selfPid = Environment.ProcessId; // ── 1. Visible-window processes (EnumWindows) ───────────────────────── // Same set macOS ScreenCaptureKit exposes: all apps with at least one // visible top-level window. Shows apps even when not currently producing audio. EnumWindows((hWnd, _) => { if (!IsWindowVisible(hWnd)) return true; GetWindowThreadProcessId(hWnd, out uint pid); if (pid == 0 || pid == (uint)selfPid || !seen.Add((int)pid)) return true; try { var proc = Process.GetProcessById((int)pid); string name = proc.MainWindowTitle.Length > 0 ? $"{proc.ProcessName} — {proc.MainWindowTitle}" : proc.ProcessName; if (!string.IsNullOrEmpty(proc.ProcessName)) result.Add(new AudioAppInfo((int)pid, name)); } catch { /* process exited between EnumWindows and GetProcessById */ } return true; // continue enumeration }, IntPtr.Zero); // ── 2. Background audio-session processes (WASAPI, supplement) ──────── // Catches apps that produce audio but have no visible window (screen reader, // background music player, etc.). Silently skipped if WASAPI is unavailable. AppendAudioSessionApps(seen, selfPid, result); return result.OrderBy(a => a.DisplayName, StringComparer.OrdinalIgnoreCase).ToList(); } // ── EnumWindows P/Invoke ────────────────────────────────────────────────── private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); // ── WASAPI audio session supplement ────────────────────────────────────── private static void AppendAudioSessionApps(HashSet seen, int selfPid, List result) { IMMDeviceEnumerator? enumerator = null; IMMDevice? device = null; IAudioSessionManager2? manager = null; IAudioSessionEnumerator? sessions = null; try { enumerator = (IMMDeviceEnumerator)Activator.CreateInstance( Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!; enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out device); var mgr2Iid = new Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"); device.Activate(ref mgr2Iid, 0x17 /*CLSCTX_ALL*/, IntPtr.Zero, out object mgr); manager = (IAudioSessionManager2)mgr; manager.GetSessionEnumerator(out sessions); sessions.GetCount(out int count); for (int i = 0; i < count; i++) { IAudioSessionControl? ctrl = null; try { sessions.GetSession(i, out ctrl); var ctrl2 = (IAudioSessionControl2)ctrl; ctrl2.GetProcessId(out uint pid); int ipid = (int)pid; if (pid == 0 || ipid == selfPid || !seen.Add(ipid)) continue; try { var proc = Process.GetProcessById(ipid); if (!string.IsNullOrEmpty(proc.ProcessName)) result.Add(new AudioAppInfo(ipid, proc.ProcessName)); } catch { /* exited */ } } catch { /* stale session */ } finally { if (ctrl != null) Marshal.ReleaseComObject(ctrl); } } } catch { /* no audio device or WASAPI unavailable — ignore */ } finally { if (sessions != null) Marshal.ReleaseComObject(sessions); if (manager != null) Marshal.ReleaseComObject(manager); if (device != null) Marshal.ReleaseComObject(device); if (enumerator != null) Marshal.ReleaseComObject(enumerator); } } // ── COM interface declarations ───────────────────────────────────────────── [ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IMMDeviceEnumerator { [PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IntPtr devices); [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint); [PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device); [PreserveSig] int RegisterEndpointNotificationCallback(IntPtr client); [PreserveSig] int UnregisterEndpointNotificationCallback(IntPtr client); } [ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IMMDevice { [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); [PreserveSig] int OpenPropertyStore(int stgmAccess, out IntPtr propStore); [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); [PreserveSig] int GetState(out int state); } [ComImport, Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IAudioSessionManager2 { [PreserveSig] int GetAudioSessionControl(ref Guid audioSessionGuid, int streamFlags, out IAudioSessionControl session); [PreserveSig] int GetSimpleAudioVolume(ref Guid audioSessionGuid, int streamFlags, out IntPtr audioVolume); [PreserveSig] int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum); [PreserveSig] int RegisterSessionNotification(IntPtr notification); [PreserveSig] int UnregisterSessionNotification(IntPtr notification); [PreserveSig] int RegisterDuckNotification([MarshalAs(UnmanagedType.LPWStr)] string sessionID, IntPtr notification); [PreserveSig] int UnregisterDuckNotification(IntPtr notification); } [ComImport, Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IAudioSessionEnumerator { [PreserveSig] int GetCount(out int sessionCount); [PreserveSig] int GetSession(int sessionIndex, out IAudioSessionControl session); } [ComImport, Guid("F4B1A599-7266-4319-A8CA-E70ACB11E8CD"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IAudioSessionControl { [PreserveSig] int GetState(out int state); [PreserveSig] int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string name); [PreserveSig] int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name, ref Guid eventContext); [PreserveSig] int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string iconPath); [PreserveSig] int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string iconPath, ref Guid eventContext); [PreserveSig] int GetGroupingParam(out Guid groupingParam); [PreserveSig] int SetGroupingParam(ref Guid groupingParam, ref Guid eventContext); [PreserveSig] int RegisterAudioSessionNotification(IntPtr notification); [PreserveSig] int UnregisterAudioSessionNotification(IntPtr notification); } [ComImport, Guid("BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IAudioSessionControl2 : IAudioSessionControl { [PreserveSig] int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string id); [PreserveSig] int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string id); [PreserveSig] int GetProcessId(out uint pid); [PreserveSig] int IsSystemSoundsSession(); [PreserveSig] int SetDuckingPreference(bool optOut); } }