Google Cloud Text-To-Speechを利用してUnityでキャラクターをフルボイスに！

タイトルの通りです．
UnityでもGoogle CloudのAPIを利用することで（簡単に）音声合成を行うことができます．

（追記：本記事はWindowsを対象にしています．Mac環境では[ランタイムで利用するライブラリをインポートする](https://qiita.com/Asalato/items/6aa23c40ff31d9d3f13d#%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B)の項目で導入しなければいけないライブラリが異なるため，異なる方法で用意する必要があります）

#余談

Google CloudのText-To-Speech APIを利用することで誰でも手軽に音声合成を行うことができます．
しかし，これを利用した記事は現状あまり公開されていません．

特にC#(.NET)環境での動作については，[Google Cloudのドキュメンテーションページ](https://cloud.google.com/text-to-speech/docs/samples)でも
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/3b07a6fd-3eae-fde3-b7e1-fa260e76854a.png)
と非常に残念な感じです．

今回これを触る機会があり，動くようにするために結構苦労しました．
実装の際特に詰まった部分について解説しながら動くサンプルを示したいと思います．

#準備
Unityプロジェクトに導入するためにあらかじめ行う作業があります．

##Google Cloud Platformの利用登録
まず，Google Cloudに開発者申請を行い，APIリクエストを送る際の認証情報を取得する必要があります．
[Google Cloud Platform](https://cloud.google.com/)のページにアクセスし，利用者登録を行います．
住所，名前，電話番号，クレジットカード番号を入力する欄がありますが，個人開発者であれば基本的に無料で利用できます．

[料金ページ](https://cloud.google.com/text-to-speech/pricing)に利用料の記載があります．
Text-To-Speechの場合，クラウドに送信した文字数に応じて料金が変動します．
通常の音声合成とWaveNet音声（ちょっとリアルな声）で値段が変わり，以下のように無料枠と有料の場合の値段が定められています．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/4f6b77ec-d693-5a0b-a6a7-ce18bd3d5da1.png)

利用者登録を行った後は分かりやすい名前を付けてプロジェクトを作成します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/eff05bef-2739-41d5-5088-498cf2c4053f.png)
その後プロジェクトでCloud Text-To-Speech APIを有効にします．
左上のHamburgerタブからメニューを開き，「APIとサービス」から「ライブラリ」を選びます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/a784d452-fc39-529b-ae01-1fe7c90f5b9d.png)
そこでCloud Text-To-Speechを選択し，APIを有効化します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/22f2e84c-735a-e277-b3c4-e951b5bb32c9.png)

次に実際に利用するAPIキーを作成します．
左上のHamburgerタブからメニューを開き，「IAMの管理」から「サービスアカウント」を選択し，サービスアカウントの作成を行います．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/eb9b0b7f-d1ef-faae-aa16-a988ff0038ec.png)
「作成」を選択し，次に出る組織の選択はスルーして大丈夫です．
次の「キーの作成」は必須です．
Json形式でキーを作成し，保存します．（このキーは厳重に管理してください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/779aae9e-6c03-c721-761a-46427918ddc1.png)
これでGoogle Could Platform側での作業は終わりです．

##Unityにライブラリをインポートする
現在Google Cloud Text-To-SpeechをUnityで利用できるようなライブラリは存在しません．
なので自分で利用するライブラリ（dll）をインポートする必要があります．

###NuGetからライブラリをインポートする
Google Cloud APIはNuGetで.Net用ライブラリが公開されており，それを利用します．
UnityにNuGetライブラリをインポートするための便利なツールに「[`NuGetForUnity`](https://github.com/GlitchEnzo/NuGetForUnity)」というものがあるので，これを利用しUnityに導入していきます．
これを利用することで依存するライブラリをまとめてインポートできるので便利です．

検索窓に「Cloud TextToSpeech」と入力することで目的のAPIが見つかるのでこれをインストールします．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/f595e83e-bd43-e295-38e6-59ca4e08bf03.png)

