All Articles

Switch Audio Device Settings Using a COM Interface from PowerShell

This page has been machine-translated from the original page.

On my PC, I usually keep a random playlist playing through the connected speakers, but switching the sound output to a headset from the Settings app every time just for online English conversation lessons was quite a hassle.

So I decided to make it possible to switch sound devices with a simple shortcut so that I would not be able to use the inconvenience of switching headsets as an excuse to skip English conversation practice.

However, on Windows, it seems there is no built-in feature for switching sound devices, so you either need to use some third-party tool or implement your own switching tool.

As a rule, I do not like installing tools I do not really understand on the main machine I use privately, so this time I decided to implement a switching tool in PowerShell.

When creating the script, I relied heavily on the following article. (I almost copied it as-is.)

Reference: Windowsの音声出力先を変えるショートカット作成 - itiblog

Trying it out taught me more than I expected, such as the need to use a COM interface, so I am summarizing it in this article.

Table of Contents

The Script I Created

The final script I created is below.

You can switch devices by changing the audio device IDs in this script as needed, saving it as a ps1 file, and running it.

$csharpCode = @"
using System;
using System.Runtime.InteropServices;

public enum EDataFlow
{
    eRender,
    eCapture,
    eAll,
}

public enum ERole
{
    eConsole,
    eMultimedia,
    eCommunications,
}

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("D666063F-1587-4E43-81F1-B948E807363F")]
public interface IMMDevice
{
    int Activate();
    int OpenPropertyStore();
    int GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
    int GetState();
}

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
public interface IMMDeviceEnumerator
{
    int EnumAudioEndpoints();
    int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppEndpoint);
}

[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
public class MMDeviceEnumerator { }

public class AudioHelper
{
    public static string GetDefaultDeviceId()
    {
        IMMDeviceEnumerator enumerator = new MMDeviceEnumerator() as IMMDeviceEnumerator;
        IMMDevice device;
        enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole, out device);
        string id;
        device.GetId(out id);
        return id;
    }
}

