Tauri + windows-rsでオーディオスイッチャーを作った話またはMicrosoftのRustへの本気度をCOM対応から見る的な何か

この記事は [Rust Advent Calendar 2022 - Qiita](https://qiita.com/advent-calendar/2022/rust) 20日目の記事です[^tikoku]！

[^tikoku]: 少し遅刻してしまい誠に申し訳ありませんm(_ _)m 前日の12/19は[serinuntiusさん](https://zenn.dev/serinuntiu)の[Plonky2というゼロ知識証明アルゴリズムで実装されているEVMの証明をチラ見してみる](https://zenn.dev/noplan_inc/articles/a30c1731a26946)という記事でした。筆者は暗号学系の研究室に所属しているのでゼロ知識証明等もゼミで取り扱ったりしているのですが、その関連技術(ブロックチェーン)が実際にRustコードとして現れていたのに少し感動しました。私もRustで暗号学に何か貢献したい...

GUIフレームワークの[Tauri](https://tauri.app/)と、Microsoft公式が出しているWindows API用クレートの[windows-rs](https://github.com/microsoft/windows-rs)を使用し、[audio-bookmark](https://github.com/anotherhollow1125/audio-bookmark)というWindows用オーディオ切り替えアプリを作成したので、その仕組みの解説と[windows-rs](https://github.com/microsoft/windows-rs)の紹介記事になります！

作ったアプリ「audio-bookmark」のリポジトリ↓

https://github.com/anotherhollow1125/audio-bookmark

- ダウンロードページ: https://github.com/anotherhollow1125/audio-bookmark/releases
    - Assets以下の `audio-bookmark_0.0.0_x64_en-US.msi` がインストーラになります。
    - まだα版です。デバッグしていだける猛者の方がいらっしゃいますと大変助かります！！

![audio_bookmark_caption.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/06dbb340-ccb1-0423-1996-dbe2ebc9cd7f.gif)

# 作った動機

挙げたGif画像のように、たくさんのよくわからないオーディオインターフェースが登録されたWindows環境[^env]で生活しているのは筆者ぐらいかもしれませんが、要は

[^env]: こんな環境の作り方ですが、大量のモニタをHDMIで繋いだりOculus Questを繋いだりOBS関連で仮想インターフェースが欲しくなって追加したりなんていうことを適当にやっているとできます。わりとこんな環境になっている人いるのでは...?

**すぐに普段使っているスピーカに切り替えられない**

という悩みがありました。この原因を細かくすると2つになります。

- 普段使わないオーディオが多数表示されている
- オーディオ名が直感に反するものになっている
    - 動画の例だと、有線ヘッドホンの名前が「スピーカー (2- USB Audio Device)」

解決策もすぐに思いつきました

- 普段使うオーディオだけのリストを作る
- オーディオに別名をつける
- 普段使うオーディオにのみホットキーを割り当てる

ホットキー機能を提供しているアプリに[DefaultAudioChanger](https://github.com/sgiurgiu/DefaultAudioChanger)という先達がいましたが、

- 結構古いアプリのよう
- 筆者が考えた機能はオーディオを管理しやすいという新規性がありそう
- [Tauriを完全に理解し始めている](https://qiita.com/namn1125/items/8153f7c0881b7d4127ed)ので実用的なアプリを作りたい

と考え、実装しアドベントカレンダーを書く今に至りました。<font color="#bbbbbb">修論の新規性もこれぐらい簡単にひねり出したい()</font>

今後Zoom等による遠隔会議の需要が増え、オーディオを素早く切り替えるアプリの需要も増しそうですし、作る意味は大いにありそうです。

# audio-bookmarkの特徴

audio-bookmarkの使い方は[リポジトリ](https://github.com/anotherhollow1125/audio-bookmark)のREADME.mdに譲るとして、軽く特徴を紹介します！

- **普段使うオーディオだけをまとめたプロファイル**を作れる
- プロファイルでは**オーディオにニックネームをつけられる**
- ショートカットキー(ホットキー)も設定できる
- ついでに音量調節とミュートもできる

機能は以上です。なるべくシンプルになるよう努めました。また、現在のところデフォルト出力(スピーカ)切り替えのみ対応になっておりデフォルト入力(マイク)切り替え機能は未定です。

# audio-bookmarkの仕組み

audio-bookmarkはGUIフレームワーク[Tauri](https://tauri.app/)で動いています！Tauriについて(そしてRustについて)は次の記事で詳細にまとめていますので、気になった方は読んでみてください！

https://qiita.com/namn1125/items/8153f7c0881b7d4127ed

ハンズオン記事を書いた時からTauriについて変わった大きな点といえば[**Tauri Mobile**のα版](https://tauri.app/blog/2022/12/09/tauri-mobile-alpha)が登場した点でしょうか。

https://www.publickey1.jp/blog/22/tauriiosandroidtauri_mobileelectronrust.html

ついにAndroidアプリもiOSアプリもRustとTypeScriptで作れるようになると思うとRustaceanとしてはとてもワクワクします！なおTauriプロジェクトのロードマップ曰くRust以外の言語もバックエンドで使えるようにしたいと考えているらしいので、今後Tauriを触る人はどんどん増えていきそうです。

このようにクロスプラットフォームが売りのTauriですが、今回はWindows版のみ作りました。(オーディオ切り替え機能自体はTauriとは関係なくプラットフォーム個別に作らなければならないのと、筆者のメイン機がWindowsだからです)

Tauriフレームワークによる本アプリ全体の設計は次のようになっています。<font color="#aaaaaa">こういう図描きたかった</font>

![audio_bookmark_system2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/7484d4e0-0601-fe18-bbcd-c888e1d3be4f.png)

バックエンドではwindows-rsというクレートを用いてWin32 APIを叩いています。windows-rsについては[次節](#windows-rs)で解説しています！本節では全体についてもう少しだけ詳しく解説します。

フロントエンド部分はReact + TypeScript + muiを使って実装しました。マテリアルデザインを取り入れることでDefaultAudioChangerと差別化を図り、**最近出たモダンなアプリケーション感を醸し出せています**。Tauriを使う最大のメリットですね。

またTauriが用意している各種素晴らしいAPI(`fs`, `globalShortcut`, `tauri/invoke`)をフロント側から叩くようにすることで、Rust側はあくまでもWin32APIの呼び出し管理のみに集中できるような作りにしました。

| API | 使用目的 |
|:----|:--------|
| `fs`  | プロファイルの保存・管理 |
| `globalShortcut` | ホットキー機能 |
| `tauri/invoke` | Rust側で用意したオーディオコントロール用コマンドの呼び出し |

そのため、目玉であるプロファイル(お気に入り)機能やショートカット機能の実装にRustは全く関わっていません。Tauri APIのお陰で、ガッツリ作り込んで安定させたい機能はRustで、後からいくらでも修正が必要そうな機能はTypeScriptで、といった具合に**RustとTSで役割分担できる**のもまたTauriの魅力です。いままで筆者が作ってきたアプリは不安要素がアプリ全体に散らばりがちでしたが、自然と機能単位で実装が分かれるので自信を持ってコーディングを進められる点が良いです。

その他地味に頑張ったのは、表示とオーディオの状態を常に一致させる機能です。オーディオ関連のイベントが発生したらそれをフロント側に通知し、`useState` [^context]で管理しているオーディオリストの更新が走ることで全体が更新されるような作りになっています。

[^context]: 小規模だったので`useContext`等のリッチな状態管理機能の使用は見送られました

# windows-rs

ここからはWin32 APIを叩けるクレート[windows-rs](https://github.com/microsoft/windows-rs)の話題です。最近[linuxカーネルへの導入が本格的に始まったり](https://japan.zdnet.com/article/35193491/)などいよいよRustが本格的な「システム」プログラミング言語として使われ始めているなぁと感じることが多くなりましたが、windows-rsの成熟度合いも負けておりません。

https://github.com/microsoft/windows-rs

以前は`windows-rs`を使うには基本的な環境構築に加えコンパイル前に依存のビルドが必要という感じで煩雑でしたが、今は5分もあれば簡単なWin32 APIを使ったプログラムが書けるぐらいにお手軽になっています。

説明より動く例の方が伝わると思うのでコード例を書きます(環境構築は割愛します)。例えばビープ音を鳴らすだけのアプリを作ってみましょう。

まず`Cargo.toml`を書きます。

```toml:Cargo.toml
[package]
name = "beep"
version = "0.1.0"
edition = "2021"

[dependencies.windows]
version = "0.43.0"
features = [
    # 後述
]
```

Win32 APIのラッパー全部をバイナリに含めるととても大きいので、必要な機能のみ`features`で指定するスタイルを取っています。今回は`MessageBeep`関数を呼び出したいので、目的の関数を[公式ドキュメント](https://microsoft.github.io/windows-docs-rs/doc/windows/)で探し、必要なfeatureを確認します。

https://microsoft.github.io/windows-docs-rs/doc/windows/

[MessageBeep](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Diagnostics/Debug/fn.MessageBeep.html)のページを確認すると、

> Required features: "Win32_System_Diagnostics_Debug", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"

という記述が確認できます。これに従って必要なfeatureを`Cargo.toml`に記述します。

```toml
[dependencies.windows]
version = "0.43.0"
features = [
    "Win32_System_Diagnostics_Debug",
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging"
]
```

後はコードを書くだけです。[MessageBeep](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Diagnostics/Debug/fn.MessageBeep.html)周りで名前空間を確認すれば、次のようなコードを公式ドキュメント以外を参照せずに書けます。

```rust:main.rs
use windows::Win32::System::Diagnostics::Debug::MessageBeep;
use windows::Win32::UI::WindowsAndMessaging::MB_OK;

fn main() {
    unsafe {
        let _ = MessageBeep(MB_OK);
    }
}
```

実行すればおそらくビープ音が鳴るでしょう。

もちろんWin32 APIが提供する各機能の使い方は複雑怪奇ですのでいつでもこんな簡単には行きませんが、そこは**Rustじゃない別な言語の(C++等の)Win32 API関連記事・情報**を確認するだけで良く、RustによるWin32 APIプログラミングの敷居はそこまで高くないことがわかるでしょう。

## COMと`interface`,`implement` feature

ここまで手軽になったというだけでもMicrosoftの本気が伝わってくるとは思いますが、今回ご紹介したいすごい機能は[interface](https://github.com/microsoft/windows-rs/blob/8f4f65ee6f4ebdd6826cdf5aa8ff0a7db4316972/docs/FAQ.md#how-do-i-declare-a-new-com-interface) featureと[implement](https://github.com/microsoft/windows-rs/blob/8f4f65ee6f4ebdd6826cdf5aa8ff0a7db4316972/docs/FAQ.md#how-do-i-implement-an-existing-com-interface) featureです。

https://github.com/microsoft/windows-rs/blob/8f4f65ee6f4ebdd6826cdf5aa8ff0a7db4316972/docs/FAQ.md

これらはCOM (Component Object Model)と呼ばれるMicrosoftが提唱していた言語に依存しないオブジェクト指向システムに関わるものです。

COMは「オブジェクトとインターフェースが定義されているのでオブジェクトにインターフェースを被せてプログラムから使う」みたいな使い方をするAPIのような感じのものになっています。(語弊がありそう...)
最近はレガシーになってしまっているようですが、COMが使えるとWindowsアプリ間での連携がかなり容易になります。例えば次の記事ではIEの立ち上げをVBSから行っています。

https://qiita.com/mintcandy/items/979a4a2669c8fc1f4f2f

RustからCOMを使うには`CoInitialize`を呼び`CoCreateInstance`を呼び云々カンヌンし、プログラム終了前に`CoUninitialize`を呼ぶみたいな流れになります。`Drop`トレイトを使うとこの辺いい感じにできます。(もとい`Drop`トレイトが輝く瞬間で個人的に好きです)

全体像の例は次の記事様がとても参考になります。

https://qiita.com/benki/items/635867b654783da0322f

### `interface` feature

今回、[Windowsの音声出力先を変えるショートカット作成 - itiblog](https://itib.hatenablog.com/entry/2021121001)というブログ記事様から最初のヒントを得てオーディオ切り替え機能を実装していったのですが、その際に`IPolicyConfig`というインターフェースとGUID`870AF99C-171D-4F9E-AF0D-E63DF40C2BC9`の謎の(隠されている?)オブジェクトが必要になりました。

Win32 APIで明示的に定義されているインターフェースならば困らなかったのですが、残念なことに`IPolicyConfig`はWin32 APIからは提供されていません。こんな時に使えるのが`interface` featureになります。本アプリでの`IPolicyConfig`インターフェースの定義は次のようになっています。

```rust
#[interface("F8679F50-850A-41CF-9C72-430F290290C8")]
unsafe trait IPolicyConfig: IUnknown {
    fn GetMixFormat(&self) -> HRESULT;
    fn GetDeviceFormat(&self) -> HRESULT;
    fn ResetDeviceFormat(&self) -> HRESULT;
    fn SetDeviceFormat(&self) -> HRESULT;
    fn GetProcessingPeriod(&self) -> HRESULT;
    fn SetProcessingPeriod(&self) -> HRESULT;
    fn GetShareMode(&self) -> HRESULT;
    fn SetShareMode(&self) -> HRESULT;
    fn GetPropertyValue(&self) -> HRESULT;
    fn SetPropertyValue(&self) -> HRESULT;
    fn SetDefaultEndpoint(&self, deviceID: *const u16, role: u32) -> HRESULT;
    fn SetEndpointVisibility(&self) -> HRESULT;
}
```

[先のブログ](https://itib.hatenablog.com/entry/2021121001)に挙げたC#でのインターフェース定義と比較しても、そこまで複雑ではないことが読み取れます。COMに関する記述がMicrosoftが贔屓しているであろうC#と同等以上に手軽に書ける...これはMicrosoftがRustにお熱である証拠としては十分ではないでしょうか...？！

### `implement` feature

オーディオの状態変更通知受け取りは、`IMMNotificationClient`というインターフェースを実装した自前オブジェクトを用いることで実現しています。インターフェースの持つメソッドとしてコールバックを実装するイメージです。

その「インターフェースを実装」するために必要となるのが`implement` featureになっています。名前のままですね！このfeatureを入れると`IMMNotificationClient_Impl`という**トレイト**が生えてきて、Rustの構造体にインターフェースを実装するという直感的なコーディングが可能になります。

```rust
#[implement(IMMNotificationClient)]
struct MyNotificationClient(Sender<Notification>);

impl IMMNotificationClient_Impl for MyNotificationClient {
    fn OnDeviceStateChanged(
        &self,
        pwstrdeviceid: &PCWSTR,
        dwnewstate: u32,
    ) -> windows::core::Result<()> {
        // OnDeviceStateChangedなときにして欲しい処理(Tauriのフロントに知らせるなど)
        Ok(())
    }

    fn OnDeviceAdded(&self, pwstrdeviceid: &PCWSTR) -> windows::core::Result<()> {
        // ...
        Ok(())
    }

    fn OnDeviceRemoved(&self, pwstrdeviceid: &PCWSTR) -> windows::core::Result<()> {
        // ...
        Ok(())
    }

    fn OnDefaultDeviceChanged(
        &self,
        _flow: EDataFlow,
        _role: ERole,
        pwstrdefaultdeviceid: &PCWSTR,
    ) -> windows::core::Result<()> {
        // ...
        Ok(())
    }

    fn OnPropertyValueChanged(
        &self,
        pwstrdeviceid: &PCWSTR,
        key: &PROPERTYKEY,
    ) -> windows::core::Result<()> {
        // ...
        Ok(())
    }
}
```

いやMicrosoftさん**Rust好きすぎだろ**！！！

ちゃんとインターフェースのこともトレイトのこともわかっているからこその芸当です。ヘンテコなマクロをたくさん呼び出したりなどということをしなくてよく、見やすい属性風マクロだけでここまで便利な機能を提供してくれています。

実装当初はこの辺の記述が煩雑になり辛くなるだろうなぁ...と予想していたのですが、本節で紹介した `interface` featureと`implement` featureのお陰でむしろスッキリと実装できたのでした。<font color="#bbbbbb">正直フロントエンドのほうがキツかった</font>

# まとめ・所感

<!-- いい感じの文言だけどボツ
[windows-rsのリリース](https://github.com/microsoft/windows-rs/releases)を見ると新しいバージョンもかなり高い頻度でリリースされていることがわかります。Tauriも前述の通りモバイル向け機能のα版がリリースされました。
-->

2022年、Rustの使用率は[50%も増加した](https://www.itmedia.co.jp/news/articles/2211/18/news120.html)そうです。本記事ではその状況について、Tauri、windows-rsという具体例を出しました。

Tauriとwindows-rsのお陰でWindowsアプリDIYが想像以上に楽しいものになりました。Tauriを盛り上げるため、そしてMicrosoftさんの熱意に応えるために是非皆さんもTauriでWindowsアプリを作りましょう！

ここまで読んでいただき誠にありがとうございました。

# 引用・参考

記事には書かなかった記事制作やアプリ制作等で参考にしたサイトです。

- [ビープ音の再生 | WINAPI入門～bituse～ https://bituse.info/winapi/33](https://bituse.info/winapi/33)
- [コンポーネント オブジェクト モデル (COM) - Win32 apps | Microsoft Learn https://learn.microsoft.com/ja-jp/windows/win32/com/component-object-model--com--portal](https://learn.microsoft.com/ja-jp/windows/win32/com/component-object-model--com--portal)
- [Component Object Model - Wikipedia https://ja.wikipedia.org/wiki/Component_Object_Model](https://ja.wikipedia.org/wiki/Component_Object_Model)
- [COM の基礎 http://chokuto.ifdef.jp/urawaza/com/com.html](http://chokuto.ifdef.jp/urawaza/com/com.html)
- [IMMNotificationClient、IAudioSessionEventsにおけるデバイス無効化時のイベントハンドラー呼び出し順序 - notes5375 https://killswitch5375.hatenablog.com/entry/20120319/p1](https://killswitch5375.hatenablog.com/entry/20120319/p1)