この記事は Rust Advent Calendar 2022 - Qiita 20日目の記事です1!
GUIフレームワークのTauriと、Microsoft公式が出しているWindows API用クレートのwindows-rsを使用し、audio-bookmarkというWindows用オーディオ切り替えアプリを作成したので、その仕組みの解説とwindows-rsの紹介記事になります!
作ったアプリ「audio-bookmark」のリポジトリ↓
- ダウンロードページ: https://github.com/anotherhollow1125/audio-bookmark/releases
- Assets以下の
audio-bookmark_0.0.0_x64_en-US.msi
がインストーラになります。 - まだα版です。デバッグしていだける猛者の方がいらっしゃいますと大変助かります!!
- Assets以下の
作った動機
挙げたGif画像のように、たくさんのよくわからないオーディオインターフェースが登録されたWindows環境2で生活しているのは筆者ぐらいかもしれませんが、要は
すぐに普段使っているスピーカに切り替えられない
という悩みがありました。この原因を細かくすると2つになります。
- 普段使わないオーディオが多数表示されている
- オーディオ名が直感に反するものになっている
- 動画の例だと、有線ヘッドホンの名前が「スピーカー (2- USB Audio Device)」
解決策もすぐに思いつきました
- 普段使うオーディオだけのリストを作る
- オーディオに別名をつける
- 普段使うオーディオにのみホットキーを割り当てる
ホットキー機能を提供しているアプリにDefaultAudioChangerという先達がいましたが、
- 結構古いアプリのよう
- 筆者が考えた機能はオーディオを管理しやすいという新規性がありそう
- Tauriを完全に理解し始めているので実用的なアプリを作りたい
と考え、実装しアドベントカレンダーを書く今に至りました。修論の新規性もこれぐらい簡単にひねり出したい()
今後Zoom等による遠隔会議の需要が増え、オーディオを素早く切り替えるアプリの需要も増しそうですし、作る意味は大いにありそうです。
audio-bookmarkの特徴
audio-bookmarkの使い方はリポジトリのREADME.mdに譲るとして、軽く特徴を紹介します!
- 普段使うオーディオだけをまとめたプロファイルを作れる
- プロファイルではオーディオにニックネームをつけられる
- ショートカットキー(ホットキー)も設定できる
- ついでに音量調節とミュートもできる
機能は以上です。なるべくシンプルになるよう努めました。また、現在のところデフォルト出力(スピーカ)切り替えのみ対応になっておりデフォルト入力(マイク)切り替え機能は未定です。
audio-bookmarkの仕組み
audio-bookmarkはGUIフレームワークTauriで動いています!Tauriについて(そしてRustについて)は次の記事で詳細にまとめていますので、気になった方は読んでみてください!
ハンズオン記事を書いた時からTauriについて変わった大きな点といえばTauri Mobileのα版が登場した点でしょうか。
ついにAndroidアプリもiOSアプリもRustとTypeScriptで作れるようになると思うとRustaceanとしてはとてもワクワクします!なおTauriプロジェクトのロードマップ曰くRust以外の言語もバックエンドで使えるようにしたいと考えているらしいので、今後Tauriを触る人はどんどん増えていきそうです。
このようにクロスプラットフォームが売りのTauriですが、今回はWindows版のみ作りました。(オーディオ切り替え機能自体はTauriとは関係なくプラットフォーム個別に作らなければならないのと、筆者のメイン機がWindowsだからです)
Tauriフレームワークによる本アプリ全体の設計は次のようになっています。こういう図描きたかった
バックエンドではwindows-rsというクレートを用いてWin32 APIを叩いています。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
3で管理しているオーディオリストの更新が走ることで全体が更新されるような作りになっています。
windows-rs
ここからはWin32 APIを叩けるクレートwindows-rsの話題です。最近linuxカーネルへの導入が本格的に始まったりなどいよいよRustが本格的な「システム」プログラミング言語として使われ始めているなぁと感じることが多くなりましたが、windows-rsの成熟度合いも負けておりません。
以前はwindows-rs
を使うには基本的な環境構築に加えコンパイル前に依存のビルドが必要という感じで煩雑でしたが、今は5分もあれば簡単なWin32 APIを使ったプログラムが書けるぐらいにお手軽になっています。
説明より動く例の方が伝わると思うのでコード例を書きます(環境構築は割愛します)。例えばビープ音を鳴らすだけのアプリを作ってみましょう。
まずCargo.toml
を書きます。
[package]
name = "beep"
version = "0.1.0"
edition = "2021"
[dependencies.windows]
version = "0.43.0"
features = [
# 後述
]
Win32 APIのラッパー全部をバイナリに含めるととても大きいので、必要な機能のみfeatures
で指定するスタイルを取っています。今回はMessageBeep
関数を呼び出したいので、目的の関数を公式ドキュメントで探し、必要なfeatureを確認します。
MessageBeepのページを確認すると、
Required features: "Win32_System_Diagnostics_Debug", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"
という記述が確認できます。これに従って必要なfeatureをCargo.toml
に記述します。
[dependencies.windows]
version = "0.43.0"
features = [
"Win32_System_Diagnostics_Debug",
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging"
]
後はコードを書くだけです。MessageBeep周りで名前空間を確認すれば、次のようなコードを公式ドキュメント以外を参照せずに書けます。
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 featureとimplement featureです。
これらはCOM (Component Object Model)と呼ばれるMicrosoftが提唱していた言語に依存しないオブジェクト指向システムに関わるものです。
COMは「オブジェクトとインターフェースが定義されているのでオブジェクトにインターフェースを被せてプログラムから使う」みたいな使い方をするAPIのような感じのものになっています。(語弊がありそう...)
最近はレガシーになってしまっているようですが、COMが使えるとWindowsアプリ間での連携がかなり容易になります。例えば次の記事ではIEの立ち上げをVBSから行っています。
RustからCOMを使うにはCoInitialize
を呼びCoCreateInstance
を呼び云々カンヌンし、プログラム終了前にCoUninitialize
を呼ぶみたいな流れになります。Drop
トレイトを使うとこの辺いい感じにできます。(もといDrop
トレイトが輝く瞬間で個人的に好きです)
全体像の例は次の記事様がとても参考になります。
interface
feature
今回、Windowsの音声出力先を変えるショートカット作成 - itiblogというブログ記事様から最初のヒントを得てオーディオ切り替え機能を実装していったのですが、その際にIPolicyConfig
というインターフェースとGUID870AF99C-171D-4F9E-AF0D-E63DF40C2BC9
の謎の(隠されている?)オブジェクトが必要になりました。
Win32 APIで明示的に定義されているインターフェースならば困らなかったのですが、残念なことにIPolicyConfig
はWin32 APIからは提供されていません。こんな時に使えるのがinterface
featureになります。本アプリでのIPolicyConfig
インターフェースの定義は次のようになっています。
#[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;
}
先のブログに挙げたC#でのインターフェース定義と比較しても、そこまで複雑ではないことが読み取れます。COMに関する記述がMicrosoftが贔屓しているであろうC#と同等以上に手軽に書ける...これはMicrosoftがRustにお熱である証拠としては十分ではないでしょうか...?!
implement
feature
オーディオの状態変更通知受け取りは、IMMNotificationClient
というインターフェースを実装した自前オブジェクトを用いることで実現しています。インターフェースの持つメソッドとしてコールバックを実装するイメージです。
その「インターフェースを実装」するために必要となるのがimplement
featureになっています。名前のままですね!このfeatureを入れるとIMMNotificationClient_Impl
というトレイトが生えてきて、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のお陰でむしろスッキリと実装できたのでした。正直フロントエンドのほうがキツかった
まとめ・所感
2022年、Rustの使用率は50%も増加したそうです。本記事ではその状況について、Tauri、windows-rsという具体例を出しました。
Tauriとwindows-rsのお陰でWindowsアプリDIYが想像以上に楽しいものになりました。Tauriを盛り上げるため、そしてMicrosoftさんの熱意に応えるために是非皆さんもTauriでWindowsアプリを作りましょう!
ここまで読んでいただき誠にありがとうございました。
引用・参考
記事には書かなかった記事制作やアプリ制作等で参考にしたサイトです。
- ビープ音の再生 | WINAPI入門~bituse~ https://bituse.info/winapi/33
- コンポーネント オブジェクト モデル (COM) - Win32 apps | Microsoft Learn 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
- COM の基礎 http://chokuto.ifdef.jp/urawaza/com/com.html
- IMMNotificationClient、IAudioSessionEventsにおけるデバイス無効化時のイベントハンドラー呼び出し順序 - notes5375 https://killswitch5375.hatenablog.com/entry/20120319/p1
-
少し遅刻してしまい誠に申し訳ありませんm(_ _)m 前日の12/19はserinuntiusさんのPlonky2というゼロ知識証明アルゴリズムで実装されているEVMの証明をチラ見してみるという記事でした。筆者は暗号学系の研究室に所属しているのでゼロ知識証明等もゼミで取り扱ったりしているのですが、その関連技術(ブロックチェーン)が実際にRustコードとして現れていたのに少し感動しました。私もRustで暗号学に何か貢献したい... ↩
-
こんな環境の作り方ですが、大量のモニタをHDMIで繋いだりOculus Questを繋いだりOBS関連で仮想インターフェースが欲しくなって追加したりなんていうことを適当にやっているとできます。わりとこんな環境になっている人いるのでは...? ↩
-
小規模だったので
useContext
等のリッチな状態管理機能の使用は見送られました ↩