You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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);
    }
}

关键注意事项

  1. 线程同步:Core Audio的回调是在后台线程触发的,如果要更新UI(比如WinForms/WPF),记得用Dispatcher.Invoke(WPF)或Control.Invoke(WinForms)切换到UI线程执行。
  2. COM对象释放:所有获取到的COM对象(比如IAudioSessionControl)必须手动释放,否则会造成内存泄漏。
  3. 权限问题:获取系统进程信息可能需要管理员权限,否则部分系统级会话(比如系统声音)可能无法获取进程名称。
  4. 异常捕获:处理进程信息时要注意,有些会话可能已经结束,Process.GetProcessById会抛出异常,一定要做好捕获。

这样就彻底摆脱无限循环了,系统主动推送通知,效率和实时性都比轮询强太多!

内容的提问来源于stack exchange,提问作者Alfie Jones

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:54:10