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

2016年8月13日

うたってユニティちゃん!」を作る際に、このサンプルを熟読したので、これから Unity with VOCALOID を使われる方のために、わかったことをまとめておきます。
ここでは、サンプルプロジェクト HelloVOCALOID の中の CreateSequence の部分だけをみていきます。

CreateSequence は、C# のスクリプトで作成したシーケンスを合成するサンプルです。
うたってユニティちゃん!」では、Vsqx を使用せずに C# のスクリプトでシーケンスを作成したので、主にこのサンプルの手順でシーケンスを作成しています。

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

HelloVOCALOID は 3 つのシーンが存在しているので、ファイルもそれぞれのシーンごとに分かれている部分があります。
具体的には、以下のファイルです。

  • Scenes/CreateSequence.unity
  • VOCALOID/Scripts/CreateSequence/VocDirector.cs
  • VOCALOID/Scripts/CreateSequence/VocAudioManager.cs
  • VOCALOID/Scripts/CreateSequence/VocAudio.cs
  • VOCALOID/Scripts/CreateSequence/VocNote.cs

VOCALOID/Scripts に置かれているスクリプトのうち、上記に挙げていないものはシーン間で共有しているファイルです。
VocDirector.cs と VocAudioManager.cs の2つのスクリプトは、Hierarchy に宣言した VOCALOIDDirector および VOCALOIDAudioManager オブジェクトに登録されています。

Scenes/CreateSequence.unity

メインカメラの正面にボタンが表示されます。
これを押すと、合成エンジンでの合成処理が開始するようになっています。
どのメソッドが呼び出されるかについては、後述します。

VOCALOID/Scripts/CreateSequence/VocDirector.cs

Unity 上で VOCALOID を動作させる際に、基幹部分となるクラスです。
この人が、VocAudioManager を持っていて、VocAudioManager が VocAudio を持っている、という形になっています。

VocDirector の Start() で、VocAudioManager の Startup() を呼び出して初期化を行ってます。

void Start()
{
    // オーディオマネージャの初期化.
    ready = audioManager.Startup(engineCount);
}

このとき Startup() に渡している engineCount の値が、起動する合成エンジンの数になっています。
リファレンスによると、最大で 16 まで設定できるようです。
つまり、同時に 16 コの合成エンジンで合成できることになるので、16 人のユニティちゃんでのコーラスは可能?ということだと思います。
このサンプルでは、増やしても特に音は変わりませんでした。

もうひとつのメソッド PlayMusic() がありますが、これが CreateSequence.unity に表示されているボタンを押したときに呼び出されるようになっています。

public void PlayMusic()
{
    if (ready)
    {
        print("Create, Render and Play Music!");
        if (audioManager.EState == VocAudioManager.EnginState.Initialized)
        {
            audioManager.Render();
        }
    }
}

PlayMusic() では、VocAudioManager の Render() が実行されていて、このタイミングで合成エンジンでの合成が始まります。

VOCALOID/Scripts/CreateSequence/VocAudioManager.cs

合成エンジンでの合成処理および出力波形を管理するためのクラスです。
VocDirector から Render() が呼び出されたタイミングで合成を開始されます。

アプリケーションの起動時に、VocDirector から Startup() が呼ばれますが、このタイミングで合成エンジンの初期化が行われています。

public bool Startup(Int32 engineCount)
{
    if (eState == EnginState.Uninitialized || eState == EnginState.Finalized)
    {
        YVF.YVFResult result = YVF.YVFStartup("personal", VocSystem.GetDBIniPath());
        if (result != YVF.YVFResult.Success)
        {
            return false;
        }
        eState = EnginState.Initialized;

        // Playbackモードに設定する.
        YVF.YVFSetStaticSetting(engineCount);

        return true;
    }
    return false;
}

YVFStartup() で合成エンジンを起動して、YVFSetStaticSetting() で合成モードを指定して初期化を行っているようです。
(Realtime 合成する場合には、このときに呼び出すメソッドが変わります。)
このとき、YVFSetStaticSetting() に渡す値によって、合成エンジンのインスタンス数が変わります。
サンプルでは、Startup() の引数で渡される値を合成エンジンの数として使用しています。

