凹みTips

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

Unity でリアルタイムにリップシンクを行うプラグインを更に作り直してみた

はじめに

前回、リップシンクするプラグインを作り直してみた記事を書きました。

tips.hecomi.com

色々とご反響を頂いたのですが、しかしながら母音推定の性能がいまいちでした。第 1 、第 2 フォルマントという周波数スペクトルの 1 つ目、2 つ目のピークを検出する方法を採用していたのですが、このピークの検出が周波数スペクトル包絡を得るために使っている LPC 解析のパラメタに大きく依存したり、またノイズや声の高さによって容易にピークが別の場所に移ってしまったりといったことが原因でした。

そこで手法を変え、MFCC(メル周波数ケプストラム係数)を求める方式へと変更しました。

speechresearch.fiw-web.net

これはディープラーニング以前の音声認識に使われていたもののようで、母音だけでなく子音も含む音素の検出が可能な特徴量になります。以前の手法と異なりピッチ変化に対する耐性が強くなっています(ピッチ特性は除去されて声道特性を抽出している)。より詳細な違いは以下のスライドをご参照ください:

www.slideshare.net

これにより使い方も大きく変わったので、本記事では新しい設定方法について説明していきます。

追記

新しいバージョンの解説を書きました。

tips.hecomi.com

デモ

できることは前回と同じです。ただ、あいうえお以外の音素(ん、や鼻息など)も登録できるようになりました。

f:id:hecomi:20210109183643g:plain

環境

前回と同じ Unity のバージョンおよび環境で調査しています。Mac / iOS でも調整すれば動作するといったご報告もいただきましたので、他のプラットフォームについても追って調査していきます。

ダウンロード

github.com

Releases ページから最新のパッケージを自身のプロジェクトへインポートしてください(執筆時は 1.0.1 です)。

また、Unity.Burst および Unity.Mathematics を必要とするので、プロジェクトにインストールされていない場合は Package Manager から追加してください。

f:id:hecomi:20210227120917p:plain:w720

使い方

uLipSync のセットアップ

まず音声解析のコンポーネントをセットアップします。uLipSync コンポーネントを声を喋らせる AudioSource を持つゲームオブジェクトにアタッチしてください。次のような UI が現れます。

f:id:hecomi:20210227121223p:plain

次にプロファイルを指定します。作成の仕方は後で説明しますのでとりあえず「Male」または「Female」ボタンをクリックしてサンプルのプロファイルをセットしてください。UI が次のように変化して、あいうえお(A, I, U, E, O)音素の特徴量が表示されます。

f:id:hecomi:20210227121351p:plain

ブレンドシェイプの紐付け

次に口パクさせるコンポーネントのセットアップです。 キャラクタのルートに uLipSyncBlendShape コンポーネントをアタッチしてください。その上で Skinned Mesh Renderer のプルダウンから口パクに対応する SkinnedMeshRenderer を指定します(Find From Childrenチェックボックスを外し、直接 Skinnd Mesh Renderer をオブジェクトのフィールドに指定すればキャラクタのルートにアタッチしなくても大丈夫です)。

f:id:hecomi:20210227121542p:plain

プロファイルでセットアップされている音素に対応するブレンドシェイプを指定します。Add New BlendShape ボタンを 5 回押下して 5 個のブレンドシェイプ設定を追加します。その上で下の画像のように音素と対応するブレンドシェイプの紐付けを行ってください。

f:id:hecomi:20210227122123p:plain

uLipSync への登録

この uLipSyncBlendShapeuLipSync コンポーネントに登録します。uLipSync は解析結果をイベントを通じて通知する仕組みになっています。Callback セクションを開き、On Lip Snyc Update (LipSyncInfo)uLipSyncBlendShape.OnLipSyncUpdate を登録してください。

f:id:hecomi:20210227122302p:plain

マイク

もし AudioSource で声を喋らせるのでなくマイクで入力した声を喋らせたい場合は uLipSyncMicrophoneuLipSync と同じゲームオブジェクト(= AudioSource を持つオブジェクト)にアタッチし、適当なマイクデバイスを選んでください。

f:id:hecomi:20210227122446p:plain

実行

これでプレイすれば動くようになるはずです!

コンポーネントについて

コンポーネントについての外観は以下のような感じです。

uLipSync

リップシンクを計算するためのコアとなるコンポーネントです。 uLipSyncMonoBehaviour.OnAudioFilterRead() からオーディオバッファを取得するので、AudioSource でオーディオを再生するのと同じ GameObject にアタッチする必要があります。計算はバックグラウンドスレッドで行われ、JobSystem と Burst Compiler によって最適化されており、大体のケースで計算は 1 ~ 2 ms 程度で完了します。

uLipSyncBlendShape

SkinndeMeshRendererブレンドシェイプを制御するためのコンポーネントです。uLipSync のプロファイルに登録した音素に対応するブレンドシェイプを登録することで、音声分析の結果を口の形に反映させることができます。

uLipSyncMicrophone

このコンポーネントはマイク入力を再生するための AudioClip を作成し、AudioSource に設定します。そのため uLipSync を持つ GameObject にアタッチしていください。録音の開始/停止はエディタ上からだけでなくスクリプトからも StartRecord() / StopRecord() を呼び出すことで行うことができます。また、index を変更することで入力ソースを変更することもでき、使いたい入力を探すには、uLipSync.MicUtil.GetDeviceList() を使うと便利です。

プロファイルについて

概要

uLipSync は、冒頭で述べたように現在再生中の音声から MFCC と呼ばれる音声特徴量をリアルタイムに抽出します。現在再生中の音声がどの音素のどの MFCC に近いかを推定し、その情報をコールバックとして発行します。uLipSyncBlendShape は、このコールバックからやってきた情報(どの音素が認識されているか、どれくらいのボリュームかなど)を使って SkinnedMeshRendererブレンドシェイプをスムーズに移動させます。そして MFCC と関連する計算パラメータの登録には、Profile というアセットを使用しています。

