voicevox で生成された音声を zello に送る方法

# 概要

この記事では voicevox で生成された音声を python を使い zello に送る方法を記載しています。また、同様の方法を voiceroid や discord.py に応用できます。

**個人の趣味の範疇で利用してください**

# ポイント

* zello の API では [opus](https://opus-codec.org/) と呼ばれる Audio Codec を利用します。
* この記事では windows で実験しています。
　　　* この記事のための一部コマンドの実行結果例は macos から貼っています。

# 事前準備

## zello developer の登録

* [zello console](https://developers.zello.com/) からアカウントの登録を行い、開発用の API キーを発行します。
* この開発用の API キーは３ヶ月有効です。

## opus-tools のインストール

* [opus-tools をダウンロードします。](https://opus-codec.org/downloads/) 
* ダウンロードした zip ファイルを展開, フォルダ内に `opusenc` のバイナリ (*.exe) があることを確認します。
* 環境変数にこのフォルダ (`opusenc`) へのパスを設定します。
* `powershell` を開き、次のコマンドが通ることを確認します。

```
// TODO 差し替える
> opusenc --version
opusenc opus-tools 0.1.9 (using libopus 1.1)
Copyright (C) 2008-2013 Xiph.Org Foundation
```

## sox のインストール

* [sox](https://sox.sourceforge.net/) は Audio を扱う十徳ナイフです。
  * [ダウンロードはこちら](https://sourceforge.net/projects/sox/)
* `sox.exe` へのパスを環境変数に設定します。
* `powershell` を開き、次のコマンドが通ることを確認します。

```
// TODO: windows 版で差し替える
sox --version
sox:      SoX v14.4.2
```

## voicevox の起動

[voicevox](https://voicevox.hiroshiba.jp/) をダウンロードします。ダウンロードしたファイルを展開後にフォルダ内の `run.exe` (`voicevox.exe`) で合成音声エンジンの機能を提供する web API アプリケーションを起動します。このアプリケーションは `http://localhost:50021` で web サーバーを起動しています。

## 合成音声の送信テスト方法

* example コードを [github](https://github.com/naru-hiyoko/zello_audio_synth_py_examples) にアップロードしています。
* [demo_on_zello.py](https://github.com/naru-hiyoko/zello_audio_synth_py_examples/blob/main/demo_on_zello.py) で動作確認ができます。
   *  環境変数 `ZELLO_AUTH_TOKEN` に zello console から発行した API キーをセット
   *  実行時引数の `username`, `password`, `channel` には zello で普段利用しているアカウントとチャンネルを設定
* 別のデバイス、Zello アカウントからテキストメッセージをチャンネルに送ると合成音声が再生されます。

## 実装上のポイント

* zello の API 仕様はこちらを[参照](https://github.com/zelloptt/zello-channel-api)します。
* audio は 16bit 48k の 1ch として 20ms の opus packet を送ります
   * codec header でこれ以外を設定しても正しく再生されませんでした。
   * `opusenc` の振る舞いも怪しいため `sox` でデータを整えています
* voicevox は 16bit 符号あり 24k の 1ch の PCM らしいので、`sox` で 48k に直しています。
* voiceroid の場合は [このような変換](https://github.com/naru-hiyoko/zello_audio_synth_py_examples/blob/main/voiceroid.py#L29) でうまくいきます。

## Raw PCM と opus エンコーディング後のファイルサイズの違い

マイク入力を扱うための備忘録のついでにマイク音源を保存した raw pcm と opus encoding 後のファイルサイズを残します。raw pcm のファイルサイズ 960k に対して opus encoding 後は 82.9k ほどに圧縮されていることがわかります。

```python
import pyaudio
import subprocess
from subprocess import PIPE


if __name__ == '__main__':
    audio = pyaudio.PyAudio()

    sampling_rate = 48000

    stream = audio.open(format=pyaudio.paInt16, channels=1, rate=sampling_rate,
                        input=True, input_device_index=1, frames_per_buffer=int(48000/12))

    chunk_size_in_seconds = 0.2  # 200ms
    number_of_frames_per_chunk = int(sampling_rate * chunk_size_in_seconds)

    record_time_in_seconds = 10
    number_of_chunks = int((1 / chunk_size_in_seconds) * record_time_in_seconds)

    frames = b''
    for chunk_id in range(number_of_chunks):
        # NOTE: wait for `chunk_size_in_seconds`.
        frames += stream.read(num_frames=number_of_frames_per_chunk)

    command = 'sox -b 16 -e signed -c 1 -r 48000 -t raw - -b 16 -e signed -c 1 -r 48000 -t wav - | opusenc --quiet - -'
    p = subprocess.Popen(command, stdin=PIPE, stdout=PIPE, shell=True)

    opus_data = p.communicate(frames)[0]
    with open('sample.opus', 'wb') as f:
        f.write(opus_data)
```

```
sample.opus:

 File Size: 82.9k     Bit Rate: 66.3k
  Encoding: Opus          
  Channels: 1 @ 16-bit   
Samplerate: 48000Hz      
Replaygain: off         
  Duration: 00:00:10.00  

In:100%  00:00:10.00 [00:00:00.00] Out:480k  [      |      ]        Clip:0    
Done.
[app]$play sample.wav 

sample.wav:

 File Size: 960k      
  Encoding: Signed PCM    
  Channels: 1 @ 16-bit   
Samplerate: 48000Hz      
Replaygain: off         
  Duration: unknown      

In:0.00% 00:00:10.00 [00:00:00.00] Out:480k  [      |      ]        Clip:0    
Done.
```

# 余談

* windows <=> linux 間で wav ファイルを直接やりとりせず、numpy.ndarray で変換した方がよい。
   *  エンディアンの問題で再生できない
* discord に投稿すると音が小さい
* `discord.py` の [FFmpegOpusAudio](https://github.com/Rapptz/discord.py/blob/799e3c5bf09cedcd9b7e2b83df733910fa11532a/discord/player.py#L301) を始め使おうとしたが上手くいかない。
   * これが外部コマンド `ffmpeg` を使うが、`ffmpeg` のコマンド実行結果がエラーになる
   * このため [demo_on_discord.py](https://github.com/naru-hiyoko/zello_audio_synth_py_examples/blob/main/demo_on_discord.py) では `PCMAudio` を使っている。