[Guid("F8679F50-850A-41CF-9C72-430F290290C8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IPolicyConfig
{
    [PreserveSig]
    int GetMixFormat();
    [PreserveSig]
    int GetDeviceFormat();
    [PreserveSig]
    int ResetDeviceFormat();
    [PreserveSig]
    int SetDeviceFormat();
    [PreserveSig]
    int GetProcessingPeriod();
    [PreserveSig]
    int SetProcessingPeriod();
    [PreserveSig]
    int GetShareMode();
    [PreserveSig]
    int SetShareMode();
    [PreserveSig]
    int GetPropertyValue();
    [PreserveSig]
    int SetPropertyValue();
    [PreserveSig]
    int SetDefaultEndpoint(
        [In] [MarshalAs(UnmanagedType.LPWStr)] string deviceId, 
        [In] [MarshalAs(UnmanagedType.U4)] ERole role);
    [PreserveSig]
    int SetEndpointVisibility();
}

[ComImport, Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")]
internal class _CPolicyConfigClient
{
}
public class PolicyConfigClient
{
    public static int SetDefaultDevice(string deviceID)
    {
        IPolicyConfig _policyConfigClient = (new _CPolicyConfigClient() as IPolicyConfig);
        try {
            Marshal.ThrowExceptionForHR(_policyConfigClient.SetDefaultEndpoint(deviceID, ERole.eConsole));
            Marshal.ThrowExceptionForHR(_policyConfigClient.SetDefaultEndpoint(deviceID, ERole.eMultimedia));
            Marshal.ThrowExceptionForHR(_policyConfigClient.SetDefaultEndpoint(deviceID, ERole.eCommunications));
            return 0;
        } catch {
            return 1;
        }
    }
}
"@

Add-Type -TypeDefinition $csharpCode

$currentDeviceId = [AudioHelper]::GetDefaultDeviceId()
$deviceId = ""
$deviceA = "<Device A ID>"
$deviceB = "<Device B ID>"

if($currentDeviceId -eq "{0.0.0.00000000}.$deviceA") {
$deviceId = $deviceB
}
else {
$deviceId = $deviceA
}

# Change Audio device
[PolicyConfigClient]::SetDefaultDevice("{0.0.0.00000000}.$deviceId")

How to Obtain Audio Device IDs on Windows

First, based on the information in the referenced article, I confirmed that by checking the registry information with the following script, I could obtain information that includes the IDs of the audio devices present on the machine.

$regRoot = "HKLM:\Software\Microsoft\"

function Get-Devices
{
    $regKey = $regRoot + "\Windows\CurrentVersion\MMDevices\Audio\Render\"
    Write-Output "Active Sound devices:"
    Get-ChildItem $regKey | Where-Object { $_.GetValue("DeviceState") -eq 1} |
        Foreach-Object {
            $subKey = $_.OpenSubKey("Properties")
            Write-Output ("  " + $subKey.GetValue("{a45c254e-df1c-4efd-8020-67d146a850e0},2"))
            Write-Output ("    " + $_.Name.Substring($_.Name.LastIndexOf("\")))
        }
}

Get-Devices

However, this time I wanted to identify the ID of the audio device currently in use and implement a toggle switch that automatically changes to another audio device that is not currently in use, so I decided to include code in the script that obtains the ID of the current audio device using the Core Audio API.

Check the Active Audio Device with the IMMDeviceEnumerator Interface

To identify the ID of the audio device currently in use on the device, I used the GetDefaultAudioEndpoint method of the IMMDeviceEnumerator interface.

Unlike IPolicyConfig, which is used when changing audio devices, IMMDeviceEnumerator appears to be an officially documented interface.

Reference: IMMDeviceEnumerator (mmdeviceapi.h) - Win32 apps | Microsoft Learn

Reference: IMMDeviceEnumerator::GetDefaultAudioEndpoint (mmdeviceapi.h) - Win32 apps | Microsoft Learn

Using this, I wrote the following code to obtain the ID of the currently used audio device through the Core Audio API.

public enum EDataFlow
{
    eRender,
    eCapture,
    eAll,
}

public enum ERole
{
    eConsole,
    eMultimedia,
    eCommunications,
}


[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("D666063F-1587-4E43-81F1-B948E807363F")]
public interface IMMDevice
{
    int Activate();
    int OpenPropertyStore();
    int GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
    int GetState();
}


[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
public interface IMMDeviceEnumerator
{
    int EnumAudioEndpoints();
    int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppEndpoint);
}


[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
public class MMDeviceEnumerator { }

public class AudioHelper
{
    public static string GetDefaultDeviceId()
    {
        IMMDeviceEnumerator enumerator = new MMDeviceEnumerator() as IMMDeviceEnumerator;
        IMMDevice device;
        enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole, out device);
        string id;
        device.GetId(out id);
        return id;
    }
}

Once you define the above, you can run [AudioHelper]::GetDefaultDeviceId() to obtain the ID of the audio device currently applied on the machine.

How to Change Audio Devices on Windows

Based on the information in the referenced article, I confirmed that the Windows audio output destination can be changed with the following steps.

  1. Create PolicyConfigClient that implements the IPolicyConfig interface.
  2. Implement SetDefaultDevice included in IPolicyConfig in PolicyConfigClient.
  3. Change the audio playback device by calling SetDefaultDevice with the device UUID and ERole, which indicates the role assigned by the system to the audio endpoint device.

What Is the IPolicyConfig Interface

IPolicyConfig is a non-public COM interface that provides methods for managing Windows audio device policy settings, and its COM CLSID is F8679F50-850A-41CF-9C72-430F290290C8.

Regarding this interface, the following issue includes a comment from an MS developer stating that it is an undocumented API.

Reference: IPolicyConfig and PolicyConfigClient not created · Issue #1105 · microsoft/CsWin32

However, you can still find implementations such as SetDefaultAudioDevice.cpp in the following repository that show how to use the IPolicyConfig interface to change Windows audio device settings.

Reference: SetDefaultAudioDevice/SetDefaultAudioDevice.cpp

Reference: AudioDeviceCmdlets/SOURCE/IPolicyConfig.cs

Below is the definition of the IPolicyConfig interface implemented in IPolicyConfig.cs.

This implementation uses a COM interface from C#.

It seems that the [PreserveSig] attribute before the method names is needed so that .NET does not treat the error codes returned from COM as exceptions.

Also, the IntPtr used in the arguments appears to be used to pass pointers to native structures and similar information used on the COM side.

Reference: C#COMを使ってみた。 - echo(“備忘録”);

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace CoreAudioApi.Interfaces
{
    [Guid("f8679f50-850a-41cf-9c72-430f290290c8"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IPolicyConfig
    {
        [PreserveSig]
        int GetMixFormat(string pszDeviceName, IntPtr ppFormat);

        [PreserveSig]
        int GetDeviceFormat(string pszDeviceName, bool bDefault, IntPtr ppFormat);

        [PreserveSig]
        int ResetDeviceFormat(string pszDeviceName);

        [PreserveSig]
        int SetDeviceFormat(string pszDeviceName, IntPtr pEndpointFormat, IntPtr MixFormat);

        [PreserveSig]
        int GetProcessingPeriod(string pszDeviceName, bool bDefault, IntPtr pmftDefaultPeriod, IntPtr pmftMinimumPeriod);

        [PreserveSig]
        int SetProcessingPeriod(string pszDeviceName, IntPtr pmftPeriod);

        [PreserveSig]
        int GetShareMode(string pszDeviceName, IntPtr pMode);

        [PreserveSig]
        int SetShareMode(string pszDeviceName, IntPtr mode);

        [PreserveSig]
        int GetPropertyValue(string pszDeviceName, bool bFxStore, IntPtr key, IntPtr pv);

        [PreserveSig]
        int SetPropertyValue(string pszDeviceName, bool bFxStore, IntPtr key, IntPtr pv);

        [PreserveSig]
        int SetDefaultEndpoint(string pszDeviceName, ERole role);

        [PreserveSig]
        int SetEndpointVisibility(string pszDeviceName, bool bVisible);
    }
}

This interface includes several methods, but the one mainly needed this time is SetDefaultEndpoint, which can set a specific device as the audio playback device.

Reference: windows - How can I programmatically set the default input and output audio device for an application? - Stack Overflow

As mentioned above, by calling SetDefaultDevice with the device UUID and ERole (an enum that specifies audio roles such as eConsole, eMultimedia, and eCommunications, which are assigned by the system to audio endpoint devices), you can change the audio playback device.

ERole is defined as follows.

public enum ERole
{
    eConsole,
    eMultimedia,
    eCommunications,
}

Implementing PolicyConfigClient Using the IPolicyConfig Interface

Next, implement PolicyConfigClient, which calls the method that changes the audio device using the IPolicyConfig interface.

Here as well, PolicyConfigClient.cs in the same repository as above is helpful.

Reference: AudioDeviceCmdlets/SOURCE/PolicyConfigClient.cs

using System;
using System.Collections.Generic;
using System.Text;
using CoreAudioApi.Interfaces;
using System.Runtime.InteropServices;

namespace CoreAudioApi
{
    [ComImport, Guid("870af99c-171d-4f9e-af0d-e63df40c2bc9")]
    internal class _PolicyConfigClient
    {
    }

    public class PolicyConfigClient
    {
        private readonly IPolicyConfig _PolicyConfig;
        private readonly IPolicyConfigVista _PolicyConfigVista;
        private readonly IPolicyConfig10 _PolicyConfig10;

        public PolicyConfigClient()
        {
            _PolicyConfig = new _PolicyConfigClient() as IPolicyConfig;
            if (_PolicyConfig != null)
                return;

            _PolicyConfigVista = new _PolicyConfigClient() as IPolicyConfigVista;
            if (_PolicyConfigVista != null)
                return;

            _PolicyConfig10 = new _PolicyConfigClient() as IPolicyConfig10;
        }

        public void SetDefaultEndpoint(string devID, ERole eRole)
        {
            if (_PolicyConfig != null)
            {
                Marshal.ThrowExceptionForHR(_PolicyConfig.SetDefaultEndpoint(devID, eRole));
                return;
            }
            if (_PolicyConfigVista != null)
            {
                Marshal.ThrowExceptionForHR(_PolicyConfigVista.SetDefaultEndpoint(devID, eRole));
                return;
            }
            if (_PolicyConfig10 != null)
            {
                Marshal.ThrowExceptionForHR(_PolicyConfig10.SetDefaultEndpoint(devID, eRole));
            }
        }
    }
}

In this code, the PolicyConfigClient interface is obtained by using the COM CLSID 870af99c-171d-4f9e-af0d-e63df40c2bc9.

Then it calls the SetDefaultEndpoint method of the IPolicyConfig interface to change the default audio device.

Using the C Sharp Code from PowerShell

Finally, I call the C# code created so far from PowerShell and create a toggle script that switches the audio device setting between device A and device B.

$csharpCode = @"
< C# Code >
"@

Add-Type -TypeDefinition $csharpCode

$currentDeviceId = [AudioHelper]::GetDefaultDeviceId()
$deviceId = ""
$deviceA = "<Device A ID>"
$deviceB = "<Device B ID>"

if($currentDeviceId -eq "{0.0.0.00000000}.$deviceA") {
$deviceId = $deviceB
}
else {
$deviceId = $deviceA
}

# Change Audio device
[PolicyConfigClient]::SetDefaultDevice("{0.0.0.00000000}.$deviceId")

The C# code created above is made callable from PowerShell with Add-Type -TypeDefinition $csharpCode, as shown in the sample in the following documentation.

Reference: Add-Type (Microsoft.PowerShell.Utility) - PowerShell | Microsoft Learn

Add-Type can also be used when calling assembly objects or native Windows APIs from PowerShell.

Summary

I only wanted an easy way to change audio devices, but it turned out to be more troublesome than expected because I had to use an unofficial COM interface. Still, I learned a lot, so I am glad I worked through it.