原文: Onset Detection Part 7: Thresholding & Peak picking

前の記事では時間が経つにつれ徐々に進化する複雑なスペクトラムを、スペクトラル・フラックスというシンプルな 1 次元の関数に変換する方法を見た。その途中で、整流 (rectify) したり音声サンプルのハミングによる平滑化を有効化したり、スペクトラル・フラックスの機能にいくつか改善を加えた。この記事ではスペクトラル・フラックスの機能から派生して、いわゆるしきい値の機能について論じることにしよう。

それは、整流 (rectify) されハミングで平滑化されたスペクトラル・フラックスの機能だ。”A Perfect Circle” の “Judith” という曲の抜粋に利用したら、音の立ち上がりとしてすぐに解釈可能ないくつかのピークが表示された。しかしながらこの機能には、たとえばクラッシュが叩かれたときなどは特に、まだいくらかノイズが混じる。たとえば少しこのサンプルを平滑化させるといった、もっとよいフィルタリングを行うことはできない。というのはたくさんの小さなピークを取り除いてしまうからだ。代わりにシンプルなことを行う。スペクトラム・フラックス機能から派生したしきい値機能を計算するのだ。このしきい値機能はのちにノイズであるスペクトラル・フラックスの値を捨てるために利用される。

しきい値機能は実際にはかなりシンプルだ。スペクトラル・フラックス機能があったとしてそれぞれのスペクトラル・フラックスの値のウィンドウの平均値を計算する。たとえば 3000 個のスペクトラル・フラックス値があり、それぞれが 1024 個のサンプル幅を持つスペクトラル・フラックスを表している。サンプリングレート 44100Hz の 1024 個のサンプルは約 43ms の時間に相当する。しきい値の機能に利用する幅のサイズはなんらかの時間の値から派生させるべきであり、たとえば 0.5 秒間のスペクトラル・フラックスの平均値がほしい。つまり 0.5 / 0.0043 = 11 個のサンプル幅、すなわち 11 個のスペクトラル・フラックス値の平均だ。それぞれのスペクトラル・フラックス値について、直前の 5 個と直後の 5 個の値と現在の値をとり、平均値を計算する。このようにしてそれぞれのスペクトラル・フラックス値についてしきい値と呼ばれる 1 の値が得られる。(その理由はすぐに明らかになる。) これを行うコードが下記だ:

public class Threshold 
{
   public static final String FILE = "samples/explosivo.mp3";	
   public static final int THRESHOLD_WINDOW_SIZE = 10;
   public static final float MULTIPLIER = 1.5f;
	
   public static void main( String[] argv ) throws Exception
   {
      MP3Decoder decoder = new MP3Decoder( new FileInputStream( FILE  ) );							
      FFT fft = new FFT( 1024, 44100 );
      fft.window( FFT.HAMMING );
      float[] samples = new float[1024];
      float[] spectrum = new float[1024 / 2 + 1];
      float[] lastSpectrum = new float[1024 / 2 + 1];
      List spectralFlux = new ArrayList( );
      List threshold = new ArrayList( );
	
      while( decoder.readSamples( samples ) > 0 )
      {			
         fft.forward( samples );
         System.arraycopy( spectrum, 0, lastSpectrum, 0, spectrum.length ); 
         System.arraycopy( fft.getSpectrum(), 0, spectrum, 0, spectrum.length );
	
         float flux = 0;
         for( int i = 0; i < spectrum.length; i++ )	
         {
            float value = (spectrum[i] - lastSpectrum[i]);
            flux += value < 0? 0: value;
         }
         spectralFlux.add( flux );					
      }		
		
      for( int i = 0; i < spectralFlux.size(); i++ )
      {
         int start = Math.max( 0, i - THRESHOLD_WINDOW_SIZE );
         int end = Math.min( spectralFlux.size() - 1, i + THRESHOLD_WINDOW_SIZE );
         float mean = 0;
         for( int j = start; j <= end; j++ )
            mean += spectralFlux.get(j);
         mean /= (end - start);
         threshold.add( mean * MULTIPLIER );
      }
		
      Plot plot = new Plot( "Spectral Flux", 1024, 512 );
      plot.plot( spectralFlux, 1, Color.red );		
      plot.plot( threshold, 1, Color.green ) ;
      new PlaybackVisualizer( plot, 1024, new MP3Decoder( new FileInputStream( FILE ) ) );
   }
}