これでAssetフォルダに利用するdllがインポートできましたが，このままでは`unload broken assembly`というエラーが出ます．
これはdll間で依存するライブラリを見つけられていないためで，これを回避するためにはいろいろ方法がありますが，今回は一番単純な「<b>すべてのdllを同じディレクトリに置く</b>」という手段を取ります．結構めんどくさいです．

インポートを行った後`NuGetForUnity`はもう不要なので，Assetのディレクトリから該当するフォルダを削除します．（<b>dllの入っているPackagesフォルダは消さない！</b>）
そしてpackages以下に存在するdllを全てpackagesの先頭フォルダに移し，纏めます．
これでエラーは出なくなったはずです．

###ランタイムで利用するライブラリをインポートする
NuGetからライブラリをインポートすることでエディタ上ではエラーが出なくなりますが，このままでは実行時に`DllNotFoundException: grpc_csharp_ext`や`EntryPointNotFoundException: grpcsharp_native_callback_dispatcher_init`といったエラーが出てしまいます．

ランタイム（実行時）で利用するライブラリはhttps://github.com/jsmouret/grpc-unity-package からお借りします．
https://github.com/jsmouret/grpc-unity-package/releases から最新のgrpc-unity-package.zipをダウンロードし，Plugins/Grpc.Core/runtimes/win（利用環境によって変えてください）/x64/grpc_csharp_ex.dllをコピーし，先ほどNuGetからインポートしたフォルダに貼り付けます．

これで実行時のエラーもなくなります．

#スクリプトの用意
TextToSpeechSampleというC#スクリプトを新たに作成し，以下のスクリプトを貼り付けます．

```csharp
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Cloud.TextToSpeech.V1;
using Grpc.Auth;
using Grpc.Core;
using UnityEngine;
using UnityEngine.UI;
using WWUtils.Audio;
using Debug = UnityEngine.Debug;

public class TextToSpeechSample : MonoBehaviour
{
    [SerializeField] private InputField inputField;
    [SerializeField] private Button button;
    [SerializeField] private AudioSource audioSource;
    [SerializeField] private string credential;
    
    private const string GcpUrl = "https://www.googleapis.com/auth/cloud-platform";
    private const string ChannelTarget = "texttospeech.googleapis.com:443";

    private TextToSpeechClient _client;
    private AudioConfig _audioConfig;
    private VoiceSelectionParams _voiceSelectionParams;
    
    private ChannelCredentials _credentials;

    private SynchronizationContext _context;

    private Stopwatch _stopwatch = new Stopwatch();
    
    private void Start()
    {
        // ボタンを押したときのイベントを追加
        button.onClick.AddListener(() =>
        {
            var str = inputField.text;
            if (string.IsNullOrEmpty(str)) return;
            inputField.text = "";
            
            CreateRequest(str);
            Debug.Log($"Send Request: {str}");
        });
        
        // 認証情報をResourceから読み込む
        var credentialStr = Resources.Load<TextAsset>(credential).text;
        var googleCredential = GoogleCredential.FromJson(credentialStr);
        _credentials = googleCredential.CreateScoped(GcpUrl).ToChannelCredentials();

        var channel = new Channel(ChannelTarget, _credentials);
        _client = new TextToSpeechClientImpl(new TextToSpeech.TextToSpeechClient(channel), new TextToSpeechSettings());
        
        // オプションを記述
        _audioConfig = new AudioConfig()
        {
            AudioEncoding = AudioEncoding.Linear16,
            SampleRateHertz = 44100
        };

        // 声のパラメータを指定
        // https://cloud.google.com/text-to-speech/docs/voices?hl=jaに記載されているものから選択できます
        _voiceSelectionParams = new VoiceSelectionParams()
        {
            SsmlGender = SsmlVoiceGender.Female,
            LanguageCode = "ja-JP"
        };
        
        _context = SynchronizationContext.Current;
    }
    
    /// <summary>
    /// リクエストを送信する
    /// </summary>
    /// <param name="text">音声合成を行う対象の文</param>
    public void CreateRequest(string text)
    {
        var request = new SynthesizeSpeechRequest
        {
            Input = new SynthesisInput {Text = text},
            AudioConfig = _audioConfig,
            Voice = _voiceSelectionParams
        };
        
        _stopwatch.Restart();
        
        // リクエストを非同期で送信し，返ってきた後に再生するメソッドに投げる
        Task.Run(async () => { SetAudioClip(await _client.SynthesizeSpeechAsync(request)); });
    }

    /// <summary>
    /// Google CloudからのレスポンスをAudioClipに書き出し，再生する
    /// </summary>
    /// <param name="response">Google Cloudからのレスポンス</param>
    private void SetAudioClip(SynthesizeSpeechResponse response)
    {
        var bytes = response.AudioContent.ToByteArray();

        // byte[]をAudioClipで利用できる形に変換する
        var wav = new WAV(bytes);
        Debug.Log("Get Response: Elapsed time " + _stopwatch.ElapsedMilliseconds + "ms.\nData Length: " +
                  (wav.SampleCount * (1f / wav.Frequency) * 1000f).ToString("F0") + "ms.");
        _context.Post(_ =>
        {
            // AudioSourceに新しいAudioClipを貼り付ける
            audioSource.clip = AudioClip.Create("TextToSpeech", wav.SampleCount, 1, wav.Frequency, false);
            audioSource.clip.SetData(wav.LeftChannel, 0);

            // AudioClipを再生
            audioSource.Play();
        }, null);
    }
}
```

