リアルタイム安全性
「リアルタイム安全」とは、同じ libsonare の DSP をライブの音声コールバック内でグリッチなく動かせる性質です。オンにする機能ではなく、音声経路が破ってはならない規則の集まりです。本ページはその規則と、libsonare がどう守るかを説明します。ストリーミング API は リアルタイムとストリーミング を参照してください。
音声コールバックには締め切りがある
音声は高優先度スレッド上でブロック単位で届き、各ブロックはスピーカーが必要とする前に埋めなければなりません。使える時間はそのブロック自身の長さ、つまりブロックサイズをサンプルレートで割った値です。
| ブロックサイズ | 44.1 kHz | 48 kHz |
|---|---|---|
| 128 フレーム | 2.9 ms | 2.7 ms |
| 256 フレーム | 5.8 ms | 5.3 ms |
| 512 フレーム | 11.6 ms | 10.7 ms |
| 1024 フレーム | 23.2 ms | 21.3 ms |
ブラウザではブロックの大きさが決まっています。AudioWorklet の process() は 128 フレームのレンダークォンタムごとに呼ばれるので、持ち時間は 3 ms を切ります。ネイティブのオーディオ API ではホストがサイズを選べ、通常は 64〜1024 フレームです。聴き手が感じる遅れは 1 ブロックより長くなります。デバイスへ届くまでにシステムがさらにバッファを積むからです(ブラウザでは AudioContext.baseLatency と outputLatency の和)。オーディオの基礎 を参照してください。
一度でもその締め切りを逃すと、聞こえるクリック・ポップ・ドロップアウトになります。コールバックには厳しいリアルタイム締め切りがあるので、所要時間が無制限になり得るものはコールバック内で禁じられます。
- 締め切り内に終わった
- 締め切りを超えた
- デバイスへ渡されたブロック
コールバック内で禁じられること
| 禁止事項 | 音声を壊す理由 |
|---|---|
メモリ確保(new/malloc) | 予測できない時間ブロックし得る |
AudioWorklet 内での JavaScript のメモリ確保(新しい配列、オブジェクト、クロージャ、ブロックごとの postMessage) | ガベージコレクションの負荷になり、その停止が音声スレッドに及ぶ |
| ロック/ミューテックス | 他スレッド待ちで止まり得る |
| ファイル/ネットワーク I/O | レイテンシが無制限 |
| 無制限の処理全般 | 1 回の超過で締め切りを逃す |
規律は 1 つです。遅い作業(確保、ファイル読み込み、セットアップ)はコールバックの外で済ませ、コールバックは事前に作ったバッファを読んで処理するだけにします。JavaScript では、毎ブロック同じ型付き配列を使い回すということです。たとえば Mixer.createRealtimeBuffer() や、1 度だけ確保した配列へ書き込む ...Into(...) 系のメソッドを使います。
ロックなしでデータを渡す: リングバッファ
音声スレッドもコマンド(フェーダーの移動)を受け取り、結果(メーター値)を送り返す必要があります。そのために使うのがリングバッファです。固定長の配列に書き込み位置と読み出し位置を持たせ、末尾で先頭へ折り返します。書き手と読み手が 1 つずつ(シングルプロデューサー・シングルコンシューマー、SPSC)なら、それぞれが自分の位置だけを動かすので、どちらも相手を待ちません。リングが満杯になると、書き手は待たずに項目を捨てるか、そのことを報告します。libsonare のキューが droppedRecords などのオーバーフローカウンターを公開しているのはこのためです。libsonare のコマンドキュー、テレメトリとメーターのキュー、AudioWorklet の共有リングはどれもこの方式で、読み手が 1 つの前提のキューは、1 つの読み手だけが取り出す必要があります。
libsonare が安全を保つ方法
libsonare のリアルタイム経路は、コールバックの規則を次の方法で守ります。
| 技法 | 意味 |
|---|---|
| 設定したブロックサイズ向けのバッファ事前確保 | 通常のブロックでセットアップ後のメモリ確保を避ける |
| ロックフリーなパラメータ更新 | UI からの変更で音声スレッドを止めない |
| パラメータスムージング | 値の変化でクリックを出さない |
| デノーマル対策 | 減衰時の極小浮動小数点値による CPU スパイクを避ける |
この 4 つで、同じ DSP コードがオフラインでもライブでも動きます。
AudioWorklet
ブラウザではリアルタイム音声は AudioWorklet で動きます。メインスレッドとは別の、音声処理専用のコンテキストです。
想定する流れは次のとおりです。
- メインスレッドでオブジェクトを準備する。
- 準備済みの処理を AudioWorklet へ渡す。
- worklet 内ではメモリ確保をせずにブロックを処理する。
このサイトはこの経路を SharedArrayBuffer なしで動かしているため、特別な COOP/COEP ヘッダは不要です。
libsonare がリアルタイム安全性をどう実装するか
ミキサーのストリップ・バスと RealtimeEngine は、リアルタイム安全なコアを共有します。StreamAnalyzer も通常のサイズに収まるブロック向けに解析用バッファを事前確保します。主な要素は次のとおりです。
- 事前確保バッファ(ブロックごとの再利用向けの
Mixer.createRealtimeBuffer()を含む) - ロックフリーなパラメータ変更
- デノーマル対策
- クリックなしの変化のためのパラメータスムーザー
- 並列経路をそろえるプラグインディレイ補正
StreamAnalyzer の process() は FFT、特徴量、進行中の音楽推定を同期的に計算します。通常の 128〜2,048 サンプル程度のブロックは構築時に予約した容量で処理できますが、大きなチャンクでは入力の非有限値除去、リサンプル、オーバーラップ用のスクラッチバッファが拡張されることがあります。厳しいリアルタイムコールバックでは、コールバック外で構築・設定し、一定サイズのブロックを供給するストリーミングソースを使い、生成済みフレームの読み出しはコンシューマー側で行ってください。スレッドをまたぐ必要のある状態はロックフリーチャンネルで移し、直接読ませるのではなくテレメトリとして公開します。設定したブロックの処理が締め切り内に収まる限り、同じ DSP 実装をオフラインレンダリングとブラウザの AudioWorklet で使えます。