CreateSequence.unity のシーンからボタンが押されると、VocDirector から Render() が呼び出され、VocAudio が Prefab から生成されます。

public void Render()
{
    SingButton.interactable = false;

    if (vAudio != null)
    {
        vAudio.Delete();
    }

    GameObject vocObj = Instantiate(VocAudioPrefab, new Vector3(), Quaternion.identity) as GameObject;  // Prefabからゲームオブジェクトを生成.
    vocObj.transform.SetParent(gameObject.transform);
    vAudio = vocObj.GetComponent<VocAudio>();
    vAudio.SetupRender(0);
}

その後、生成された VocAudio の SetupRender() が実行されます。
ここでは SetupRender() に 0 を決め打ちで渡していますが、合成エンジンを複数起動している場合には、使用したい合成エンジンに該当する Handle をここに渡すことになるようです。
このサンプルでは、VocAudio をひとつしか生成しないので、Startup() で複数エンジンを指定しても出力に変化がありません。
複数合成したい場合には VocAudio の生成数を増やしましょう。
負荷を分散させたい場合には、このとき VocAudio に設定する合成エンジンの Handle を切り替える感じになると思います。

VOCALOID/Scripts/CreateSequence/VocAudio.cs

実際に VOCALOID の合成エンジンを使用して合成処理を行うクラスです。
合成に使用するシーケンスデータも、ここに記述されています。

VocAudioManager から呼び出される SetupRender() では、合成を行うためのスレッドを立てます。

public void SetupRender(Int32 engineHandle)
{
    this.engineHandle = engineHandle;

    thread = new Thread(threadWork);
    thread.Start();
}

このとき立てたスレッドでは、RState が Waiting になったら RenderMelody() での合成処理が始まります。
それまでは何もしません。

private void threadWork()
{
    while (true)
    {
        Thread.Sleep(0);
        if (RState == RenderState.Waiting)
        {
            try
            {
                rState = RenderState.Rendering;
                RenderMelody();
                rState = RenderState.Rendered;
            }
            catch(VocException e)
            {
                YVF.YVFCloseSong(e.Handle);
                rState = RenderState.RenderError;
                print(e);
            }
            finally
            {
                thread.Abort();
            }
        }
    }
}

RState を Waiting にするのは、Sing() です。

public void Sing()
{
    source.Play();
    rState = RenderState.Waiting;
}

このサンプルでは、事前に合成しておくわけではなく、ボタンが押されてから合成を始めるようになっています。
Sing() を呼び出すのは VocAudioManager の Play() で、Play() は VocAudioManager の FixedUpdate() から呼び出されます。

流れとしては、以下のような感じになります。

シーン上のボタン押下

VocAudioManager.Render()

VocAudio.SetupRender()

VocAudio.threadWork()

VocAudio.RenderMelody()

VocAudioManager.FixedUpdate() での合成終了判定

VocAudioManager.Play()

VocAudio.Sing()

AudioSource.Play()

ということで、たぶん一番重要な RenderMelody() です。
まとめてみると長いのでひとつずつ。

YVFOpenSong() を実行すると、シーケンスが生成されます。
この YVFOpenSong() は、YVFCloseSong() と対になっています。

Int32 handle = YVF.YVFOpenSong();

(中略)

YVF.YVFCloseSong();

シーケンスが作成できたら、イベントを追加していきます。
このサンプルでは、YVFTempo、YVFSinger、VocNote(YVFNote)、YVFParamData(Dynamics、PBSens)、YVFPitchBendを設定しています。
イベントの設定メソッドの使い方は基本的にはどれも同じようなので、ここではひとつのノートを設定する場合のスクリプトで説明します。

// ノートの編集.
VocNote nt = new VocNote();
nt.Note.noteTime = preTick;         // ノート領域の先頭 (プリメジャー領域を考慮する必要があります).
nt.Note.noteLength = 480 * 2;
nt.Note.noteNumber = 65;
nt.Note.noteVelocity = 100;
nt.Note.lyricStr = "は";

