65.9K
CodeProject 正在变化。 阅读更多。
Home

MMAudioDeviceApi 的托管封装

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (6投票s)

2014年10月21日

CPOL

3分钟阅读

viewsIcon

36251

downloadIcon

2501

一个围绕 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);
        }
};

谢谢

关注点

一些感兴趣的点是,如何使原生 Api 在 .Net 中可用,以及原生代码和托管代码之间棘手的通知转发。

历史

2015 年 1 月 5 日 如何将扬声器与耳机分开
2014 年 10 月 22 日 在默认设备上播放声音
2014 年 10 月 16 日 初始版本

© . All rights reserved.