f:id:hecomi:20210227124208p:plain

  • MFCC
    • Add Phoneme を押すと、新しい音素が登録されます。音素名(A, I, E, O, Uなど)と対応する MFCC を後述する方法で登録することで、登録した音素を uLipSync が推定できるようになります。
  • Parameters
    • Mfcc Data Count
    • Mel Filter Bank Channels
      • MFCC を計算する際に必要なメルフィルタバンクのチャンネル数
    • Target Sample Rate
      • 入力されたデータをダウンサンプリングして計算を軽くする際の周波数を指定します。例えば、PC 環境では 48000 Hz のデータが OnAudioFilterRead() に入力されていますが、デフォルトでは 1/3 の16000Hz にダウンサンプリングされています。
    • Sample Count
      • MFCC の計算に必要な音のバッファの数。デフォルトは 16000 Hz で 1024 サンプルなので、約 0.064 秒(〜4フレーム)のデータが使用されます(計算自体は毎フレームオーバーラップして行われます)。
    • Min Volume
      • 入力ボリュームの最小値(Log10 を適用するので 0.001 は -3 になります)。
    • Max Volume
      • 入力された音量の最大値。Min Volume と組み合わせた計算する正規化されたボリュームが、コールバック(OnLipSyncUpdate())のボリュームとして出力されます。
  • Import / Export JSON
    • ProfileJSON に出力したり、逆にインポートしたりすることができます。詳細は後述します。

いじるのは MFCCMin VolumeMax Volume あたりになると思います。

キャリブレーション

プロファイルの作成

Profile の右下にある「Create」ボタンをクリックしてください。新しい Profile アセットが作成され割り当てられます。

f:id:hecomi:20210227130713p:plain

音素の追加

次に Add Phoneme を押してあいうえお(A, I, U, E, O)のような音素を追加します(アルファベットでもひらがなでも大丈夫です)。

f:id:hecomi:20210227141856p:plain

キャリブレーション(マイク)

マイクを使う場合はゲームを実行して、「あーーー」と喋りながら登録した「あ」に対応する音素の Calib ボタンを押し続けます。同様に、いうえお、の音素に対してもキャリブレーションを行います。

f:id:hecomi:20210227142824g:plain

他にも「ん」や「息」といったものも登録してもある程度識別できます。

キャリブレーション(データ)

事前録音の AudioClip を再生する場合はちょっと面倒ですが「あ」や「い」に対応する音のみを既存の録音データからトリミングしてそれを AudioSource に割当ててループ再生し、対応する音の音素の Calib ボタンをマイクの説明と同じように押下してキャリブレーションを行います。

パラメタ調整

あとはマイクの特性や入力ボリュームに応じて、Min VolumeMax Volume を調整してください。

コールバック

リップシンクの解析が終了していたら Update() のタイミングで登録されたコールバックが呼び出されます(終了していない場合はフレームをまたがって計算が行われ、終わり次第呼び出されます)。引数として渡ってくる LipSyncInfo は次のような構造体になっています。

public struct LipSyncInfo
{
    public int index; // MFCC のインデックス番号
    public string phenome; // 対応する音素名
    public float volume; // 正規化されたボリューム
    public float rawVolume; // 正規化されていないボリューム
    public float distance; // キャリブレーション値との誤差(小さいほど信頼性が高い)
}

たとえば認識結果をデバッグ出力したい場合は次のようなスクリプトを書きます。

using UnityEngine;
using uLipSync;

public class DebugPrintLipSyncInfo : MonoBehaviour
{
    public void OnLipSyncUpdate(LipSyncInfo info)
    {
        Debug.LogFormat(
            $"PHENOME: {info.phenome}, " +
            $"VOL: {info.volume}, " +
            $"DIST: {info.distance} ");
    }
}

パラメタ

Parameters には Output Sound Gain という項目があります。音声は別撮りで Unity からは音声を出力したくない場合などはこれを 0 にしてください。AudioSource の方を 0 にしてしまうと、OnAudioFilterRead() にやってくるバッファも 0 になってしまうため、この仕組を用意しています。

リアルタイムの解析情報

Runtime Information というセクションでは現在の解析状況をリアルタイムに見ることが出来ます。

f:id:hecomi:20210227125843g:plain

  • Volume
    • Current Volume
      • 現在のボリューム(生値)
    • Min Volume
      • 認識された中での最小ボリューム、これをもとに Profile の最小ボリュームを調整してください
    • Max Volume
      • 認識された中での最大ボリューム
    • Normalized Volume
      • Min VolumeMax Volume で正規化されたボリューム
  • MFCC
    • 現在の MFCC

なお、このセクションを表示している間は毎フレームエディタの再描画が行われるようになるため、ゲームのパフォーマンスが低下します。確認したい際のみ開くようにしてください。

その他

前回は [BurstCompile] アトリビュートをつけているのにルールを守っていなくて Burst の恩恵を受けていない(エラーを吐いている)関数がいくつかありました。。今回は指定したものは全部対応できていると思います。

f:id:hecomi:20210227123125p:plain

おわりに

Job + Burst で処理するため外部ライブラリが使えず数式を全部 C# のサブセットで書かないとならないのに苦労しました...。まず C++ で書いて既存ライブラリと比較したりしながら数値検証しつつ、また、たまたま身近に先生がいたためアルゴリズムなどの調整については相談させてもらいながらなんとか完成出来ました。まだバグなどもあると思いますので、以降バージョンを重ねて色々改善していきたいと思います。

参考

aidiary.hatenablog.com