どうも、前回に引き続き「AIで対話できるキャラづくり」を記事にしていきます。
今回は合成音声の使い方に入ります。
合成音声の生成
そもそも、合成音声とは「人工的に生成された人の声」であり、ライブ配信や事前に録音などをすることなく、まるで人が話しているような音声を出力することを目的としています。初音ミクをはじめとしたVOCALOIDや「ゆっくりボイス」でおなじみのAquesTalkもその一種です。
音声合成も数多くのソフトウェアが出ていますが、今回はその中でも「VOICEPEAK」と「VOICEVOX」を取り上げます。
VOICEVOX
VOICEVOXは「無料で使える中品質なテキスト読み上げソフトウェア(公式談)」というオープンソフトの音声合成ソフトウェアで、触れ込みの通り無料でインストールできます。商用・非商用に関わらず無料で使える(要クレジット表記)上、音声の品質も高いのでとても使い勝手の良いソフトウェアです。
ただ、非公式ですが少しでもストレージの容量を圧迫したくない様な人のためにWeb API版もあります。
今回は、このAPIを使って音声を合成してみました。早速ですがソースはこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; //https://voicevox.su-shiki.com/su-shikiapis/ttsquest/ public class VoiceVoxConnection : MonoBehaviour { /// <summary> /// Format of the response from the VOICEVOX API /// </summary> [System.Serializable] public class AudioQuery { public bool success; public string host; public string audioId; public string audioStatusUrl; public string wavDownloadUrl; public string mp3DownloadUrl; public int canTakeUpTo; public static AudioQuery CreateFromJson(string json) { return JsonUtility.FromJson<AudioQuery>(json); } } /// <summary> /// Format of the response status /// </summary> [System.Serializable] public class AudioStatus { public bool success; public bool isAudioReady; public bool isAudioError; public string status; public int updatedTime; public static AudioStatus CreateFromJson(string json) { return JsonUtility.FromJson<AudioStatus>(json); } } /// <summary> /// URL of VOICEVOX API (unofficial) /// </summary> static readonly string URL_VOICEVOX_API = "https://api.tts.quest/v1/voicevox/"; [SerializeField] AudioSource audio; [SerializeField, Multiline] string text = "こんにちは"; [SerializeField] int speaker = 0; [SerializeField] UnityEngine.Events.UnityEvent onPlayVoice; /// <summary> /// text to speech /// </summary> public string Text { get { return text; } } /// <summary> /// speaker ID /// </summary> public int Speaker { get { return speaker; } } // Start is called before the first frame update void Start() { audio = audio ?? GetComponent<AudioSource>(); } /// <summary> /// Select the speaker (voice) /// </summary> /// <param name="id">number of the speaker</param> public void SetSpeakerID(int id) => speaker = id + 1; /// <summary> /// Set the phrase spoken by the system /// </summary> /// <param name="content">text to speech</param> public void SetText(string content) => text = content; /// <summary> /// Download and play a voice data generated by /// </summary> public void Voice() { StartCoroutine(DownloadVoice()); } /// <summary> /// Coroutine to download and play a voice /// </summary> /// <returns></returns> IEnumerator DownloadVoice() { var query = GetAudioQuery(Text, Speaker); var coroutine = StartCoroutine(query); // waiting process yield return coroutine; // get the audio query from api server var result = query.Current as AudioQuery; // halt downloading process if the result is not convertible to the format AudioQuery if (result == null) yield break; // retrieve the audio data and play it using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(result.wavDownloadUrl, AudioType.WAV)) { // send a request yield return www.SendWebRequest(); // retrieve a voice data if (www.result == UnityWebRequest.Result.Success) { // trigger an event onPlayVoice.Invoke(); // play the acquired voice audio.clip = DownloadHandlerAudioClip.GetContent(www); audio.Play(); } else { Debug.Log(www.error); } } } /// <summary> /// Get the voice data by sending a request to API /// </summary> /// <param name="content">text to speech</param> /// <param name="id">speaker ID</param> /// <returns></returns> IEnumerator GetAudioQuery(string content, int id) { // set the URL with query parameter (text & speaker) var url = $"{URL_VOICEVOX_API}?text={content}&speaker={id}"; // send a request using (var request = UnityWebRequest.Get(url)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { if (request.responseCode == 200) { // リクエスト成功 //Debug.Log("AudioQuery:" + request.downloadHandler.text); yield return AudioQuery.CreateFromJson(request.downloadHandler.text); } else { // リクエスト失敗 Debug.Log("AudioQuery:" + request.responseCode); } } else { // 接続エラー Debug.Log("Speaker Query:" + request.error); } } } } |
スクリプトの使い方は以下の通り。
- 適当なGameObject(Emptyなど)に貼り付け
- AudioSourceを割り当て
- メソッドSetSpeakerIDで声の選択(入力は1~)
- メソッドSetTextでしゃべらせたい内容を設定
- [任意] イベントOnPlayVoiceに音声再生時の処理を割り当て
- メソッドVoiceを呼び出す(他のスクリプト、GUIなど)
ちょっと長いので、特に重要なところだけ抜粋。
まず、APIを叩いて音声合成をリクエストするコルーチンを作成します。基本的には、UnityWebRequestクラスでパラメータを追加したURLにGETを送り、通信が成功(ステータス200)の場合のみサーバーから取得したJSONを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
IEnumerator GetAudioQuery(string content, int id) { // set the URL with query parameter (text & speaker) var url = $"{URL_VOICEVOX_API}?text={content}&speaker={id}"; // send a request using (var request = UnityWebRequest.Get(url)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { if (request.responseCode == 200) { // リクエスト成功 //Debug.Log("AudioQuery:" + request.downloadHandler.text); yield return AudioQuery.CreateFromJson(request.downloadHandler.text); } else { // リクエスト失敗 Debug.Log("AudioQuery:" + request.responseCode); } } else { // 接続エラー Debug.Log("Speaker Query:" + request.error); } } } |
続いて、音声データを取得して再生するためのメソッドを作ります。上記のメソッドの戻り値(JSON)もここで使います。
先ほどのメソッド(GetAudioQuery)をコルーチンで実行し、戻り値がAudioQueryクラスの時だけ、今度はUnityWebRequestMultimediaクラスのGetAudioClipメソッドでwav形式の音声ファイルをリクエストします。通信が成功したら、Unityで再生可能な形式(AudioClip)に変換してダウンロードし、音声を再生します。AudioQueryクラスは、リクエストに対するAPIのレスポンスで、通信成功の可否や音声のダウンロードURLなどが定義されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
IEnumerator DownloadVoice() { var query = GetAudioQuery(Text, Speaker); var coroutine = StartCoroutine(query); // waiting process yield return coroutine; // get the audio query from api server var result = query.Current as AudioQuery; // halt downloading process if the result is not convertible to the format AudioQuery if (result == null) yield break; // retrieve the audio data and play it using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(result.wavDownloadUrl, AudioType.WAV)) { // send a request yield return www.SendWebRequest(); // retrieve a voice data if (www.result == UnityWebRequest.Result.Success) { // trigger an event onPlayVoice.Invoke(); // play the acquired voice audio.clip = DownloadHandlerAudioClip.GetContent(www); audio.Play(); } else { Debug.Log(www.error); } } } |
ちなみに、MP3も取得可能ですが、GetAudioClipメソッドがMP3に対応していないためWAVを選択しています。また、当初はOnPlayVoiceで3Dモデルの表情や動きも付けていましたが、発声の瞬間とタイミングが合わなかったので、6.のVoiceメソッド呼び出しと同時にモーションも動かすように変えました。
VOICEPEAK
一方、こちらは列記とした株式会社AHSの製品。必ずインストールする必要がありますが、無料版もあるのでそちらを使用しても問題ありません。
こちらを使う場合、とにかくまずはパッケージをダウンロードしてインストールします(手順は簡単なので省略)。
続いて、VOICEPEAKをUnityから動かす処理ですが、残念ながらSDKは用意されてません。ただ、代わりにC#のProcessクラスを駆使すれば外部アプリケーションを実行できるので、UnityからProcess(コマンドプロンプト)経由でVOICEPEAKを実行し、生成された音声を読み込んで再生します。
それを踏まえて、スクリプトを見てみましょう(OnStandardOutなど、一部不要な部分もあります…orz)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; using System.Diagnostics; public interface IVoicePlayer { void Play(string text); } public class ControlVoicePeak : MonoBehaviour, IVoicePlayer { //読み上げテキスト public string Message = "読み上げテスト中"; //実行可能間隔(秒) public float WaitTime = 1F; //実行EXEのフルパス [SerializeField] private string exepath = @"C:\Program Files\VOICEPEAK\voicepeak.exe"; [SerializeField] AudioSource audio; [SerializeField] UnityEngine.Events.UnityEvent<string> OnPlayVoice; //wav出力先 string outpath = "/Voice/output.wav"; //ナレーター選択 private Process process; private float exedtime = 0.00F; // trigger when a voice data (WAV) is created bool isReadyVoice = false; void Start() { outpath = Application.dataPath + outpath; } void Update() { // execute only if a wav file is created if (isReadyVoice) { StartCoroutine(GetAudioClip("file://" + outpath)); isReadyVoice = false; OnPlayVoice.Invoke(Message); } } void OnDestroy() => DisposeProcess(); public void PlayVoice() { if (Time.time - exedtime > WaitTime) { var path = "\"" + outpath + "\""; process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = exepath, // 起動するファイルのパスを指定する UseShellExecute = false, // プロセスの起動にオペレーティング システムのシェルを使用するかどうか(既定値:true) Arguments = "-s " + Message + " -o " + path, //"-s " + Message + " -n " + narrator + " -o " + path //RedirectStandardInput = false, // StandardInput から入力を読み取る(既定値:false) //RedirectStandardOutput = true, // 出力を StandardOutput に書き込むかどうか(既定値:false) CreateNoWindow = true, // プロセス用の新しいウィンドウを作成せずにプロセスを起動するかどうか(既定値:false) }; // イベント処理の有効化 process.EnableRaisingEvents = true; // 実行完了時の処理 process.Exited += (sender, e) => { isReadyVoice = true; //StartCoroutine(GetAudioClip("file://" + outpath)) cannot be executed properly }; //process.OutputDataReceived += OnStandardOut; // 実行 process.Start(); //実行時間を記録 exedtime = Time.time; } } public void ChangeMessage(string msg) { Message = msg; } private void OnStandardOut(object sender, DataReceivedEventArgs e) { UnityEngine.Debug.Log("standard out"); var output = e.Data; } IEnumerator GetAudioClip(string path) { using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(path, AudioType.WAV)) { yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.DataProcessingError) { UnityEngine.Debug.Log(www.error); } else { audio.clip = DownloadHandlerAudioClip.GetContent(www); audio.Play(); } } } private void DisposeProcess() { if (process == null) return; else if (process.HasExited) return; //process.StandardInput.Close(); //process.StandardOutput.Close(); //process.CloseMainWindow(); process.Dispose(); process = null; } public void Play(string text) { // update text to speech ChangeMessage(text); // generate and play voice PlayVoice(); } } |
スクリプトの使い方は以下の通り。
- 適当なGameObject(Emptyなど)に貼り付け
- Exepath にVOICEPEAK実行ファイルのフルパスを入力
- AudioSourceを割り当て
- メソッドChangeMessageでしゃべらせたい内容を設定
- メソッドPlayVoiceを呼び出す(他のスクリプト、GUIなど)。または、Play(string)で喋らせたい内容を直接入力してもOK
ちょっと長いので、こちらも重要なところだけ抜粋します。
PlayVoiceは、VOICEPEAKを呼び出して合成音声を生成させるメソッドです。
まず、Processのインスタンスを作成して実行時の設定をStartInfoプロパティに入れます。特にArgumentsで実行時のパラメータを追加できます。
- o : 音声ファイルの出力先
- s : 喋らせるメッセージ
- n : ナレーター(対応時のみ。「邪神ちゃん」無料版では使えませんでした)
そして、終了時のイベントを有効にした後、StartメソッドでVOICEPEAKを起動します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public void PlayVoice() { if (Time.time - exedtime > WaitTime) { var path = "\"" + outpath + "\""; process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = exepath, // 起動するファイルのパスを指定する UseShellExecute = false, // プロセスの起動にオペレーティング システムのシェルを使用するかどうか(既定値:true) Arguments = "-s " + Message + " -o " + path, //"-s " + Message + " -n " + narrator + " -o " + path CreateNoWindow = true, // プロセス用の新しいウィンドウを作成せずにプロセスを起動するかどうか(既定値:false) }; // イベント処理の有効化 process.EnableRaisingEvents = true; // 実行完了時の処理 process.Exited += (sender, e) => { isReadyVoice = true; //StartCoroutine(GetAudioClip("file://" + outpath)) cannot be executed properly }; // 実行 process.Start(); //実行時間を記録 exedtime = Time.time; } } |
終了後に音声を直接読み込めなかったので、代わりにフラグ(isReadyVoice)を立てて、音声再生はUpdateメソッド内で処理します。
Updateでは常にフラグを見ており、trueになったらGetAudioClipメソッドで声を再生します。GetAudioClipでは受け取ったURLから音声ファイルを読み込んでAudioSourceから鳴らすようにしていますが、実はUnityWebRequestMultiMedia.GetAudioClipがローカルファイルも読み込めるので、出力先のファイルパスに「file://」を付けて呼び出すことで生成された音声ファイルを読込・再生しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void Update() { // execute only if a wav file is created if (isReadyVoice) { StartCoroutine(GetAudioClip("file://" + outpath)); isReadyVoice = false; OnPlayVoice.Invoke(Message); } } IEnumerator GetAudioClip(string path) { using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(path, AudioType.WAV)) { yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.DataProcessingError) { UnityEngine.Debug.Log(www.error); } else { audio.clip = DownloadHandlerAudioClip.GetContent(www); audio.Play(); } } } |
どちらを使うべき?
使い勝手や出力音声の品質でいうとVOICEVOXに軍配が上がります。
VOICEPEAKは感情設定やピッチ等の調整が無いと無機質になる上、わざわざ外部アプリを実行(コマンドプロンプトから呼び出し)するので余計に処理時間がかかる感じがしました。どちらもUnityからだと細かい調整は難しいですが、それとなく自然な声を生成できるVOICEVOXがやはりUnityからだと使いやすいと思います。
逆に、特定のキャラクターの音声を使いたいときにはVOICEPEAKを選択する事もあるかもしれません。
次回は?
モーションのつけ方について触れていきます。