【Unity×C#×VoiceVox】VoiceVoxのAPIを使ってテキストから音声を再生する

# 概要
Unity×C#の環境でVoiceVoxのAPIを叩いて、テキストから音声を再生するまでのデモです。

# 開発環境
Windows 10
Unity 2019.4.31f1
Api Compatibility Level .NET Standard 2.0
VoiceVox 0.14.10

# 使用したパッケージ
### UniTask
リンク先からunitypackageをダウンロードして、事前にプロジェクトにインポートしておきます。
https://github.com/Cysharp/UniTask
### System.Text.Json
Jsonの解析に使いました。リンク先からパッケージをダウンロードし、dllファイルをUnityのAssetsの中にPluginsフォルダを作って、その中に入れておきます。
https://www.nuget.org/packages/System.Text.Json/

また、同様にして以下の依存パッケージのdllも入れておきます。
入れ忘れがあったら、Unity側のエラーで知らせてくれます。
https://www.nuget.org/packages/System.Threading.Tasks.Extensions
https://www.nuget.org/packages/System.Text.Encodings.Web
https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe
https://www.nuget.org/packages/System.Memory
https://www.nuget.org/packages/System.Buffers
https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces

UnityNugetやNugetForUnityというパッケージ管理ツールでも入れられるようですが、今回は使用していません。

# 実装
詳細はスクリプト中にコメントで記載しています。

## VoiceVoxRequestModel.cs
VoiceVox APIのクエリに渡す構造を定義します。
```csharp
namespace VoiceVox.QueryJson
{
    using System;
    using System.Text.Json.Serialization;

    /// <summary>
    /// VoiceVoxのJsonフォーマットをクラス定義
    /// System.Text.Jsonを利用したため、書式はSystem.Text.Jsonのものです。
    /// </summary>
    [Serializable]
    public class QueryJson
    {
        [JsonPropertyName("accent_phrases")]
        public AccentPhrase[] AccentPhrases { get; set; }

        [JsonPropertyName("speedScale")]
        public float SpeedScale { get; set; }

        [JsonPropertyName("pitchScale")]
        public float PitchScale { get; set; }

        [JsonPropertyName("intonationScale")]
        public float IntonationScale { get; set; }

        [JsonPropertyName("volumeScale")]
        public float VolumeScale { get; set; }

        [JsonPropertyName("prePhonemeLength")]
        public float PrePhonemeLength { get; set; }

        [JsonPropertyName("postPhonemeLength")]
        public float PostPhonemeLength { get; set; }

        [JsonPropertyName("outputSamplingRate")]
        public int OutputSamplingRate { get; set; }

        [JsonPropertyName("outputStereo")]
        public bool OutputStereo { get; set; }

        [JsonPropertyName("kana")]
        public string Kana { get; set; }
    }

    [Serializable]
    public class AccentPhrase
    {
        [JsonPropertyName("moras")]
        public Mora[] Moras { get; set; }

        [JsonPropertyName("accent")]
        public int Accent { get; set; }

        [JsonPropertyName("pause_mora")]
        public Mora PauseMora { get; set; }

        [JsonPropertyName("is_interrogative")]
        public bool IsInterrogative { get; set; }
    }

    [Serializable]
    public class Mora
    {
        [JsonPropertyName("text")]
        public string Text { get; set; }

        [JsonPropertyName("consonant")]
        public string Consonant { get; set; }

        [JsonPropertyName("consonant_length")]
        public float? ConsonantLength { get; set; } // Nullable を使用

        [JsonPropertyName("vowel")]
        public string Vowel { get; set; }

        [JsonPropertyName("vowel_length")]
        public float? VowelLength { get; set; } // Nullable を使用

        [JsonPropertyName("pitch")]
        public float Pitch { get; set; }
    }
}


```

