凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた

はじめに

昨年に Unity 上でリアルタイムにリップシンクを行う uLipSync というものをリリースしました。

tips.hecomi.com

AudioClip の入力を Job SystemBurst コンパイラを利用してリアルタイムに解析して MFCC と呼ばれる特徴量を抽出し、予めキャリブレーションして作成しておいたプロファイルと照らし合わせて一番近い音素を推定、それをブレンドシェイプへと反映させる、という仕組みのものでした。ただ、マイク入力など、その場で毎回異なる音声波形が与えられる場合は良いのですが、歌やカットシーンのような予め決まった音源があるケースにおいてはリアルタイムに計算する必要はなく、結果を事前に確認できるという利点の上でも処理負荷削減の面でも、予め計算しておけると便利です。

そこで、今回新しく事前計算を行う仕組みを導入し、またタイムラインの対応も行いました。併せてコードや UI、キャリブレーション支援コンポーネントの追加、サンプルの追加修正(VRM 対応など)も行い、v2 としてリリースしました。本記事では新しくなった uLipSync の使い方の説明を行っていきます。

uLipSync の特徴

以下のような特徴があります。

  • Job SystemBurst を使いネイティブプラグインは不使用で OS を問わずに高速動作可能
  • キャリブレーションすることでキャラクタ毎のプロファイル作成が可能
  • 🆕 ランタイム解析事前ベイク処理どちらも利用可能
  • 🆕 事前ベイク処理の場合、Timeline との連携可能

OVRLipSync などと比べると設定が多くなってしまう(+ 汎用的な精度は劣る)のですが、キャリブレーションによって認識させたい音が調整可能だったり、対応プラットフォームが多かったり、Timeline との連携が出来たりといった点が差別化ポイントです。

以下、もう少し雰囲気を掴んで頂けるよう画像と一緒に各機能を紹介します:

リップシンク

登録した音素に併せたリップシンクが出来ます。

プロファイルの設定

キャラクタ毎にプロファイルを作成し、キャリブレーションすることで調整が出来ます。

リアルタイム解析

AudioSource で再生された音声波形を JobSystem と Burst Compiler の恩恵でそこそこ高速にリアルタイムで解析し、ブレンドシェイプへ反映します。

マイク入力

マイク入力を反映するコンポーネントが付属していますので、配信用途などにも使えると思います。

VRM 対応

VRM 用のブレンドシェイプを動かすコンポーネントがサンプル中に付属しています。

事前計算

単純な AudioClip 再生向けには、事前に計算しておくことが出来ます。結果が確認できて調整しやすかったりパフォーマンス面での利点などがあります。

タイムライン連携

また事前計算データとの組み合わせでタイムライン連携が可能です。

追記(2022/02/19): アニメーションベイク

tips.hecomi.com

追記(2022/08/31):テクスチャ変更

tips.hecomi.com

追記(2022/12/28):Animator

tips.hecomi.com

ダウンロード

github.com

Package Manager からインストールもしくは .unitypackage をダウンロード・プロジェクトへ展開する方法のどちらかで導入できます。Package Manager をオススメしますが、慣れていない人は .unitypackage をお使いください(ただし、この場合も Package Manager 上で必要な公式パッケージをインストールする必要があります)。

Package Manager

導入する方法はどちらかをお選びください。個人的にはバージョン管理が出来るので Scoped Registry 登録がおすすめです。執筆時のバージョンは v2.0.2 になります

Scoped Registry
  • Project Settings > Package Manager から Scoped Registries に以下を登録
    • Name: hecomi
    • URL: https://registry.npmjs.com
    • Scope: com.hecomi
  • Package Manager の Packages: My Registries から uLipSync をインストール
  • 必要なサンプルはインストール後の画面から適宜インストール
Git URL

UPM についての詳細は以下の記事をご参照下さい:

tips.hecomi.com

