Background
I need to display a list of connected headsets (combined microphone and earphone) in a softphone application. For testing I have the following devices:
The user should be able to select a headset from a ComboBox
, without selecting the microphone and earphone separately.
The information
I know where to find the information (in Windows) about the microphone and earphone, but I have not been able to get it using WMI
or MMDevice API
.
To find the information, right click on Sound
(Speaker icon) in the right side of the taskbar and select Playback devices
.
Properties
.Properties
button on the properties window.Details
tab and find the Children
property in the ComboBox
.This will give me the following information:
SWD\MMDEVAPI\{0.0.0.00000000}.{f2e09e37-8389-46c4-8b2b-53e08b874399}
SWD\MMDEVAPI\{0.0.1.00000000}.{3402ee6e-d862-47ca-8ab8-bb8254216032}
The first line matches my Headset Earphone (Jabra PRO 9470)
and the second Headset Microphone (Jabra PRO 9470)
.
To get the same information in C#, I'm loop through the Win32_USBControllerDevice class and outputting all values containing "MMDEVAPI". On my PC it will return 6 values (3 x microphones, 3 x earphones).
foreach (var device in new ManagementObjectSearcher("SELECT * FROM Win32_USBControllerDevice").Get())
{
foreach (var property in device.Properties)
{
// Gets the value of the property on the device.
var value = property.Value == null ? string.Empty : property.Value.ToString();
if (value.IndexOf("mmdevapi", StringComparison.OrdinalIgnoreCase) > -1)
{
// Output connected USB microphones and earphones.
Console.WriteLine(property.Value);
}
}
}
For reference, the above code will output:
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.0.00000000}.{F2E09E37-8389-46C4-8B2B-53E08B874399}"
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.1.00000000}.{3402EE6E-D862-47CA-8AB8-BB8254216032}"
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.0.00000000}.{985F2B5C-2EE2-4733-BBD6-48BFDE2D5582}"
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.1.00000000}.{71D824EA-DAE9-4F0D-B673-4425385E3777}"
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.0.00000000}.{D29C0970-D515-4F91-9924-F0063CF1A196}"
\\PC9018\root\cimv2:Win32_PnPEntity.DeviceID="SWD\\MMDEVAPI\\{0.0.1.00000000}.{C4B331E2-C56B-4D9B-A486-2ED6C11FDB8C}"
The problem
My big problem now is, how do I associate the correct headsets microphone and earphone into a Headset
object?
The attempts
I have tried searching Google and StackOverflow for an answer or hints, but I cannot find any common ground or relationship between the microphone and earphone using WMI
or MMDevice API
.
If there is a way to create a Dictionary<string, List<string>>
where the Key
is something unique to the physical device or USB port, and the Value
is a list of associated Win32_PnPEntity.DeviceID
, then I cannot find it.
In the spirit of Star Wars Day in a few days: "Help me, StackOverflow. You are my only hope."
It looks like I have found the answer myself, with the help of NAudio and Windows Registry. The code in the question is still correct, but NAudio makes it a bit easier.
I found the key to my problem inside Windows Registry under:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render
Every device has a property called {b3f8fa53-0004-438e-9003-51a46e139bfc},2
with a value similar to this {1}.USB\VID_047F&PID_0416&MI_00\7&21995D75&0&0000
.
This looks to be a unique hardware ID, and is shared by the headset microphone and earphone (the same as the Children
property from the question).
The solution
An interface with a single method to locate and return a list of headsets.
public interface IHeadsetLocator
{
/// <summary>
/// Locate all connected audio devices based on the given state.
/// </summary>
/// <param name="deviceState"></param>
/// <returns></returns>
IReadOnlyCollection<Headset> LocateConnectedAudioDevices(DeviceState deviceState = DeviceState.Active);
}
And the complete implementation. The real magic happens with the help of the GetHardwareToken
method.
public class HeadsetLocator : IHeadsetLocator
{
/// <summary>
/// Locate all connected audio devices based on the given state.
/// </summary>
/// <param name="deviceState"></param>
/// <returns></returns>
public IReadOnlyCollection<Headset> LocateConnectedAudioDevices(DeviceState deviceState = DeviceState.Active)
{
var enumerator = new MMDeviceEnumerator();
var relatedAudioDevices = new ConcurrentDictionary<string, List<MMDevice>>();
var headsets = new List<Headset>();
// Locate all connected audio devices.
foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.All, deviceState))
{
// Gets the DataFlow and DeviceID from the connected audio device.
var index = device.ID.LastIndexOf('.');
var dataFlow = device.ID.Substring(0, index).Contains("0.0.0") ? DataFlow.Render : DataFlow.Capture;
var deviceId = device.ID.Substring(index + 1);
// Gets the unique hardware token.
var hardwareToken = GetHardwareToken(dataFlow, deviceId);
var audioDevice = relatedAudioDevices.GetOrAdd(hardwareToken, o => new List<MMDevice>());
audioDevice.Add(device);
}
// Combines the related devices into a headset object.
foreach (var devicePair in relatedAudioDevices)
{
var capture = devicePair.Value.FirstOrDefault(o => o.ID.Contains("0.0.1"));
var render = devicePair.Value.FirstOrDefault(o => o.ID.Contains("0.0.0"));
if (capture != null && render != null)
{
headsets.Add(new Headset("Headset", render.DeviceFriendlyName, capture, render));
}
}
return new ReadOnlyCollection<Headset>(headsets);
}
/// <summary>
/// Gets the token of the USB device.
/// </summary>
/// <param name="dataFlow"></param>
/// <param name="audioDeviceId"></param>
/// <returns></returns>
public string GetHardwareToken(DataFlow dataFlow, string audioDeviceId)
{
using (var registryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
{
var captureKey = registryKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\" + dataFlow + @"\" + audioDeviceId + @"\Properties");
if (captureKey != null)
{
return captureKey.GetValue("{b3f8fa53-0004-438e-9003-51a46e139bfc},2") as string;
}
}
return null;
}
}
I hope this will help others in a similar situation.
Happy coding.