.NET / SharpDX audio devices enumeration for XAudio2

Hello there,

ASG is running on a custom game engine based on SharpDX to access DirectX in a managed .NET framework environment. I just made the move from SharpDX 2.6.3 to the latest 4.2.0 version (better late than never! ^^) to be able to use XAudio2 directly on Win10 machines. That is XAudio2.8/9 instead of XAudio2.7 and thus without the dependency on the “June 2010 DirectX” redistributables.

However the initialization of XAudio2.8 is very different and acquiring the right “DeviceIDs” to be able to select a specific audio device proved quite painful. I could not find a C#/SharpDX-ready solution so following is my take on a “SoundDevicesEnumerator” class for this task.
The code is based on the SetupAPI approach described here (thanks a lot!), with additional help from pinvoke.net. It can handle both the XAudio2.7 and 2.8+ cases, and requires the SharpDX.XAudio2 (4.2.0) and PInvoke.SetupAPI nugets.

Cheers 🙂

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SharpDX.XAudio2;
using PInvoke;
using static PInvoke.SetupApi;

namespace LibDirectX.Sound
{
    /// <summary>
    /// A sound devices enumerator for XAudio2 through SharpDX (4.2.0), XAudio2_7 (Win7) or XAudio2_8+ (Win8, Win10)
    /// </summary>
    public static class SoundDevicesEnumerator
    {
        /// <summary>
        /// Attempts to enumerate the audio devices
        /// </summary>
        /// <param name="xAudio2Device">the XAudio2 device, must have been created</param>
        /// <param name="deviceNames">the names of the found devices, or null if error</param>
        /// <param name="deviceIDs">the deviceID of the found devices (XAudio2_8 or above only), or null if XAudio2_7 or error</param>
        /// <param name="errMsg">"OK" if returned true else the error message</param>
        /// <returns>true if suceeded, check errMsg if false</returns>
        /// <remarks>
        /// XAudio2_7: create the mastering voice using the device index (integer).
        /// XAudio2_8+: create the mastering voice using the device ID = deviceIDs[device index]
        /// </remarks>
        public static bool TryEnumerateDevices(XAudio2 xAudio2Device, out string[] deviceNames, out string[] deviceIDs, out string errMsg)
        {
            deviceNames = deviceIDs = null;
            errMsg = "OK";

            // check the XAudio2 device
            if (xAudio2Device == null || xAudio2Device.IsDisposed)
            {
                errMsg = "The XAudio2 device must have been created";
                return false;
            }

            bool enumerationSucceeded = true;

            // the enumeration depends on the XAudio2 dll version
            if (xAudio2Device.Version == XAudio2Version.Version27)
            {
                /////////////////////////////////////////////////////////////////////////////////////////////
                // XAudio2_7.dll
                try
                {
                    int numDevices = xAudio2Device.DeviceCount;
                    if (numDevices < 1)
                    {
                        throw new Exception("No device found");
                    }
                    deviceNames = new string[numDevices];
                    for (int i = 0; i < numDevices; i++)
                    {
                        deviceNames[i] = xAudio2Device.GetDeviceDetails(i).DisplayName;
                    }
                }
                catch (Exception ex)
                {
                    deviceNames = null;
                    enumerationSucceeded = false;
                    errMsg = "Enumeration failed: " + ex.Message;
                }
            }
            else
            {
                /////////////////////////////////////////////////////////////////////////////////////////////
                // XAudio2_8 and above
                SafeDeviceInfoSetHandle deviceInfoSetHandle = null;
                List<string> devNames = new List<string>(4);
                List<string> devIDs = new List<string>(4);
                try
                {
                    // The GUID for audio renderers (Win8 and above, defined in mmdeviceapi.h)
                    Guid GUID_DEVINTERFACE_AUDIO_RENDER = new Guid(0xe6327cad, 0xdcec, 0x4949, 0xae, 0x8a, 0x99, 0x1e, 0x97, 0x6a, 0x79, 0xd2);

                    // returns a handle to the set of audio renderers
                    deviceInfoSetHandle = SetupDiGetClassDevs(GUID_DEVINTERFACE_AUDIO_RENDER, null, IntPtr.Zero, GetClassDevsFlags.DIGCF_PRESENT | GetClassDevsFlags.DIGCF_DEVICEINTERFACE);

                    // check the handle validity
                    if (deviceInfoSetHandle == null || deviceInfoSetHandle.IsInvalid)
                    {
                        throw new Exception("Can't retrieve a handle to the set of audio renderers");
                    }

                    // enumerate the devices

                    // will receive the current device interface while enumerating
                    SP_DEVICE_INTERFACE_DATA devIntData = SP_DEVICE_INTERFACE_DATA.Create();

                    bool goOnEnumerating = true;
                    int deviceIndex = 0;
                    while (goOnEnumerating)
                    {
                        // get the device interface data for the device index
                        goOnEnumerating = SetupDiEnumDeviceInterfaces(deviceInfoSetHandle, (SP_DEVINFO_DATA?)null, ref GUID_DEVINTERFACE_AUDIO_RENDER, deviceIndex, ref devIntData);
                        if (goOnEnumerating)
                        {
                            unsafe
                            {
                                // dummy call to get the required size for the device interface detail data structure
                                int requiredSize = 0;
                                SetupDiGetDeviceInterfaceDetail(deviceInfoSetHandle, ref devIntData, null, 0, &requiredSize, null);

                                // to receive the device interface detail data
                                SP_DEVICE_INTERFACE_DETAIL_DATA* pDevIntDetailData = null; // pointer to a SP_DEVICE_INTERFACE_DETAIL_DATA struct
                                pDevIntDetailData = (SP_DEVICE_INTERFACE_DETAIL_DATA*)Marshal.AllocHGlobal(requiredSize); // allocate the required memory
                                pDevIntDetailData->cbSize = SP_DEVICE_INTERFACE_DETAIL_DATA.ReportableStructSize; // represents the fixed members besides the returned string

                                // to receive the device information data 
                                SP_DEVINFO_DATA devInfoData = SP_DEVINFO_DATA.Create();

                                // get the interface detail (device ID) and device information data
                                if (!SetupDiGetDeviceInterfaceDetail(deviceInfoSetHandle, ref devIntData, pDevIntDetailData, requiredSize, null, &devInfoData))
                                {
                                    Marshal.FreeHGlobal((IntPtr)pDevIntDetailData); // free the allocated memory
                                    throw new Exception(Kernel32.GetLastError().GetMessage());
                                }
                                devIDs.Add(SP_DEVICE_INTERFACE_DETAIL_DATA.GetDevicePath(pDevIntDetailData)); // store the device ID
                                Marshal.FreeHGlobal((IntPtr)pDevIntDetailData); // free the allocated memory

                                // get the device name
                                IntPtr pDeviceNameBuf = Marshal.AllocHGlobal(2048); // allocate memory for the string 
                                if (!SetupDiGetDeviceRegistryProperty(deviceInfoSetHandle, ref devInfoData, SPDRP_FRIENDLYNAME, out uint propertyRegDataType, pDeviceNameBuf, 2048, out uint pBufSize))
                                {
                                    Marshal.FreeHGlobal(pDeviceNameBuf); // free the allocated memory
                                    throw new Exception(Kernel32.GetLastError().GetMessage());
                                }
                                devNames.Add(Marshal.PtrToStringAuto(pDeviceNameBuf)); // store the device name
                                Marshal.FreeHGlobal(pDeviceNameBuf); // free the allocated memory
                            }

                            // OK next device
                            deviceIndex++;
                        }
                        else
                        {
                            // "ERROR_NO_MORE_ITEMS" is the normal output
                            Win32ErrorCode errCode = Kernel32.GetLastError();
                            if (errCode != Win32ErrorCode.ERROR_NO_MORE_ITEMS)
                            {
                                throw new Exception(errCode.GetMessage());
                            }
                        }
                    }

                    // done return the results
                    if (deviceIndex == 0)
                    {
                        throw new Exception("No device found");
                    }
                    deviceNames = devNames.ToArray();
                    deviceIDs = devIDs.ToArray();

                }
                catch (Exception ex)
                {
                    errMsg = "Enumeration failed: " + ex.Message;
                    enumerationSucceeded = false;
                }
                finally
                {
                    // release the handle
                    deviceInfoSetHandle?.Close();
                }
            }

            return enumerationSucceeded;
        }

        /// <summary>
        /// The code to query the device's name
        /// </summary>
        private const uint SPDRP_FRIENDLYNAME = 0x0000000C;

        /// <summary>
        /// To read device properties
        /// </summary>
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool SetupDiGetDeviceRegistryProperty(
            SafeDeviceInfoSetHandle deviceInfoSet,
            ref SP_DEVINFO_DATA deviceInfoData,
            uint property,
            out UInt32 propertyRegDataType,
            IntPtr propertyBuffer,
            uint propertyBufferSize,
            out UInt32 requiredSize
        );
    }

}

This Post Has 2 Comments

  1. csgo

    Hi. Do you allow guest posts on alliancespaceguard.com ?

    1. David

      Hello, I’m afraid not because this blog is dedicated to the game’s development. But if you have a specific question don’t hesitate to ask me using the contact form.

Leave a Reply to csgo Cancel reply