Pythonから棒読みちゃんに読み上げさせる(Socket通信)

# 目的
Pythonとsocket通信を用いて[棒読みちゃん](https://chi.usamimi.info/Program/Application/BouyomiChan/)にメッセージを読み上げさせる。
# 環境

- python3.6
- 棒読みちゃんVer0.1.11.0 β21
- Windows10 20H2

後述の「Pythonでコード作成」にはPythonの古いバージョンだと実装されていないメソッドがあるため注意。

# まず付属のサンプルソースを読み解く
2021年6月10日現在の最新の棒読みちゃんVer0.1.11.0 β21の配布ファイルの中には、socket通信で読み上げ指示を送るサンプルソースが付属している。内容は再配布禁止となっているため載せられないが、誰でもダウンロードできるので確認してほしい。
サンプルソースはC++版だが、socketさえ使えれば他の言語からでも読み上げ指示を送れるはずだ。
ありがたいことにPythonには標準ライブラリとしてsocketモジュールが付属している。

## データのフォーマット
読み上げ指示に使われるデータのフォーマットは付属ファイルに記述されている。
大きく分けて２つのパートからなるようだ。

- 15Byte固定のヘッダー部
- 可変長のメッセージ部

### ヘッダー部のフォーマット
ヘッダー部はひと続きで15Byte固定だ。
サンプルソースを見るに、それぞれの要素は符号付き整数(2の補数を使う)と思われる。バイトオーダーはリトルエンディアンのようだ。

| 順番 | 名前 | 大きさ | 値 |
|:-:|:-:|:-:|:-:|
| 1 | コマンド | 2Byte | 読み上げ指示では1固定 |
| 2 | 読み上げ速度 | 2Byte | 50～300, または-1で現在の設定値 |
| 3 | 読み上げ音程 | 2Byte | 50～200, または-1で現在の設定値 |
| 4 | 音量 | 2Byte | 0～100, または-1で現在の設定値 |
| 5 | 声質 | 2Byte | 0で現在の設定値, 1～8でAquesTalk, 10001以上でSAPI5 |
| 6 | メッセージ部の文字コード | 1Byte | 0でUTF-8, 1でUnicode, 2でShift-JIS |
| 7 | メッセージ部の長さ | 4Byte | 付属ファイルには書かれていないがサンプルを見るに0～2147483647だろう(signed intの最大値) |

###メッセージ部のフォーマット
メッセージ部はヘッダー部の直後に現れる可変長のデータだ。ここに読み上げさせたいメッセージを格納する。
文字コードはヘッダー部の6番で指定する。
メッセージの長さはヘッダー部の7番で指定する。単位はByteなので注意。

## 読み上げ指示の流れ
付属のサンプルソースでは

0. socketを接続
0. ヘッダ部を送信
0. メッセージ部を送信
0. socketを閉じる

という流れで通信していた。
読み上げ指示のみならデータを送信するだけで、何かを受信する必要はないようだ。

# Pythonでコード作成
下記のようなコードを作成した。
実行すると変数`MESSAGE`の内容を読み上げてくれた。

```python:python_to_bouyomi.py
BOUYOMICHAN_IP = '127.0.0.1'
BOUYOMICHAN_PORT = 50001
MESSAGE = 'ゆっくりしていってね'

import socket

def make_header(command=1, speed=-1, tone=-1, volume=-1, voice=0, char_code=0, msg_length=0):
    """
    棒読みちゃんへの読み上げ指示に使うヘッダー部を生成します。
    長さ15のbytes型オブジェクトが返されます。
    """
    buffer = bytes()
    buffer += command.to_bytes(length = 2, byteorder = 'little', signed = True)
    buffer += speed.to_bytes(length = 2, byteorder = 'little', signed = True)
    buffer += tone.to_bytes(length = 2, byteorder = 'little', signed = True)
    buffer += volume.to_bytes(length = 2, byteorder = 'little', signed = True)
    buffer += voice.to_bytes(length = 2, byteorder = 'little', signed = True)
    buffer += char_code.to_bytes(length = 1, byteorder = 'little', signed = True)
    buffer += msg_length.to_bytes(length = 4, byteorder = 'little', signed = True)
    return buffer

if __name__ == '__main__':
    message = MESSAGE.encode('utf-8') #文字コードをutf-8としてbytesオブジェクトに変換する
    header = make_header(msg_length = len(message)) #ヘッダー部を生成
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #socket生成
    s.connect((BOUYOMICHAN_IP, BOUYOMICHAN_PORT)) #socket接続
    s.sendall(header) #ヘッダー部送信
    s.sendall(message) #メッセージ部送信
    s.close() #socketを閉じる
```

str型のオブジェクト`MESSAGE`をbytes型のオブジェクト`message`に変換している点に注意してほしい。
これはヘッダー部を生成する際に`len(message)`で文字数ではなくバイト数を得るためだ。

```python
>>> message = "テスト" #これはstr型
>>> bytes_message = message.encode('utf-8') #これはbytes型
>>> print(len(message)) #3文字
3
>>> print(len(bytes_message)) #9バイト
9
```

# 他にも試して分かったこと
- 読み上げ指示は1件ごとにsocketを閉じなければならない。つまり1度の接続で複数の読み上げ指示を送ることはできない。複数読み上げさせたいときはその都度socketを生成し直そう。
- ヘッダー部のコマンドの値を変更することで他にも色々な指示が送れる。詳しくは棒読みちゃん付属のサンプルを参照。

# 展望
Pythonから読み上げ指示を送ることができた。つまりPythonの豊富なライブラリ群を使って色々なものを読み上げさせられるということだ。夢が広がる。
また、socket通信を用いることによってリモートマシンに読み上げ指示を送ることができる。これはローカル専用のHTTP連携やIpcClientChannel連携にはない利点だ。使い所は思いつかないが...。
とりあえず筆者はfeedparserモジュールとrequestsモジュールを使ってニュースサイトのRSSと記事を読み上げさせたいと思っている。

# 参考文献

- [int.to_bytes(length, byteorder, *, signed=False) - 組み込み型](https://docs.python.org/ja/3/library/stdtypes.html#int.to_bytes)
- [socket --- 低水準ネットワークインターフェース](https://docs.python.org/ja/3/library/socket.html)
- 棒読みちゃん付属のサンプルソース