Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Managed Wrapper around MMAudioDeviceApi

4.67/5 (6 votes)
5 Jan 2015CPOL3 min read 35.6K   2.4K  
A C++/CLI wrapper around MMAudioDeviceApi with notification support.

Introduction

The Windows Multimedia Device (MMDevice) API enables clients to discover audio endpoint devices. You can find a few examples on the internet, how to use MMDeviceApi from C++, but I didn't find any application in .Net environment. So I wrote a wrapper class in C++/CLI and a C# example to demonstrate the functionalities.

The program can play test sound on selected devices and it updates the list automatically on changes eg. through the control panel or in case of plugging new device physically.

Using the code

Add AudioDeviceUtil project to your .Net solution and instantiate AudiDeviceManager. The class provides the following functionalities:

C#
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 property is used to get or set the default device by its friendly name. The getter and the setter calls finally the corresponding method from the C++ Api: IMMDeviceEnumerator::GetDefaultAudioEndpoint or IPolicyConfigVista::SetDefaultEndpoint.

PlaybackDevices is used to access the list of the stored devices in our current manager object.

UpdatePlaybackDeviceList() actualises the stored list by enumerating the devices on the system.

The list contains AudioDevice objects, where we store some information about the device.

MC++
System::String^ DeviceID;
System::String^ FriendlyName;
AudioDeviceStateType DeviceState;
bool IsDefaultDevice;

It could be extended by other informations e.g. role or property, but we don't need them in this example.

The following states are supported:

MC++
public enum class AudioDeviceStateType
{
     Active        = 1,
     Disabled      = 2,
     NotPresent    = 4,
     Unplugged     = 8,
     StateMaskAll  = 15,
};

This appears in the listbox of the test application too.

With RegisterForNotification property we register our own callback method in the Api.

And AudioDeviceEvent will raise from our callback. The event contains the AudioDeviceNotificationEventArgs object, where the Reason property stores one of the following values:

MC++
public enum class AudioDeviceNotificationEventType
{
    DefaultDeviceChanged,
    DeviceAdded,
    DeviceRemoved,
    DeviceStateChanged,
    PropertyValueChanged
}

In this example on receiving the event, we simple update the content of the listbox.

Playing sound on a device

If we want to play some sound on a device, wich is not the default one, we should simple set it as default (1.), play the sound (2.) and after the play ends (SoundMakerEventArgs.EventType) set back the original one (3.). The whole process runs in a BackgroundWorker thread, called soundMakerThread:

C#
  soundMakerThread.WorkerSupportsCancellation = true;
  soundMakerThread.DoWork += new DoWorkEventHandler(soundMakerThread_DoWork);

  soundTest.SoundMakerEvent += new EventHandler<SoundMakerEventArgs>(OnSoundMakerEvent);
}

There are many trace entry in the code example, but I deleted them here:

C#
    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();

    }

Our player is also a resource. It can operate only on one device, so playerFreeEvent has to be set appropriatelly:

C#
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;
   }
 }

Update UI on changes

I think the enumeration, setting and getting the default device is pretty straight forward, the most tricky part of the wrapper is to forward the native notification to the managed side of the application. ( This mechanism is also described here http://www.codeproject.com/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-minutes#A9 )

The callback can be registered with IMMDeviceEnumerator::RegisterEndpointNotificationCallback

MC++
pNotifyClient = new CMMNotificationClient(audioDeviceNotificationHelper);
...
IMMNotificationClient* pNotify = (IMMNotificationClient*)(pNotifyClient);
hr = pEnum->RegisterEndpointNotificationCallback(pNotify);

CMMNotificationClient is a native class which connects the native and the managed part of the notification chain.

MC++
public class CMMNotificationClient : IMMNotificationClient
{ 
...
   msclr::auto_gcroot<AudioDeviceNotificationHelper^> _notificationForwarder;

The helper class is allready on the managed side, it forwards the notification to  AudioDeviceNotification.
This class seems to be unnecessray, but without this helper, our code won't compile.

MC++
private ref class AudioDeviceNotificationHelper : public AudioDeviceNotification
{
    private:
        AudioDeviceNotification ^ patient;
    public:
        AudioDeviceNotificationHelper()
        {
            patient = gcnew AudioDeviceNotification();
        }
        void ForwardNotification(AudioDeviceNotificationEventArgs^ e) 
        {
            patient->NotifyEvent(e);
        }
};

Our managed event will be fired by the AudioDeviceNotification class on which our AudioDeviceManager subscribes.

MC++
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);
        }
};

Thanks

Points of Interest

Some points of interest were, how to make a native Api usable in .Net, and the tricky notification forwarding between the native and the managed part of the code.

History

05.01.2015 How to separate speakers from headphones
22.10.2014 Playing sound on default device
16.10.2014 Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)