## VoiceVoxSpeaker.cs
VoiceVox APIを処理するメインのスクリプトです。
VoiceVoxにクエリを投げる処理は非同期処理にしたいため、UniTaskを利用しています。UniTaskの説明は末尾の参考資料が詳しいかったです。
```csharp
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
using System.Text.Json;
using Cysharp.Threading.Tasks;
using VoiceVox.QueryJson;

/// <summary>
/// Unity 2019.4.31fでの動作環境を前提にしています。
/// isNetworkError と isHttpError プロパティを使用していますが
/// Unity 2020.1以降の場合は、UnityWebRequestにresultプロパティが追加されているため、そちらを利用します。
/// speaker変数を利用していますが、廃止が予定されているようです。うまく動かない場合は、VoiceVox Engineのバージョン
/// APIドキュメントを確認するようにしてください。
/// https://voicevox.github.io/voicevox_engine/api/
/// 非同期処理にはUniTaskを利用しました。
/// UniTask：https://github.com/Cysharp/UniTask
/// AudioClipの操作にはWavUtilityを利用しました。
/// WevUtility：https://github.com/deadlyfingers/UnityWav/tree/master
/// 実行前に必ず、ローカルでVoiceVox Engineを起動してください。
/// </summary>
public class VoiceVoxSpeaker
{
    private readonly string _voiceVoxUrl = "http://127.0.0.1:50021";
    private readonly int _speaker = 3;

    public VoiceVoxSpeaker(int speaker)
    {
        _speaker = speaker;
    }

    public async UniTask<AudioClip> TextToSpeech(string text, float speedScale, float pitchScale, float intonationScale, float volumeScale)
    {
        // クエリ作成
        string queryJson = await SendAudioQuery(text);
        // Debug.Log(queryJson);
        // クエリ修正
        string newQueryJson = ChangeQueryJson(queryJson, speedScale, pitchScale, intonationScale, volumeScale);
        // Debug.Log(newQueryJson);

        // 音声合成とオーディオクリップへのセット
        var clip = await GetAudioClip(newQueryJson);
        return clip;
    }

    /// <summary>
    /// クエリを作成します。
    /// </summary>
    private async UniTask<string> SendAudioQuery(string text)
    {
        var form = new WWWForm();
        using (var request = UnityWebRequest.Post($"{_voiceVoxUrl}/audio_query?text={text}&speaker={_speaker}", form))
        {
            await request.SendWebRequest();

            if (request.isNetworkError || request.isHttpError)
            {
                Debug.LogError(request.error);
            }
            else
            {
                var jsonString = request.downloadHandler.text;
                return jsonString;
            }
            return null;
        };
    }

    /// <summary>
    /// クエリを修正します。
    /// Jsonの解析にUnity標準のJsonUtilityを利用するとnullなどの一部のフィールドが正確に扱えなかったので
    /// System.Text.Jsonを利用します。
    /// </summary>
    /// <param name="queryJson">生成されたオリジナルのJson</param>
    /// <param name="speedScale">話速(0.50~2.00 Def. 1.00)</param>
    /// <param name="pitchScale">音高(-0.15~0.15 Def. 0.00)</param>
    /// <param name="intonationScale">抑揚(0.00~2.00 Def. 1.00)</param>
    /// <param name="volumeScale">音量(0.00~2.00 Def. 1.00)</param>
    /// <returns>修正後のJson</returns>
    public static string ChangeQueryJson(string queryJson, float speedScale, float pitchScale, float intonationScale, float volumeScale)
    {
        var queryObject = JsonSerializer.Deserialize<QueryJson>(queryJson);

        queryObject.SpeedScale = speedScale;
        queryObject.PitchScale = pitchScale;
        queryObject.IntonationScale = intonationScale;
        queryObject.VolumeScale = volumeScale;

        string modifiedQueryJson = JsonSerializer.Serialize(queryObject);
        return modifiedQueryJson;
    }

    /// <summary>
    /// 音声を合成し、オーディオクリップにセット
    /// </summary>
    private async UniTask<AudioClip> GetAudioClip(string queryJson)
    {
        var url = $"{_voiceVoxUrl}/synthesis?speaker={_speaker}";
        using (var req = new UnityWebRequest(url, "POST"))
        {
            // Content-Type を設定
            req.SetRequestHeader("Content-Type", "application/json");

            // リクエストボディを設定
            byte[] bodyRaw = Encoding.UTF8.GetBytes(queryJson);
            req.uploadHandler = new UploadHandlerRaw(bodyRaw);
            req.downloadHandler = new DownloadHandlerBuffer();

            // 音声合成
            await req.SendWebRequest();

            if (req.isNetworkError || req.isHttpError)
            {
                Debug.LogError(req.error);
            }
            else
            {
                // 音声をオーディオクリップにセット
                var audioClip = WavUtility.ToAudioClip(req.downloadHandler.data);
                return audioClip;
            }
            return null;
        }
    }
}
```

