初めてのプログラミングブログ記事は何にしようか悩んだのですが、C#で音声出力デバイスを切り替えるアプリを作ったときに調べた内容や、実装した内容を備忘録も兼ねて書こうと思います。
なお、本ブログ記事では「C#で音声出力デバイスを切り替える方法」だけに焦点を合わせます。簡単な使用方法は提示しますが、どういったアプリに組み込むか、どう発展させていくかなどは特に取り扱いません。
コードの使用はすべて自己責任でお願いします。また、動作検証環境はWindows11 Home(23H2)となります。そのほかの環境では試しておりませんでご了承ください。
前回のブログ記事は「お試しでメガネを購入しました」です。
作成経緯
音声出力デバイスにはスピーカーを使用するのが好きなのですが、音が回りに漏れないように配慮しないといけないタイミングではヘッドセットやイヤホンを使用するなど、使い分けをしています。しかし、音声出力デバイスの切り替えには、デバイス一覧を表示してデバイスを選択する必要があり、地味に面倒です。
具体的には、デバイス一覧をマウス操作で表示する場合は2ステップほど必要です。キーボードでは「Ctrlキー+Windowsキー+vキー」の同時押しが必要ですがなかなか押しずらい組み合わせです(左手だけでは押しづらく、両手で押すとなるとマウスから手を放す必要があり非効率です)。またデバイス一覧を表示したあとは切り替えたいデバイスを選択する必要がありますが、一覧から対象のデバイスを探し選択する、というのも面倒です。以上のように切り替え操作はそこそこな操作数を求められます。
使用しないデバイスはOFFにすることでデバイス一覧に表示されなくなり、多少すっきりさせることもできます。
そのため、C#で音声出力デバイスの切り替えが出来れば色々と応用が利き、例えば「アプリ実行で特定のデバイスを選択する」や「複数デバイスをアプリ実行でトグルする」など、操作数の削減につなげられると考えました。C#で音声出力デバイスを切り替える方法について
色々調べてみたのですが、C#のAPIでこれを叩けばOK!というものは見つけられませんでした・・・(もしありましたらコメントで教えてほしいです。よろしくお願いします。)
着手当初は「API叩いて終わりでしょ」くらいの軽い気持ちだったのですが、そのうち、C#にAPIは無さそう・・・それならNAudioでできるかな・・・NAudioは指定したデバイスで音を鳴らすとかはできるが、音声出力デバイスの切り替えまではできそうにない・・・と、どんどんハマっていきました(苦笑)。
最終的に「PCに接続されているマイク(オーディオデバイス)を取得し、音量設定をする(EnumAudioEndpoints)(C#版)」や「Windowsの音声出力先を変えるショートカット作成」を参考にさせていただき、以下ような実装としました。
COMベースの処理呼び出し
具体的にはCOM(コンポーネント オブジェクト モデル)ベースの処理を呼び出すことにより、音声出力デバイスを切り替えます。COMベースの処理呼び出しを実現するため、属性(attribute)に「ComImport」を使用します。呼び出す対象は「MMDevice」関連のものや「IPolicyConfig」関連のものになります。
以下がコードとなります。namespaceやクラス名などはご使用の環境に合わせ変更してください。using System.Runtime.InteropServices;
namespace xxxx
{
internal class ComImport
{
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
internal class MMDeviceEnumerator
{
}
internal enum EDataFlow
{
eRender,
eCapture,
eAll,
EDataFlow_enum_count
}
internal enum DEVICE_STATE
{
ACTIVE = 1,
DISABLED = 2,
NOTPRESENT = 4,
UNPLUGGED = 8,
}
internal enum ERole
{
eConsole,
eMultimedia,
eCommunications,
ERole_enum_count
}
internal enum STGM
{
READ,
WRITE,
READWRITE,
}
public struct PROPERTYKEY
{
public PROPERTYKEY(Guid InputId, uint InputPid)
{
fmtid = InputId;
pid = InputPid;
}
private Guid fmtid;
private uint pid;
}
[StructLayout(LayoutKind.Explicit)]
public struct PROPVARIANT
{
[FieldOffset(0)]
public ushort vt;
[FieldOffset(2)]
public ushort wReserved1;
[FieldOffset(4)]
public ushort wReserved2;
[FieldOffset(6)]
public ushort wReserved3;
[FieldOffset(8)]
public IntPtr pwszVal;
}
[ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMMDeviceEnumerator
{
[PreserveSig]
int EnumAudioEndpoints(EDataFlow dataFlow, ulong dwStateMask, out IMMDeviceCollection ppDevices);
}
[ComImport, Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMMDeviceCollection
{
[PreserveSig]
public int GetCount(out uint pcDevices);
[PreserveSig]
public int Item(uint nDevice, out IMMDevice ppDevice);
}
[ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMMDevice
{
[PreserveSig]
int dummy1();
[PreserveSig]
public int OpenPropertyStore(ulong stgmAccess, out IPropertyStore ppProperties);
[PreserveSig]
public int GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
}
[ComImport, Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ]
internal interface IPropertyStore
{
[PreserveSig]
public int dummy1();
[PreserveSig]
public int dummy2();
[PreserveSig]
public int GetValue(PROPERTYKEY key, out PROPVARIANT prop);
}
[ComImport, Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")]
internal class PolicyConfigClient
{
}
[ComImport, Guid("F8679F50-850A-41CF-9C72-430F290290C8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IPolicyConfig
{
[PreserveSig]
int dummy1();
[PreserveSig]
int dummy2();
[PreserveSig]
int dummy3();
[PreserveSig]
int dummy4();
[PreserveSig]
int dummy5();
[PreserveSig]
int dummy6();
[PreserveSig]
int dummy7();
[PreserveSig]
int dummy8();
[PreserveSig]
int dummy9();
[PreserveSig]
int dummy10();
[PreserveSig]
int SetDefaultEndpoint(string pszDeviceName, ERole role);
}
}
}
制御処理
あとは上記を使用し、以下のように処理を実装すれば音声出力デバイスを変更できます。
- 切り替えたい音声出力デバイス名を定義しておく。
- 音声出力デバイス一覧を取得する。
- 取得した一覧に切り替えたい音声出力デバイス名と一致するものがあるかチェックする。
- あった場合、そのデバイスのIDを取得する。
- デバイスのIDを指定して音声出力デバイスを切り替える。
以下がコードになります。
PROPERTYKEY PKEY_Device_FriendlyName = new PROPERTYKEY(new Guid(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14);
string deviceName = "<切り替えたいデバイス名。例:スピーカー (Realtek(R) Audio)>";
// 音声出力デバイス一覧を取得する
IMMDeviceEnumerator? deviceEnumerator = new MMDeviceEnumerator() as IMMDeviceEnumerator;
deviceEnumerator!.EnumAudioEndpoints(EDataFlow.eRender, (ulong)DEVICE_STATE.ACTIVE, out var pCollection);
pCollection.GetCount(out var deviceCount);
for (uint i = 0; i < deviceCount; i++)
{
// デバイス名を一つずつ比較する
pCollection.Item(i, out var pEndpoint);
pEndpoint.OpenPropertyStore((ulong)STGM.READ, out var pProps);
pProps.GetValue(PKEY_Device_FriendlyName, out var vNm);
var devName = Marshal.PtrToStringUni(vNm.pwszVal);
if (deviceName.Equals(devName))
{
// デバイス名が一致したら、デバイスIDを取得する
pEndpoint.GetId(out var pRetId);
// 音声出力デバイスを切り替える
var policy = new PolicyConfigClient() as IPolicyConfig;
policy!.SetDefaultEndpoint(pRetId, ERole.eConsole);
policy!.SetDefaultEndpoint(pRetId, ERole.eMultimedia);
policy!.SetDefaultEndpoint(pRetId, ERole.eCommunications);
break;
}
}
「deviceName」にデバイス名を指定し、実行すれば音声出力デバイスが切り替わります。デバイス名はWindowsのデバイス一覧で表示される名称を指定してください。スペースやかっこの有無、全角/半角もきっちり合わせてください。
※初めにも記載しておりますが、「使用は自己責任で」、「動作検証環境はWindows11のみ」です。よろしくお願いします。
最後に
音声出力デバイスの切り替えにC#でそのまま使えるAPIはないものかと、色々と調べてみましたがなかなか難航し時間だけが過ぎていきました(苦笑)。結局見つけられずでしたが、実装が終わってしまえばこれくらいのものか・・・と思えなくもないです。実装中も色々試行錯誤して時間を使ってしまいましたが(笑)
こういった調べものをするといつも思います。先人のみなさまはすごいと。引用させていただいたサイト様以外にも色々なところを覗きました。本当にありがとうございます。
解説などはほとんどなく、コードを示したくらいですが、私の記事も誰かの参考になれば幸いです。


0件のコメント: