D言語でTeamSpeak3プラグインを作った

この記事は [D言語 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/dlang) 16日目の記事です。
# はじめに
我が家ではTeamSpeak3(以下TS3)のサーバーを運用しているのですが、それに読上げ機能をつけるためにD言語でプラグインを書きました。その過程でのあれこれを書いていきたいと思います。

# システム概要
このシステムで実装したのは以下の機能です。

* チャットの読上げ  
これがしたかったので当然ですね
* 入退室の読上げ  
これもあると便利
* ユーザーごとに読上げ音声の切り替え  
誰の発言かすぐわかって便利です
* 音声再生  
いろいろな効果音などを再生できます

# 実装
システム構成を簡単な図にしてみました。
![TSシステム構成.svg.png](https://qiita-image-store.s3.amazonaws.com/0/130197/3315fe75-1e7d-5d3c-b06b-1936a04b3371.png)
Linux側とWindows側の両方にTS3クライアントがありますが、[問題](#問題)があり1つにまとめられませんでした。Linux側で今回作成するプラグインが動いており、Windows側では音声ループバックを送信することで、読上げを実現します。

## 棒読みちゃん
棒読みちゃんにはTCPで待ち受けてコマンドを受信する機能があるのでそれを活用していきます。パケットのフォーマットなども棒読みちゃんのドキュメントに説明があるので比較的実装は楽でした。コードなどは省略します。
棒読みちゃんはSAPI5には対応しているのでCeVIOは何もせずに使えます。Voiceroidは[プラグイン](http://ch.nicovideo.jp/Wangdora/blomaga/ar126461)が配布されているのでそれを使います。

## TS3
メインのTS3のプラグインです。C言語とC#のSDKが[配布されている](https://www.teamspeak.com/en/downloads/#sdk)のでそれを参考に実装します。残念なことにDPPを知ったのはこれを作ってしばらく後のことでした。(DPPについては[2日目の記事](https://qiita.com/lempiji/items/2cc5d56a2cb2ff992123#dpp)に書かれています)
プラグインは動的ライブラリをプラグインディレクトリに置くことでTS3から認識してくれます。
動的ライブラリを作るのは初めてだったので、とりあえず最低限TS3に認識されるものを作ることにしました。

```d
import std.string;


extern(C) immutable(char)* ts3plugin_name()
{
	// プラグインの名前
	return "plugin name".ptr;
}

extern(C) immutable(char)* ts3plugin_version()
{
	// バージョン情報
	return "version".toStringz;
}
extern(C) int ts3plugin_apiVersion()
{
	// 対応しているTS3のAPIバージョン情報
	return 22;
}
extern(C) immutable(char)* ts3plugin_author()
{
	// プラグインの作成者
	return "auther".toStringz;
}
extern(C) immutable(char)* ts3plugin_description()
{
	// プラグインの説明
	return "description".toStringz;
}
extern(C) int ts3plugin_init()
{
	// プラグインの初期化処理
	return 0;
}
extern(C) void ts3plugin_setFunctionPointers(const TS3Functions funcs)
{
	// TS3の機能へアクセスするための構造体を受け取る
}
extern(C) void ts3plugin_shutdown()
{
	// プラグインの終了処理
}

struct TS3Functions
{
	// TS3の機能にアクセスするための関数ポインタが格納されている
	// 長いので省略
}
```
extern(C)をつけることで生成されるオブジェクトの命名がC言語と同じになるようです。
dubでビルドする際はデフォルトの設定では動的ライブラリにならないのでdub.jsonに

```json
"targetType": "dynamicLibrary",
```
を追加してビルドします。
これでとりあえず認識されるものはできました。
![スクリーンショット_2018-12-16_01-51-12.png](https://qiita-image-store.s3.amazonaws.com/0/130197/c4fb4aeb-9def-62d7-edcc-9ec5f691d36a.png)
しかしこの状態ではランタイムが動いていないのでD言語の力を活かしきれません。そこでランタイムを初期化したいと思います。
ランタイムの初期化は簡単です。

```d
import core.runtime;
```
して適当な場所で

```d
Runtime.initialize();
```
するだけです。
今回はプラグインを初期化するts3plugin_initといういかにもな関数があるのでそこに書けばよさそう、と思いましたがこれは罠でした。プラグイン関数はここに書いた関数の上から順番に実行されるらしく、ts3plugin_nameなどは初期化処理前に呼ばれるようです。今回のようにハードコードする場合は問題ないですが...

### チャットの読上げ
チャットが来たときのイベントハンドラの定義はこうです。

```d
extern(C) int  ts3plugin_onTextMessageEvent(ulong serverConnectionHandlerID, ushort targetMode, ushort toID, ushort fromID, const char* fromName, const char* fromUniqueIdentifier, const char* message, int ffIgnored);
```
文字列はconst char*で渡されるのでDのstringに変えると処理が簡単になります。std.conv の to!string を使いましょう。
音声再生もここで処理します。メッセージをスペース区切りで分割した上で、合致するファイル名があったときに、棒読みちゃんの再生機能を使えるようにメッセージを変換して送ります。
送信者ごとに音声を変える処理もしてしまいます。fromNameで送信者の名前が取得できるのでリストから割当済みの音声を取得するか、適当な音声を割り振って棒読みちゃんの音声切り替え機能(文の頭に”音声名)”)を使い音声を切り替えます。

### 入退室の読上げ
入退室と言っても接続、切断、部屋の移動などがあります。関連するイベントハンドラの定義はこうです。

```d
// 部屋の移動時に呼ばれる
//oldChannelIDが0の場合は新規接続、newChannelIDが0の場合は切断
extern(C) void ts3plugin_onClientMoveEvent(ulong serverConnectionHandlerID, ushort clientID, ulong oldChannelID, ulong newChannelID, int visibility, const char* moveMessage);
// 接続タイムアウト時に呼ばれる
extern(C) void ts3plugin_onClientMoveTimeoutEvent(ulong serverConnectionHandlerID, ushort clientID, ulong oldChannelID, ulong newChannelID, int visibility, const char* timeoutMessage);
```
これらのイベントではユーザー名が渡されないのでクライアントIDから取得します。取得には初期化のときに渡された関数ポインタの詰まった構造体を使います。
構造体内のgetClientVariableAsStringを使って名前を取得します。

```d
char* userNamePtr;
getClientVariableAsString(serverConnectionHandlerID, id, 1, &userNamePtr)
```
戻り値は0が成功、それ以外は失敗で引数で名前を受け取ります。完全にC言語です。
これで適当に文章を作って棒読みちゃんに投げればOKですね。

# 問題
Windows側のTS3でプラグインを動かす予定だったのですが[ビルドエラー](https://gist.github.com/ray45422/7e6ca2dca7d24fd1cfb23fd6a66d061c)が出て断念しました。D言語とWindowsに詳しい方教えてください。

# まとめ
D言語を使うことでC言語では面倒な文字列処理などを簡単にできるようになったのが良かったです。しかし、C言語の知識が無いといろいろ困る部分もありました。C言語からは逃れられないですが、そういう環境下でも使えるのは便利でいいですね。