目新しいことはそんなに出てきていない。最初に前の記事でやったようにスペクトラル・フラックスの機能を計算する。それから、このスペクトラル・フラックスに基づきしきい値の機能を計算する。spectralFlux ArrayList の中のそれぞれのスペクトラル・フラックス値について THRESHOLD_WINDOW_SIZE 分の前後のスペクトラル・フラックス値をとり、平均を計算する。その結果の平均値は threshold という名前の ArrayList に保存される。それぞれのしきい値を MULTIPLIER (この例では 1.5 に設定されている) でかけていることにも注目してほしい。すべての機能の計算が終わったらプロットする。その結果は下記のようになる:

(TODO: 画像)

今やったのはスペクトラル・フラックス機能の移動平均値 (running average) の計算だ。これを使えばいわゆる異常値を検出することができる。しきい値機能より高い値を持つものはなんでも異常値でありなんらかの音の立ち上がりを示している! しきい値の機能の値を 1 より大きい値でかけている理由も明確だろう。異常値は平均値よりも大きくなければならず、この場合では平均より 1.5 倍大きくなければならない。この掛け算の値は感度をコントロールするため、音の立ち上がり検出器にとって重要なパラメーターだ。しかしながら、検出器をいくつかの曲に適用したとき、すべてでうまくいくような 1 つの値が見つけ出そうとしてみた。実際にやってみて上記で利用している 1.5 という掛け算の値に行き着いた。この値は svn で見つかるすべてのサンプルや、私がテストした他のたくさんの曲でもうまく機能した。

さてスペクトラル・フラックス機能としきい値機能を連結させよう。基本的にはしきい値機能以上の値を持つ除去版スペクトラルフラックス機能がほしい。上記のプログラムを拡張し、下記の行を追加する:

for( int i = 0; i < threshold.size(); i++ )
{
   if( threshold.get(i) <= spectralFlux.get(i) )
      prunnedSpectralFlux.add( spectralFlux.get(i) - threshold.get(i) );
   else
      prunnedSpectralFlux.add( (float)0 );
}

prunnedSpectralFlux 変数はただの ArrayList だ。ループは極めてシンプルで、現在のスペクトラル・フラックスが対応するしきい値機能より小さい場合には prunnedSpectalFlux に 0 を追加し、そうでない場合には i の位置のスペクトラル・フラックス値からしきい値を引いたものを追加する。結果の除去版スペクトラル・フラックス機能は下記のようになる:

(TODO: 画像)

すばらしい、あとちょっとで完了だ! 残りはこの除去版スペクトラル・フラックスの中のピークを探すだけだ。ピークは次の値より大きい値のことだ。ピーク検出というのはただそれだけのことだ。ほぼ完了したようなもの。peak という ArrayList を生成するピーク検出の小さなコードを書いてみよう:

for( int i = 0; i < prunnedSpectralFlux.size() - 1; i++ )
{
   if( prunnedSpectralFlux.get(i) > prunnedSpectralFlux.get(i+1) )
      peaks.add( prunnedSpectralFlux.get(i) );
   else
      peaks.add( (float)0 );				
}

これだけだ。peaks 配列の中で 0 より大きい値はどれも音の立ち上がり、つまりビートだ。peaks の中のそれぞれのピークについて時間的な位置を計算したければ、単にインデクスをとり、元のサンプル幅が持っていた時間幅をかければよい。たとえば今回はサンプリングレート 44100Hz で 1024 個のサンプルから成るサンプル幅を使ったので、シンプルな式、時間 = インデクス * (1024 / 44100) という式で計算可能だ。それだけ。出力結果は下記の通り:

