Unity with VOCALOIDのサンプルプロジェクトHelloVOCALOIDを読んでみる Realtime 編

2016年8月13日

CreateSequence 編Playback 編と続きまして HelloVOCALOID の最後になるシーンは Realtime です。
Unity と連携した使い方としては、この機能がいちばん重要になってくると思います。

Realtime に関連するファイルはどれか?

このサンプルはユニティちゃんと鍵盤が表示されます。
鍵盤を押すと該当する音高で VOCALOID が発音するようになっているので、スクリプトが他のサンプルより多くなっています。

  • Scenes/Realtime.unity
  • VOCALOID/Scripts/Realtime/VocDirector.cs
  • VOCALOID/Scripts/Realtime/VocAudioManager.cs
  • VOCALOID/Scripts/Realtime/VocAudio.cs
  • VOCALOID/Scripts/Realtime/Keyboard.cs
  • VOCALOID/Scripts/Realtime/LipSyncController.cs
  • VOCALOID/Resources/Prefab/VOCALOIDAudioRealtime.prefab

Scenes/Realtime.unity

いろいろ配置されていますが、重要なのは画面の半分を埋めている鍵盤と、左右に 3 つずつあるボタンですね。
鍵盤を押すと、鍵盤ごとの音高に合わせてユニティちゃんが発音します。
ボタンを押すと、ボタンに書かれているテキストを発音するように切り替わります。
結構早く操作してもちゃんと歌ってくれるので、VOCALOID とか Unity とかよくわからない人も遊べると思います。

VOCALOID/Scripts/Realtime/VocDirector.cs

他のサンプルとの違いとしては、まず歌詞と発音記号を Unity 側で保持している点があります。

public Image[] Panels;
public string[] Lyrics;

private string currentLyrics = "";
private List<string> phoneticList = new List<string>();     // 発音記号リスト.
public List<string> PhoneticList
{
    get{ return phoneticList;}
}

Panels と Lyrics は配列サイズが一致していて、Panels を参照する際のインデックスで Lyrics を参照すると該当する歌詞が取得できるようになっています。
SetLyrics() に渡す index が、上記の配列の参照インデックスになります。

public void SetLyrics(int index)
{
    if (currentLyrics == Lyrics[index])
    {
        return;
    }
    currentLyrics = Lyrics[index];

    phoneticList.Clear();
    // 発音記号を取得する.
    if (YVF.YVFSetLyricsToConverter(currentLyrics, YVF.YVFLang.Japanese) != YVF.YVFResult.Success)
    {
        return;
    }
    int syllableCount = YVF.YVFGetSyllableCountFromConverter();
    for (int i = 0; i < syllableCount; ++i)
    {
        StringBuilder phoneticBuffer = new StringBuilder(8, 8);
        YVF.YVFGetPhoneticFromConverter(i, phoneticBuffer, 8);

        phoneticList.Add(phoneticBuffer.ToString());
    }

    audioManager.SetLyrics(currentLyrics);

    foreach (Image panel in Panels)
    {
        panel.color = defaultPanelColor;
    }
    Panels[index].color = focusedPanelColor;
}

SetLyrics() では、YVF を使用して歌詞を発音記号に変換しています。
変換後の発音記号を phoneticList に格納していますが、ここで格納しておいた発音記号をユニティちゃんのリップシンクに使用しています。

VOCALOID/Scripts/Realtime/VocAudioManager.cs

基本的には CreateSequence と Playback と同じです。
大事なのは Startup() で行っている YVF への初期設定のところ。

public bool Startup()
{
    if (eState == EnginState.Uninitialized || eState == EnginState.Finalized)
    {
        (略)

        // Realtimeモードに設定する.
        YVF.YVFRealtimeSetStaticSetting(YVF.YVFRealtimeMode.Mode3);

        (略)

        return true;
    }
    return false;
}

Realtime 合成を使用する場合には、初期化時に呼び出すメソッドが変わります。
YVFRealtimeSetStaticSetting() は、YVFSetStaticSetting() とは異なり、合成エンジンの数ではなく Realtime 合成時のモード を引数で指定します。
リファレンスによると以下のように宣言されている YVFRealtimeMode を設定することになっています。

  • Mode1 10 バッファサイズ最小
  • Mode2 11 バッファサイズ小
  • Mode3 12 バッファサイズ中
  • Mode4 13 バッファサイズ大
  • Mode5 14 バッファサイズ最大

