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

本記事は ZOZO Advent Calendar 2021 の10日目の記事です。

みなさん在宅ワークしていますか? 自分はフルリモートの会社で働いているためどれだけ在宅環境をよくできるか日々考えています。

本記事では在宅ワークの際に手間だと感じていた 「パソコンの音声出力先変更」をショートカット1つでできるようにした方法を解説します。

どのように調べ、実現までもっていったか、参考になれば幸いです。

やりたいこと

パソコンで音楽を流したり好きなラジオ、配信を聞きながらテンションを上げて作業している方は多いと思います。 お昼時など席を離れた際にわざわざスマホに切り替えて、とかするのは大変ですよね?

我が家では普段の作業時には机の上のスピーカ、キッチンに移動したらおいてあるAlexaからBluetooth接続で音楽を再生させていました。 スピーカボタンを押してデバイスの選択という流れで音声の出力先を切り替えています。 f:id:itib:20211209005317g:plain

この作業がなかなかに手間。 移動開始前に数ステップある作業は嬉しくない。
理想は1クリックで移動可能、 なんなら移動したことを検知して勝手にスピーカを切り替えてほしい。

ということで本記事では「1クリックで音声出力先を切り替えられるようなショートカットを作成する」を目標に調査、実装を行っていきます。

調査

Windowsがどのようにデフォルトの音声出力先を決めているのか調査します。 どうやって設定しているかがわかれば対応したコードを書いて実行できるはず!

Windows APIの調査

Windowsの多くの動作はAPIを用いて操作することができます。 「Windows API インデックス」を漁っていきます。 音声出力先の設定に関わりそうなドキュメントとしては「Windows Core Audio API について」がありました。

コア オーディオ API は次のとおりです。
・マルチメディアデバイス(MMDevice)API。クライアントは、このAPIを使用して、システム内のオーディオエンドポイントデバイスを列挙します。
・...

列挙してくれるだけらしい🤔
フォーラムとかを見るとデバイスの切り替えのAPIは提供されていないように見えます。

ref: Programatically setting the default playback device (and recording device)

どこかのレジストリに格納されているのでは?

WIndowsの設定情報はレジストリに格納される。 当然音声出力先の情報もあるはず。

レジストリエディタで探すと \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render\ 下に接続されたデバイスらしきデータが見つかりました。

f:id:itib:20211209010601p:plain

音声出力先を変更した際にどこの値が変化するか観察してみると出力先に設定したタイミングでUUID({xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}形式の値)下に含まれる Level: 0 ~ Level: 2 の値が変化するようです。

  • 値は出力に設定されるたびに+nされる
  • 別の値が出力先になっても値は変化しない
  • 足し算される値は期間を開けると大きくなる

ここから時間等でLevelに代入される値が決まっており、 値が最新のもの が音声出力先として使われていると考えることができます。

f:id:itib:20211209012429p:plain

レジストリの値は手動で編集することができるため右クリックで修正してみました。 再生先になっていないデバイスの値を現在の音声出力先となっているデバイスの値より大きくすれば出力先が変更されると予測していました。 とりあえず Level: 2 の値を +1 してみました。

変更前 変更後
Level: 0 0x000003cc 0x000003cc
Level: 1 0x000003cc 0x000003cc
Level: 2 0x0000020d 0x0000020e

値を変更した直後からWindows の右下サウンドマークで表示される出力先には値を大きくしたデバイスが表示されるようになったものの 音声の出力先はいままでの出力先のままという状態になりました。

f:id:itib:20211209020101p:plain

デスクトップの表示だけされても意味がないんだ...

既存実装調査

ここまでで自力では手詰まりだったのでGithubで先人たちの書いた実装を調べます。

Audioやいままでの調査で出てきたMMDevices等のキーワードたちでGithubを調べます。検索してみるとやりたいことを実装しているコードが何個も出てきます。以下は実装の際に大きく参考にしたリポジトリです。

先人は偉大。

github.com

github.com

気合で読んで以下のような流れで音声出力先を変更していることがわかりました。

  1. IPolicyConfig インタフェースを実装したPolicyConfigClientを作成
  2. PolicyConfigClientIPolicyConfig に含まれる SetDefaultDevice() を実装
  3. SetDefaultDevice()にデバイスのUUIDとシステムがオーディオエンドポイントデバイスに割り当てた役割を示すERoleを渡して呼び出す
  4. 出力先が変更される

