Androidその2 Advent Calendar 2016
の5日目の記事です!
作成してるアプリで音楽のBPMを知りたかったので
音声ファイルから音楽のBPMを調べるプログラムを書いてみました。
記事として書いてみましたがわかってないところも多い為
おかしいところがあるかもしれません。
そこはご指摘いただけると嬉しいですm(_ _)m
手順
- 音声データをデコードする
- デコードしたデータをサンプルごとに格納
- フレームごとの音量を求める
- 隣り合うフレームの音量の増加分を求める
- どのテンポがマッチするかを求める
って手順でやりました!
ここのサイトをほぼほぼ参考にさせていただきました。
http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html
詳しいところを知りたい方はこっちをみてください。
今回利用した音源
今回の音源は以下のようなメトロノームの音を利用しました。
- サンプリングレート:48000
- チャンネル数:1
- ビット/サンプル:16bit
- BPM:120
- 秒数:30s
書いてみたコード
音声データをデコードする
MediaCodecっていう低レベルのメディアコーディック(エンコーダ/デコーダ)にアクセスできるapiを利用してデコードを行いました。
デコーダの作成
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 音楽のデータなら
if (mime.startsWith("audio/")) {
extractor.selectTrack(i);
try {
decoder = MediaCodec.createDecoderByType(mime);
} catch (IOException e) {
e.printStackTrace();
}
decoder.configure(format, null, null, 0);
break;
}
}
デコード
decoder.start();
ByteBuffer[] inputBuffers = decoder.getInputBuffers();
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean isDecoding = true;
while (!Thread.interrupted()) {
if (isDecoding) {
int inIndex = decoder.dequeueInputBuffer(TIMEOUT_US);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
// 終了
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isDecoding = false;
} else {
// 次へ
decoder.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
}
int outIndex = decoder.dequeueOutputBuffer(info, TIMEOUT_US);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = decoder.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG, "New format " + decoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "time out");
break;
default:
ByteBuffer buffer = outputBuffers[outIndex];
final byte[] chunk = new byte[info.size];
buffer.get(chunk);
buffer.clear();
soundDataList.add(chunk);
decoder.releaseOutputBuffer(outIndex, true);
break;
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
}
decoder.stop();
decoder.release();
extractor.release();
デコードしたデータをサンプルごとに格納
今回はサンプリングレートが48000なので
1秒間に48000のサンプルが入ってることになります。
今回はビット/サンプルが16bitなので
2バイト分づつsamples[]
に格納していきます
int c = 0;
for (int i = 0; i < soundDataList.size(); i++) {
byte[] chunk = (byte[]) soundDataList.get(i);
for (int j = 0; j < chunk.length; j = j + 2) {
int value = 0;
value = (value << 8) + (chunk[j]);
value = (value << 8) + (chunk[j + 1]);
samples[c] = value;
c++;
}
}
フレームごとの音量を求める
1フレームのサンプル数は参考サイト同様512としています。
// フレームの数
int n = c / FRAME_LEN;
// フレームごとの音量を求める
double[] vols = new double[n];
for (int i = 0; i < n; i++) {
double vol = 0;
for (int j = 0; j < FRAME_LEN; j++) {
int sound = samples[i * FRAME_LEN + j];
vol += Math.pow(sound, 2);
}
vol = Math.sqrt((1.0 / FRAME_LEN) * vol);
vols[i] = vol;
}
隣り合うフレームの音量の増加分を求める
// 隣り合うフレームの音量の増加分を求める
double[] diffs = new double[n];
for (int i = 0; i < n - 1; i++) {
double diff = vols[i] - vols[i + 1];
if (diff > 0) {
diffs[i] = diff;
} else {
diffs[i] = 0;
}
}
下の図は音量の増加分の一部をグラフにしたものです
横が時間軸で縦が音量の増加分
どのテンポがマッチするかを求める
増加量の時間変化の周波数成分を求めて
double s = (double) sampleRate / FRAME_LEN;
double[] a = new double[240 - 60 + 1];
double[] b = new double[240 - 60 + 1];
double[] r = new double[240 - 60 + 1];
for (int bpm = 60; bpm <= 240; bpm++) {
double aSum = 0;
double bSum = 0;
double f = (double) bpm / 60;
for (int i = 0; i < n; i++) {
aSum += diffs[i] * Math.cos(2.0 * Math.PI * f * i / s);
bSum += diffs[i] * Math.sin(2.0 * Math.PI * f * i / s);
}
double aTmp = aSum / n;
double bTmp = bSum / n;
a[bpm - 60] = aTmp;
b[bpm - 60] = bTmp;
r[bpm - 60] = Math.sqrt(Math.pow(aTmp, 2) + Math.pow(bTmp, 2));
}
一番マッチする配列のインデックスを求める
int maxIndex = -1;
// 一番マッチするインデックスを求める
double dy = 0;
for (int i = 1; i < 240 - 60 + 1; ++i) {
double dyPre = dy;
dy = r[i] - r[i - 1];
if (dyPre > 0 && dy <= 0) {
if (maxIndex < 0 || r[i - 1] > r[maxIndex]) {
maxIndex = i - 1;
}
}
}
実際のBPMはmaxIndex + 60
になります。
(maxIndexは配列の要素数であるため)
実行した結果、120という値が出てきました。
これのソースはgithubにあげてます。
https://github.com/music431per/MusicBpm
最後に
BPMを調べてみて
ぴったり120という値が出てきてびっくりしました。
同様にテンポ100のメトロノームの音源でも試してみましたが同じく
ぴったりの値が出てきました。
今回はメトロノームの音源という
テンポがわかりやすそうな音源を利用しましたが
まだ、音楽のデータで実際にBPMは取れていません。
チャンネル数が違うため、またこまかなところが違うのかなと思います。
音楽でも実際にBPMを取れるようにぐぐっていこうと思いますが
2チャンネルの場合どんな風にBPMを調べればいいかなど
わかる方、アドバイスいただけると嬉しいです。
では!
参考
MediaCodecのドキュメント
https://developer.android.com/reference/android/media/MediaCodec.html
MediaCodecのドキュメント和訳
http://qiita.com/imatomi/items/bd9d49cfb1f73383ca12
デコードの時に参考にしたコード
https://github.com/taehwandev/MediaCodecExample
https://github.com/vecio/MediaCodecDemo
C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム
http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html
メトロノームの音源取得
http://metronomer.com/