.unitypackage のダウンロード

  • 最新のバージョンを Releases からダウンロード、本体と必要なサンプルをプロジェクトに展開
  • Package Mananager から Mathematics および Burst をインストール

使い方 - ランタイム解析編

仕組み

Unity 組み込みの AudioSource で音が再生されると、同じ GameObject にアタッチされたコンポーネントOnAudioFilterRead() というメソッドにその音のバッファが入ってきます。このバッファを書き換えてリバーブ処理を施したりといったことが可能なのですが、今どんな波形が再生されているかもわかるので、これを解析して メル周波数ケプストラム係数(Mel-Frequency Cepstrum Coefficients、MFCC)という人間の声道特性を表す特徴量を計算します。つまり、うまく計算すれば再生されている現在の波形が「あ」なら「あ」っぽいパラメタが、「え」なら「え」っぽいパラメタが得られるわけですね(母音以外にも「すー」みたいな子音も分かります)。これを予め登録しておいた「あいうえお」それぞれのパラメタと比較し、現在の音にそれぞれの音素がどれだけ近いかを算出、それを SkinnedMeshRendererブレンドシェイプへと反映させればリップシンクが可能となるわけです。マイクからの入力を AudioSource に流し込んであげれば、今喋っている声に合わせて口パクも可能になります。

この解析を行うコンポーネントuLipSync、このコンポーネントにセットする各音素パラメタのデータが Profile、そしてブレンドシェイプを動かすためのコンポーネントuLipSyncBlendShape となります。また、マイクの音声を再生する uLipSyncMicrophone アセットも用意してあります。これらを図解するとこんな感じです。

なので登場人物としては大きく分けて「キャリブレーションされたデータ」、「データをもとに音声解析をする人」、「解析された結果を使ってなにかする人」の 3 種類がいる形です。ではこれを踏まえてランタイム解析を行ってリップシンクをさせるための手順について見ていきましょう。

セットアップ

まずは、ユニティちゃんでセットアップしてみましょう。サンプルシーンは Samples / 01. Play AudioClip / 01-1. Play Audio Clip になります。UPM からインストールした人は Samples / 00. Common サンプルをインストールしておいてください(ユニティちゃんのアセットが含まれています)。

まず、ユニティちゃんを配置した後、AudioSource コンポーネントを任意のゲームオブジェクト(音がなる場所)に追加し、クリップを設定して音がなるようにします。

次に同じゲームオブジェクトに uLipSync コンポーネントを追加します。ここにProfile を指定するのですが、今は uLipSync-Profile-UnityChan をリストから選択してアサインしてください(Male など違うものを指定すると正しくリップシンク出来ません)。これでリップシンクの解析が行われるようになります。

次に、解析結果を受け取ってブレンドシェイプに反応するようセットアップします。uLipSyncBlendShape をユニティちゃんの SkinnedMeshRenderer のルートに配置し、Sinned Mesh Renderer から対象となるブレンドシェイプである MTH_DEF を選択、Blend Shapes > Phoneme - BlendShape Table で A、I、U、E、O、N、 -の計 7 つの項目を + をポチポチして追加します(N は「ん」、- はノイズを登録してあります)。そして、それぞれの音素に対応するブレンドシェイプを画像のように選択します。

最後に、この 2 つをつなげます。uLipSync コンポーネントParameters > On Lip Sync Updated (LipSyncInfo) という場所で + を押してイベントを 1 つ追加し、None (Object) となっているところに uLipSyncBlendShape コンポーネントがついているゲームオブジェクト(またはコンポーネント)をドラッグドロップします。No Function のプルダウンリストから uLipSyncBlendShape を探し、その中にある OnLipSyncUpdate を選択します。

これでシーンを実行すると口を動かしながら喋ってくれるようになります。

口パクの反応の調整

