スペクトログラムと STFT
libsonare のほぼすべての MIR 機能(クロマ、メル、MFCC、オンセット強度、さらには BPM やキー)は、音声の時間-周波数表現から計算されます。この 1 つの土台を理解すれば、なぜ多くの関数が同じ nFft と hopLength を共有するのかが分かります。本ページは MIR の全体像 の地図より 1 段深く掘り下げます。
波形から周波数へ: FFT
波形は時間に対する振幅であり、どの周波数が含まれるかは教えてくれません。フーリエ変換はそれに答えます。信号を、それを合成する正弦波へ分解します。プリズムが光を色に分けるようなものです。FFT(高速フーリエ変換)は、それをサンプルのブロックに対して効率的に計算するアルゴリズムにすぎません。
問題は、1 回の FFT はブロック全体を、その周波数内容がまったく変化しないかのように記述する点です。音楽は絶えず変化するので、曲全体の単一 FFT はほとんど役に立ちません。
時間変化を追う: STFT
短時間フーリエ変換(STFT)は、音声を短く重なり合う窓に切り分け、それぞれに FFT をかけることでこれを解決します。結果は 2 次元のグリッドで、各時刻の周波数内容を表します。スペクトログラムはそのグリッドを画像として描いたもので、一方の軸が時間、もう一方が周波数、エネルギーが明るさです。
このグリッドを制御するため、2 つのパラメータがどこにでも現れます。
| パラメータ | 設定するもの | トレードオフ |
|---|---|---|
nFft | FFT の長さ(サンプル数。例 2048)。窓を別途短くしない限り、解析窓の長さも兼ねます | 窓が長いほど周波数が細かく、時間は粗くなります |
hopLength | 窓を進める量(例 512) | 小さいほど 1 秒あたりのフレームが増え(動きが滑らか)、CPU 負荷も増えます |
これが時間-周波数の分解能トレードオフです。完璧な周波数分解能と完璧な時間分解能を同時には得られません。これは物理的な限界であり、libsonare の制約ではありません。
厳密に言えば、このトレードオフを決めるのは変換の長さではなく窓の長さです。既定では両者は同じ値で、JS/WASM バインディング経由では常に一致します。C++ では StftConfig::win_length を n_fft より短く設定でき、その状態で n_fft を大きくしても各フレームのゼロ詰めが増えるだけです。ビンは増えて同じスペクトルを細かく補間しますが、実際の周波数分解能は上がらず、時間分解能も犠牲になりません。
サンプル数から秒とヘルツへ
どちらのパラメータもサンプル数で数えるので、時間や周波数としての意味はサンプルレート(sr、1 秒あたりのサンプル数)によって変わります。
| 量 | 式 | 22050 Hz で nFft 2048、hopLength 512 | 44100 Hz で同じ値 |
|---|---|---|---|
| 窓の長さ | nFft / sr | 92.9 ms | 46.4 ms |
| ホップの長さ | hopLength / sr | 23.2 ms | 11.6 ms |
| 1 秒あたりのフレーム数 | sr / hopLength | 43.1 | 86.1 |
| 周波数ビンの間隔 | sr / nFft | 10.8 Hz | 21.5 Hz |
| 周波数ビンの数 | nFft / 2 + 1 | 1025 | 1025 |
フレーム t の時刻(既定の中央合わせフレーミング) | t × hopLength / sr | フレーム 100 ≈ 2.32 秒 | フレーム 100 ≈ 1.16 秒 |
同じサンプル数でも、44100 Hz では窓の長さが半分、ビン間隔が 2 倍になります。音楽アナライザーが nFft を入力レートに合わせて換算し直すのはこのためで(下の詳細を参照)、窓の秒数をそろえています。
窓掛け
各ブロックは FFT の前に、端をゼロへなめらかに落とす窓関数(Hann 窓など)と掛け合わされます。これがないと、ブロック境界の急な切れ目が周波数全体へエネルギーを漏らし(スペクトル漏れ)、結果がにじみます。窓掛けはフレームが重なる理由でもあります。テーパーした端は隣のフレームが補います。
- 窓掛けされたフレーム
- 同じサンプルを再び解析する範囲
- スペクトログラムの列
音楽的な間隔: CQT と VQT
標準の STFT は周波数ビンを Hz で均等に並べます。しかし音名は対数的に並びます。オクターブごとに周波数が倍になるからです。
そのため均等な Hz のグリッドは、高域では分解能を浪費し、低域では足りなくなります。
- CQT のビン:半音ごとに 1 本
- STFT のビン:Hz 間隔が一定
| 変換 | 周波数の並べ方 | 使いどころ |
|---|---|---|
| STFT | Hz で均等 | 一般的な時間-周波数の見取り図が欲しいとき |
| CQT(定 Q 変換) | 音楽的な音程ごと。通常は半音ごとに 1 組で、オクターブあたり同じ分解能になる | ピッチの関係が重要なとき |
| VQT(可変 Q 変換) | ビンの中心周波数は CQT と同じ半音刻みのまま、低域のフィルターだけ帯域幅を広げる(Q を下げる)。帯域を広げたフィルターは時間的に短くなるため、低い音が時間方向ににじまない | CQT のような音高を意識したビンを使いつつ、ベースや低域の打撃成分の時間挙動をきれいに保ちたいとき |
libsonare が STFT をどう計算するか
libsonare の STFT・フレーミングユーティリティは、窓(既定で Hann)を適用し、hopLength ずつ進め、フレームごとに実数 FFT を実行して、メル・クロマ・オンセット・テンポグラムの各段が再利用する振幅/パワースペクトルを生成します。ただしこの再利用には範囲があります。analyze()(MusicAnalyzer)の内部では中間のスペクトログラムを 1 回だけ計算し、クロマ段とメル段がそれを共有しますが、chroma() や melSpectrogram() などの単独ヘルパー、およびキー・コード・セクションの各アナライザーは、それぞれが自前で STFT を計算します。FFT のコストを 1 回に抑えたい場合は、analyze() からまとめて特徴量を取得してください。nFft/hopLength の既定値(2048/512)は一般的な librosa の使い方に合わせ、リファレンステストで出力を比較できるようにしています。CQT/VQT は同じフレーミング規約の上に対数周波数ビンを重ねます。
窓長のサンプル数は、サンプルレートごとに異なる長さの時間を意味します。そのため、音声を本来のレートで受け取る解析器(ビート、テンポ、オンセット、キー、リズム、コードのビートグリッド、音色)は nFft を 22050 Hz でのサンプル数として読み、入力のレートに換算します。ホップは入力のサンプル数のままです。これにより、同じ素材は 22050、44100、48000 Hz のどれでも同じ長さの窓で解析されます。ストリーミング解析器は nFft を 44100 Hz でのサンプル数として読み(それより低いレートでは縮め、ホップより短くはしません)、ポリフォニー解析は nFft、winLength、hopLength を 44100 Hz で読みます。