凹みTips

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

uLipSync の WebGL 対応を調査してみた

はじめに

過去に何度かリクエストを頂いているのですが、まだ uLipSync で対応していないことの一つに WebGL 対応があります。WebGL ではいくつかの機能が制限されており、そのまま使えない機能が多々あります。例えば現在解析に用いている OnAudioFilterRead コールバックは WebGL では利用できませんし、他にもマイクも同様に使用できません。

tips.hecomi.com

こういった制約の関係で uLipSync は WebGL では現在動作しない形になっています。ただ、うえぞうさんがオーディオ再生側に関しては動作するような仕組みを作成して下さっています(ちょうど Unity 2021 以降でも動作する v0.3 がリリースされました)。

github.com

今回はマイク含め包括的にサポートを行うために、現状の問題点と使用できそうな技術調査を行います。

公式ドキュメント

Unity の WebGL ビルドにおけるオーディオ周りの制約が以下の公式ドキュメントに記載されています。

docs.unity3d.com

問題点調査

WebGL でビルドすると以下のようにエラーとなります。

error CS0103: The name 'Microphone' does not exist in the current context

Microphone クラスは WebGL プラットフォームでは使えない、というエラーですね。ひとまず他の問題調査のため、いったん UNITY_WEBGL プリプロセッサ ディレクティブを使って Microphone を使ってるコードを排除してみます。例えば、uLipSync.MicUtil では以下のように WebGL ではリストが空になるようにしてみます。

...
public struct MicDevice
{
    ...
}

public static class MicUtil
{
    public static List<MicDevice> GetDeviceList()
    {
        var list = new List<MicDevice>();

#if !UNITY_WEBGL
        for (int i = 0; i < Microphone.devices.Length; ++i)
        {
            ...
        }
#endif

        return list;
    }
}

}

さて、これで実行すると...

実行はされるのですが、特に Console などにも情報はなく口パクがされていない形になります。OnAudioFilterRead() へ適当な Debug.Log() を仕込んでも何も出力されないことから、WebGL ではこのコールバックが呼び出されない実装になっているようです。

マイク対応について

Web 上でマイクの入力は getUserMedia を通じて取得することができます(ユーザの許可が必要な形です)。

developer.mozilla.org

ここから得られるストリームを createMediaStreamSourcecreateScriptProcessor で接続することで生のバッファが取得できます。

developer.mozilla.org

developer.mozilla.org

こうして取得したバッファを uLipSync の解析の口につなげてあげれば、Web 上でもマイクを使ったリップシンクが可能になります。uLipSyncMicrophone 相当にするには、マイクの選択など色々きれいに整えてあげる必要がありますが…、必要なものは揃っている印象です。

なお、createScriptProcessor は deprecated なようなので、AudioWorklets を使うほうが望ましいようです。

実装時はこちらで試してみます。

mtg.github.io

真面目にやるとキャリブレーション UI の用意や、キャリブレーションデータのローカルストレージへの保存など色々やれる余地はありそうです。ただここいらは自分のユースケースがまだないので...、しばらくまた寝かせるかもしれません。

オーディオ対応について

うえぞうさん方式

概要

うえぞうさんによる uLipSyncWebGL では jslib を通じて JavaSciprt 上でオーディオのバッファを取得し、それを Unity 側の C# の世界(正確には wasm と化した C# の世界)に渡す方式です。

github.com

uLipSyncWebGL を導入すると、同名の uLipSyncWebGL コンポーネントuLipSync コンポーネントのついた GameObject にアタッチするだけで簡単に動きます。めちゃめちゃお手軽ですごいです。

詳細な流れは次のとおりです。

setInterval しないとならないのは謎だとのことでした。。

github.com

調査

WEBAudio はあまりネット上に情報がありませんでしたが、コードを読むと Unity が定義しているオブジェクトのようです。

audioInstances はそのプロパティとしてここで生成されています。

困ったらこのあたりのコードを直接読むと色々とわかりそうです。

該当するオーディオを見つける際の問題

audioInstances から最初に見つかった(gain ノードを持つ)インスタンスを connect しているため、例えば PlayOnAwake でない何かしらのトリガで再生開始されるような AudioSource などはリップシンク対象ではなくなってしまいます(適当なキー押下で再生開始するように組んでみると適切な音声が割り当てられませんでした)。本来であれば、MonoBehaviour 同様、同じ GameObject についている AudioSource を対象に扱いたいところです。エンジン側から呼ばれる _JS_Sound_Create_Channel に渡されている userData がおそらく何らかの識別 ID なのではと考えているのですが、見てみても GUID でも無さそうですし使えるか不明です。

こういう時にエンジンコードが読めれば…と思うのですが。ひとまず loopEnd で長さ一致を追加で見る、あたりが唯一使える方法かもしれません。

サンプリングレート合わせ

どこかでサンプリングレートを引っ張ってきて合っているか調べるコードが必要そうです。

GetData() 方式

Issue で書いていただいた方式は、OnAudioFilterRead は使えないものの AudioClip.GetData() は一部使えるので、それを利用する方式です。

github.com

WebGL 環境下での GetData() については以下の公式リファレンスに記載があります。

docs.unity3d.com

AudioSource(再生インスタンス)から現在の再生位置を取得し、AudioClip(データ)の適切な位置のバッファにアクセスする方式です。メリットとしては C# の世界から出なくて良いのでメンテナンス性が比較的高い点で、デメリットはちょっと処理速度やメモリ的な不安が残る点でしょうか。Issue に書いてくださっている幾つかの点(1フレームディレイ、ステレオデータ)は、少し先読みしたり、適切にステレオデータを扱うことで修正可能です。また、WebGL 上ではユーザー操作がある(マウスクリックやキー操作)まで音声を再生しないという縛りがあるのですが、これも何かしらの手段で回避はできそうに思われます。

まとめ

技術的な面白さとしてはうえぞうさん方式の Web Audio API を使う方式なのですが、ちょっと現状は汎用性面で解決できない問題が幾つかあるため、まずはシンプルに GetData() 方式で実装を試してみようかと思います。Web Audio API 的な面白さの部分は、ひとまずはマイクの実装つなぎのところで知識欲求は満たそうかな、と思います。