認識するボリュームや口の反応速度は uLipSyncBlendShape コンポーネントの、Paramteters の中で行なえます。

  • Volume Min/Max (Log10)
    • 認識する最小と最大(口が閉じている / 最も大きく開く)のボリュームを設定(Log10 しているので、0.1 なら -1、0.01 なら -2 となってます)
  • Smoothness
    • 口パクの応答速度、小さすぎるとパクパクしすぎてしまうが、逆に大きいほど滑らかになるが追従性が悪くなるので適当な値を選んでください

ボリュームに関しては、現在・最大・最小の音量がどのくらいだったかという情報が uLipSync コンポーネントRuntime Information という中で見ることが出来ますので、これを参考に設定してみてください。

オーディオソースの位置

AudioSource は口の位置に、uLipSync はどこか別のゲームオブジェクトにアタッチしたいケースもあると思います。その際は、少し面倒ですが、uLipSyncAudioSource というコンポーネントAudioSource と同じゲームオブジェクトに追加し、uLipSyncParameters > Audio Source Proxy にこれをセットしてください。

Samples / 03. AudioSource Proxy がサンプルシーンとなります

中身としては uLipSyncAudioSourceAudioSource の結果を OnAudioFilterRead で拾ってきて、それをイベント経由で uLipSync に渡しています。

マイクの利用

マイクを入力としたい場合は uLipSyncMicrophone というコンポーネントuLipSync と同じゲームオブジェクトに追加します。これはマイク入力をクリップとした AudioSource を生成するコンポーネントです。サンプルシーンは Samples / 02-1. Mic Input になります。

Device から入力に使用するデバイスを選択、Is Auto Start にチェックが入っていると自動で開始します。マイク入力の開始・停止は実行時に次のような UI になるので、Stop Mic / Start Mic を押下することで可能です。

スクリプトから行う場合は、次のように uLipSync.MicUtil.GetDeviceList() を利用して使用するマイクを特定し、そのインデックスをこのコンポーネントindex に指定して渡して StartRecord() / StopRecord() することで、開始・停止を行ってください。

なお、マイクの入力はそのままでは自分の発話から少し遅れて Unity 内で再生されてしまいます。配信などで声は別のソフトでキャプチャしたものを利用したいケースなどは、uLipSync コンポーネントの、Parameters > Output Sound Gain を 0 にしてください。AudioSourceVolume を 0 にしてしまうと、OnAudioFilterRead() に渡ってくるデータが無音になってしまい解析が出来ないので、解析後に 0 にする処理をこれで行います。

uLipSync コンポーネントProfile > Profile で適当なサンプル中のプロファイル(男性なら Male、女性なら Female など)を選択し実行すると、声に合わせて口パクすると思います。ただ、個人にあった音声ではないので、デフォルトの音声だと精度は良くないかもしれません。次にこれを自身の声に合わせたキャリブレーションデータを作成する方法について見ていきます。

キャリブレーション

先ほどは元から用意してあった Profile データを使ったのですが、他の音声向け(他の声優さんのデータや自分の声)に調整したデータを作成する方法を次に紹介します。

プロファイルの作成

uLipSync コンポーネントProfile > Profile > Create ボタンを押すとデータが Assets のルートに作成され、そのプロファイルがセットされます。Project ウィンドウから右クリック > uLipSync > Profile でも作成できます。

次に認識させたい音素を Profile > MFCC > MFCCs に登録していきます。基本的には AIUEO で良いと思いますが、息用の音素(「-」など適当な文字)も追加しておくと息が当たってしまったときに口がパクパクしてしまうのを抑制できるのでおすすめです。「ん」を表す N も登録しても良いかもしれません。登録する文字は uLipSyncBlendShape と一致させさえすれば良いので、アルファベットでもひらがなでもカタカナでも構いません。

次に、作成した音素をそれぞれキャリブレーションしていきます。

マイク入力を使った調整