これだけだと何がどう違うのか分からないですね。
使うと分かりますが、バッファサイズが小さくなるとリアルタイム合成時のレイテンシが小さくなります。
逆にバッファサイズを大きくするとレイテンシが大きくなります。

もっと具体的に言えば、このサンプルで鍵盤を押してから実際に発音されるまでの時間が変わります。
Mode1 と Mode5 を試してみると、違いが体感できると思います。

これだけ聞くと「じゃあずっと 1 番早い Mode1 でいいじゃん」となると思いますが、プラットフォームの性能が悪いと、バッファサイズが小さすぎる場合にうまく発音されなくなることがあります。
例えば、OSX 上だとうまく動いているけど、iPhone に入れたら音が途切れるようになった、とかですね。
こういうときには、まずは Mode を切り替えてバッファサイズを大きくすると改善するかもしれません。

詳しく知りたい人は「レイテンシ 音」などで検索すると親切に教えてくれているところが見つかると思います。
VOCALOID の場合に、レイテンシがどこにかかる時間を指しているかをよくわかっていないのでここでは説明しません。

VOCALOID/Scripts/Realtime/VocAudio.cs

他のサンプルと大きく違うのはこのファイルです。

まず、「合成後の音声をどこで Unity に出力しているか」というところから違います。
CreateSequence と Playback では、VOCALOID の合成音声を OnAudioRead() で書き込んでいましたが、Realtime では OnAudioFilterRead() で書き込みを行っています。

void OnAudioFilterRead(Single[] data, Int32 channels)
{
    if (!ready)
    {
        return;
    }
    UInt32 numBufferdSamples = YVF.YVFRealtimeGetAudioNumData();

    Int32 numOutSamples = data.Length / channels;
    if (numBufferdSamples < numOutSamples)
    {
        numOutSamples = (Int32)numBufferdSamples;
    }

    // リアルタイム合成の結果をYVFから受け取る.
    if (renderData.Length < numOutSamples)
    {
        renderData = new Int16[numOutSamples];
    }
    YVF.YVFRealtimePopAudio(renderData, numOutSamples);

    // 合成結果をAudioClipに書き込む.
    for (int i = 0; i < numOutSamples; ++i)
    {
        Single value = renderData[i] / 32768.0f;    // convert uint16_t (-32768 ~ 32767) to float (-1.0 ~ 1.0)
        int index = i * channels;
        for (int j = index; j < index + channels; ++j)
        {
            data[j] = value;
        }
    }

    // 不足した場合はゼロで埋める.
    for (int i = numOutSamples * channels; i < data.Length; ++i)
    {
        data[i] = 0;
    }
}

Realtime 合成では、YVFRealtimeStart() / YVFRealtimeStop() が対になっています。
YVFRealtimeStart() を実行してから YVFRealtimeStop() が実行されるまでの間は入力を受付けているようです。

CreateVocAudio() のコメントに「メロディと再生タイミングを取得して,合成を行うスレッドを走らせる」と書かれているので、YVFRealtimeStart() を行うとリアルタイム合成用のスレッドを立ててるみたいです。
だとすると、YVFRealtimeStop() をやり忘れるとスレッドが残ってしまいそうですね。
サンプルを拡張して実装する場合には、CreateVocAudio() / DeleteVocAudio() を呼び出していれば大丈夫そうです。

VOCALOID/Scripts/Realtime/Keyboard.cs

鍵盤入力用のスクリプトです。
鍵盤をインターフェイスとして、MIDI イベントを合成エンジンに送り付けるのがメインです。
重要なのは、ノートオン/ノートオフの送信ですね。

void OnMouseDown ()
{
    activeKey = noteNumber;
    renderState = true;

    // リップシンクを設定する.
    LipSync();

    // ノートオンを送信する.
    YVF.YVFRealtimeAddMidi(YVF.YVFMIDIEventType.NoteOn, noteNumber);
    YVF.YVFRealtimeCommitMidi();
}

MIDI イベントの送信には YVFRealtimeAddMidi() を使用します。
送信するイベントの種類は、第 1 引数の YVFMIDIEventType で指定します。