Int32 noteHandle = -1;
if(YVF.YVFEditNoteInPart(handle, partHandle, ref nt.Note, YVF.YVFLang.Japanese, out noteHandle) != YVF.YVFResult.Success)
{
    throw new VocException("YVFEditNoteInPart", handle);
}

使い方は、追加したいイベントを生成して、それを適切なメソッドで設定するだけです。
引数としては、シーケンスの Handle, シーケンス内の PartHandle を渡すことでイベントの追加先を指定することになります。
メソッドは、シンガー以外は YVFEdit〜 という名称になっています。シンガーだけは YVFSet〜 となっているので注意が必要です。

すべてのイベントを設定し終わったら、YVFSetupMidiEventsToEONInPart() を実行します。
これをやらないと、設定が反映されないみたいです。

// 開始tickを指定し,MIDI EVENTデータを生成.
if (YVF.YVFSetupMidiEventsToEONInPart(handle, partHandle, partHead.posTick) != YVF.YVFResult.Success)
{
    throw new VocException("YVFSetupMidiEventsToEONInPart", handle);
}

このメソッドは、サンプルのコメントにもあるように第 3 引数で合成の開始位置を指定できます。
サンプルでは partHead.posTick を渡しているので、パートの先頭から合成が行われることになります。
例えば、サンプルで適当に大きな数値を入れると、設定したイベントよりも後方から合成が始まるので、音が出なくなります。

ここでひとつ注意点があります。
YVFSetupMidiEventsToEONInPart() を実行して合成した後に、シーケンスを閉じないで再度イベントを追加して合成したい場合には、YVFClearMidiEventsInPart() を実行する必要があります。
これをやらないと、合成時の情報が残ってしまっているようで、正常に動作しない場合がありました。
YVFCloseSong() でシーケンスを閉じた後、YVFOpenSong() で別のシーケンスを作成した場合には問題なさそうです。

イベントの設定が完了したら、最後に合成処理を行います。

Int32 totalFrame = YVF.YVFGetTotalFrameByPart(partHandle);
if(totalFrame <= 0)
{
    throw new VocException("YVFGetTotalFrameByPart", handle);
}

// レンダリング処理の開始.
if(YVF.YVFBeginRender(engineHandle) != YVF.YVFResult.Success)
{
    throw new VocException("YVFBeginRender", handle);
}

Int32 targetRenderSamples = totalFrame * YVF.YVFSamplesPerFrame;
renderData = new Int16[targetRenderSamples];
totalRenderSamples = 0;

while (true)
{
    Int32 renderSamples = 0;

    // レンダリング.
    if(YVF.YVFRender(partHandle, renderData, targetRenderSamples, engineHandle, out renderSamples) != YVF.YVFResult.Success)
    {
        throw new VocException("YVFRender", handle);
    }

    totalRenderSamples += renderSamples;
    if (renderSamples < targetRenderSamples)
    {
        break;
    }
}

// レンダリング処理の終了.
YVF.YVFEndRender(engineHandle);

YVFBeginRender()

YVFRender()

・・・

YVFRender()

YVFEndRender()

という感じですね。名前のまんまです。
YVFRender() での合成回数は、YVFGetTotalFrameByPart() で取得できるフレーム数を元に決まります。
フレーム数は、合成するパートの長さを元に算出されるので、パートの長さが変わるとフレーム数も変わってきます。
この辺はコピペで動きます。

VOCALOID/Scripts/CreateSequence/VocNote.cs

YVFNote をラップして、初期化処理やパラメータの取得を少し簡単にするためのクラスです。
YVFNote には初期化メソッドがないので、このクラスで宣言している Clear() を使用して、VocNote を使い回しています。

ということで、ひと通り書きました。
この処理がわかってくると、とりあえず好きなシーケンスをプログラムで書いて鳴らすところまではできるようになります。
Unity with VOCALOID に興味のある方は、試してみてはいかがでしょうか。

おしまい。

スポンサーリンク