レイテンシの軽減とリミテッドアニメーションを実現するAniLipSyncを使ってみる

#まず、AniLipSyncとは?
AniLipSyncとは、Unite Tokyo 2018にてGOROman(@GOROman )氏による講演「[AniCast！東雲めぐちゃんの魔法ができるまで](https://www.youtube.com/watch?v=1rfoFg6LJgU)」にて発表されたOVRLipSyncの拡張ライブラリです。OVRLipSyncとは？という方は凹さんの[こちら](http://tips.hecomi.com/entry/2016/02/16/202634)の記事が参考になると思います。OVRLipSyncとの違いは、音声入力から口を動かすまでの遅延が軽減されていること、リミテッドアニメーションっぽいリップシンクを実現していることです。MITライセンスとなっており、[こちら](https://github.com/XVI/AniLipSync)のリポジトリで公開されています。

百聞は一見に如かず、ということでOvrLipSyncとAniLipSyncを見比べてみましょう。

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr">OvrLipSync <a href="https://t.co/Dbyr1zHyLL">pic.twitter.com/Dbyr1zHyLL</a></p>&mdash; かいき (@kaikiofkaiki) <a href="https://twitter.com/kaikiofkaiki/status/1004946224477163520?ref_src=twsrc%5Etfw">2018年6月8日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="tl" dir="ltr">AniLipSync <a href="https://t.co/rUKI4CWeOd">pic.twitter.com/rUKI4CWeOd</a></p>&mdash; かいき (@kaikiofkaiki) <a href="https://twitter.com/kaikiofkaiki/status/1004945787892060160?ref_src=twsrc%5Etfw">2018年6月8日</a></blockquote>
（音声は[効果音ラボ](https://soundeffect-lab.info/sound/voice/line-girl1.html)からお借りしました。）

いかがでしょうか、AniLipSyncの方が口がパクパクと動いて、キャラクターが生き生きとしているように見えませんか？AniLipSyncはあえてフレートを落としたり、口の形を母音に絞ったりすることで、実際のアニメのような表現を実現しているのです。アニメはコマ割りで作られており、1秒あたり何枚と制限された中で表現されています。AniLipSyncはそのアニメ特有のコマ割りの表現を再現したような感じですね。また、音声入力から口を動かすまでの遅延に関しても大幅な改善が施されており、AniLipSyncはOvrLipSyncと比べて**約150msも遅延を軽減しています**。

#AniLipSyncのざっくりとした処理の流れ
**LowLatencyLipSyncContext.cs**が`Microphone.Start()`でAudioClipを生成し、そのAudioClipから`AudioClip.GetData()`で音声データを取得し、コンテキストに渡します。そしてその音声データを**OVRLipSync.cs**にインポートされているDLLの認識アルゴリズムに渡し、音素などの解析を行います。その結果を元に**AnimMorphTarget.cs**がskinnedMeshRendererのBlendshapeを変更し、口の動きに反映させています。また、この**AnimMorphTarget.cs**にはリミテッドアニメーションを再現する機能も備わっており、各パラメータを元に口の動きを制御しています。

![AniLipSyncImage.png](https://qiita-image-store.s3.amazonaws.com/0/140271/dbfe15bd-808b-0af8-61e3-a921058d755f.png)


#遅延の軽減について
OVRLipSyncでは`Microphone.Start()`と`OnAudioFilterRead()`の組み合わせによって音声を取得しており、AniLipSyncは`Microphone.Start()`と`AudioClip.GetData()`の組み合わせによって音声を取得しています。遅延に差が出ているのは主にこの部分によるものです。詳しい内容は @TyounanMOTI さんによる [Unityでのマイク録音３種盛り：レイテンシ比較](https://qiita.com/TyounanMOTI/items/5306336823316b0b289c)にあるので、ぜひ読んでみてください。

#動作環境
自分が使用した動作環境

- Windows 10 Version 1709 Build 16299.431
- OVRLipSync Version 1.25.0
- Unity2017.1.2f1
- UnityChan_1_2_1
- AniLipSync Ver.1.0.0

AniLipSyncのREADMEに記載されている動作環境

- Windows 10 Version 1709 Build 16299.371
- OVRLipSync Version 1.25.0
- Unity 2018.1.0f2

AniLipSyncはOVRLipSyncのバージョンに依存しています。逐次OVRLipSyncは更新されており、バージョンによっては動かないこともあるので注意しましょう。

#導入
公式のREADMEが非常に優れているので、説明する必要があるかどうかも謎ですが、せっかくなので補完する形として画像多めで解説しようと思います。
>
1. OVRLipSyncをインポート
2. AniLipSync.unitypackage をインポート
3. Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置
4. Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置
5. AniLipSync GameObjectの AnimMorphTarget の各プロパティをインスペクタで編集（とくにSkinned Mesh RendererとViseme To Blend Shapeは変更が必要です）<br><br>AniLipSyncのREADMEから引用(https://github.com/XVI/AniLipSync/blob/master/README.md)

###1.OVRLipSyncをインポート
[公式](https://developer.oculus.com/downloads/package/oculus-lipsync-unity/)からファイルをダウンロードします。
![ovr-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/0fdf1a73-bc6f-47c4-1361-386b0f0f5435.png)


ダウンロードした「ovr_audio_lipsync_unity_~.zip」を解凍します。その後、Unityから「Assets→Import Packages→Custom Package」を選び、「ovr_audio_lipsync_unity_~\LipSync\UnityPlugin\OVRLipSync.unitypackage」を選択してインポートを行います。

###2.AniLipSync.unitypackage をインポート
[AniLipSync.unitypackage](https://github.com/XVI/AniLipSync/releases)をダウンロードします。

![AniLipSync.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/77bcbe1a-c645-f167-6ce9-986f09aaefbb.png)

ダウンロードができたら、Unityから「Assets→Import Packages→Custom Package」を選び、「AniLipSync.unitypackage」を選択してインポートします。この時点で、UnityProjectのAssets配下にAniLipSyncとOculusディレクトリがあれば、とりあえずAniLipSyncを使うためのファイル準備は完了です。「AniLipSync/Examples/Scenes」に「AniLipSync」というサンプルシーンがあるので、ここで一回動くかどうか試してみるのも良いと思います。

<!--![AniDemo.png](https://qiita-image-store.s3.amazonaws.com/0/140271/180d48cf-599a-0a4c-884b-61aab2e30a67.png) -->

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">おっ、UniteでGOROmanさんが言ってたAniLipSync動いた。動作検証済環境はUnity 2018.1.0f2になってるけど、Unity 2017.1..2f1でも普通に動いた。<a href="https://t.co/5gWugiOscB">https://t.co/5gWugiOscB</a> <a href="https://t.co/SMpQW18JuG">pic.twitter.com/SMpQW18JuG</a></p>&mdash; かいき (@kaikiofkaiki) <a href="https://twitter.com/kaikiofkaiki/status/998935066662191105?ref_src=twsrc%5Etfw">2018年5月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



###3.Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置
Assets/Oculus/LipSync/Prefabs/LipSyncInterface プレハブをシーンに配置しましょう。もし、サンプルシーンを試していた方は別のシーンに移動してからこれ以降の作業を行いましょう。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/140271/4279adf3-98c5-68d8-30c4-9d45a5e16c5a.png)

###4.Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置
Assets/AniLipSync/Prefabs/AniLipSync プレハブをシーンに配置しましょう。
![AniPreb.png](https://qiita-image-store.s3.amazonaws.com/0/140271/b2ba365d-96d0-1219-0f70-846231c3f229.png)



###5. AniLipSync GameObjectの AnimMorphTarget の各プロパティをインスペクタで編集
さて、AniLipSyncを動かす下準備は整ったので、口パクさせたいキャラクターにセットアップしていきます。今回は口パクするキャラクターとしてユニティちゃんを例として説明しようと思います。
まず、[ユニティちゃん](http://unity-chan.com/contents/guideline/)をダウンロードします。ダウンロードができたらUnityから「Assets→Import Packages→Custom Package」を選び、「UnityChan_~.unitypackage」を選択してインポートを行います。インポートができたら、Assets/UnityChan/Modelsにあるunitychan Prefabをシーンに配置します。注意ですが、**アセットストアのユニティちゃんにはBlendShapeに母音が用意されていない**ため、そちらからインポートはしないようにしましょう。

![UnitychanThis.png](https://qiita-image-store.s3.amazonaws.com/0/140271/68c3a7f8-4688-d44b-24c8-65aa3deaae4c.png)

では、ユニティちゃんも配置できましたし、AniLipSync GameObjectのセットアップをしていきましょう。
まずは、Skinned Mesh Rendererにユニティちゃんの表情のBlendShapeがあるオブジェクトを選択して入れます。ユニティちゃんの場合、MTH_DEFというものがそれにあたります。MTH_DEFはとても深い層(unitychan>Character1_Reference>Character1_Hips>Character1_Spine>Character1_Spine1>Character1_Spine2>Character1_Neck>Character1_Head>MTH_DEF)にあって見つけにくいのでHierarchyビューから検索をかけて探すことをおススメします。

![MTHDEF.png](https://qiita-image-store.s3.amazonaws.com/0/140271/73776e93-781a-1e40-7ca5-689518291432.png)

次に、Viseme To Blend Shapeに上からaa, E, ih, oh, ouの順でモーフのインデックス番号を指定していきます。ユニティちゃんのMTH_DEFを見てみると、aa:6、E:9、ih:7、oh:10、ou:8というように対応していることが分かります。なので、Viseme To Blend ShapeのElement 0に6、Element 1に9、Element 2に7、Element 3に10、Element 4に8を入力します。

![morf.png](https://qiita-image-store.s3.amazonaws.com/0/140271/350ce055-2b14-a0c8-d4cd-5ff45b9ebd00.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/140271/c7b5e000-4bc9-2f86-822b-f18f291b615e.png)

これでセットアップは終了です。マイクに向かって話しかけてみてください。ユニティちゃんも一緒に口パクしてくれるでしょうか？

#音声と口パクの遅延合わせについて
AniLipSyncを使っても完全に遅延を消せるわけではありません。なので、配信ソフトや動画編集ソフトを使ってそちら側で合わせることをUniteの講演では推奨していました。AniLipSyncだと250msほどズラすと丁度いいそうです。
OBSを使っている場合は、左上の　編集>オーディオの詳細プロパティ と進み、そこからマイクの同期オフセットに入力するとよいです。

![2018-06-13 (1).png](https://qiita-image-store.s3.amazonaws.com/0/140271/d8248f24-6282-967e-af7b-84d3e620af9b.png)
![2018-06-13 (2).png](https://qiita-image-store.s3.amazonaws.com/0/140271/45fde7a1-78c5-e5b8-1639-9cd5b6c04ea7.png)



#AnimMorphTargetの各プロパティについて
これでキャラクターの口を動かせるようになりましたね。ただ、このままではモデルや環境によっては綺麗に動いて見えないかもしれませんし、リミテッドアニメーションも自分で調節したいということもあるでしょう。AnimMorphTargetの各プロパティを調整することでそのあたりの調整をすることができます。

以下、[公式のREADME](https://github.com/XVI/AniLipSync)の引用です。

>
##Transition Curves
aa, E, ih, oh, ou のそれぞれの音素へ遷移する際に、BlendShapeの重みを時間をかけて変化させるためのカーブです。
>
例えば、黙っている状態から aa の音素を検知した場合、Element 0 のカーブに従って時々刻々とBlendShapeの重みを変化させます。aa の状態で ih の音素を検知した場合、Element 2 のカーブに従います。
>
徐々に重みを増やすことで、ゆるやかに口の形が変化するような表現が可能です。
>
##Curve Amplifier
Transition Curveの縦軸の値に倍率をかけます。
>
Transition Curveの縦軸を 0.0 ～ 1.0 の範囲にしておいて 100 の倍率をかけることで、カーブ編集時の煩雑な操作を省略できます。
>
##Weight Threashold
この値より小さな音素の重みを無視します。
>
小さなノイズ等で口が開いてしまう現象を回避できます。
>
##Frame Rate
1秒間にBlendShapeの重みを更新する頻度の設定です。
>
リミテッドアニメ風の効果を得ることができます。
>
##Skinned Mesh Renderer
唇のBlendShapeを持ったSkinnedMeshRendererを指定してください。
>
##Viseme To Blend Shape
aa, E, ih, oh, ouの順でBlendShapeのIndexを指定してください。
>
##Smooth Amount
OVRLipSyncのSmooth amountの値を設定できます。

#よくありそうなバグ
よくありそうというか、自分が出会ったバグとその解決方法です。
###アニメーションを設定すると動かなくなってしまった。
**原因**
AniLipSyncがアニメーションで上書きされてしまっている。

**解決**
AnimMorohTarget.csの`void Update ()` を`void LateUpdate ()`に変更して、アニメーションの適応後にBlendShapeの変更を行うように変更した。 

```c#:AnimMorohTarget.cs
//変更前(43行目)
void Update() {
        if (context == null || skinnedMeshRenderer == null) {
            return;
        }
```
```c#:AnimMorohTarget.cs
//変更後(43行目)
void LateUpdate() {
        if (context == null || skinnedMeshRenderer == null) {
            return;
        }
```


###OVRPhonemeContext.Start ERROR: Could not create Phoneme context
**原因**
OVRLipSyncとAniLipSync共存できるか試そうとOVRLipSyncのスクリプトをシーン上に配置していたら出てきた。OVRLipSyncContext.csとLowLatencyLipSyncContext.csより先にOVRLipSyncがAwakeしてしまっており、うまくコンテキストが生成できていない。

OVRLipSyncContext.csとLowLatencyLipSyncContext.csはOVRLipSyncContextBase.csを継承しており、そのOVRLipSyncContextBase.csにあるコンテキストを生成するプログラムがエラーを吐いている。プログラムを編集し、何のエラーを返しているか試してみたところ`"CannotCreateContext"`が返ってきていた。コンテキストの生成に異常があることが分かる。

```c#:OVRLipSyncContextBase.cs
//変更前(78行目付近)
void Awake()
    {
        // Cache the audio source we are going to be using to pump data to the SR
        if (!audioSource)
        {
            audioSource = GetComponent<AudioSource>();
        }

        lock (this)
        {
            if (context == 0)
            {
                if (OVRLipSync.CreateContext(ref context, provider) != OVRLipSync.Result.Success)
                {
                    Debug.Log("OVRPhonemeContext.Start ERROR: Could not create Phoneme context.");
                    return;
                }
            }
        }
    }
```
```c#:OVRLipSyncContextBase.cs
//変更後(78行目付近)
void Awake()
    {
        OVRLipSync.Result rel;
        // Cache the audio source we are going to be using to pump data to the SR
        if (!audioSource)
        {
            audioSource = GetComponent<AudioSource>();
        }
        lock (this)
        {
            if (context == 0)
            {
                if ((rel = OVRLipSync.CreateContext(ref context, provider)) != OVRLipSync.Result.Success)
                {
                    Debug.Log("OVRPhonemeContext.Start ERROR: Could not create Phoneme context.");
                    Debug.Log(rel);
                    return;
                }
            }
        }
    }
```
```c#:OVRLipSync.cs
//OVRLipSync.CreateContext実行時に返ってくる値。(37行目付近)
public enum Result 
    {
    	Success =				0,
        Unknown = 				-2200,	//< An unknown error has occurred
        CannotCreateContext = 	-2201, 	//< Unable to create a context
        InvalidParam = 			-2202,	//< An invalid parameter, e.g. NULL pointer or out of range
        BadSampleRate = 		-2203,	//< An unsupported sample rate was declared
        MissingDLL = 			-2204,	//< The DLL or shared library could not be found
        BadVersion = 			-2205,	//< Mismatched versions between header and libs
        UndefinedFunction = 	-2206	//< An undefined function 
    };
```

**解決**
OVRLipSync.csを一回外して付け直すか、スクリプトの実行順序を編集してOVRLipSync.csを優先にさせると直った。スクリプトの実行順序の編集に関しては[【Unity】コンポーネントのイベント実行順についてのTips](http://tsubakit1.hateblo.jp/entry/2017/02/05/003714)に詳しくあるのでそちらを参考にしてみてください。



#おわりに
遅延軽減やリミテッドアニメーションが簡単に実装できるほか、OVRLipSyncのゴチャゴチャ(デバッグ用の機能とか、セットアップのためのスクリプトとか)がかなり改善されていて、とても使いやすいです。あまりこの記事では触れていませんでしたが、セットアップという観点からみても非常に優れています。世のVtuberのみなさま、OVRLipSyncからこちらに乗り換えを検討してみてはいかがでしょうか。

#参考記事

- [AniCast！東雲めぐちゃんの魔法ができるまで](https://www.youtube.com/watch?v=1rfoFg6LJgU)
- [ユニティちゃんのBlendShapeに母音(a,i,u,e,o)がない](https://qiita.com/kaikiofkaiki/items/ef7c56d092e5f3348730)
- [Unityでのマイク録音３種盛り：レイテンシ比較](https://qiita.com/TyounanMOTI/items/5306336823316b0b289c)
- [Unity でリップシンクができる OVRLipSync を試してみた](http://tips.hecomi.com/entry/2016/02/16/202634)
- [AniLipSync README](https://github.com/XVI/AniLipSync/blob/master/README.md#viseme-to-blend-shape)
- [シバニャンだニャン！ OVRPhonemeContext.Start ERROR: Could not create Phoneme context.](http://shiba6v.hatenablog.com/entry/2018/03/01/205926)
- [【Unity】コンポーネントのイベント実行順についてのTips](http://tsubakit1.hateblo.jp/entry/2017/02/05/003714)

特に凸さんの[Unity でリップシンクができる OVRLipSync を試してみた](http://tips.hecomi.com/entry/2016/02/16/202634)は記事構成において非常に参考にしました。素晴らしい記事をありがとうございます。

![imageLicenseLogo.png](https://qiita-image-store.s3.amazonaws.com/0/140271/d679518d-1869-1b57-d1f0-bceb5c3e87b1.png)
この作品はユニティちゃん[ライセンス条項](http://unity-chan.com/contents/license_jp/)の元に提供されています