完了。ほぼ完了。これが書きうる中でもっとも基本的な音の立ち上がり検出器だ。いくつか検出器の性質を復習しよう。多くのシステムと同じように調整できるパラメーターが 2,3 個ある。最初のパラメーターはサンプル幅のサイズ。私はいつも 1024 を使う。この値を低くするともっと精緻なスペクトラムが得られるがあまり利益はないように思う。次のパラメーターは FFT を行う前にハミングによる平滑化を使うかどうか。実際これをオンにすると少し改善されることは確認したが、これはまた計算を行うのにいくらか CPU サイクルのコストがかかる。これは、FPU を持たないモバイルデバイスで音の立ち上がり検出を行う計画がある場合には重要な観点となるだろう。そのような場合では削減できる CPU サイクルはすべて削減すべきだからだ。次は、しきい値の幅のサイズ。これは検出段階での結果に甚大な影響を及ぼす。私は上記の例にもあるように、いつも合計にしてスペクトラル・フラックスの値 20 個分の幅を使うようにしている。これはその 20 個の値が表しているのがどのくらいの長さの時間なのかを計算することがモチベーションとなっている。サンプリングレート 44100Hz で 1024 サンプルのサンプル幅の場合には、だいたい a second worth (TODO) のデータだ。この値はシンプルな検出器でテストしたすべてのジャンルでうまく動いたが、読者諸君の走行距離は異なるかもしれない。最後のパラメーターがおそらくもっとも重要で、しきい値の機能のための掛け算の値である。この値を低くすると検出器が敏感になり、高くすると何も検出されなくなる。私の日々の仕事ではここでやったのにかなり似ている異常値分析をたまに行わなければならない。時々シンプルなしきい値をベースとした手法を用いるが、1.3-1.6 の間の値は、ある平均値に関連した異常値検出をベースとした統計を行うときには、ある意味マジックナンバーのように見える。しきい値の掛け算の値と、しきい値の幅の数だけがチューニングすべきパラメーターだと言ってもいいぐらいだろう。ほかは忘れて、デフォルトの通り 1024 のサンプル幅数を使い、ハミングによる平滑化をオンにしておけばよい。たくさんのパラメーターの中でどれが有効なのか探すのはぜんぜんおもしろくないし、結局のところこの場合では 2 つ「だけ」に意味があったというわけだ。上記で利用したデフォルト値はこのシリーズで開発した検出器でまあまあうまく動く。複数の周波数帯の分析を行う際には、それぞれの周波数帯でそれらの値をチューニングしなければならない。

いくつか改善の余地はあって、たとえばこのプロセス全体をスペクトラム全体ではなく下位の周波数帯へ適用し、さまざまな楽器の音の立ち上がり検出ができるようにすることだ。(さまざまな楽器の周波数帯が重なり合っているから、完全にやるのは難しいが。) 別の改善としては重なりあうサンプルの幅を使い、よりよい結果を得ること。FT へ渡す前に音声信号全体にダイナミックレンジ圧縮を行ってみるとか、いろいろ試すことはある。フレームワーク中のコードを使えば、さまざまな追加のステップを簡単に実験することができる。あと、結果として得られたピークの一覧を整理し、たとえば 10ms 以内とか、互いに近すぎるピークを削除したほうがいいかもしれない。

下記は今回の音の立ち上がり検出器をうまく動かす過程で非常に役立った論文やリンクだ:

  • http://www.dspguide.com/ すばらしいオンライン書籍 (ハードカバーでも購入可能) “The Scientist and Engineer’s Guide to Digital Signal Processing” (エンジニアとして :)) デジタル信号処理について知りたいと思うようなことはなんでもフィーチャーされている。
  • https://ccrma.stanford.edu/~jos/dft/ もう 1 つのオンライン書籍 “Mathematics of the discrete Fourier Transform with audio applications” 離散フーリエ変換に関することならなんでも使っている。かなり数学寄りだが読むと非常に参考になる。
  • http://old.lam.jussieu.fr/src/Membres/Daudet/Publications_files/Onset_Tutorial.pdf 非常に品質のよい科学論文 “A tutorial on onset detection for musical signals” さまざまな音の立ち上がり検出の機能がフィーチャーされているし (今回は un-banded (TODO) スペクトラル・フラックスを利用した) some data on the performance of various functions on a standardized data set (Mirex 2006 Onset Detection Challenge). Bello はこの分野でのビッグ・プレイヤーらしいので、ほかの論文も探してみるとよい。
  • http://www.dafx.ca/proceedings/papers/p_133.pdf Dixon の “Onset detection revisited” このフィールドのもう一人のビッグネームだ。 (少なくとも引用の数からはそう見えるのだが、知っての通り引用の数というのは非常に重要だ…) 非常に良い論文、必読。

Google Scholar で上記 2 つの論文の引用グラフをチェックすれば、あなたの興味を引く論文がもっと見つかるはずだ。こんなときは Google 頼りになる。いつでもよい論文を読もう!