如何在C#中实现Windows Volume Mixer的音频会话监听功能?
在C#中实现类似Volume Mixer的功能(监听音频会话的启动/结束)
嘿,我完全懂你的痛点——用无限循环轮询音频会话不仅占CPU,实时性还拉胯。其实Windows Core Audio API早就提供了事件通知机制,能让系统主动告诉你什么时候有新音频会话启动或结束,根本不用傻等轮询!下面是具体的实现步骤和代码示例:
核心思路:借助IMMNotificationClient接口
Core Audio里的IMMNotificationClient是专门用来接收音频设备、会话相关通知的COM接口。我们只需要实现这个接口,再把它注册到系统的音频设备枚举器上,就能自动收到会话创建/销毁的回调了。
第一步:定义Core Audio相关的COM互操作接口
因为这些是原生Windows API,没有现成的.NET封装,所以我们需要先在C#里定义对应的接口:
using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; // 音频会话状态枚举 public enum AudioSessionState { AudioSessionStateInactive = 0, AudioSessionStateActive = 1, AudioSessionStateExpired = 2 } // 基础音频会话控制接口 [Guid("F4B1A599-7266-4319-A8CA-E70ACB11E8CD")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IAudioSessionControl { int GetState(out AudioSessionState state); int GetDisplayName(out string displayName); int SetDisplayName(string displayName, ref Guid eventContext); int GetIconPath(out string iconPath); int SetIconPath(string iconPath, ref Guid eventContext); int GetGroupingParam(out Guid groupingId); int SetGroupingParam(ref Guid groupingId, ref Guid eventContext); int RegisterAudioSessionNotification(IAudioSessionNotification sessionNotification); int UnregisterAudioSessionNotification(IAudioSessionNotification sessionNotification); } // 扩展接口,可获取进程ID等信息 [Guid("BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IAudioSessionControl2 : IAudioSessionControl { new int GetState(out AudioSessionState state); new int GetDisplayName(out string displayName); new int SetDisplayName(string displayName, ref Guid eventContext); new int GetIconPath(out string iconPath); new int SetIconPath(string iconPath, ref Guid eventContext); new int GetGroupingParam(out Guid groupingId); new int SetGroupingParam(ref Guid groupingId, ref Guid eventContext); new int RegisterAudioSessionNotification(IAudioSessionNotification sessionNotification); new int UnregisterAudioSessionNotification(IAudioSessionNotification sessionNotification); int GetSessionIdentifier(out string sessionId); int GetSessionInstanceIdentifier(out string sessionInstanceId); int GetProcessId(out uint processId); int IsSystemSoundsSession(); int SetDuckingPreference(bool enableDucking); } // 核心通知接口,监听会话创建/销毁 [Guid("7991EEC9-7E89-4D85-8390-6C703CEC60C0")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IMMNotificationClient { int OnDeviceStateChanged(string deviceId, int newState); int OnDeviceAdded(string deviceId); int OnDeviceRemoved(string deviceId); int OnDefaultDeviceChanged(int flow, int role, string defaultDeviceId); int OnPropertyValueChanged(string deviceId, PropertyKey key); // 会话创建时触发的回调 int OnSessionCreated(IAudioSessionControl newSession); // 会话销毁时触发的回调 int OnSessionDestroyed(IAudioSessionControl session); } // 辅助会话通知接口 [Guid("641DD20B-4D41-49CC-ABA3-174B9477BB08")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IAudioSessionNotification { int OnSessionCreated(IAudioSessionControl newSession); } // 音频设备枚举器接口 [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IMMDeviceEnumerator { int EnumAudioEndpoints(int dataFlow, int stateMask, out IMMDeviceCollection devices); int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device); int GetDevice(string deviceId, out IMMDevice device); int RegisterEndpointNotificationCallback(IMMNotificationClient client); int UnregisterEndpointNotificationCallback(IMMNotificationClient client); } // 音频设备接口 [Guid("D666063F-1587-4E43-81F1-B948E807363F")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IMMDevice { int Activate(ref Guid iid, int clsCtx, IntPtr activationParams, out object instance); int OpenPropertyStore(int stgmAccess, out IPropertyStore properties); int GetId(out string deviceId); int GetState(out int state); } // 音频设备集合接口 [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IMMDeviceCollection { int GetCount(out int count); int Item(int index, out IMMDevice device); } // 属性键结构体 [StructLayout(LayoutKind.Sequential)] public struct PropertyKey { public Guid fmtid; public uint pid; } // 创建设备枚举器的COM类 [ComImport] [Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] public class MMDeviceEnumerator { }
第二步:实现自定义通知客户端
接下来我们写自己的通知处理类,重点实现OnSessionCreated和OnSessionDestroyed方法:
public class AudioSessionNotificationClient : IMMNotificationClient { // 新会话创建时的处理逻辑 public int OnSessionCreated(IAudioSessionControl newSession) { try { // 转换为扩展接口,获取进程ID等详细信息 if (newSession is IAudioSessionControl2 sessionControl2) { sessionControl2.GetProcessId(out uint processId); sessionControl2.GetDisplayName(out string displayName); // 根据进程ID获取进程名称 var process = System.Diagnostics.Process.GetProcessById((int)processId); string processName = process.ProcessName; Console.WriteLine($"✅ 新音频会话启动:进程名={processName},显示名={displayName},进程ID={processId}"); // 这里可以添加你的业务逻辑,比如更新UI、记录会话等 } } catch (Exception ex) { Console.WriteLine($"❌ 处理会话创建出错:{ex.Message}"); } finally { // 必须释放COM对象,避免内存泄漏 Marshal.ReleaseComObject(newSession); } return 0; } // 会话销毁时的处理逻辑 public int OnSessionDestroyed(IAudioSessionControl session) { try { if (session is IAudioSessionControl2 sessionControl2) { sessionControl2.GetProcessId(out uint processId); var process = System.Diagnostics.Process.GetProcessById((int)processId); string processName = process.ProcessName; Console.WriteLine($"❌ 音频会话结束:进程名={processName},进程ID={processId}"); } } catch (Exception ex) { Console.WriteLine($"❌ 处理会话销毁出错:{ex.Message}"); } finally { Marshal.ReleaseComObject(session); } return 0; } // 以下是接口的其他方法,不需要处理直接返回0即可 public int OnDeviceStateChanged(string deviceId, int newState) => 0; public int OnDeviceAdded(string deviceId) => 0; public int OnDeviceRemoved(string deviceId) => 0; public int OnDefaultDeviceChanged(int flow, int role, string defaultDeviceId) => 0; public int OnPropertyValueChanged(string deviceId, PropertyKey key) => 0; }
第三步:注册通知并启动监听
最后,我们创建设备枚举器,注册通知客户端,就能开始监听会话变化了:
class Program { static void Main(string[] args) { // 创建设备枚举器实例 var enumerator = new MMDeviceEnumerator() as IMMDeviceEnumerator; var notificationClient = new AudioSessionNotificationClient(); // 注册通知回调 enumerator.RegisterEndpointNotificationCallback(notificationClient); Console.WriteLine("🎧 正在监听音频会话的启动和结束...按任意键退出"); Console.ReadKey(); // 退出时清理资源 enumerator.UnregisterEndpointNotificationCallback(notificationClient); Marshal.ReleaseComObject(enumerator); } }
关键注意事项
- 线程同步:Core Audio的回调是在后台线程触发的,如果要更新UI(比如WinForms/WPF),记得用
Dispatcher.Invoke(WPF)或Control.Invoke(WinForms)切换到UI线程执行。 - COM对象释放:所有获取到的COM对象(比如
IAudioSessionControl)必须手动释放,否则会造成内存泄漏。 - 权限问题:获取系统进程信息可能需要管理员权限,否则部分系统级会话(比如系统声音)可能无法获取进程名称。
- 异常捕获:处理进程信息时要注意,有些会话可能已经结束,
Process.GetProcessById会抛出异常,一定要做好捕获。
这样就彻底摆脱无限循环了,系统主动推送通知,效率和实时性都比轮询强太多!
内容的提问来源于stack exchange,提问作者Alfie Jones
相关产品推荐
相关产品推荐

