Unity向けの簡易的なVADライブラリの紹介

こんにちは、エンジニアの渡辺(@mochi_neko_7)です。

今回は VAD (Voice Activity Detection、音声区間検出) を Unity で利用するためのライブラリを自作してみたのでこちらを紹介します。

github.com

Unity で発話している音声区間を判定したり、その音声データを切り取って何かに利用したい場合に便利なライブラリになっています。

ざっくりした説明は README にも書いてはいるのですが、こちらの記事では背景や設計などもう少し踏み込んだ話も紹介します。

以前の記事

synamon.hatenablog.com

で紹介した Logging のライブラリを使用するため、Unity のバージョンは Unity 2022.3 以上になっている点にご注意ください。

背景

※ ライブラリの説明だけ知りたい方は読み飛ばしてもらって大丈夫です。

これまで趣味で ChatGPT の API を Backend とした Chat Agent や AITuber の仕組みを開発していますが、 実は音声入力ベースの仕組みを作ることを意図的に避けていました。

OpenAI の Whisper の API は公開当初に Unity 向けの Client Library も作っていたのですが、実際にマイクの音声を入力して使用するとなると、録音した音声をそのまま全て API に流すのはコスト的にも挙動的にも現実的ではありません。*1*2

そのため、人間が話をしていることを検出し、その話をしている区間の音声のみ切り出して API に流す、といった工夫が必要になります。

その際に利用するのが、今回のメインテーマのいわゆる VAD (Voice Activity Detection、音声区間検出) で、要するに音声入力がアクティブなのか否か判定したいという話です。

もちろん自作する前に既存の VAD ライブラリなどないか調べてみたのですが、Photon Voice や Vivox、WebRTC などの音声通信ライブラリに組み込まれているものは知っているものの、GitHub にも Asset Store にも単体で使えそうなものが見当たりませんでした。(どなたか知っていたら教えていただきたいです。)

VAD のロジックは真面目に考えると WebRTC 内部での実装MFCC(メル周波数ケプストラム係数) など音声解析ベースでの実装が王道ではありますが、音声解析は計算コストが大きいので最適化も考えると実装が大変です。

以前 Photon Voice の VAD の調整をしていた感触から音量ベースのシンプルな実装でも十分実用性がありそうだと思っていましたので、いったんは音量ベースの簡易的な VAD ロジックを採用しました。

というわけで結局「なければ自分で作ってみればいいじゃないか」の精神で自分で作って動かしてみることにしました。

様々なユースケースを想定した VAD の設計

背景にも書いたように、VAD の利用用途の一つとしてアクティブな状態における音声データを Whisper の WebAPI などの外部に投げることも想定しています。

様々なユースケースを考えると、音声データの入力も出力も自由度が必要ですし、VAD のロジックも好きにカスタマイズができても良いと考えています。

そのため、VAD の設計として大きく三つのモジュールの構成にしています。

  1. 音声入力ロジック -> IVoiceSource
    • 音声をどこから拾うか決める部分
    • Unity 標準のマイク機能 (UnityEngine.Microphone)、Unity 内の音声 (AudioSource)、OS から直接マイク入力を拾う、音声ライブラリを使用する、などの選択肢があります
  2. 音声検出 (VAD) ロジック -> IVoiceActivityDetector
    • Source から受け取った音声データを利用して、VAD の判定を実際に行う部分
    • 簡易的なデフォルト実装を用意していますが、必要なら他のロジックを持ってきて利用することも可能です
  3. 音声データの利用ロジック -> IVoiceBuffer
    • VAD ロジックでアクティブと判定された区間の音声データを受け取って、何か処理をする部分
    • 必要がなければデータの利用はしなくてもいいですし、AudioClip に流し込むこと、WAV ファイルなどに書き込むことも想定できます

モジュール単位で抽象化を行い、利用者が任意に選択・カスタマイズができるような仕組みにしています。

この構成が分かればソースコードやサンプルの見通しも立つかと思います。

それぞれのパート別にもう少し掘り下げて紹介していきます。

音声入力ロジック

基本的な音声入力ソースとして Unity 標準のマイク機能 (UnityEngine.Microphone) が考えられます。

Unity標準のマイク機能といっても直接受け取れるのは AudioClip で、そこから音声データを取得する実装方針は大きく二つあります。

  1. AudioSource で再生 -> OnAudioFilterRead イベントで受け取る
  2. AudioClip.GetData で直接抜き取る

1 の方針は AudioSourceAudioMixer を仲介する際にエフェクトをかけたり(Low-pass filter や音量調整など)できるメリットもありますが、一度音声再生のルートを通る分遅延も大きめに発生します。

2 の方針はそれに比べると直接的ですが、AudioClip のデータをループさせて録音していることを考慮しながらデータの読み取りをする点に注意が必要になります。

参考:

qiita.com

今回は 2 の AudioClip から直接データを抜き取る方針を採用しています。

github.com

実装では UnityMicrophoneProxy というクラスをわざわざ中間に噛ませています。

これは UnityEngine.Microphone でマイク入力の AudioClip を何度も取得すると古いものが更新されなくなるため、複数がマイクを利用しようとした場合に競合しやすいためです。

なお、音声データの配列はそれなりにデータ量が大きいためメモリ使用量やアロケーションコストを考慮する必要がありますが、このライブラリでは ArrayPool を利用しています。

learn.microsoft.com

github.com

マイク以外の入力ソースとして Unity の内部で再生されている音声を利用する、つまり AudioSource から入力音声を取得する実装も一応用意しています。