MIDI イベントの送信処理は Add と Commit に分かれていて、Add するだけでは送信されません。
Commit して初めて MIDI イベントとして扱われるようなので、忘れずに Commit しましょう。
一度に複数の MIDI イベントを Add すると、すべて同じタイミングに存在する MIDI イベントとして扱われるようです。

次にリップシンクです。
リップシンク自体のコードは後述する LipSyncController の方に書かれていますが、リップシンクの発動は鍵盤の操作をトリガーに行われています。

private void LipSync ()
{
    string targetPhoneticSymbol = phoneticList[YVF.YVFRealtimeGetPhoneticIndex()];
    YVF.YVFVowelIDJapanese id = YVF.YVFConvertPhoneticSymbolToVowelID(targetPhoneticSymbol, YVF.YVFLang.Japanese);
    if (id != YVF.YVFVowelIDJapanese.None)
    {
        // 母音の場合には,口の形を更新する.
        unityChan.GetComponent<LipSyncController>().UpdateMouth(id);
        return;
    }
    bool isSyllabicNasal = YVF.YVFIsSyllabicNasal(targetPhoneticSymbol);
    if (isSyllabicNasal == true)
    {
        // 撥音の場合には,口を閉じる.
        unityChan.GetComponent<LipSyncController>().ResetMouth();
        return;
    }
}

・phoneticList
VocDirector の SetLyrics() で取得しておいた発音記号情報を代入したメンバ変数。

・YVFRealtimeGetPhoneticIndex()
次に発音する発音記号のインデックスを取得します。
このインデックスは phoneticList を参照する際のインデックスと一致しています。

・YVFConvertPhoneticSymbolToVowelID()
発音記号を元に Vowel(母音) を抽出します。
母音が含まれていない場合には、None が返されます。

MIDI イベントでノートオンを設定するタイミングで LipSync() を実行することで、発音と同時にユニティちゃんの口の形を変えています。
ただし、リップシンク時に Weight の設定はできないので、ちょっと動きは不自然になってますね。

VOCALOID/Scripts/Realtime/LipSyncController.cs

BlendShape を使って口を動かすためのスクリプトです。
このスクリプトでは、Awake() で設定している 5 つの BlendShape だけを操作できます。
ここで、BlendShape と YVFVowelIDJapanese を関連付けています。

void Awake ()
{
    _skinnedMeshRenderer = GameObject.Find("MTH_DEF").GetComponent<SkinnedMeshRenderer>();

    blendShapeList.Add(new BlendShape(YVF.YVFVowelIDJapanese.A, _skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex("blendShape1.MTH_A")));
    blendShapeList.Add(new BlendShape(YVF.YVFVowelIDJapanese.I, _skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex("blendShape1.MTH_I")));
    blendShapeList.Add(new BlendShape(YVF.YVFVowelIDJapanese.U, _skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex("blendShape1.MTH_U")));
    blendShapeList.Add(new BlendShape(YVF.YVFVowelIDJapanese.E, _skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex("blendShape1.MTH_E")));
    blendShapeList.Add(new BlendShape(YVF.YVFVowelIDJapanese.O, _skinnedMeshRenderer.sharedMesh.GetBlendShapeIndex("blendShape1.MTH_O")));
}

BlendShape を更新するのがこのメソッドです。
weight を 0 か 100 で切り替えているので、スムーズには動きません。
この辺を時間経過で weight が変わるようにしたり、複数の BlendShape を組み合わせられるように拡張すると、より自然なリップシンクになります。

public void UpdateMouth (YVF.YVFVowelIDJapanese id)
{
    foreach (var shape in blendShapeList)
    {
        if (shape.id == id)
        {
            shape.weight = 100;
        }
        else
        {
            shape.weight = 0;
        }
    }
}

他のサンプルに比べると複雑だった

Realtime は通常の合成とは処理が違うということですね。
でも、この機能があるのとないのとでは、できることが大きく変わる(気がする)ので使いこなしたいところです。
Realtime 合成を行うエンジンを複数起動する方法とか、Playback 合成を行うエンジンと Realtime 合成を行うエンジンを共存させる方法とか気になるところです。

おしまい。

スポンサーリンク