## AudioPlayer.cs
AudioClipを再生する処理です。
```csharp
using UnityEngine;
public class AudioPlayer : MonoBehaviour
{
    private AudioSource _audioSource;

    void Start()
    {
        // AudioSource コンポーネントの取得または追加
        _audioSource = GetComponent<AudioSource>();
        if (_audioSource == null)
        {
            _audioSource = gameObject.AddComponent<AudioSource>();
        }
    }

    // オーディオ再生
    public void PlayAudioClip(AudioClip clip)
    {
        _audioSource.clip = clip;
        _audioSource.Play();
    }
}
```

## WavUtility.cs
ToAudioClipメソッドをそのまま使わせてもらいました。

https://github.com/deadlyfingers/UnityWav

## DemoVoiceVox.cs
実際に、テキストを音声合成して、再生するデモスクリプトです。

```csharp
using UnityEngine;
using Cysharp.Threading.Tasks;

public class DemoVoiceVox : MonoBehaviour
{
    private string text;
    private float speedScale;
    private float pitchScale;
    private float intonationScale;
    private float volumeScale;
    private VoiceVoxSpeaker voiceVoxSpeaker;
    // Start is called before the first frame update
    void Start()
    {
        text = @"
        私はキューブオブジェクトです。名前はまだありません。どこで生成されたのか、まったく覚えがありません。
        何でもクリックやドラッグという操作とともにポップアップし、三次元空間に配置されたことだけは記憶しています。
        ";
        // text = @"
        // 私は「がにまた」。名前はまだありません。どこで生成されたのか、まったく覚えがありません。
        // 何でも「ひまた工房」というところで作られ、三次元空間に産み落とされたことだけは記憶しています。
        // ";
        speedScale = 1.0f;
        pitchScale = 0.0f;
        intonationScale = 1.0f;
        volumeScale = 1.0f;

        // voiceVoxSpeaker = new VoiceVoxSpeaker(2); // スピーカーID:2 -> VOICEVOX:四国めたん(ノーマル)
        voiceVoxSpeaker = new VoiceVoxSpeaker(3); // スピーカーID:3 -> VOICEVOX:ずんだもん(ノーマル)
        StartVoiceVoxPlayer().Forget(); // Forget()メソッドで、awaitが無いけれど大丈夫？という警告を無視;
    }

    private async UniTask StartVoiceVoxPlayer()
    {
        // VoiceVoxからオーディオを取得し、AudioClipにセット
        AudioClip clip = await voiceVoxSpeaker.TextToSpeech(text, speedScale, pitchScale, intonationScale, volumeScale);

        // AudioClip を再生
        // なお、UnityのMonoBehaviourを継承するクラスはnewでインスタンスを作れません。
        AudioPlayer audioPlayer = GetComponent<AudioPlayer>();
        if (audioPlayer != null)
        {
            audioPlayer.PlayAudioClip(clip);
        }
    }

    // Update is called once per frame
    void Update() { }
}
```

# 利用方法
* Unityを起動します。
* Cube等のGameObjectを用意します。
* 用意したGameObjectのコンポーネントにDemoVoiceVox.cs、AudioPlayer.csを追加します。
* 用意したGameObjectにAudioSourceコンポーネントを追加します。
* VoiceVoxを起動します。
* Console画面を開き、Playモードに入ります。
* 数秒して、読み上げが始まったら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3604990/e3dbfadb-c616-956c-9cce-87a3ba0d68bb.png)

上の画像はゲームオブジェクトに各種コンポーネントをセットした状態の参考画像です。
なお、画像中のゲームオブジェクトはCubeではなく、「がにまた」（製作者：ひまた）です。

https://booth.pm/ja/items/4169367

# 参考資料
https://qiita.com/SY81517/items/1cf6246dd99869f7b9c5

https://qiita.com/mhama/items/0f2cba4a4a762589fc53

https://note.com/negipoyoc/n/n081e25f5ee9e