実装する

先人たちのツールを使えば簡単ですが自分でもいろいろいじりたいと思い手を動かしていきます。実装する内容は以下の2つです。

  1. 出力先として利用可能なデバイス一覧を表示
  2. 音声出力先の切り替え

先人たちのコードが C# で書かれていましたが自分は C# の知見があまりないのできっとPowerShellならWindows 関連なんとかなるでしょ!という安直な考えでPowerShellで実装しました。

出力先として利用可能なデバイス一覧を表示

オーディオ関連のレジストリを調査した際に機器情報が格納されている場所がわかりました。指定した箇所のレジストリ内容を出力することでデバイス名を取得します。

$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

実行すると以下のようにデバイス名とUUIDが表示されます。

PS > .\deviceList.ps1
Active Sound devices:
  スピーカー
    \{476a4519-c338-4f9e-9746-294cd6429136}
  1-2
    \{4c884bad-d966-441a-b1f9-4ec0fced2fbc}
  3-4
    \{cd21a3a3-061e-4064-94a6-f5769eda2979}
  ヘッドホン
    \{ee3cf230-46d3-4043-8fc2-7cd4dff984b0}
  Digital Audio (S/PDIF)
    \{fa27ad22-6853-430a-b1e5-10b83f01dbbb}

音声出力先の切り替え

ここは先人たちのC#で書かれたコードをもとに実装しました。

あまりC#の知見がない + デバイス一覧取得をpowershellで書いたためできればpowershellで完結させたい、 と思っていたところpowershellは変数として与えたC#のコードを実行できるらしい。宇宙感じた。

social.technet.microsoft.com

必要となるERole, IPolicyConfig, PolicyConfigClientを実装しPowershellから呼び出すようにします。

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

using System.Collections.Generic;
internal enum ERole : uint
{
    eConsole         = 0,
    eMultimedia      = 1,
    eCommunications  = 2,
    ERole_enum_count = 3
}
[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 $cSharpSourceCode

function Set-DefaultAudioDevice
{
    Param
    (
        [parameter(Mandatory=$true)]
        [string[]]
        $deviceId
    )

    If ([PolicyConfigClient]::SetDefaultDevice("{0.0.0.00000000}.$deviceId") -eq 0)
    {
        Write-Host "SUCCESS: The default audio device has been set."
    }
    Else
    {
        Write-Host "ERROR: There has been a problem setting the default audio device."
    }
}

$id = $args[0]
Set-DefaultAudioDevice $id

1つ目の引数として音声出力先に設定したいデバイスのUUIDを渡すとSetDefaultDeviceが実行され音声出力先を変更することができました。

PS > .\deviceSet.ps1 "{4c884bad-d966-441a-b1f9-4ec0fced2fbc}"
SUCCESS: The default audio device has been set.

スクリプト実行用のショートカット作成

作成したPowerShellコードを呼び出すショートカットを作成します。 Windowsのショートカットには引数を与えて作ることができるので出力先に対応したショートカットをデスクトップに作成します。

ショートカットを作成し、そのプロパティからリンク先を powershell.exe <作成したdeviceSet.ps1へのパス> <設定対象のUUID> に編集します。

サンプル: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe C:\Users\guest\deviceSet.ps1 "{4c884bad-d966-441a-b1f9-4ec0fced2fbc}"

f:id:itib:20211209214848p:plain

出来上がったショートカットをデスクトップに置くことで以下のように音声出力先を1クリックで変更できるようになりました。 自分の環境ではアイコン画像も変えてわかりやすくしました。

f:id:itib:20211209212542g:plain

おわりに

目標だった「1クリックでWindowsの音声出力先を変更する」を達成することができました。

手軽に再生先を変えられるようになっただけでなく、 Windowsの音声出力設定方法やシステムについて詳しくなれた、 PowerShellC#のコードを読みだせることを知れた点で挑戦してみてよかったと思います。

つぎはPowerShellから別の言語に書き換えたり、人の部屋移動を検知してこのショートカットを叩かせたりしたいです。