読者です 読者をやめる 読者になる 読者になる

asumism

あすみん(@an_asumin)のブログやで

AndroidのVisualizerでFFTするぽよ


やってみるぽよ(´・_・`)

AndroidとかiOS*1のプログラミングに関すること,プログラマな人たちが日夜ブログとかにコードの断片を添えて記事にしてくれているので,リファレンスを隅々読まなくても,ググった情報で結構なんとかなったりする.ありがたい話である.

でも,AndroidのVisualizerってAPI level 9からあるのにあんまり情報がなくて,なんだかつらぽよ.あんまりお仕事で使わないからかな…私だって周波数に合わせてギュンギュン動くビジュアライザ作りたいょ…(´・_・`)

幸い,波形の扱い方についてはTechboosterさんが記事で紹介してくれているよ.

じゃあ今回はVisualizerのもう一つの機能であるFFTをブログ記事にしてみよ〜!

Viewを作る前に

Viewは用意されていないので,自分でViewを作ろう.
その前に……

パーミッション

Visualizerには android.permission.RECORD_AUDIO が必要です.

	<uses-permission android:name="android.permission.RECORD_AUDIO" />

Visualizerの使い方

まず,Visualizerを作ってViewにデータを渡してあげるところまでを見てみよう.ActivityとかFragmentとか作るときにこんな感じに一緒に書けばよいと思う.リスナを設定して,データが来たら投げる感じ.
ちなみに,androidはフィールド名の頭に'm'つけようみたいなのあったけど,知ったのがわりと最近で,倣ってなくて,よくない感じする*2

// セッションIDで紐付ける
visualizer_ = new Visualizer(musicPlayerService_.getAudioSessionId());
// enabledな状態ではsetCaptureSizeが設定できないので明示的に切る
visualizer_.setEnabled(false);
// キャプチャサイズは2のべき乗で,
// getCaptureSizeRangeで取れる配列の[0]から[1]の間の値を設定
visualizer_.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
// リスナを設定する
visualizer_.setDataCaptureListener(new OnDataCaptureListener() {
	@Override
	public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
	}
	@Override
	public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
		// データを受け取ったらViewに投げる
		fftView_.update(fft);
	}
}, 
Visualizer.getMaxCaptureRate(), false/*waveform*/, true/*FFT*/);
// 有効な状態に戻す
visualizer_.setEnabled(true);
// サンプリング周波数を事前に教えてあげる
fftView_.setSamplingRate(visualizer_.getSamplingRate());

一旦,setEnabled(false)するの,最近のOSバージョンだと問題ないけど,2.3とかだと初期状態がよろしくないらしいので,記述しておくといいっぽい.

セッションIDについて

Visualizer作るときに,MediaPlayerかAudioTrackのセッションIDがいるんだけど,0を設定すると出力ミックスが設定されるらしく,デバイスが全体として鳴らしてる音を扱いたい場合に便利っぽい*3.これを使う場合は, android.permission.MODIFY_AUDIO_SETTINGS が必要だから注意ね.

	<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

VisualizerのFFTでの注意点

FFTのデータに関することはリファレンスに少しだけ説明が載ってるけど,結構つらぽよが溢れる感じになってるので整理する.特に,最後の方の周波数の計算はリファレンスの説明が食い違ってる部分なので注意*4

  • データは8bit(byte)の深さ
  • データ長はgetCaptureSize()と一致
  • 始めのデータは直流成分(0Hz/DC)の値
    • 波形全体の高さみたいなものであり,信号処理では波形の形が重要なので切りわけて扱われる
  • その次のデータは一番高い周波数の実部の値
    • 一番高い周波数とは設定したサンプリング周波数の半分の周波数*5
  • それ以降のデータは実部と虚部がニコイチで交互に並んでいる
    • 実質的には3,4番目のデータが1つ目のデータ
    • よって,実部と虚部が揃うデータ数は,データ長をnとしたとき,(n/2)-1個*6
    • 虚部は位相を表現する
    • 周波数は順に高くなり,データ長をn,サンプリング周波数をFsとした時,k番目のデータの周波数は(k*Fs/2)/(n/2)
    • getSamplingRate()とかで返ってくるのはミリヘルツなので注意

Viewを作る

独自Viewの作り方は一般的なのでここでは特に触れませんが,データをビジュアライズする上でのポイントってのがあるようなので書いときます.
主に,

  • 縦方向をデシベル表示にする
  • 横方向を対数表示にする