まずはマイクを使う方法です。uLipSyncMicrophone をオブジェクトに追加してください。キャリブレーションは実行時に行うので、この状態でゲームを開始します。すると各音素の右側に Calib というボタンが現れるので、マイクに向かってそれぞれの音素の音を喋りながらこのボタンを押下し続けてください。A なら「あーーーー」、I なら「いーーーーーー」といった具合のものです。ノイズだったら何も喋らなかったり息を吹きかけたりといった感じですね。

予め uLipSyncBlendShape を設定していれば、こんな感じでだんだん口が合っていくのが面白いです。ProfileScriptableObject なので実行を停止してもデータは保存されます。

なお、地声と裏声などで口が一致させられない場合は、同名の複数音素を Profile に登録することも出来ますので適宜調整してください。

音声データを使った調整

次に音声データを使ったキャリブレーション方法です。「あーーーーー」や「いーーーーー」と喋ってくれる音声があればそれをループ再生した状態で同様に Calib ボタンを押下してください。ただ大体のケースではそんな音声が無いので既存の音声の「あーー」っぽい部分や「いーー」っぽい部分をトリミングして再生することでキャリブレーションを実現したいです。そこで便利なコンポーネントuLipSyncCalibrationAudioPlayer コンポーネントです。これは音声波形を見ながら再生したい部分をちょっとクロスフェードさせながらループ再生してくれるコンポーネントです。

境界をドラッグしながら「あー」と言っていそうな部分を選択し、その状態でそれぞれの音素の Calib ボタンを押下して Profile に MFCC を登録してください。

キャリブレーションのコツ

キャリブレーションする際は以下のことに気をつけると良いです。

  • なるべくノイズのない環境で行う
  • 登録した MFCC がなるべく一定になってるようにする
    • 横軸 12 次元があるフレームの MFCC で、内部的には縦の平均を取って比較します
  • キャリブレーション後に何度かチェックしてうまく行かない音素はキャリブレーションし直したり別の音素として登録したりする
    • 複数の同名音素が登録できるので声色を変えたときに合わない場合は、それらを追加で登録してみてください
    • どうしても合わないときは誤った音素が無いか確認する
      • 同名の音素で MFCC の色のパターンがぜんぜん違うものは間違っている可能性があります(同じ音素は似たようなパターンになるはず)
  • キャリブレーション後のチェック時は Runtime Information を折りたたんでください
    • 毎フレームエディタの再描画が行われるのでフレームレートが 60 を割ることがあります

リップシンクの事前計算

さて、これまではランタイムの処理について見てきました。ここからは事前計算によるデータの制作について見ていきます。

仕組み

オーディオデータがある場合は毎フレームどんな解析結果が渡ってくるか事前に計算可能なので、これを BakedData という ScriptableObject に焼き込んでおきます。実行時にはランタイムで解析していた uLipSync の代わりに、uLipSyncBakedDataPlayer というコンポーネントを介して再生するようにします。これは uLipSync と同様に解析結果をイベントで通知することができるため、uLipSyncBlendShape を登録しておくことでリップシンクが実現できます。この流れを図示すると以下のようになります。

JobSystem は実行中でなくても動かすことが出来るのが良いですね。

セットアップ

サンプルシーンは Samples / 05. Bake になります。Project Window から Create > uLipSync > BakedDataBakedData を作成できます。

ここで、キャリブレーションした Profile とその音声である AudioClip を指定し、Bake ボタンを押下するとデータの解析が行われデータが完成します。

比較的うまくいくと以下のようなデータが出来ます。

このデータを uLipSyncBakedDataPlayer にセットします。

これで再生すれば OK です。エディタ上で何度もチェックしたい場合は Play ボタンを押下、別スクリプトから再生したい場合は Play() を呼んでください。

結果

何個か母音が間違っていても正直それほど変には見えずそれっぽく見えます。

パラメタ

Time Offset というスライダを調整すると、口パクのタイミングを早めたり遅くしたり出来ます。ランタイム解析だと声より先に口を開けておく、みたいな調整はできませんが、事前解析だと少し早めて口を開けられるので少し自然に見せたり調整ができます。

