Skip to content

リアルタイム安全性 ​

「リアルタイム安全」とは、同じ libsonare の DSP をライブの音声コールバック内でグリッチなく動かせる性質です。オンにする機能ではなく、音声経路が破ってはならない規則の集まりです。本ページはその規則と、libsonare がどう守るかを説明します。ストリーミング API は リアルタイムとストリーミング を参照してください。

音声コールバックには締め切りがある ​

音声は高優先度スレッド上でブロック単位で届き、各ブロックはスピーカーが必要とする前に埋めなければなりません。使える時間はそのブロック自身の長さ、つまりブロックサイズをサンプルレートで割った値です。

ブロックサイズ44.1 kHz48 kHz
128 フレーム2.9 ms2.7 ms
256 フレーム5.8 ms5.3 ms
512 フレーム11.6 ms10.7 ms
1024 フレーム23.2 ms21.3 ms

ブラウザではブロックの大きさが決まっています。AudioWorklet の process() は 128 フレームのレンダークォンタムごとに呼ばれるので、持ち時間は 3 ms を切ります。ネイティブのオーディオ API ではホストがサイズを選べ、通常は 64〜1024 フレームです。聴き手が感じる遅れは 1 ブロックより長くなります。デバイスへ届くまでにシステムがさらにバッファを積むからです(ブラウザでは AudioContext.baseLatency と outputLatency の和)。オーディオの基礎 を参照してください。

一度でもその締め切りを逃すと、聞こえるクリック・ポップ・ドロップアウトになります。コールバックには厳しいリアルタイム締め切りがあるので、所要時間が無制限になり得るものはコールバック内で禁じられます。

持ち時間と、締め切りを超えた 1 ブロック
1 ブロック分の周期(1 つの締め切り)処理余裕メモリ確保やロック待ち出力聞こえるクリック128 フレーム @ 48 kHz ≈ 2.7 ms
  • 締め切り内に終わった
  • 締め切りを超えた
  • デバイスへ渡されたブロック
ここで問題になるのは平均的な速さではありません。どのブロックにも同じ持ち時間しかなく、通常のブロックは余裕を残して終わります。ところがミューテックスやアロケータで一度待たされるだけで 1 つの締め切りを超え、バッファが間に合わず、聴き手にはそれが聞こえます。禁止リストが並べているのが「単に遅い処理」ではなく「所要時間に上限がない処理」なのは、このためです。

コールバック内で禁じられること ​

禁止事項音声を壊す理由
メモリ確保(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 で動きます。メインスレッドとは別の、音声処理専用のコンテキストです。

想定する流れは次のとおりです。

  1. メインスレッドでオブジェクトを準備する。
  2. 準備済みの処理を AudioWorklet へ渡す。
  3. worklet 内ではメモリ確保をせずにブロックを処理する。

このサイトはこの経路を SharedArrayBuffer なしで動かしているため、特別な COOP/COEP ヘッダは不要です。

libsonare がリアルタイム安全性をどう実装するか

ミキサーのストリップ・バスと RealtimeEngine は、リアルタイム安全なコアを共有します。StreamAnalyzer も通常のサイズに収まるブロック向けに解析用バッファを事前確保します。主な要素は次のとおりです。

  • 事前確保バッファ(ブロックごとの再利用向けの Mixer.createRealtimeBuffer() を含む)
  • ロックフリーなパラメータ変更
  • デノーマル対策
  • クリックなしの変化のためのパラメータスムーザー
  • 並列経路をそろえるプラグインディレイ補正

StreamAnalyzer の process() は FFT、特徴量、進行中の音楽推定を同期的に計算します。通常の 128〜2,048 サンプル程度のブロックは構築時に予約した容量で処理できますが、大きなチャンクでは入力の非有限値除去、リサンプル、オーバーラップ用のスクラッチバッファが拡張されることがあります。厳しいリアルタイムコールバックでは、コールバック外で構築・設定し、一定サイズのブロックを供給するストリーミングソースを使い、生成済みフレームの読み出しはコンシューマー側で行ってください。スレッドをまたぐ必要のある状態はロックフリーチャンネルで移し、直接読ませるのではなくテレメトリとして公開します。設定したブロックの処理が締め切り内に収まる限り、同じ DSP 実装をオフラインレンダリングとブラウザの AudioWorklet で使えます。

関連: リアルタイムとストリーミング、ストリーミング解析、リアルタイムエンジン、ブラウザ内ローカル処理