ベンチマーク
libsonare と librosa(Python)の音声解析タスクにおける性能比較。
このページは性能の文脈を確認するための資料であり、機能チュートリアルではありません。API を先に学ぶ場合は はじめに、機能マップ、利用する言語の実行環境別ページを読んでください。
ベンチマーク値の読み方
レイテンシが低いほど、この条件では処理が速く終わったという意味です。2倍 の高速化は「このベンチマークでは 2 倍速い」という意味で、すべてのファイルで常に 2 倍速いという意味ではありません。ハードウェア、サンプルレート、クリップ長、デコード時間、中間特徴量を再利用できるかで結果は変わります。
ネイティブの数値はブラウザの数値ではありません
このページの数値はすべてネイティブかブラウザ(WebAssembly)のどちらかを明示しており、両者は入れ替えて読めません。
ネイティブビルドは変換に SIMD の FFT カーネルを使います。PFFFT が因数分解できる変換長をすべて引き受け、残りを KissFFT が受け持ちます。対して WebAssembly 構成は KissFFT だけを持ちます。CMakeLists.txt が BUILD_WASM の下で SONARE_USE_PFFFT=OFF を強制しており、FFT を 2 つ抱えるコストが解析専用モジュールのサイズ予算に収まらないためです。
そのため、以下の変換律速な高速化はブラウザには当てはまりません。コストが FFT にある処理、つまり STFT とその上に積まれているものはすべてネイティブ限定の数値です。ブラウザ側の計測は WebAssembly で何を失うか に、ランタイムを問わず持ち越せる 2 つは ブラウザにも届く改善 にあります。
このページで身につくこと
このページを読むと、次のことを判断・説明できるようになります。
- ベンチマーク値を、万能な速度保証ではなく、特定ワークロードの測定値として読める。
- ネイティブの数値とブラウザの数値を見分けられ、変換律速な数値が一方から他方へ持ち越せない理由を説明できる。
- 総合パイプラインの高速化と、機能別比較の違いを区別できる。
- 中間結果の共有、ネイティブ実行、パイプライン設計がなぜ性能に効くかを理解できる。
- 前後比較の数値を「明示された条件下での 1 つの最適化の効果」として読み、入力長に比例して増える数値とそうでない数値を見分けられる。
- ハードウェア、入力、実装が変わったときに、ベンチマークソースを見つけて再計測・更新できる。
計測方法
以下の数値はすべて「生音声からのスタンドアロン計測」です。各呼び出しは必要な中間状態(STFT=短時間フーリエ変換、メルスペクトログラムなど)を元のサンプルから毎回再構築します。これは両 API を単発で使うユーザーが体験するのと同じコードパスなので、フェアな比較になっています。ベンチマークのソースと結果 JSON は libsonare リポジトリの benchmarks/ にあります。
各ケースは3回実行し、表に載せているのはその中央値です。これは両側とも同じで、libsonare 側は bench_cpp.cpp、librosa 側は run_bench.py がそれぞれ3回の中央値を取ります。個々の実行時間は結果 JSON に書き出していないため、ばらつきはここには載せていません。3サンプルでは標準偏差もあまり意味を持たないためです。
ハードウェア
Apple M5 Max(18 ハードウェアスレッド、128 GB ユニファイドメモリ)で計測。ベンチマークは両側とも実行時のロードアベレージを記録しており、この実行はアイドル状態ではありませんでした。C++ 側が 2.29〜2.43、librosa 側が 2.49〜3.09 です。benchmarks/README.md は負荷のかかったマシンでの実行結果を公開しないよう明記しています。負荷は両者に均等にはかからず、倍率そのものが動くためです。したがってこれらは軽い背景負荷のもとでの実行として読み、議論の決着に使うなら静かなマシンで測り直してください。絶対値はお使いのハードウェアでスケールします。持ち越せるのは倍率のほうです。
比較対象のバージョン
Python 側は benchmarks/requirements.lock に固定したバージョンで実行しています。
- librosa 0.11.0
- scipy 1.17.1
- numpy 2.4.6
- numba 0.67.0
インタプリタは CPython 3.11 以降です(下限は benchmarks/pyproject.toml にあり、実際に使ったバージョンは結果に記録していません)。このうち2つは数値に直接効きます。librosa は FFT を scipy に委譲しており、pYIN の内側ループは numba が JIT コンパイルします。
総合パイプライン解析(ネイティブ)
総合的な楽曲解析: BPM + キー + ビート + コード + セクション + 音色 + ダイナミクス + リズム + メロディ。両側ともネイティブ実行で、libsonare はネイティブの C++ バイナリ、Python 側のパイプラインは CPython 上で走ります。
テスト音声: 合成 WAV、73 秒、44100 Hz ステレオ。バイナリを同梱せず、コミット済みの benchmarks/generate_audio.py でローカル生成します。生成は決定論的なので、手元のコピーはこの計測に使ったものとバイト単位で同一です。スクリプトは SHA-256 を出力し、一致しない場合は警告します。
3be88171eb87f8569189b9acef994e18263f89e7adf05119cbb48591c4953cb3All-In-One Analysis Latency (lower is better)
| ライブラリ | 言語 | ランタイム | 時間 | 相対 |
|---|---|---|---|---|
| libsonare | C++ | ネイティブ | 0.54秒 | 1倍 |
bpm-detector 1.1.0 --comprehensive (librosaベース) | Python | CPython | 38.9秒 | 約72倍遅い |
この 72 倍が何に対する数字か
比較対象は librosa ではありません。bpm-detector 1.1.0 を --comprehensive で走らせたもの、つまり librosa の上に構築されたパイプラインです。しかもこれは libsonare と同じ作者が書いたもので、libsonare はその後継にあたります。この倍率は両側とも自作である、という前提で読んでください。
librosa は特徴量ライブラリであって単発の解析器ではなく、librosa.analyze() に相当するものが存在しません。そのためパイプライン全体を比較するには、librosa の上に載った何かを選ぶ必要があります。bpm-detector は同じ特徴量群をエンドツーエンドで計算するため、比較可能です。
72 倍は「この Python パイプラインに対して、このフィクスチャで、ネイティブで測った値」と読んでください。別の librosa ベースのパイプラインなら別の倍率になります。自分のコードで libsonare が何をしてくれるかは、下の機能別比較のほうが参考になります。
総合パイプライン値は、libsonare の設計が最も活きる場面です。スペクトログラムを共有し、特徴量計算を並列化します。C++ パイプライン内では、44.1→22.05 kHz への自動ダウンサンプリングを一度だけ行います(librosa 側のパイプラインもリサンプリングするので、比較はフェアなままです)。パイプライン内に Python 境界もありません。
この差の大部分は言語の違いではなく構造の違いによるものです。まったく同じ仕事を C++ と Python で比べたのが下の機能別比較で、そちらの個別の差は 72 倍よりはるかに小さくなっています。
機能別比較(ネイティブ)
同じ 73 秒の音声(22050 Hz にリサンプリング後)における個別の特徴抽出。librosa は time.perf_counter、libsonare はネイティブビルドの sonare_bench 内の chrono::steady_clock で計測しています。これは SIMD 変換を使う構成なので、この表の各行はブラウザの挙動を表しません。ブラウザ側の対応値は後ろの節にあります。
Per-Feature Latency (lower is better)
| 機能 | librosa | libsonare(ネイティブ) | 高速化 |
|---|---|---|---|
| STFT (2048, hop 512) | 12.6ms | 6.81ms | 1.85倍 |
| メルスペクトログラム(128 バンド) | 19.9ms | 15.9ms | 1.25倍 |
| HPSS(倍音・打撃分離、カーネルサイズ 31) | 1,716ms | 64.4ms | 26.6倍 |
| オンセット強度 | 21.0ms | 16.8ms | 1.25倍 |
| クロマ(STFT ベース) | 42.9ms | 8.46ms | 5.07倍 |
| ビートトラック | 34.7ms | 44.4ms | 0.78倍(遅い) |
| MFCC(13 係数) | 21.3ms | 16.9ms | 1.26倍 |
| pYIN | 5,745ms | 364ms | 15.8倍 |
| スペクトル重心 | 24.7ms | 12.1ms | 2.04倍 |
単体でのビートトラッキングは librosa より遅い
ビートトラック はネイティブでも libsonare が負ける唯一の行です。同じ音声に対して librosa の 34.7 ms に対し 44.4 ms。なお sonare beats はビートグリッドに加えて拍子とダウンビートも返すので、librosa.beat.beat_track と同じ仕事をしているわけではありません。とはいえ拍位置だけが欲しいなら、その分は見返りのないコストです。
ビートを他の情報と一緒に必要とするなら、analyze() を呼んでください。パイプラインはオンセット包絡線を一度だけ計算して共有するため、この表にある単体呼び出しのコストはかかりません。sonare beats を単体で呼ぶ用途については、現時点の libsonare に librosa より優れた点はありません。
その答えは合っているのか: 同じフィクスチャでの精度
速度からわかるのは答えが早く返ることだけで、その答えが正しいかどうかはわかりません。このフィクスチャはテンポ・ビート位置・コード進行・キーを明示的に指定して合成しており、generate_audio.py がその内容を WAV の隣に書き出します。実録音と違い、このベンチマークには答えがあります。
| 対象 | 正解 | 結果 |
|---|---|---|
| テンポ | 120.00 BPM | 119.80 BPM(誤差 0.17%、MIREX の 4% 以内) |
| ビート | 146 拍 | 145 拍を検出、標準の ±70 ms 許容で F 値 0.997、中央値オフセット +25 ms |
| キー | A minor | analyze() → A minor(完全一致) |
| キー | A minor | sonare key → C major(平行調。後述) |
| コード | 16 小節 | analyze() フレーム単位 0.940(ルートのみ/ルート+性質、どちらも同値) |
| コード | 16 小節 | sonare chords 0.643(こちらもルートのみ/ルート+性質で同値) |
python3 benchmarks/generate_audio.py
python3 benchmarks/measure_accuracy.py --cli build-release/bin/sonare-cliこのうち 2 行は、ライブラリの呼び方に関わります。
- コードにはパイプラインを使う。
analyze()が 0.940 なのに対し、単体のsonare chordsは 0.643 です。パイプライン側にはコード境界を突き合わせるビートグリッドと小節分割があり、単体コマンドにはありません。 - 2 つのキー検出器は食い違うことがある。 ここでは
analyze()が A minor、sonare keyが C major(A minor の平行調)を返します。両方に平行調が成り立つ進行なのでどちらの解釈も成り立ちますが、答えを 1 つに絞る必要があるならanalyze()から取ってください。
これは最低限の確認であって、精度のベンチマークではない
合成音声には演奏のゆらぎも、音色の曖昧さも、ミックスもなく、小節の始まりがどこかで迷う余地もありません。ここで良いスコアが出るということは、復元できるように作られた信号をアナライザが復元できた、という意味です。
実録音での精度を予測するものではありません。それには注釈付きの実楽曲が必要で、ここで再配布することはできません。
自分のコーパスに対して採点できます。tests/fixtures/music_eval/ に BPM・ビート・ダウンビート・コード・キー・拍子のマニフェストがあります。SONARE_MUSIC_FIXTURE_ROOT を自分の注釈付き音源に向け、行を追加し、SONARE_ENABLE_OPTIONAL_FIXTURE_TESTS=ON でビルドしてください。
そうでなければ、精度の判断はブラウザデモか CLI で自分の音源を試して行ってください。
WASM Mastering ISP Guard
サンプル間ピーク(ISP)とは、2 つのサンプルのあいだに生じるピークのことです。生のサンプル値の上では見えませんが、DAC(デジタル・アナログ変換器)が波形を再構成すると実際に現れます。そのためリミッターはこれを捉えるためにオーバーサンプリングする必要があります。このベンチマークは、その検出器がブラウザで動かせる程度に十分速いことを確認するものです。
マスタリングの True Peak 経路も WebAssembly 上で検証しています。48 kHz ステレオの 1 ms ブロックを、4 倍オーバーサンプリングと最終リミッターと同じ sliding-max ガードで処理するベンチマークです。
| ベンチマーク | ランタイム | 1 ms audio あたりの中央値 | しきい値 | 結果 |
|---|---|---|---|---|
mastering_isp_4x_stereo_1ms | WASM / Node | 0.0062ms | 5.0ms | 合格 |
これによって、サンプル間ピーク検出器がブラウザレンダリングに十分な余裕を持つことを確認しています。libsonare リポジトリで cd bindings/wasm && yarn bench:wasm:isp を実行すれば再現できます。
WebAssembly で何を失うか
ブラウザビルドがネイティブと違う点は 2 つあります。シングルスレッドなので複数コアに広がる機能はその優位を失うこと、そして KissFFT だけを持つため SIMD 変換も受け取れないことです。下の 2 列は同じフィクスチャ・同じベンチマークですが、ブラウザ列はネイティブ列と同一の実行ではなく、WebAssembly 側の独立した実行によるものです。ペナルティは差の傾向として読み、対で測った値としては読まないでください。
| 機能 | ネイティブ | ブラウザ(WASM) | ブラウザのペナルティ |
|---|---|---|---|
| 総合 analyze | 541ms | 3,159ms | 5.8倍 |
| STFT | 6.81ms | 16.4ms | 2.4倍 |
| メルスペクトログラム | 15.9ms | 47.3ms | 3.0倍 |
| HPSS | 64.4ms | 422ms | 6.6倍 |
| オンセット強度 | 16.8ms | 48.5ms | 2.9倍 |
| クロマ | 8.46ms | 20.5ms | 2.4倍 |
| ビートトラック | 44.4ms | 80.4ms | 1.8倍 |
| MFCC | 16.9ms | 48.9ms | 2.9倍 |
| pYIN | 364ms | 437ms | 1.20倍 |
| スペクトル重心 | 12.1ms | 23.3ms | 1.9倍 |
ペナルティは一様ではないので、ブラウザで何を失うかは使う機能によって変わります。HPSS は両方の影響を同時に受けます。コア全体へ広がるルーチンであり、かつ変換の上に載っているためです。ブラウザでの倍音・打撃分離はおよそ 6.6 倍を見込んでください。メルフィルタバンク系の特徴量は 3 倍前後です。
逆方向の例外が pYIN で、1.20 倍とこの表で最小のペナルティです。実行時間の大半はビタビ格子(最も尤もらしい 1 本のピッチ経路を探す処理)で、遷移重みは対数として持っているため中身は倍精度の比較と加算だけになり、WASM はそれをネイティブに近い速度で回します。FFT バックエンドの違いが効くのは格子の手前にある FFT ベースの YIN 差分だけで、そちらは仕事量として小さいほうです。
総合パイプラインは約 5.8 倍を見込んでください。このフィクスチャでは 73 秒の音声を 3.2 秒で解析しており、再生速度に対しては依然として約 23 倍です。
ライブラリ選択の判断材料として
答えはランタイムで分かれ、その分かれ方こそが要点です。
ネイティブでは、このフィクスチャ上で librosa が勝つ軽量特徴量はありません。SIMD 変換により libsonare は STFT で 1.85 倍、その上に載るメルスペクトログラム・MFCC・オンセット強度で約 1.25 倍の優位を持ちます。この 3 つの差はそれだけで乗り換えを決める大きさではありませんが、避ける理由にもなりません。
ブラウザではその優位がありません。SIMD 変換もスレッドもないためです。上の 2 つの表を突き合わせると、WebAssembly の STFT とメルフィルタバンク系の特徴量は、同じマシン上でネイティブに動く librosa より遅くなります。ブラウザで libsonare を選ぶ理由は、そこに librosa が存在しないことであって、2 つのうち速いほうだからではありません。
いずれにせよ libsonare を選ぶ価値があるのは、HPSS(ネイティブで 27 倍)、ピッチトラッキング(同 16 倍)、クロマ(同 5.1 倍)、あるいは複数の特徴量を同時に必要とする場合です。最後のケースでは中間結果の共有と Python 境界の不在が支配的になり、ブラウザへ持ち込んでも残る唯一の理由でもあります。単体のビートトラッキングについては、どちらのランタイムでも librosa のほうが高速です。
ブラウザがダウンロードするもの
ブラウザには、ネイティブビルドにはないコストが 1 つあります。モジュールの取得です。以下の数値は src/wasm/meta.json から読み込んでいます。これはこのサイトが配信している成果物の記録で、WebAssembly ビルドを取り込むたびに書き換わります。つまりページに書き込んだ数字ではなく、現在のビルドに追従する値です。
| アセット | 圧縮前 | gzip |
|---|---|---|
sonare.wasm(フルエントリ) | ~7,808 KB | ~2,656 KB |
sonare.wasm、sonare.js、index.js の合計 | ~8,500 KB | ~2,792 KB |
合計にはリアルタイム Worklet バンドル(worklet.js)を含めていません。これは AudioWorklet でリアルタイムエンジンを動かすページだけが読み込みます。gzip 列が回線を流れる量で、圧縮前の列がエンジンとしてコンパイルされメモリに載る量です。どちらも利用者の操作に応じて増減しません。10 秒のクリップでも 2 時間のファイルでもモジュールの大きさは同じで、一度読み込んだページはセッション中それを使い続けます。@libraz/libsonare/analysis の裏にある解析専用エントリはフルエントリよりかなり小さくなります。ファイルごとの内訳とローダーのトレードオフは WebAssembly ガイド にあります。
大きな差がつく場所(ネイティブ)
総合パイプライン(72 倍): 共有中間結果 + Python 境界なし
libsonare の analyze() は STFT と Mel スペクトログラムを 一度だけ 計算し、下流のアナライザで再利用します。
この再利用が効きます。
| アナライザ | 再利用できる中間表現 |
|---|---|
| コード検出 | キー検出と同じクロマグラム |
| ビートトラッキング | セクション検出器が使うオンセット包絡線 |
独立したパスは CPU コア間で並列実行されます。これらはどれも Python 境界を跨がないため、呼び出しごとのディスパッチオーバーヘッドが消えます。
bpm-detector(および他の librosa ベースのパイプライン)は各アナライザでこれらの中間結果を再構築し、Python から全体をオーケストレーションします。コストが積み重なります。
HPSS(26.6 倍): キャッシュフレンドリーなマルチスレッドメディアンフィルター
librosa の HPSS は scipy.ndimage.median_filter を水平方向と垂直方向に1回ずつ呼びます。これは各ピクセルを逐次処理する汎用 C 実装です。
libsonare はこれをカスタムスライディングメディアンに置き換えています:
- ソート済みフラット配列(O(log k) の二分探索 + O(k) の memmove)。ツリー構造を使わないため、一般的なカーネルサイズなら L1 キャッシュに収まる
- マルチスレッド実行: 行と列がすべてのコアで並列処理される
- 結果: このハードウェアのネイティブ実行で scipy 版の約 27 倍高速。そして、スレッドのないブラウザビルドで最も遅くなるルーチンでもある
メディアンフィルター(とスライディングメディアン)とは?
メディアンフィルター は、各値を小さな窓内の近傍の中央値で置き換えます。平均と違ってスパイクや外れ値を取り除きつつエッジを保つため、HPSS で使われます。水平方向のメディアンは定常的な(倍音)ラインを、垂直方向のメディアンは鋭い(打撃)成分を残します。スライディングメディアン は、毎回ソートし直すのではなく窓を動かしながらこれを効率よく計算します。
pYIN(15.8 倍): ネイティブの YIN 差分とビタビ復号
pYIN のコストはフレームごとの候補評価とビタビ復号です。libsonare は両方を C++ で実装して librosa の Numba-JIT 内ループを置き換え、YIN 差分関数は直接計算ではなく FFT 経由で求めています。
支配的なのは復号のほうです。既定の 65〜2093 Hz では格子の状態数が 1,202、状態あたりの到達可能な遷移が約 100 あるため、73 秒のクリップで内側の更新を数億回通ります。ループの外に出せる計算はすべて出してあり、遷移重みは対数として保持し、有声・無声の切り替えコストは定数です。残るのは倍精度の比較と加算だけになります。
この格子はシングルスレッドです。HPSS と違ってコアを増やしても速くなりませんが、同じ理由でブラウザでもほとんど遅くなりません。WASM のペナルティは 1.20 倍で、その分も復号ではなく格子の手前にある FFT に由来します。
クロマ(5.07 倍): STFT → フィルタバンクの密なパス
クロマはスペクトログラムから 12 ピッチクラス表現を STFT のビンに対するピッチクラスのフィルタバンク経由で導出します。libsonare の STFT とフィルタバンク乗算は単一の連続バッファ上の Eigen3 ベクトル化行列演算として動くため、librosa の NumPy 演算スタックのディスパッチオーバーヘッドを回避できます。
変換(2.3 倍): SIMD FFT カーネル
ネイティブの表に含まれる唯一の前後比較が FFT バックエンドそのもので、上の表全体の形を説明するのもこの比較です。同じツリーを 2 回、SONARE_USE_PFFFT を有効にした構成と、KissFFT だけの変換になる -DSONARE_USE_PFFFT=OFF の構成でビルドし、それぞれで sonare_bench を走らせています。条件はこのページの他の数値と同じです。ネイティブの Release ビルドで、フィクスチャは 44.1 kHz の 73 秒を 22.05 kHz にリサンプリングしたものを使います。STFT は 2048 ポイント・ホップ 512 で、各ケース 3 回実行した中央値を、Apple シリコン上で計測しています。
| 計測対象 | KissFFT のみ | SIMD FFT | 変化 |
|---|---|---|---|
| STFT(2048、ホップ 512) | 13.6 ms | 6.0 ms | 2.3倍 |
| クロマ | 15.0 ms | 7.5 ms | 2.0倍 |
| スペクトル重心 | 19.2 ms | 10.8 ms | 1.8倍 |
| メルスペクトログラム | 22.7 ms | 15.2 ms | 1.5倍 |
| MFCC | 23.5 ms | 16.1 ms | 1.5倍 |
| オンセット強度 | 23.1 ms | 15.5 ms | 1.5倍 |
| HPSS | 88.2 ms | 61.9 ms | 1.4倍 |
| pYIN | 425 ms | 337 ms | 1.3倍 |
analyze() パイプライン | 963 ms | 611 ms | 1.6倍 |
どの行も同じ変換の効果を見ており、違うのは変換以外の処理量だけです。STFT 単体は 2.3 倍速くなり、各特徴量は実行時間のうち FFT が占めていた割合に応じて速くなっています。計算済みのスペクトログラムを再利用する計測はまったく変化しておらず、これが対照群です。変換の外では何も変わっていません。結果の変化は浮動小数点の丸めだけで、0.1 dB を大きく下回ります。
ここから 3 つのことが言えます。利得はフレーム単位なので、倍率はクリップ長によらず成り立ちます。長いファイルはフレームが増えるだけです。機能別比較の表にある STFT 6.81 ms は、追跡している benchmarks/results_cpp.json の実行から取った同じ行で、別セッションでロードアベレージを記録しながら測ったものです。6.0 と 6.8 の差はセッション間のばらつきで、効果のほうは 2.3 倍です。そして、このどれもブラウザには当てはまりません。ページ冒頭に書いたサイズの理由で WebAssembly モジュールは KissFFT だけを持つため、タブの中の STFT は KissFFT のみの値に上記の WebAssembly ペナルティを掛けたものになります。
ブラウザにも届く改善
上の表の大半がネイティブ限定なのは FFT バックエンドのためです。次の 2 つはそうではありません。命令セットではなく処理の組み立て方の性質なので、タブの中でもそのまま効きます。どちらも前後比較で、1 つの変更の効果を 1 つの入力で測ったものです。どちらも sonare_bench の一部ではなく(sonare_true_peak_bench が測るのはリアルタイムメーターのストリーミング経路で、別物です)、入力条件はここに全部書いておきます。
| 計測対象 | 入力 | 変更前 | 変更後 | 入力長に比例して増えるもの |
|---|---|---|---|---|
sonare::metering::lufs のピーク常駐メモリ | 48 kHz モノラル 1 時間(1 億 7,280 万サンプル、float 入力で 691 MB) | 2.09 GB | 706 MB | 呼び出し側の入力バッファのみ。計測自身のウィンドウは 3 秒で頭打ち |
sonare::metering::true_peak(4 倍オーバーサンプリング)の実行時間 | 48 kHz の 10 秒、本体より高いトランジェントを持つ素材 | 6.1 ms | 0.6 ms | 両方とも線形。刈り込み後の値は素材のクレストにも依存する |
オフライン LUFS はワーキングセットがクリップ長に比例しません。 K 特性をかけた信号をまるごと実体化すると、1 サンプル 1 チャンネルあたり double が 1 つ必要になります。上の 1 時間では 1.38 GB で、691 MB の入力バッファとプロセス自体のベースラインを足したものが 2.09 GB です。計測がその全体を同時に必要とすることはないので、信号はチャンク単位でフィルタされ、まだ終わっていない最も古いゲーティングブロックが必要とする分だけを保持するスライディングウィンドウに入ります。この保持量は入力がどれだけ長くても 3 秒のショートタームウィンドウで抑えられます。ピークメモリは入力バッファに約 15 MB(ベースライン込み)を足した量です。フィルタ状態はチャンクをまたいで引き継がれ、各ブロックは従来どおり自分のウィンドウを 1 パスで合計するので、報告されるラウドネスはどちらの方式でもビット単位で同一です。
長いファイルがタブに収まるかどうかを決めるのがこの数値です。配信している WebAssembly モジュールは ALLOW_MEMORY_GROWTH 付きで MAXIMUM_MEMORY を引き上げずにビルドされているため、線形メモリは Emscripten の既定である 2 GB で頭打ちになります。48 kHz モノラル 1 時間は余裕をもって収まり、制限になるのはデコード済み入力そのもので、48 kHz ステレオ 1 時間は計測が触れる前の時点で float 1.38 GB です。LUFS 側では、メモリのために長いファイルを区切って処理する必要はありません。区切るとすれば入力バッファのためです。
True Peak は、より高いピークがまだ到達可能な箇所でのみ補間します。 補間器は、ある区間のサンプルがそれまでに見つけたピークを超えられないと事前に判断でき、その区間を飛ばします(True Peak とは何かはTrue Peak の安全性を参照)。具体的には、各ポリフェーズ位相は、そのステンシル(出力 1 サンプルが依存する入力サンプルの短い窓)が届く範囲の入力振幅の最大値によって出力を上から抑えられます。したがって、そこまでのピークを超えられないと分かっている出力サンプル群は、計算せずに飛ばせます。計測は厳密なままです。それまでのピークを超えられないと証明できる補間だけを省いているためです。自身の本体より高いトランジェントを持つ素材(つまりほとんどの素材)はこの恩恵をまるごと受けます。全区間がフルスケールに張り付いた信号は刈り取る余地がないので総当たりと同じコストになり、0.6 ms が通常のケース、6.1 ms が上限と読んでください。いずれにせよ音声 1 秒あたり 0.06〜0.6 ms で、3 分のトラックでも刈り込みなしで 1 秒を大きく下回ります。ここに区切って処理する価値のあるものはなく、ブラウザで変わるものもありません。
速くならない箇所(とどのランタイムで)
- STFT 単体: ネイティブでは libsonare が 1.85 倍速い。変換が SIMD カーネルで動くのに対し、librosa は
scipy.fftに委譲しているため。ブラウザにはその SIMD バックエンドがなく、上の表を突き合わせると WebAssembly の STFT は同じフィクスチャで librosa より遅い。つまりこの行はどこで走らせるかで逆転する。 - Mel / MFCC / オンセット強度: 基底の STFT コストに支配される。STFT 後のフレームごとの Mel フィルタバンク乗算や DCT は、別言語で書いても差が出るほど重くない。そのため変換の挙動をそのまま引き継ぐ。ネイティブでは 3 つとも libsonare が約 1.25 倍速く、ブラウザではネイティブの約 3 倍のコストになり librosa に抜かれる。
- ビートトラッキング: 単体で呼ぶとどちらのランタイムでも librosa より遅い。しかもネイティブで負ける行はここだけ。他の情報と一緒にビートが必要なら、オンセット包絡線を共有する
analyze()を使う。 - パイプライン内での利用:
analyze()内ではこれらの特徴量はネイティブで 0.47 ms から 8.75 ms のあいだで動きます。オンセット強度 0.47 ms、MFCC 0.89 ms、クロマ 1.10 ms、スペクトル重心 2.65 ms、メルスペクトログラム 8.75 ms です。STFT と Mel が一度だけ計算され共有されるためです。上記のスタンドアロン値は「単体で呼んだときのコスト」を示すもので、パイプライン内コストではありません。
自分で再現する
ベンチマークは libsonare/benchmarks/ にあり完全に再現可能です:
# ローカルの libsonare チェックアウト内で
cmake -B build-bench -DCMAKE_BUILD_TYPE=Release \
-DBUILD_BENCH=ON -DBUILD_TESTING=OFF -DBUILD_CLI=OFF
cmake --build build-bench -j
rye sync --pyproject benchmarks/pyproject.toml
rye run --pyproject benchmarks/pyproject.toml python benchmarks/generate_audio.py
./build-bench/bin/sonare_bench \
benchmarks/fixtures/bench_73s_44100.wav \
benchmarks/results_cpp.json
rye run --pyproject benchmarks/pyproject.toml python benchmarks/run_bench.py統合された benchmarks/results.json には C++ 計測の libsonare 値と librosa 値、そして bpm-detector が PATH にあれば bpm-detector の総合パイプライン時間とバージョンも含まれます。
同じベンチマークは WebAssembly 向けにもビルドできます。スレッド化された経路がブラウザでどれだけ目減りするかは、信じる対象ではなく計測する対象です。
emcmake cmake -S . -B build-wasm-bench -DBUILD_WASM=ON -DBUILD_BENCH=ON \
-DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm-bench --target sonare_bench
node build-wasm-bench/bin/sonare_bench.js benchmarks/fixtures/bench_73s_44100.wavアイドル状態のマシンで実行すること
C++ 側の sonare_bench と librosa 側の run_bench.py が、それぞれ実行の前後で 1 分間のロードアベレージを記録します。負荷は両者に均等にはかからないので、忙しいマシンでは両列が一様に膨らむのではなく倍率そのものが変わります。比較を信用する前に results.json の load_average_before を確認してください。このページの数値についても同じです。WASM ベンチはロードアベレージを報告しないので、そちらは自分で確認してください。
libsonare を Python から呼ぶ場合
上のネイティブ列は libsonare の C++ 性能をそのまま測ったものです。個別の特徴量関数を Python バインディング経由で呼ぶ場合(例: libsonare.stft(samples, sr))、各呼び出しでサンプルバッファが FFI(外部関数インターフェース)境界を跨いでマーシャリングされ、軽量な特徴量ではこれが実行時間を支配します。総合パイプラインの analyze() は影響を受けません。エンドツーエンドで C++ 内で動き、小さな結果構造体のみが境界を越えるためです。
備考
- 数値はハードウェア依存。ここでは Apple M5 Max。相対差はマシン間で安定していますが、絶対値のミリ秒はそうではありません。自分のハードウェアでも参考になるのは倍率のほうです。
- 合成テスト音声(決定論的なコード進行 + 打撃音バースト)はバイナリを同梱せず、コミット済みのスクリプトでローカル生成します。精度の節が突き合わせる正解データも同時に出力されます。
- WASM ビルドはシングルスレッドなので、コアに広げて稼いでいる倍率は縮みます。とりわけ HPSS です。さらに KissFFT だけを持つため、変換律速な行はもう 1 つ別の理由でも縮みます。スレッドにも変換にも依存しない経路(pYIN のビタビ格子など)はほぼそのまま持ち越せます。ネイティブの値を勘で割り引くのではなく、上の WASM ベンチをビルドして対象のランタイムで測ってください。
- ブラウザにも届く改善で挙げたオフライン LUFS と True Peak の挙動は例外です。アルゴリズム側の性質なので、どのランタイムでも成り立ちます。