github.com

他にも AudioClip から直接引っ張る方式やネイティブのマイク入力を拾う方式などもアイデアとして考えられますが、実装するかは要望次第です。

VAD ロジックの検討

VAD のロジック本体ですが、現在は 2 パターンを用意しています。

  • QueueingVoiceActivityDetector
    • 一定時間分の Queue に溜めつつ、Queue 内部の発話時間の割合で VAD を判定するロジック
    • メモリ使用量抑えめ、挙動は少し不安定
  • CumulativeVoiceActivityDetector
    • 発話している時はゲージが増え、発話していない時間はゲージが減り、ゲージがなくなったら非アクティブになるイメージの VAD ロジック
    • メモリ使用量多め、挙動は安定め

当初は前者のメモリに配慮した実装を使用していましたが、非アクティブ -> アクティブに切り替わるタイムラグ分の音声の処理で結局メモリを使ったり、二回目以降の VAD が少し不安定な様子があったため後者のロジックを追加しました。

現在は後者のロジックを推奨しています。

Whisper で利用することを想定してロジックを組む場合には気を付けるべき点がいくつかあります。

  1. 短すぎる音声を使わないこと
  2. 無音の音声を使わないこと
  3. 一度に処理できる音声の長さ(コンテキストサイズ)が 30 秒であること

1 や 2 は empty が返ってきたり Whisper モデル自体が持つ Hallucination を起こして「字幕視聴...」のような謎の結果を出力したり特定の単語を何度も繰り返したりする現象をなるべく防ぐためです。

参考:

Whisper 論文

arxiv.org

6. Limitations and Future Work / Improved decoding strategies.

の部分に Hallucination のコメントがあります。

音声データの利用ロジック

VAD で切り取ったアクティブな判定の音声データの利用方法はいくつか考えられます。

基本的に想定できるユースケースの実装は用意していますが、欲しいものがない場合には自分で IVoiceBuffer の実装を用意して利用することもできます。

サンプル

使用例の一つとして、UnityEngine.Microphone から取得した音声を使って VAD を行い、AudioSource で再生するサンプルを簡単に紹介します。

github.com

まずマイク入力を利用する IVoiceSource をセットアップします。

proxy = new UnityMicrophoneProxy();
IVoiceSource source = new UnityMicrophoneSource(proxy);

次にアクティブな音声を AudioClip に流す IVoiceBuffer をセットアップします。

var buffer = new AudioClipBuffer(
    maxSampleLength: (int)(parameters.MaxActiveDurationSeconds * source.SamplingRate),
    frequency: source.SamplingRate);

そして VAD のロジックとパラメータを指定して IVoiceActivityDetector をセットアップします。

vad = new QueueingVoiceActivityDetector(
    source,
    buffer,
    parameters.MaxQueueingTimeSeconds,
    parameters.MinQueueingTimeSeconds,
    parameters.ActiveVolumeThreshold,
    parameters.ActivationRateThreshold,
    parameters.InactivationRateThreshold,
    parameters.ActivationIntervalSeconds,
    parameters.InactivationIntervalSeconds,
    parameters.MaxActiveDurationSeconds);

生成される AudioClipAudioClipBuffer.OnVoiceInactive のイベントで取得できるので、これを AudioSource にセットして再生します。

buffer
    .OnVoiceInactive
    .Subscribe(clip =>
    {
        Log.Info("[VAD.Sample] OnInactive and receive AudioClip and play.");
        audioSource.clip = clip;
        audioSource.Play();
    })
    .AddTo(this);

MonoBehaviour のライフサイクルに合わせて更新処理、破棄処理も忘れないよう叩いておきます。

private void OnDestroy()
{
    vad?.Dispose();
    proxy?.Dispose();
}

private void Update()
{
    vad?.Update();
}

組み合わせのパターンが多いためすぐに利用可能なコンポーネントは用意していませんが、どの Source / Logic / Buffer を使うのかユースケースに合わせてサンプルを見ながら実装するのはそこまで難しくないかと思います。

まとめ

自作の Unity 向け VAD ライブラリ voice-activity-detection-unity を紹介しました。

ユースケースに合わせて Source / Logic / Buffer を選択・カスタマイズして利用することができます。

Unity のマイクや音声データの取り扱いで気を遣う点や音声データを Whisper で利用する場合に注意すべき点などの細かな工夫もいくつか紹介しました。

おわりに

Unity での音声データの取り扱いに慣れてさえいれば実装自体はそこまで複雑ではありませんでした。

主な動機であった Whisper との連携も実際にしばらく使用してみて、Cumulative のロジックの方では実用レベルで安定することも確認できました。

とはいえやや急ぎ気味に作ったライブラリでもあるのでまだまだチューニングの余地があったり、VAD のロジックもより良い実装があると思います。

他にも WebRTC の VAD や最近の機械学習ベースの VAD など高度な実装も世の中にはありますので、処理負荷と精度、安定性がどんなレベルなのか気になります。

音声入力を VAD でコントロールして、ローカルにせよ API にせよ常に Whisper を使い続けることを避けられますので、会話ができる AI システムなど音声入力ベースのシステムなどにも便利に利用できるかと思います。

Unity で VAD を単体で利用したい場合に参考になれば幸いです。

*1:コスト面だけなら Local で Whisper を動かすという選択肢もありますが、音声をどこで区切るべきかという問題は残ります

*2:Whisper の API は一度にアップロード可能なファイルサイズが25MBの制限もあります