5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VoicevoxClientSharp: C#やUnityからVOICEVOXで音声合成するライブラリの紹介

Posted at

はじめに

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共通)

  1. VOICEVOXをネットワーク上でアクセスできる場所で起動する

    • ローカルマシンまたはdockerコンテナ上で起動しておいてください
  2. VoicevoxClientSharpより次のどちらかのクライアントを生成する

    • VoicevoxSynthesizer : とにかく簡単にVOICEVOXを使いたい人向け
    • VoicevoxApiClient : VOICEVOXが提供するAPIを個別に使いたい人向け
  3. 生成したクライアントをもとに、VOICEVOXを用いて音声合成を行う

    • 音声合成した結果はwav形式のbyte[]として得られます
  4. wavをC#アプリケーション側で再生する

    • Unityの場合はAudioClipに変換することで再生できます
    • VoicevoxClientSharp.Unityを用いると簡単にUnity上で再生ができます

VoicevoxSynthesizer:とにかく簡単にVOICEVOXを使いたい人向けクライアント

VoicevoxSynthesizerを用いることでテキストからの音声合成を簡単に実行することができます。

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, "こんにちは、世界!");
SynthesisResult
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とメソッドが対応しているかは対応表があるのでそちらを御覧ください。

使用例,StyleIdを取得して音声合成を行う
// 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アバターをリップシンクするコンポーネント
  • エディタ拡張

VoicevoxSpeakPlayer:Unityで合成した音声を再生するコンポーネント

VoicevoxSpeakPlayerVoicevoxSynthesizerより得られるSynthesisResultをUnity上で再生するコンポーネントです。
GameObjectにアタッチし、AudioSourceを割り当てて使用してください(動的に生成して割り当ててもOKです)。

image.png

Unity上で音声再生する
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アバターをリップシンクするコンポーネント

seedsan2.gif

VoicevoxVrmLipSyncPlayerはVRMアバターをVOICEVOXで合成した音声再生のタイミングに合わせてリップシンクするコンポーネントです。
さきほどのVoicevoxSpeakPlayerと組み合わせて使用してください。

image.png

VRMとリップシンクする例かつ、すべて動的に組み立てるパターン
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段階に別れています。

  1. テキストよりクエリ(AudioQuery)を作成する
  2. 作成したクエリをもとに音声合成を行いwavを生成する

VoicevoxSynthesizerは内部でこの2つの処理を実行しています。合成結果として返されるデータ構造にSynthesisResultに含まれているAudioQueryがこの合成に用いたクエリです。

SynthesisResult
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などを確認するときに使用してください。

image.png

おまけ機能:AvisSpeech連携

AvisSpeechvoicevox_engineをベースとしたVOICEVOX派生の音声合成ソフトウェアです。
VoicevoxClientSharpも部分的に対応しており、VoicevoxApiClientの一部メソッドが使用できます。

AvisSpeech向けにApiClientを生成する
// VoicevoxApiClientを生成する際にstaticメソッドでAviSpeechを指定して生成してください
using var avisSpeechApiClient = VoicevoxApiClient.CreateForAvisSpeech();

全体構成

APIクライントはIVoicevoxApiClientインタフェースにより抽象化が行われています。またVoicevoxSynthesizerはこのIVoicevoxApiClientに依存しています。
もしテストを書くようなことがあればこの抽象化をうまく利用してください。

image.png

まとめ

VoicevoxClientSharpを使うことでC#およびUnityから気軽に音声合成し、再生することができるようになるので是非使ってみてください。(より詳しい使い方はREADMEに書いてあるのでこっちを読んでください。)

余談として、VOICEVOXOpenAPI Specを公開してくれています。が、C#のコードジェネレートをかけたところ上手く動くコードが生成できませんでした。
なのでこのVoicevoxClientSharpはREST APIを叩く部分を手動で実装してあったりします。

Unityでのサンプル実装

VoicevoxClientSharpを用いて、VRMを読み込んで発話させるサンプルプロジェクトを作ってみました。

seedsan.gif

  • ローカルのVRMを読み込んで表示
  • 背景色を変更できる(OBSなどでクロマキー合成ができる)
  • REST APIにより外部から発話命令を送ることができる

このサンプルプロジェクト、今年のUnityアドベントカレンダーのネタだったりします。 プロダクト規模に見合わないほど「クリーンなアーキテクチャ」をかなり意識して設計して作ってあります。もはやVoicevoxClientSharpのサンプル実装というよりも、「Unityにおけるクリーンなアーキテクチャのサンプル実装」の方が主題になってしまっています。VoicevoxClientSharpのサンプル実装としておそらく適切ではない。

この記事はアドベントカレンダーに向けた布石でもありました。ということでサンプルプロジェクトおよびその解説記事を後日(12/23)公開する予定なのでお楽しみに。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?