はじめに
C#およびUnityからVOICEVOXを操作するクライアントライブラリを作成したので紹介します。
VOICEVOXとは
VOICEVOXはhiroshiba氏が開発した無償の音声合成ソフトウェアです。テキスト入力を音声合成しwavとして得ることができます。
商用・非商用問わずに無償で使うことができ、OSSとして開発されています。
VoicevoxClientSharp
VoicevoxClientSharp
はC#からVOICEVOXおよびvoicevox_engineを制御するクライアントライブラリです。ライセンスはMITです。
詳細はリポジトリのREADMEにも書いてあるのですが、この場でも紹介します。
できること
- C#(Unity含む)からVOICEVOXを使って簡単に音声合成ができる
- VOICEVOXが提供する各APIを実行できるAPIクライアント実装
- APIをラップして簡単に扱えるようにしたクライアント実装
- Unity上でVOICEVOXの合成結果を使って音声再生できる
- 音声再生と合わせてVRMのリップシンクができる
対象プラットフォーム
-
.NET Standard 2.0
- .NET Core 2.0以降
- .NET Framework 4.6.1以降
- Unity 2018.4以降
- など
-
対応VOICEVOXエンジンバージョン:
0.21.1
またUnity向けのサポートプラグインも別途配布しています。そちらのプラグインを持ちることでUnity上の音声再生やVRMとの連携を行うことができます。
導入方法
NuGetパッケージとして配布しているため、NuGetより導入してください。
Install-Package VoicevoxClientSharp
Unity向け導入方法
Unityに導入する場合はNuGetForUnityの使用を推奨します。
また同時にUniTask
およびVoicevoxClientSharp.Unity
を導入することで、Unity上での音声再生などを行うことができるようになります。
VoicevoxClientSharp.Unity
の導入はUPMより次のURLを入力して下さい。
https://github.com/TORISOUP/VoicevoxClientSharp.git?path=VoicevoxClientSharp.Unity/Assets/VoicevoxClientSharp.Unity
※ VoicevoxClientSharp.Unity
は要UniTask
使い方(C#,Unity共通)
-
VOICEVOX
をネットワーク上でアクセスできる場所で起動する- ローカルマシンまたはdockerコンテナ上で起動しておいてください
-
VoicevoxClientSharp
より次のどちらかのクライアントを生成する-
VoicevoxSynthesizer
: とにかく簡単にVOICEVOXを使いたい人向け -
VoicevoxApiClient
: VOICEVOXが提供するAPIを個別に使いたい人向け
-
-
生成したクライアントをもとに、VOICEVOXを用いて音声合成を行う
- 音声合成した結果は
wav
形式のbyte[]
として得られます
- 音声合成した結果は
-
wav
をC#アプリケーション側で再生する- Unityの場合は
AudioClip
に変換することで再生できます -
VoicevoxClientSharp.Unity
を用いると簡単にUnity上で再生ができます
- Unityの場合は
VoicevoxSynthesizer:とにかく簡単にVOICEVOXを使いたい人向けクライアント
VoicevoxSynthesizer
を用いることでテキストからの音声合成を簡単に実行することができます。
// VoicevoxSynthesizerの初期化
using var synthesizer = new VoicevoxSynthesizer();
// スタイル(発話するキャラクターや種類)のIdを取得する
// StyleIdが既知の場合はこのステップは不要
int styleId = (await synthesizer.FindStyleIdByNameAsync(speakerName: "ずんだもん", styleName: "あまあま"))!.Value;
// 音声合成を実行
// resultに合成結果のwavデータ(byte[])が可能されている
SynthesisResult result = await synthesizer.SynthesizeSpeechAsync(styleId, "こんにちは、世界!");
public readonly struct SynthesisResult : IEquatable<SynthesisResult>
{
/// <summary>
/// 合成した音声データ
/// </summary>
public byte[] Wav { get; }
/// <summary>
/// 音声合成に使用したクエリ
/// </summary>
public AudioQuery AudioQuery { get; }
/// <summary>
/// 音声合成に使用したテキスト
/// </summary>
public string Text { get; }
}
また、細かく発話時のパラメータを調整して合成することもできます。
var result = await synthesizer.SynthesizeSpeechAsync(styleId, "こんにちは、世界!",
speedScale: 1.1M, // 読み上げ速度の倍率
pitchScale: 0.1M, // ピッチの変化
intonationScale: 1.1M, // イントネーションの強さ
volumeScale: 0.5M, // 音量
prePhonemeLength: 0.1M, // 読み上げ前の待機時間
postPhonemeLength: 0.1M, // 読み上げ後の時間
pauseLength: 0.1M, // 読み上げ途中の待機時間
pauseLengthScale: 1.5M); // 読み上げ途中の待機時間の倍率
VoicevoxApiClient :VOICEVOXが提供するAPIを個別に使いたい人向けクライアント
VoicevoxApiClient
はVOICEVOXが提供するREST APIと1:1に対応した"シンプルな"クライアントです。上級者向けです。
VoicevoxSynthesizer
はこのVoicevoxApiClient
をラップしているだけです。そのためVoicevoxSynthesizer
でできることはすべてVoicevoxApiClient
でも実装が可能です。
どのREST APIとメソッドが対応しているかは対応表があるのでそちらを御覧ください。
// APIクライアントを生成
using var apiClient = VoicevoxApiClient.Create();
// GET /speakers
// スピーカー一覧を取得
var speakers = await apiClient.GetSpeakersAsync();
// スピーカー名とスタイル名からスタイルIDを取得
var speaker = speakers.FirstOrDefault(s => s.Name == "ずんだもん");
var styleId = speaker?.Styles.FirstOrDefault(x => x.Name == "あまあま")!.Id ?? 0;
// POST /audio_query
// 音声合成用のクエリを作成
var audioQuery = await apiClient.CreateAudioQueryAsync("こんにちは、世界!", styleId);
// POST /synthesis
// 音声合成を実行
byte[] wav = await apiClient.SynthesisAsync(styleId, audioQuery);
Unity向けの使い方
Unityの場合はVoicevoxClientSharp.Unity
を導入することで次の機能が使用可能になります。
-
VoicevoxSpeakPlayer
:Unityで合成した音声を再生するコンポーネント -
VoicevoxVrmLipSyncPlayer
: VRMアバターをリップシンクするコンポーネント- ただしこちらはVRMパッケージの導入も必要
- エディタ拡張
VoicevoxSpeakPlayer:Unityで合成した音声を再生するコンポーネント
VoicevoxSpeakPlayer
はVoicevoxSynthesizer
より得られるSynthesisResult
をUnity上で再生するコンポーネントです。
GameObjectにアタッチし、AudioSource
を割り当てて使用してください(動的に生成して割り当ててもOKです)。
using System.Threading;
using Cysharp.Threading.Tasks; // UniTaskが必須
using UnityEngine;
using VoicevoxClientSharp;
using VoicevoxClientSharp.Unity;
namespace Sandbox
{
// 使用例
public class Sample : MonoBehaviour
{
// 設定済みのVoicevoxSpeakPlayerをバインドしておく
[SerializeField]
private VoicevoxSpeakPlayer _voicevoxSpeakPlayer;
// VoicevoxSynthesizerを用いて音声合成する
private readonly VoicevoxSynthesizer _voicevoxSynthesizer
= new VoicevoxSynthesizer();
private void Start()
{
var cancellationToken = this.GetCancellationTokenOnDestroy();
// テキストを音声に変換して再生する
SpeakAsync("こんにちは、世界", cancellationToken).Forget();
}
// 音声合成して再生する処理
private async UniTask SpeakAsync(string text, CancellationToken ct)
{
// テキストをVoicevoxで音声合成
var synthesisResult = await _voicevoxSynthesizer.SynthesizeSpeechAsync(
0, text, cancellationToken: ct);
// 結果を再生する
await _voicevoxSpeakPlayer.PlayAsync(synthesisResult, ct);
}
private void OnDestroy()
{
_voicevoxSynthesizer.Dispose();
}
}
}
VoicevoxVrmLipSyncPlayer:VRMアバターをリップシンクするコンポーネント
VoicevoxVrmLipSyncPlayer
はVRMアバターをVOICEVOXで合成した音声再生のタイミングに合わせてリップシンクするコンポーネントです。
さきほどのVoicevoxSpeakPlayer
と組み合わせて使用してください。
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UniVRM10;
using VoicevoxClientSharp;
using VoicevoxClientSharp.Unity;
namespace Sandbox
{
/// <summary>
/// すべてをスクリプトからセットアップする場合
/// </summary>
public sealed class LoadVrmAndSpeech : MonoBehaviour
{
// 読み込むVRMのパス
[SerializeField] private string _vrmPath = "";
private readonly VoicevoxSynthesizer _voicevoxSynthesizer = new VoicevoxSynthesizer();
private void Start()
{
var cancellationToken = this.GetCancellationTokenOnDestroy();
LoadVrmAndSpeechAsync(_vrmPath, cancellationToken).Forget();
}
// pathで指定したVRMを読み込み、リンプシンクして喋らせる
private async UniTask LoadVrmAndSpeechAsync(string path, CancellationToken ct)
{
// バイナリファイルを読み込んでVRMをロード
var vrm10Instance = await Vrm10.LoadPathAsync(path, ct: ct);
var vrmGameObject = vrm10Instance.gameObject;
// AudioSourceをVRMにアタッチ
var audioSource = vrmGameObject.AddComponent<AudioSource>();
// VoicevoxSpeakPlayerを追加してAudioSourceを紐づける
var voicevoxSpeakPlayer = vrmGameObject.AddComponent<VoicevoxSpeakPlayer>();
voicevoxSpeakPlayer.AudioSource = audioSource;
// VoicevoxVrmLipSyncPlayerをアタッチし、VRMを紐づけ
var voicevoxVrmLipSyncPlayer = vrmGameObject.AddComponent<VoicevoxVrmLipSyncPlayer>();
voicevoxVrmLipSyncPlayer.VrmInstance = vrm10Instance;
// VoicevoxSpeakPlayerにVoicevoxVrmLipSyncPlayerを追加
voicevoxSpeakPlayer.AddOptionalVoicevoxPlayer(voicevoxVrmLipSyncPlayer);
// テキストを音声に変換
var synthesisResult = await _voicevoxSynthesizer.SynthesizeSpeechAsync(
0, "こんにちは、世界", cancellationToken: ct);
// 音声を再生しながらリップシンク
// voicevoxSpeakPlayerに再生命令を送ればVoicevoxVrmLipSyncPlayerと協調して動作する
await voicevoxSpeakPlayer.PlayAsync(synthesisResult, ct);
}
}
}
おまけ:VoicevoxVrmLipSyncPlayerの仕組み
本筋とは関係ないので折りたたみ。興味があれば読んで下さい。
VoicevoxVrmLipSyncPlayerの仕組み
VOICEVOXの音声合成は2段階に別れています。
- テキストよりクエリ(
AudioQuery
)を作成する - 作成したクエリをもとに音声合成を行い
wav
を生成する
VoicevoxSynthesizer
は内部でこの2つの処理を実行しています。合成結果として返されるデータ構造にSynthesisResult
に含まれているAudioQuery
がこの合成に用いたクエリです。
public readonly struct SynthesisResult : IEquatable<SynthesisResult>
{
/// <summary>
/// 合成した音声データ
/// </summary>
public byte[] Wav { get; }
/// <summary>
/// 音声合成に使用したクエリ
/// </summary>
public AudioQuery AudioQuery { get; }
/// <summary>
/// 音声合成に使用したテキスト
/// </summary>
public string Text { get; }
}
このAudioQuery
ですが、VOICEVOXが返すもとのjsonを覗いてみると次のようなデータとなっています。
{
"accent_phrases": [
{
"moras": [
{
"text": "コ",
"consonant": "k",
"consonant_length": 0.07622026652097702,
"vowel": "o",
"vowel_length": 0.1315000206232071,
"pitch": 5.785459518432617
},
{
"text": "ン",
"consonant": null,
"consonant_length": null,
"vowel": "N",
"vowel_length": 0.058612607419490814,
"pitch": 5.884223937988281
},
{
"text": "ニ",
"consonant": "n",
"consonant_length": 0.029203424230217934,
"vowel": "i",
"vowel_length": 0.08954069763422012,
"pitch": 5.925149917602539
},
{
"text": "チ",
"consonant": "ch",
"consonant_length": 0.0847683846950531,
"vowel": "i",
"vowel_length": 0.05707687884569168,
"pitch": 5.91061544418335
},
{
"text": "ワ",
"consonant": "w",
"consonant_length": 0.0631946474313736,
"vowel": "a",
"vowel_length": 0.15520663559436798,
"pitch": 5.906591892242432
}
],
"accent": 5,
"pause_mora": {
"text": "、",
"consonant": null,
"consonant_length": null,
"vowel": "pau",
"vowel_length": 0.2892865836620331,
"pitch": 0
},
"is_interrogative": false
},
{
"moras": [
{
"text": "セ",
"consonant": "s",
"consonant_length": 0.0821339562535286,
"vowel": "e",
"vowel_length": 0.08564445376396179,
"pitch": 5.917510032653809
},
{
"text": "カ",
"consonant": "k",
"consonant_length": 0.07719596475362778,
"vowel": "a",
"vowel_length": 0.09904120117425919,
"pitch": 5.946408271789551
},
{
"text": "イ",
"consonant": null,
"consonant_length": null,
"vowel": "i",
"vowel_length": 0.10033080726861954,
"pitch": 5.755062103271484
}
],
"accent": 1,
"pause_mora": null,
"is_interrogative": false
}
],
"speedScale": 1,
"pitchScale": 0,
"intonationScale": 1,
"volumeScale": 1,
"prePhonemeLength": 0.1,
"postPhonemeLength": 0.1,
"pauseLength": null,
"pauseLengthScale": 1,
"outputSamplingRate": 24000,
"outputStereo": false,
"kana": "コンニチワ'、セ'カイ"
}
ここには各音素とその再生時間が含まれています。consonant_length
が子音の再生時間、vowel_length
が母音の再生時間です。つまりこのAudioQuery
の情報をもとにすれば「どの時間にどの音素を再生しているか」を割り出すことが可能となります。
そこでVoicevoxVrmLipSyncPlayer
は音声再生と同じタイミングでこのAudioQuery
を順次解析し、時間が一致するようにVRMのExpressionを制御することでリップシンクを実現しています。
また次のような調整も行っています。
- 「ま行」や「ぱ行」などの発音時に口を閉じる音素はちゃんと口を閉じる
- Unity上での時間計測では分解能が足りないため、
await
時に誤差が蓄積しないように逐次再生タイミングを補正する
なお動作原理上、「音声が1.0倍速で再生されている」ということを前提にしているため、音声の再生速度やゲーム中のtimeScale
を変更した場合はリップシンクできません。
Unityエディタ拡張
[VoicevoxClientSharp -> Open HelperWindow]より、VOICEVOXからスピーカー一覧を取得するエディタ拡張を開くことができます。
StyleIdなどを確認するときに使用してください。
おまけ機能:AvisSpeech連携
AvisSpeechはvoicevox_engine
をベースとしたVOICEVOX派生の音声合成ソフトウェアです。
VoicevoxClientSharp
も部分的に対応しており、VoicevoxApiClient
の一部メソッドが使用できます。
// VoicevoxApiClientを生成する際にstaticメソッドでAviSpeechを指定して生成してください
using var avisSpeechApiClient = VoicevoxApiClient.CreateForAvisSpeech();
全体構成
APIクライントはIVoicevoxApiClient
インタフェースにより抽象化が行われています。またVoicevoxSynthesizer
はこのIVoicevoxApiClient
に依存しています。
もしテストを書くようなことがあればこの抽象化をうまく利用してください。
まとめ
VoicevoxClientSharp
を使うことでC#およびUnityから気軽に音声合成し、再生することができるようになるので是非使ってみてください。(より詳しい使い方はREADMEに書いてあるのでこっちを読んでください。)
余談として、VOICEVOX
はOpenAPI Spec
を公開してくれています。が、C#のコードジェネレートをかけたところ上手く動くコードが生成できませんでした。
なのでこのVoicevoxClientSharp
はREST APIを叩く部分を手動で実装してあったりします。
Unityでのサンプル実装
VoicevoxClientSharp
を用いて、VRMを読み込んで発話させるサンプルプロジェクトを作ってみました。
- ローカルのVRMを読み込んで表示
- 背景色を変更できる(OBSなどでクロマキー合成ができる)
- REST APIにより外部から発話命令を送ることができる
このサンプルプロジェクト、今年のUnityアドベントカレンダーのネタだったりします。 プロダクト規模に見合わないほど「クリーンなアーキテクチャ」をかなり意識して設計して作ってあります。もはやVoicevoxClientSharp
のサンプル実装というよりも、「Unityにおけるクリーンなアーキテクチャのサンプル実装」の方が主題になってしまっています。VoicevoxClientSharp
のサンプル実装としておそらく適切ではない。
この記事はアドベントカレンダーに向けた布石でもありました。ということでサンプルプロジェクトおよびその解説記事を後日(12/23)公開する予定なのでお楽しみに。