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:
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.
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:
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:
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
:
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:
void soundMakerThread_DoWork(object sender, DoWorkEventArgs e)
{
DoWorkForSoundTestBackgroundWorker();
}
private void DoWorkForSoundTestBackgroundWorker()
{
string tmpDevice = audioDeviceSwitcher.DefaultPlaybackDeviceName;
string[] devices = new string[Properties.Settings.Default.AlarmDevices.Count];
Properties.Settings.Default.AlarmDevices.CopyTo(devices, 0);
do
{
foreach (var device in devices)
{
if (device == string.Empty) continue;
if (soundMakerThread.CancellationPending) break;
if (!playerFreeEvent.WaitOne(loopSoundLimit)) {
soundTest.CancelSound();
}
playerFreeEvent.Reset();
if (soundMakerThread.CancellationPending) break;
audioDeviceSwitcher.DefaultPlaybackDeviceName = device;
if (device != audioDeviceSwitcher.DefaultPlaybackDeviceName)
{
playerFreeEvent.Set();
}
else
{
soundTest.MakeSound();
}
}
}
while (playInLoop && !soundMakerThread.CancellationPending);
if (!playerFreeEvent.WaitOne(loopSoundLimit)) {
soundTest.CancelSound();
}
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:
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
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.
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.
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.
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