MMAudioDeviceApi 的托管封装






4.67/5 (6投票s)
一个围绕 MMAudioDeviceApi 的 C++/CLI 包装器,带有通知支持。
引言
Windows 多媒体设备 (MMDevice) API 使客户端能够发现音频端点设备。 你可以在互联网上找到一些关于如何从 C++ 使用 MMDeviceApi
的例子,但我没有在 .Net 环境中找到任何应用程序。 所以我在 C++/CLI 中写了一个包装类和一个 C# 示例来演示这些功能。
该程序可以在选定的设备上播放测试声音,并自动更新列表,例如通过控制面板或在物理插入新设备的情况下。
使用代码
将 AudioDeviceUtil
项目添加到你的 .Net 解决方案中并实例化 AudiDeviceManager
。 该类提供以下功能
public class AudiDeviceManager : IDisposable
{
public AudiDeviceManager();
public AudiDeviceManager(bool registerForNotification);
public string DefaultPlaybackDeviceName { get; set; }
public List<AudioDevice> PlaybackDevices { get; }
public bool RegisterForNotification { get; set; }
public event EventHandler<AudioDeviceNotificationEventArgs> AudioDeviceEvent;
public List<AudioDevice> UpdatePlaybackDeviceList();
}
DefaultPlaybackDeviceName
属性用于通过其友好名称获取或设置默认设备。 获取器和设置器最终会调用 C++ Api 中的相应方法:IMMDeviceEnumerator::
GetDefaultAudioEndpoint
或 IPolicyConfigVista::SetDefaultEndpoint
。
PlaybackDevices
用于访问我们当前管理器对象中存储的设备列表。
UpdatePlaybackDeviceList()
通过枚举系统上的设备来更新存储的列表。
该列表包含 AudioDevice
对象,我们在其中存储有关设备的一些信息。
System::String^ DeviceID;
System::String^ FriendlyName;
AudioDeviceStateType DeviceState;
bool IsDefaultDevice;
它可以通过其他信息扩展,例如角色或属性,但在此示例中我们不需要它们。
支持以下状态
public enum class AudioDeviceStateType
{
Active = 1,
Disabled = 2,
NotPresent = 4,
Unplugged = 8,
StateMaskAll = 15,
};
这也会出现在测试应用程序的列表框中。
使用 RegisterForNotification
属性,我们在 Api 中注册我们自己的回调方法。
并且 AudioDeviceEvent
将从我们的回调中触发。 该事件包含 AudioDeviceNotificationEventArgs
对象,其中 Reason
属性存储以下值之一
public enum class AudioDeviceNotificationEventType
{
DefaultDeviceChanged,
DeviceAdded,
DeviceRemoved,
DeviceStateChanged,
PropertyValueChanged
}
在此示例中,在接收到事件时,我们只需更新列表框的内容。
在设备上播放声音
如果我们想在不是默认设备的设备上播放声音,我们应该简单地将其设置为默认设备 (1.),播放声音 (2.) 并在播放结束 (SoundMakerEventArgs.EventType
) 后将其设置回原始设备 (3.)。 整个过程在 BackgroundWorker
线程(称为 soundMakerThread
)中运行
soundMakerThread.WorkerSupportsCancellation = true;
soundMakerThread.DoWork += new DoWorkEventHandler(soundMakerThread_DoWork);
soundTest.SoundMakerEvent += new EventHandler<SoundMakerEventArgs>(OnSoundMakerEvent);
}
代码示例中有很多跟踪条目,但这里我删除了它们
void soundMakerThread_DoWork(object sender, DoWorkEventArgs e)
{
DoWorkForSoundTestBackgroundWorker();
}
private void DoWorkForSoundTestBackgroundWorker()
{
//store original default device
string tmpDevice = audioDeviceSwitcher.DefaultPlaybackDeviceName;
//get choosen devices for the loop
string[] devices = new string[Properties.Settings.Default.AlarmDevices.Count];
// AlarmDevices might change during the loop
Properties.Settings.Default.AlarmDevices.CopyTo(devices, 0);
do
{
foreach (var device in devices)
{
if (device == string.Empty) continue;
if (soundMakerThread.CancellationPending) break;
// wait for stop player
if (!playerFreeEvent.WaitOne(loopSoundLimit)) {
soundTest.CancelSound();
}
// PlayerFree set nonsignaled
playerFreeEvent.Reset();
if (soundMakerThread.CancellationPending) break;
// 1. ---> Set new default device
audioDeviceSwitcher.DefaultPlaybackDeviceName = device;
if (device != audioDeviceSwitcher.DefaultPlaybackDeviceName)
{
// Error: device not set!
// Set PlayerFree signaled
playerFreeEvent.Set();
}
else
{
// 2. ---> Play sound
soundTest.MakeSound();
}
}
}
while (playInLoop && !soundMakerThread.CancellationPending);
if (!playerFreeEvent.WaitOne(loopSoundLimit)) {
// Player timeout occured. Try to stop playing...
soundTest.CancelSound();
}
// 3. ---> Set orig default device
audioDeviceSwitcher.DefaultPlaybackDeviceName = tmpDevice;
soundTest.EnableSound();
}
我们的播放器也是一个资源。 它只能在一个设备上运行,因此 playerFreeEvent
必须相应地设置
void OnSoundMakerEvent(object sender, SoundMakerEventArgs e)
{
switch (e.EventType)
{
case SoundMakerEventType.PlayStarted:
playerFreeEvent.Reset();
soundTestButton.Text = stopTestText;
break;
case SoundMakerEventType.PlayStopped:
playerFreeEvent.Set();
soundTestButton.Text = startTestText;
break;
}
}
更新 UI 以响应更改
我认为枚举、设置和获取默认设备非常简单,包装器最棘手的部分是将原生通知转发到应用程序的托管端。 (此机制也在此处描述 https://codeproject.org.cn/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-minutes#A9 )
回调可以使用 IMMDeviceEnumerator::RegisterEndpointNotificationCallback
注册
pNotifyClient = new CMMNotificationClient(audioDeviceNotificationHelper);
...
IMMNotificationClient* pNotify = (IMMNotificationClient*)(pNotifyClient);
hr = pEnum->RegisterEndpointNotificationCallback(pNotify);
CMMNotificationClient
是一个原生类,它连接通知链的原生部分和托管部分。
public class CMMNotificationClient : IMMNotificationClient
{
...
msclr::auto_gcroot<AudioDeviceNotificationHelper^> _notificationForwarder;
助手类已经在托管端,它将通知转发到 AudioDeviceNotification
。
这个类似乎是不必要的,但如果没有这个助手,我们的代码将无法编译。
private ref class AudioDeviceNotificationHelper : public AudioDeviceNotification
{
private:
AudioDeviceNotification ^ patient;
public:
AudioDeviceNotificationHelper()
{
patient = gcnew AudioDeviceNotification();
}
void ForwardNotification(AudioDeviceNotificationEventArgs^ e)
{
patient->NotifyEvent(e);
}
};
我们的托管事件将由 AudioDeviceNotification
类触发,我们的 AudioDeviceManager
订阅该类。
public ref class AudioDeviceNotification
{
public:
delegate void NotifyDelegate(AudioDeviceNotificationEventArgs^);
static event NotifyDelegate ^NotifyEvent;
event System::EventHandler<AudioDeviceNotificationEventArgs^>^ AudioDeviceEvent;
AudioDeviceNotification()
{
NotifyEvent += gcnew NotifyDelegate(this,
&AudioDeviceNotification::ManagedNotification);
}
...
void ManagedNotification(AudioDeviceNotificationEventArgs^ e)
{
AudioDeviceEvent(this, e);
}
};
谢谢
- 感谢 Ed Nutting 提供的 Media Player (https://codeproject.org.cn/Articles/125413/Media-Player-Class),它能够播放 mp3 并支持
PlayerStart
和PlayerStopEvent
- 感谢 Elias Bachaalany 提供的关于 C++/CLI 的精彩描述 (https://codeproject.org.cn/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-minutes)
- 感谢 bebop460 回答这个问题 "如何将扬声器与耳机分开?"
关注点
一些感兴趣的点是,如何使原生 Api 在 .Net 中可用,以及原生代码和托管代码之间棘手的通知转发。
历史
2015 年 1 月 5 日 如何将扬声器与耳机分开
2014 年 10 月 22 日 在默认设备上播放声音
2014 年 10 月 16 日 初始版本