はじめに
Unity でリップシンクを行う手段としては、事前にデータを作るものからリアルタイムに解析するものまで色々あります。本ブログでも以前、自分で作ったり Oculus 製のものを解説したりしました。
前者の記事で自分が 7 年程前に作ったものは様々な依存(外部ライブラリやツールなど)が多く扱いづらいものでした。そこで今回これらの依存を廃してリップシンク単機能に絞り、またもう少しナウい実装にして uLipSync という名前で作り直してみました。
- Job と Burst コンパイラを利用して処理を裏に回しつつ高速化
- Unity のパッケージ以外の依存を排除
本エントリでは使い方をちょっとした解説を交えて紹介したいと思います。
追記(2021/02/27)
新しいバージョンの説明を書きました。
デモ
GIF
Youtube
ダウンロード
Releases ページから最新のパッケージを自身のプロジェクトへインポートしてください(執筆時は 0.0.1 です)。
また、Unity.Burst
および Unity.Mathematics
を必要とするので、プロジェクトにインストールされていない場合は Package Manager から追加してください。
使い方
uLipSync は AudioSource
と連携して動作します。ボイスを喋らせる AudioSource
のアタッチされた GameObject に uLipSync
コンポーネントをアタッチしてください。すると次のような UI が見えると思います。
次にブレンドシェイプを持つキャラクタのルートに uLipSyncBlendShape
コンポーネントをアタッチしてください(テクスチャでリップシンクする便利コンポーネントは未作成ですが、対応方法については後述します)。Find From Children
にチェックを入れて対象の口パクのブレンドシェイプを持つ Skinned Mesh Renderer
を探し、A ~ O の母音に対応するブレンドシェイプをプルダウンから割り当てます。
uLipSync
のコンポーネントに戻り、セットアップした uLipSyncBlendShape
の OnLipSyncUpdate
を Callback セクション内にある On Lip Sync Update (LipSyncInfo)
に割り当てます。
最後に喋る声のキャラクタに応じて男性(Man
)か女性(Woman
)を LipSync Profile セクションの中で選択します(カスタマイズする方法は後述します)。
これでゲームを開始し、音声を再生すると口パクするようになります。
コンポーネントについて
uLipSync
リップシンクのための計算を行うコンポーネントです。MonoBehaviour
には同じゲームオブジェクトで再生されている音(サウンドのバッファ)を OnAudioFilterRead()
という関数で受け取るための仕組みが用意されており、これを利用するので AudioSource
と同じゲームオブジェクトにアタッチする必要があります。
uLipSyncBlendShape
SkinnedMeshrenderer
に設定された口パクのブレンドシェイプを動作させるコンポーネントです。子以下の SkinnedMeshRenderer
とブレンドシェイプを探しやすいようにプルダウンでサポートしてます。uLipSync
に OnLipSyncUpdate()
を登録することによって動作します。
uLipSyncMicrophone
事前録音データの代わりにマイクを使いたいときにこのコンポーネントを利用します。マイク入力を流し込む AudioClip
を作成し AudioSource
へ登録します。なので uLipSync
と同じく AudioSource
がついているゲームオブジェクトにアタッチしてください。エディタ上からはテスト用にボタンでスタート・ストップできますが、スクリプトからも StartRecord()
、StopRecord()
することで制御できます。また、index
でマイクを指定することが出来ます。どのマイクがどのインデックスかは uLipSync.MicUtil.GetDeviceList()
から調べてください。
パラメタ
Parameters セクションでは次のような設定ができます。主に口の開きかたに関連したパラメタが用意されています。
- Volume
- Normalized Volume
Min Volume
とMax Volume
で 0.0 ~ 1.0 にスケールされたボリュームがビジュアライズされます
- Min Volume
- これよりも小さい音量の音は無視されます
- Max Volume
- このボリュームが口を最大開く(ブレンドシェイプの値が 100)値になります
- Auto Volume
- チェックすると Max Volume が現在の入力から自動計算されます
- Auto Volume Amp
- Max Volume = 現在の入力ボリューム * Amp
- Auto Volume Filter
- 毎フレーム Max Volume がこの値だけ減少 (MaxVolume *= Filter)
- Normalized Volume
- Smoothness
- Open Smoothness
- 口を開くスピード (1.0 に近づくほどスムーズ)
- Close Smoothness
- 口を閉じるスピード
- Vowel Transition Smoothness
- 母音によって口の形が変化するスピード
- Open Smoothness
- Output
- Output Sound Gain
- サウンドの出力のゲイン
- マイク入力を利用していてマイクの声を再生したくないとき(別口で再生したいとき)は 0.0 にしてください
- Output Sound Gain
プロファイル
声は色々な周波数の波が重なってできています。それぞれの母音は周波数の組み合わせ具合に特徴を持っており、大体この付近の周波数の成分が多いみたいな形になっています。
この山の 1 番目と 2 番目の周波数を第 1 フォルマント、第 2 フォルマントと呼びます。
例えば男性の声で「あ」の第 1 フォルマントは他の母音よりも高い 900 kHz 付近で、第 2 フォルマントは 1400 kHz くらいに位置する、といった形になっています。現在解析している音声が、この第 1 フォルマント、第 2 フォルマントをプロットしたグラフ上でどの母音に近いかを見てあげれば大体今喋っている母音を判別できます。この設定を行うのが LipSync Profile セクションです。
Profile はアセットになっており、プリセットで男性用の Man
、女性用の Woman
を用意しています(ボタンを押すとセットされます)。プリセットは編集できなくしていますが、人によってフォルマントの周波数は異なるので個人ごとに調整できるよう Create ボタンを押すと Profile
アセットを作成できるようになっています。
後述するビジュアライザセクションと組み合わせてフォルマントを確認しながら調整してください。以下、各パラメタ詳細です:
- Formant
- F1
- 第 1 フォルマントの周波数(Hz)
- F2
- 第 2 フォルマントの周波数(Hz)
- F1
- Tips
- 平均的なフォルマントの紹介
- Visualizer
- X 軸が第 1 フォルマント、Y 軸が第 2 フォルマント
- 母音同士が近すぎると判定が出来ないのである程度開いていることを確認してください
- Settings
- Use Error Range
- 範囲外の入力を除外したいときはチェックを入れてください
- Max Error Range
- ↑でチェックを入れた場合の範囲です
- Min Log 10H
- フォルマントの山がこの値よりも低いときはスキップします(そのままをオススメ)
- Use Error Range
Callback
リップシンクの解析結果は UnityEvent
経由で受け取ることが出来ます。幾つでも登録できるので口パク以外にも音量で光るギミックとかにも使えます。
登録するためのコールバックは次のように LipSyncInfo
を受け取る形で用意しておきます。
using UnityEngine; using uLipSync; public class DebugPrintLipSyncInfo : MonoBehaviour { public float threshVolume = 0.01f; public bool outputLog = true; public void OnLipSyncUpdate(LipSyncInfo info) { if (info.volume > threshVolume && outputLog) { Debug.LogFormat("MAIN VOWEL: {0}, [ A:{1} I:{2}, U:{3} E:{4} O:{5} N:{6} ], VOL: {7}, FORMANT: {8}, {9}", info.mainVowel, info.volume, info.vowels[Vowel.A], info.vowels[Vowel.I], info.vowels[Vowel.U], info.vowels[Vowel.E], info.vowels[Vowel.O], info.vowels[Vowel.None], info.formant.f1, info.formant.f2); } } }
これをイベントとして登録すれば該当の処理が実行されます。uLipSyncBlendShape
コンポーネントも似たようなシンプルな実装で次のように SkinnedMeshRenderer
のブレンドシェイプを設定しているだけです(後は便利設定用のエディタ拡張が付属してます)。
public class uLipSyncBlendShape : MonoBehaviour { public SkinnedMeshRenderer skinnedMeshRenderer; public List<BlendShapeInfo> blendShapeList = new List<BlendShapeInfo>(); ... public void OnLipSyncUpdate(LipSyncInfo info) { foreach (var kv in info.vowels) { int i = (int)kv.Key; blendShapeList[i].blend = kv.Value; } volume_ = info.volume; } void LateUpdate() { if (!skinnedMeshRenderer) return; foreach (var info in blendShapeList) { if (info.index < 0) continue; float blend = info.blend * info.factor * volume_ * 100; skinnedMeshRenderer.SetBlendShapeWeight(info.index, blend); } } }
コンフィグ
Calculation Config のセクションでは計算のパラメタを設定します。こちらも Profile
と同じくアセットになっています。計算の詳細は以前作ったものと同じです:
- Config
- Default
- 推奨の計算設定です。
- Calibration
- 安定するまでに 1 秒くらいかかりますが「あ~」「い~」という入力に対してフォルマントがどの周波数なのかを詳しく見やすいです。
- Create
Config
アセットを新規に作成します。
- Default
- Sample Count
- 解析に用いるバッファのサンプル数です(48000 Hz なら 1 フレームは 800 サンプルです)。
- Lpc Order
- LPC(線形予測符号)の係数の数
- Frequency Resolution
- 包絡線の分割数(Max Frequency までの周波数の分割数)
- Max Frequency
- 解析に用いる最大周波数(大体 3000 Hz 付近が一番高いフォルマント)
- Window Func
- Check Second Derivative
- O の第 1 / 2 フォルマントが近く見えづらいケースがあるのでピーク検出の代わりに 2 階微分値のピークを調べます。
- Check Third Formant
- ノイズにより低周波領域に不要なフォルマントが見えてしまうことがあるので 3 つ目のフォルマントまで一致具合を調べます。
- Filter H
ビジュアライザ
入力が正確にフォルマントとして見えているかグラフで確認できる仕組みとしてビジュアライザを用意しています。
- Draw On Every Frame
- エディタが毎フレーム
Repaint()
を呼ぶことによってリアルタイムに描画が見えますが、ゲームのパフォーマンスを低下させるので確認したいときのみチェックを入れてください(入れないとカクカクと動きます)
- エディタが毎フレーム
- Formant Map
- 横軸が第 1 フォルマント、縦軸が第 2 フォルマントです。
- ラインタイム時は現在の入力が白丸で見えます。
- LPC Spectral Envelope
余談ですがグラフ描画は以下のページで解説している Handle
を使っています。
ジョブについて
計算はジョブ化しメインループの裏で回すことにします。バッファが溜まったら Update()
のタイミングでジョブをスケジュールし、解析が終わったタイミングの以降の Update()
で結果を回収します(デフォルト設定なら計算は 1ms 程度なので次フレームになります)。ジョブを作るのは簡単で次のようなものを書きます。LPC の計算は Unity の(API の)世界とは関係なく純粋に数値計算なのですが、こういったものはジョブにしてしまって Burst を効くようにするととても速くなります。
using Unity.Jobs; using Unity.Collections; using Unity.Mathematics; using Unity.Burst; namespace uLipSync { [BurstCompile] public struct LipSyncJob : IJob { public struct Result { public float f1; public float f2; public float f3; public float volume; } [ReadOnly] public NativeArray<float> input; ... [ReadOnly] public int lpcOrder; [ReadOnly] public int sampleRate; ... public NativeArray<Result> result; public void Execute() { ... // input を解析、結果を result につめる } } }
ジョブの発行側はこんな感じです。
public class uLipSync : MonoBehaviour { ... NativeArray<float> inputData_; NativeArray<LipSyncJob.Result> jobResult_; JobHandle jobHandle_; public LipSyncUpdateEvent onLipSyncUpdate = new LipSyncUpdateEvent(); ... void Update() { if (!jobHandle_.IsCompleted) return; GetResult(); InvokeCallback(); ScheduleJob(); ... } void AllocateBuffers() { ... inputData_ = new NativeArray<float>(sampleCount, Allocator.Persistent); jobResult_ = new NativeArray<LipSyncJob.Result>(2, Allocator.Persistent); ... } void GetResult() { jobHandle_.Complete(); ... } void InvokeCallback() { ... onLipSyncUpdate.Invoke(result); } void ScheduleJob() { var lipSyncJob = new LipSyncJob() { input = inputData_, result = jobResult_, ... }; jobHandle_ = lipSyncJob.Schedule(); ... } [BurstCompile] void OnAudioFilterRead(float[] input, int channels) { // inputData_ をセット } }
Unity.Mathematics
の計算は SIMD を有効化してくれて速いですが 1 次元の音声の計算ではどれくらい有効なのかは不明です。。ただ、Burst を ON / OFF では以下のように桁が 1 つ変わるような大きな差が出ます。
Burst ON 時
Burst OFF 時
なお、FFT 用のジョブはあくまでエディタ描画用のためエディタだけで走りビルドでは省かれます。
ライセンス
uLipSync 自体は MIT ライセンスです。パッケージでなくプロジェクトを DL した場合はユニティちゃんのアセットをサンプルとしてを含むので利用する場合は UCL にも従ってください。
© Unity Technologies Japan/UCL
おわりに
Unity 5 時代とは使える機能も増え、また自分の理解ももう少し進んだのでまともな実装に出来ました。リップシンク自体の性能はまだまだ改善の余地があると思います(バグもあるかもしれません)。あとはブレンドシェイプだけでなくテクスチャによる口パク向けのサンプルの追加と、オフラインでの解析とアセット保存のような機能も、追々余力があれば入れていきたいです。