一括変換①

すべてのキャラクタボイスの AudioClip を一括で BakedData へ変換したいケースもあると思います。その際は、Window > uLipSync > Baked Data Generator を利用してください。

一括変換する際に使用したい Profile を選択し、対象となる AudioClip を選択します。Input TypeList の場合は直接 AudioClip* を登録してください(*Project* ウィンドウから複数選択でドラッグドロップすると楽です)。*Directory* にした場合は、ファイルダイアログが開きディレクトリを指定でき、そのディレクトリ以下のAudioClip` を自動で列挙してくれます。

Generate ボタンを押すと変換が始まります。

一括変換②

既に作成したデータが有る際、キャリブレーションを見直して Profile を変更するケースも出てくると思います。この際は、各プロファイルの Baked Data タブの中に Reconvert ボタンがあるので、それを押下することで一括変換することが可能です。

タイムライン連携

BakedData を使用することで、Timeline 連携も可能になりました。

仕組み

Timeline に専用のトラックとクリップを追加できるようにしました。その上でこのタイムラインのデータを使ってどのオブジェクトを動かすのかというバインディングを行わなければなりません。そこで、再生情報を受け取って uLipSyncBlendShape へと通知するための uLipSyncTimelineEvent というコンポーネントを導入しました。図示すると以下のような流れになります。

セットアップ

Timeline 上のトラックのエリアで右クリックし、uLipSync.Timeline > U Lip Sync Clip から専用のトラックを追加します。このトラックで右クリックしクリップを追加します。BakedData を直接ドラッグドロップしても OK です。

追加したトラックを選択するとインスペクタには次のような UI が表示されます。BakedData の差し替えもここで出来ます

そしてこれを実際に再生して口パク出来るようにバインディングをしていきます。uLipSyncTimelineEvent を何らかのゲームオブジェクトに追加します。

このとき、On Lip Sync Update (LipSyncInfo) には uLipSyncBlendShape を登録しておきます。

そして PlayableDirector のついているゲームオブジェクトをクリックし、Timeline ウィンドウの uLipSyncTrack 上のバインディングするスロットに、そのゲームオブジェクトをドラッグドロップします。

これで uLipSyncTimelineEventリップシンク情報が飛んでくるようになり、uLipSyncBlendShape への接続が出来ました。再生はランタイムでなくても出来ますのでアニメーションと併せたり調整が出来ます。

VRM 対応

VRMブレンドシェイプは VRMBlendShapeProxy というコンポーネントを介して制御されます。

virtualcast.jp

uLipSyncBlendShape では SkinnedMeshRenderer を直接弄ってましたが、代わりにこれを使うよう修正した uLipSyncBlendShapeVRM というコンポーネントSamples/04. VRM に含まれています。04. VRM シーンは VRM のセットアップをしてニコニ立体ちゃんをインポートしていれば再生できます。

その他 Tips

独自イベントの追加

uLipSyncBlendShape は 3D ですが、代わりにテクスチャアニメーションをさせたい、みたいなケースでは自分でコンポーネントを書いて対応させることが出来ます。具体的には uLipSync.LipSyncInfo を受け取る関数を用意したコンポーネントを用意し、これを uLipSyncuLipSyncBakedDataPlayerOnLipSyncUpdate(LipSyncInfo) に登録します。

以下は認識した結果を Debug.Log() 出力する簡単なスクリプトの例です。

using UnityEngine;
using uLipSync;

public class DebugPrintLipSyncInfo : MonoBehaviour
{
    public void OnLipSyncUpdate(LipSyncInfo info)
    {
        if (!isActiveAndEnabled) return;

        if (info.volume < Mathf.Epsilon) return;

        Debug.LogFormat($"PHENOME: {info.phoneme}, VOL: {info.volume} ");
    }
}

LipSyncInfo は次のような構造体です。

public struct LipSyncInfo
{
    public string phoneme; // 最も近い音素
    public float volume; // 正規化されたボリューム(0 ~ 1)
    public float rawVolume; // ボリュームの生値
    public Dictionary<string, float> phonemeRatios; // 音素とその割合のテーブル
}

JSON への保存 / 読み込み

外部ファイルとのやり取り用に、ProfileJSON への保存・読み込みも備えています。エディタからは Import / Export JSON タブから保存・読み込みしたい JSON を指定し、Import または Export ボタンを押下してください。

コードで行いたい場合は次のようなコードで可能です。

var lipSync = GetComponent<uLipSync>();
var profile = lipSync.profile;

// Export
profile.Export(path);

// Import
profile.Import(path);

ランタイムでのキャリブレーション

ランタイムでキャリブレーションを行いたいときは、次のように uLipSync に対して uLipSync.RequestCalibration(int index) でリクエストを行うことで可能です。現在再生されている音から計算した MFCC が指定した音素に設定されます。

lipSync = GetComponent<uLipSync>();

for (int i = 0; i < lipSync.profile.mfccs.Count; ++i)
{
    var key = (KeyCode)((int)(KeyCode.Alpha1) + i);
    if (Input.GetKey(key)) lipSync.RequestCalibration(i);
}

実際の動作は CalibrationByKeyboardInput.cs で確認してください。また、ビルド後の Profile の保存や復元は JSON で行うのが良いかと思います。

WebGL での動作

うえぞうさんWebGL とのつなぎを試して下さってました。少し手を入れると WebGL でも動作できるようです。ご調査ありがとうございます。

github.com

Mac でのビルド

Mac でビルド時に次のようなエラーに出くわすことがあります。

Building Library/Bee/artifacts/MacStandalonePlayerBuildProgram/Features/uLipSync.Runtime-FeaturesChecked.txt failed with output: Failed because this command failed to write the following output files: Library/Bee/artifacts/MacStandalonePlayerBuildProgram/Features/uLipSync.Runtime-FeaturesChecked.txt

これはマイクのアクセスコードに関連している可能性がありますが、Project Settings > PlayerOther Settings > Mac Configuration > Microphone Usage Description に何かを書くと修正されます。

前回からの小さな変更

これまでの利用者の方向けに上記新機能以外の差分情報です。

パラメタの整理

前回からは Profile に登録する情報と uLipSyncBlendShape に登録する情報の切り分けをもっときれいにしました。具体的には、MFCC の算出に必要な情報のみを Profile に格納するようにし、リップシンク関連のパラメタなどは uLipSyncBlendShape が受け持つようにしました。また併せて不要なパラメタは廃止し、極力設定項目が少なくなるよう調整しました。

ブレンドシェイプへの反映改善

以前は、解析の結果から最も近い単一の母音を選んでいましたが、「い」と「え」の中間みたいな音だったら口がパラパラ切り替わってしまっていたので、距離をそれぞれ計算し、本当に中間だったら 0.5、0.5 となるような形でブレンドシェイプに反映されるように修正しました。

リスト表示改善

後は細かいですが、自前で書いていたリストの UI も ReorderableList を使うようにしてより直感的に追加・削除・並び替えを出来るようにしました。

おわりに

バージョンを重ねる毎に破壊的変更をしていて申し訳ありません…。次の大きな更新はいつになるか分かりませんが、現状プロファイルに登録された各音素との誤差を調べて最小になるものを選択する部分を、何かしら賢い手法にするところをやりたいと考えています(そもそも最小距離というのがかなり手抜きではあります…)。また、ブレンドシェイプのアニメーションへの変換も考えています。こちらはマイナーバージョンによるアップデートで近日中に行う予定です。

今回、タイムラインの拡張なども試して色々と面白かったので、この辺りはまた別記事にまとめたいと思います。

ライセンス

© Unity Technologies Japan/UCL