はじめに
過去に何度かリクエストを頂いているのですが、まだ uLipSync で対応していないことの一つに WebGL 対応があります。WebGL ではいくつかの機能が制限されており、そのまま使えない機能が多々あります。例えば現在解析に用いている OnAudioFilterRead コールバックは WebGL では利用できませんし、他にもマイクも同様に使用できません。
こういった制約の関係で uLipSync は WebGL では現在動作しない形になっています。ただ、うえぞうさんがオーディオ再生側に関しては動作するような仕組みを作成して下さっています(ちょうど Unity 2021 以降でも動作する v0.3 がリリースされました)。
今回はマイク含め包括的にサポートを行うために、現状の問題点と使用できそうな技術調査を行います。
公式ドキュメント
Unity の WebGL ビルドにおけるオーディオ周りの制約が以下の公式ドキュメントに記載されています。
問題点調査
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
を通じて取得することができます(ユーザの許可が必要な形です)。
ここから得られるストリームを createMediaStreamSource
と createScriptProcessor
で接続することで生のバッファが取得できます。
こうして取得したバッファを uLipSync の解析の口につなげてあげれば、Web 上でもマイクを使ったリップシンクが可能になります。uLipSyncMicrophone
相当にするには、マイクの選択など色々きれいに整えてあげる必要がありますが…、必要なものは揃っている印象です。
なお、createScriptProcessor
は deprecated なようなので、AudioWorklets
を使うほうが望ましいようです。
実装時はこちらで試してみます。
真面目にやるとキャリブレーション UI の用意や、キャリブレーションデータのローカルストレージへの保存など色々やれる余地はありそうです。ただここいらは自分のユースケースがまだないので...、しばらくまた寝かせるかもしれません。
オーディオ対応について
うえぞうさん方式
概要
うえぞうさんによる uLipSyncWebGL では jslib を通じて JavaSciprt 上でオーディオのバッファを取得し、それを Unity 側の C# の世界(正確には wasm と化した C# の世界)に渡す方式です。
uLipSyncWebGL を導入すると、同名の uLipSyncWebGL
コンポーネントを uLipSync
コンポーネントのついた GameObject にアタッチするだけで簡単に動きます。めちゃめちゃお手軽ですごいです。
詳細な流れは次のとおりです。
- Unity のオーディオのインスタンス情報を格納している
WEBAudio.audioInstances
を調べて再生されているオーディオインスタンスを取得 - gain ノードを持つ最初のインスタンスを
createScriptProcessor
に接続 - onaudioprocess で得たバッファを文字列としてシリアライズし C# 側へ
SendMessage
で伝送 - C# 側ではこれをデシリアライズして uLipSync へ渡す
setInterval
しないとならないのは謎だとのことでした。。
調査
WEBAudio
はあまりネット上に情報がありませんでしたが、コードを読むと Unity が定義しているオブジェクトのようです。
audioInstances
はそのプロパティとしてここで生成されています。
困ったらこのあたりのコードを直接読むと色々とわかりそうです。
該当するオーディオを見つける際の問題
audioInstances
から最初に見つかった(gain
ノードを持つ)インスタンスを connect しているため、例えば PlayOnAwake でない何かしらのトリガで再生開始されるような AudioSource などはリップシンク対象ではなくなってしまいます(適当なキー押下で再生開始するように組んでみると適切な音声が割り当てられませんでした)。本来であれば、MonoBehaviour 同様、同じ GameObject についている AudioSource を対象に扱いたいところです。エンジン側から呼ばれる _JS_Sound_Create_Channel
に渡されている userData
がおそらく何らかの識別 ID なのではと考えているのですが、見てみても GUID でも無さそうですし使えるか不明です。
こういう時にエンジンコードが読めれば…と思うのですが。ひとまず loopEnd
で長さ一致を追加で見る、あたりが唯一使える方法かもしれません。
サンプリングレート合わせ
どこかでサンプリングレートを引っ張ってきて合っているか調べるコードが必要そうです。
GetData() 方式
Issue で書いていただいた方式は、OnAudioFilterRead
は使えないものの AudioClip.GetData()
は一部使えるので、それを利用する方式です。
WebGL 環境下での GetData()
については以下の公式リファレンスに記載があります。
AudioSource(再生インスタンス)から現在の再生位置を取得し、AudioClip(データ)の適切な位置のバッファにアクセスする方式です。メリットとしては C# の世界から出なくて良いのでメンテナンス性が比較的高い点で、デメリットはちょっと処理速度やメモリ的な不安が残る点でしょうか。Issue に書いてくださっている幾つかの点(1フレームディレイ、ステレオデータ)は、少し先読みしたり、適切にステレオデータを扱うことで修正可能です。また、WebGL 上ではユーザー操作がある(マウスクリックやキー操作)まで音声を再生しないという縛りがあるのですが、これも何かしらの手段で回避はできそうに思われます。
まとめ
技術的な面白さとしてはうえぞうさん方式の Web Audio API を使う方式なのですが、ちょっと現状は汎用性面で解決できない問題が幾つかあるため、まずはシンプルに GetData()
方式で実装を試してみようかと思います。Web Audio API 的な面白さの部分は、ひとまずはマイクの実装つなぎのところで知識欲求は満たそうかな、と思います。