このスクリプトでは以下の処理を記述しています

- ButtonとInputFieldのイベントを追加
- 認証キー（Json）を読みこむ
- Google Cloudとの接続を行うクライアントの作成
- リクエストを送信する
- リクエストの返信からAudioClipを作成し，再生する

Google Cloudからのレスポンスはbyte[]で送られてくるので，それをAudioClipで利用できるようfloat[]に変換する必要があります．
そのために[ここ](https://answers.unity.com/questions/737002/wav-byte-to-audioclip.html)で示されているWAVクラスを導入します．

新しいスクリプトを作成し，記述されているスクリプトを新しいC#スクリプトに貼り付けます．

#Sceneの用意
これで完成です．
以下のようにInputFieldとButtonを持つ新しいシーンを作成します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/4438550f-e8a8-8515-c514-b319e28f79bb.png)
空のGameObjectを作成し，`AudioSource`と先ほど作成した`TextToSpeechSample`をアタッチします．

先の<b>Google Cloud Platformの利用登録</b>で取得したJsonの認証情報をResourcesのフォルダ内に置きます．
`TextToSpeechSample`にInputFieldとButton，AudioSourceを登録し，"Credential"にResource以下のJsonファイルのディレクトリを指定します．（例：`Assets/Resources/Credentials/credential.json`に格納した場合，`Credentials/credential`と記述）

これで完成です．

#実行する
実行し，InputFieldに発言させたい文を記述し，"Send"ボタンを押すことでリクエストが送られ，少し待つと合成された音声が再生されます．
細かくは調べていませんが，リクエスト最初の一回は少し時間がかかり，二回目以降は「こんにちは」といった短い文であれば0.3秒ほどで再生されます．

今回のスクリプトでは応答にかかった時間，文を読み上げるのにかかる再生のログ表示も併せて行われます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433211/41dbfe11-23e6-edc4-1871-ddc27498cda9.png)

#終わりに
Google Cloud Text-To-Speechを利用することで，簡単に~~（記事を書いてて思いましたが，結構大変でした）~~Unityで音声合成を行うことができます．

[【Unity】自分の声をテキスト化する方法【Google Cloud Speech Recognition 】](https://qiita.com/yaegashi0852/items/3173e768484104c6ef96)など，Google Cloud Speech Recognitionを利用した音声認識に関する記事は結構存在するので，少し調べればUnity上で音声認識もできます．
同じGoogle Cloudを利用することで今回冒頭のGoogle Cloud Platform利用者登録の大変な部分を次回以降はなくせるので比較的楽に実装できるかと思います．

これを利用したチャットボットなど作成するのも面白そうです．