ベンチマーク
libsonare と librosa(Python)の音声解析タスクにおける性能比較。
このページは性能の文脈を確認するための資料であり、機能チュートリアルではありません。API を先に学ぶ場合は はじめに、機能マップ、利用する言語の実行環境別ページを読んでください。
ベンチマーク値の読み方
レイテンシが低いほど、この条件では処理が速く終わったという意味です。2倍 の高速化は「このベンチマークでは 2 倍速い」という意味で、すべてのファイルで常に 2 倍速いという意味ではありません。ハードウェア、サンプルレート、クリップ長、デコード時間、中間特徴量を再利用できるかで結果は変わります。
このページで身につくこと
このページを読むと、次のことを判断・説明できるようになります。
- ベンチマーク値を、万能な速度保証ではなく、特定ワークロードの測定値として読める。
- 総合パイプラインの高速化と、機能別比較の違いを区別できる。
- 中間結果の共有、ネイティブ実行、パイプライン設計がなぜ性能に効くかを理解できる。
- ハードウェア、入力、実装が変わったときに、ベンチマークソースを見つけて再計測・更新できる。
計測方法
以下の数値はすべて「生音声からのスタンドアロン計測」です。各呼び出しは必要な中間状態(STFT=短時間フーリエ変換、メルスペクトログラムなど)を元のサンプルから毎回再構築します。これは両 API を単発で使うユーザーが体験するのと同じコードパスなので、フェアな比較になっています。ベンチマークのソースと結果 JSON は libsonare リポジトリの benchmarks/ にあります。
各ケースは3回実行し、表に載せているのはその中央値です。これは両側とも同じで、libsonare 側は bench_cpp.cpp、librosa 側は run_bench.py がそれぞれ3回の中央値を取ります。個々の実行時間は結果 JSON に書き出していないため、ばらつきはここには載せていません。3サンプルでは標準偏差もあまり意味を持たないためです。
ハードウェア
Apple M5 Max(18 ハードウェアスレッド、128 GB ユニファイドメモリ)のアイドル状態で計測。ベンチマークは両側とも実行時のロードアベレージを記録しており、ここでは 2.0 でした。絶対値はお使いのハードウェアでスケールします。持ち越せるのは倍率のほうです。
比較対象のバージョン
Python 側は benchmarks/requirements.lock に固定したバージョンで実行しています。
- librosa 0.11.0
- scipy 1.17.1
- numpy 2.4.4
- numba 0.65.1
インタプリタは CPython 3.11 以降です(下限は benchmarks/pyproject.toml にあり、実際に使ったバージョンは結果に記録していません)。このうち2つは数値に直接効きます。librosa は FFT を scipy に委譲しており、pYIN の内側ループは numba が JIT コンパイルします。
総合パイプライン解析
総合的な楽曲解析: BPM + キー + ビート + コード + セクション + 音色 + ダイナミクス + リズム + メロディ。
テスト音声: 合成 WAV、73 秒、44100 Hz ステレオ。バイナリを同梱せず、コミット済みの benchmarks/generate_audio.py でローカル生成します。生成は決定論的なので、手元のコピーはこの計測に使ったものとバイト単位で同一です。スクリプトは SHA-256 を出力し、一致しない場合は警告します。
3be88171eb87f8569189b9acef994e18263f89e7adf05119cbb48591c4953cb3All-In-One Analysis Latency (lower is better)
| ライブラリ | 言語 | 時間 | 相対 |
|---|---|---|---|
| libsonare | C++ | 1.15秒 | 1倍 |
bpm-detector 1.1.0 --comprehensive (librosaベース) | Python | 34.5秒 | 約30倍遅い |
この 30 倍が何に対する数字か
比較対象は librosa ではありません。bpm-detector 1.1.0 を --comprehensive で走らせたもの、つまり librosa の上に構築されたパイプラインです。しかもこれは libsonare と同じ作者が書いたもので、libsonare はその後継にあたります。この倍率は両側とも自作である、という前提で読んでください。
librosa は特徴量ライブラリであって単発の解析器ではなく、librosa.analyze() に相当するものが存在しません。そのためパイプライン全体を比較するには、librosa の上に載った何かを選ぶ必要があります。bpm-detector は同じ特徴量群をエンドツーエンドで計算するため、比較可能です。
30 倍は「この Python パイプラインに対して、このフィクスチャで測った値」と読んでください。別の librosa ベースのパイプラインなら別の倍率になります。自分のコードで libsonare が何をしてくれるかは、下の機能別比較のほうが参考になります。
総合パイプライン値は libsonare の設計が最も活きる場面です: スペクトログラム共有、特徴量計算の並列化、C++ パイプライン内で一度だけ行う 44.1→22.05 kHz への自動ダウンサンプリング(librosa 側のパイプラインもリサンプリングするので、比較はフェアなままです)、そしてパイプライン内に Python 境界がないこと。
この差の大部分は言語の勝利ではなく構造の差です。まったく同じ仕事を C++ と Python で比べたのが下の機能別比較で、そちらでは軽い特徴量は librosa のほうがやや速いという結果になっています。
機能別比較
同じ 73 秒の音声(22050 Hz にリサンプリング後)における個別の特徴抽出。librosa は time.perf_counter、libsonare は sonare_bench バイナリ内の chrono::steady_clock で計測。
Per-Feature Latency (lower is better)
| 機能 | librosa | libsonare | 高速化 |
|---|---|---|---|
| STFT (2048, hop 512) | 12.6ms | 13.5ms | 0.93倍 — 遅い |
| メルスペクトログラム(128 バンド) | 19.5ms | 22.2ms | 0.88倍 — 遅い |
| HPSS(倍音・打撃分離、カーネルサイズ 31) | 1,681ms | 81.9ms | 20.5倍 |
| オンセット強度 | 20.5ms | 22.7ms | 0.90倍 — 遅い |
| クロマ(STFT ベース) | 42.0ms | 14.8ms | 2.84倍 |
| ビートトラック | 32.9ms | 55.0ms | 0.60倍 — 遅い |
| MFCC(13 係数) | 20.8ms | 23.0ms | 0.90倍 — 遅い |
| pYIN | 5,461ms | 428ms | 12.8倍 |
| スペクトル重心 | 24.9ms | 18.1ms | 1.37倍 |
単体でのビートトラッキングは librosa より遅い
ビートトラック は libsonare の負け幅が最も大きい行です。同じ音声に対して librosa の 32.9 ms に対し 55.0 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 — その平行調 — を返します。両方に平行調が成り立つ進行なのでどちらも擁護できますが、答えを 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 で何を失うか
ブラウザビルドはシングルスレッドなので、複数コアを使う機能はそこで優位を失います。同じフィクスチャ、同じ実行内でのネイティブと WASM の対比です。
| 機能 | ネイティブ | WASM | WASM のペナルティ |
|---|---|---|---|
| 総合 analyze | 1,153ms | 3,159ms | 2.7倍 |
| STFT | 13.5ms | 16.4ms | 1.2倍 |
| メルスペクトログラム | 22.2ms | 47.3ms | 2.1倍 |
| HPSS | 81.9ms | 422ms | 5.2倍 |
| オンセット強度 | 22.7ms | 48.5ms | 2.1倍 |
| クロマ | 14.8ms | 20.5ms | 1.4倍 |
| ビートトラック | 55.0ms | 80.4ms | 1.5倍 |
| MFCC | 23.0ms | 48.9ms | 2.1倍 |
| pYIN | 428ms | 437ms | 1.02倍 |
| スペクトル重心 | 18.1ms | 23.3ms | 1.3倍 |
ペナルティは一様ではないので、ブラウザで何を失うかは使う機能によって変わります。HPSS はコア全体へ広がるルーチンなので、コアが 1 つしかない環境で最も失います。ブラウザでの倍音・打撃分離はおよそ 5 倍を見込んでください。メルフィルタバンク系の特徴量は 2 倍前後です。
逆方向の例外が pYIN で、タブの中でもネイティブとほぼ変わりません。実行時間の大半はビタビ格子で、遷移重みは対数として持っているため中身は倍精度の比較と加算だけになり、WASM はそれをネイティブに近い速度で回します。
総合パイプラインは約 2.7 倍を見込んでください。このフィクスチャでは 73 秒の音声を 3.2 秒で解析しており、再生速度に対しては依然として約 23 倍です。
ライブラリ選択の判断材料として
軽量な特徴量を 1 つだけ必要とする場合 — STFT、メルスペクトログラム、MFCC、オンセット包絡線 — librosa のほうが 1 割ほど速いです。libsonare に乗り換える理由にはなりませんし、離れる理由にもなりません。librosa は FFT を scipy.fft(高度に最適化された C/Fortran)に委譲しており、FFT のコストを払ってしまえば単発の呼び出しでどちらかが稼げる余地はほとんど残っていません。
libsonare が見合うのは、HPSS(20 倍)、ピッチトラッキング(13 倍)、クロマ(2.8 倍)、あるいは複数の特徴量を同時に必要とする場合です。最後のケースでは中間結果の共有と Python 境界の不在が支配的になります。単体のビートトラッキングについては librosa のほうが高速です。
大きな差がつく場所
総合パイプライン(30 倍): 共有中間結果 + Python 境界なし
libsonare の analyze() は STFT と Mel スペクトログラムを 一度だけ 計算し、下流のアナライザで再利用します。
この再利用が効きます。
| アナライザ | 再利用できる中間表現 |
|---|---|
| コード検出 | キー検出と同じクロマグラム |
| ビートトラッキング | セクション検出器が使うオンセット包絡線 |
独立したパスは CPU コア間で並列実行されます。これらはどれも Python 境界を跨がないため、呼び出しごとのディスパッチオーバーヘッドが消えます。
bpm-detector(および他の librosa ベースのパイプライン)は各アナライザでこれらの中間結果を再構築し、Python から全体をオーケストレーションします。コストが積み重なります。
HPSS(20.5 倍): キャッシュフレンドリーなマルチスレッドメディアンフィルター
librosa の HPSS は scipy.ndimage.median_filter を水平方向と垂直方向に1回ずつ呼びます。これは各ピクセルを逐次処理する汎用 C 実装です。
libsonare はこれをカスタムスライディングメディアンに置き換えています:
- ソート済みフラット配列(O(log k) の二分探索 + O(k) の memmove) — 一般的なカーネルサイズで L1 キャッシュに収まる、ツリー構造ではなく
- マルチスレッド実行 — 行と列がすべてのコアで並列処理される
- 結果: このハードウェアで scipy 版の約 20 倍高速。そしてブラウザビルドがスレッドを取り上げたとき最も失うルーチンでもある
メディアンフィルター(とスライディングメディアン)とは?
メディアンフィルター は、各値を小さな窓内の近傍の中央値で置き換えます。平均と違ってスパイクや外れ値を取り除きつつエッジを保つため、HPSS で使われます。水平方向のメディアンは定常的な(倍音)ラインを、垂直方向のメディアンは鋭い(打撃)成分を残します。スライディングメディアン は、毎回ソートし直すのではなく窓を動かしながらこれを効率よく計算します。
pYIN(12.8 倍): ネイティブの YIN 差分とビタビ復号
pYIN のコストはフレームごとの候補評価とビタビ復号です。libsonare は両方を C++ で実装して librosa の Numba-JIT 内ループを置き換え、YIN 差分関数は直接計算ではなく FFT 経由で求めています。
支配的なのは復号のほうです。デフォルトの 65〜2093 Hz では格子の状態数が 1,202、状態あたりの到達可能な遷移が約 100 あるため、73 秒のクリップで内側の更新を数億回通ります。そこから外に出せるものはすべて出してあり、遷移重みは対数として保持し、有声・無声の切り替えコストは定数です。残るのは倍精度の比較と加算だけになります。
この経路はシングルスレッドです。HPSS と違ってコアを増やしても速くなりませんが、同じ理由でブラウザでも失うものがありません。WASM のペナルティは 1.02 倍です。
クロマ(2.84 倍): STFT → フィルタバンクの密なパス
クロマはスペクトログラムから 12 ピッチクラス表現を constant-Q 風のフィルタバンク経由で導出します。libsonare の STFT とフィルタバンク乗算は単一の連続バッファ上の Eigen3 ベクトル化行列演算として動くため、librosa の NumPy 演算スタックのディスパッチオーバーヘッドを回避できます。
速くならない箇所(とその理由)
- STFT 単体: librosa は
scipy.fftに委譲しており、これは C/Fortran 実装。約 7% 向こうが速い。処理がすでに C にあるので、C++ で書いたところで得るものがない。 - Mel / MFCC / オンセット強度: 基底の STFT コストに支配される。STFT 後のフレームごとの Mel フィルタバンク乗算や DCT は、別言語で書いても差が出るほど重くない。3 つとも librosa が 1 割ほど速い。
- ビートトラッキング: 単体で呼ぶと librosa より遅い。しかも負け幅は全行中で最大。他の情報と一緒にビートが必要なら、オンセット包絡線を共有する
analyze()を使う。 - パイプライン内での利用:
analyze()内ではこれらの特徴量は <1ms で動きます。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 です。pYIN のようなシングルスレッドの経路はほぼそのまま持ち越せます。ネイティブの値を勘で割り引くのではなく、上の WASM ベンチをビルドして対象のランタイムで測ってください。