Search code examples
c#.netwmimicrophoneheadset

How to associate a USB headsets microphone and earphone/speaker


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:

  • Jabra PRO 9470
  • Plantronics BT300M
  • Logitech G430 Gaming Headset

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.

Headset properties

  1. Open the properties window by double clicking or click on Properties.
  2. Click the Properties button on the properties window.
  3. Click the 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."


Solution

  • 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.