ってところです.
ヒトは対数変換して刺激を知覚しているよ*7っていうウェーバーフェヒナーの法則があるからなんだけど,これは別のお話じゃ.

描画処理

データを描画する部分を見てみましょう.

// Viewのサイズ情報
int top = getTop();
int height = getHeight();
int bottom = getBottom();
int right = getRight();
int left = getLeft();

// データの個数
int fftNum = fftData_.length / 2;

// 直流成分(0番目)以外を計算する
for(int i = 1; i < fftNum; ++i){
	// 注目しているデータの周波数
	float frequency = (float) (i * samplingRate_ / 2) / fftNum;
	// 振幅スペクトルからデシベル数を計算
	float amplitude = (float) Math.sqrt(Math.pow((float)fftData_[i * 2], 2) + Math.pow((float)fftData_[i * 2 + 1], 2));
	float db = (float) (20.0f * Math.log10(amplitude / FFT_PEAK_VALUE));
	// 描画 (Yは下方向を正にとる)
	float x = (float) (Math.log10(frequency) * logBlockWidth_) - logOffsetX_;
	if(x >= left && x <= right){
		float y = (float) (top - db / -DISPLAY_MINIMUM_DB * height );
		canvas.drawLine(x, bottom, x, y, fftDataPaint_);
	}
}

まず,上述した内容を踏まえて,注目しているデータの周波数を計算します.
次に振れ幅スペクトルを計算します.ここでルートを取らなければパワースペクトルというものになるらしいです.ビジュアライズ目的であればどちらを使っても構わないように思います.そして,ピーク値*8との比をデシベルにすることで表示するデータの高さを求めています.横方向も対数にしています.周波数を対数変換してオフセットの分だけずらすことで表示位置を決定しています.

このようにすると,左の様な見た目で表示できます.音の周波数に合わせてピョコピョコ動くのが確認できます.ただ,このままでは見た目があまり良くないので調整していきます.

気になる点を調整していく

データの見た目の不均一さ

対数変換して表示しましたが,降ってくるデータは等間隔の周波数データであるため先の画像のようにデータの密度が不均一で不格好です.よって,等しい大きさのバーを並べて,その区間での最大値を表示するようにしてみました.

かなーり,それっぽい感じになりましたね!シェーダの色の変化がキレイに見えるようになりました.この画像では16本のバンドで表示させているのですが,低音域ではデータの密度が低いのでこれ以上バンド数を増やすことは難しそうです.

表示する最小デシベル

対数の特性上,信号がないと-infinityになっちゃうと思うのですが,どれくらいのデシベル数まで有効なのでしょうか.試しに,私のアンプ設計マニュアルさんが公開されているサンプル音源(1kHz/0db)を鳴らしてみてみました.

1kHzで0dbに近い値が出ているので,FFTのビジュアライズ自体はいい感じにできているようですね!(∩´∀`)∩ワーイ
ただ,1kHz以外の領域を見ると低音側で-28dbくらいまで出てしまっているようです.おそらくデータの深さが8bitしかないからなのでしょう.もうちょっと抑えこんで欲しい感じがありますが,表示するとしたら-30dbくらいまでが妥当ではないでしょうか.

その他

恥ずかしながら,ViewのコードをGistに上げました.何か間違いがあればこっそり教えてくださると嬉ぽよです.


VisualizerのFFTが何をやってるのか気になって,コードを直接見たいけど面倒だなって感じだったんですけど,Androidソースコード検索サービスなるものがあって,便利だったので合わせて載せておきます.ちなみに,FFTC++のシフト演算なループで書かれていて,それを見て満足しました(?).私は高速フーリエ変換アルゴリズムについてはさほど詳しくないもじゃ(´~`)

今回はこのへんで!)^o^(

*1:私はアンヨヨイヨデバイスしか持ってない₍₍⁽⁽(ી( ˚เ°)ʃ)₎₎⁾⁾

*2:詳しくは Code Style for Contributors | Android Open Source Project

*3:詳しくは Visualizer | Android Developers

*4:リファレンスだと(k*Fs)/(n/2)となっていて,私が勘違いしていなければ,これだと最大でナイキスト周波数を超えたサンプリング周波数領域の値が取得できることになって,すなわちFFTでもなんでもなかったみたいなことになる(´・ω:;.:...

*5:ナイキスト周波数の関係上こうなってる

*6:リファレンスだと(n-1)/2ってなってるケド,さ……

*7:近似できるという意味で

*8:信号の値が最大のときの振れ幅スペクトル