Androidで音声ファイルから音楽のBPMを調べてみた

[Androidその２ Advent Calendar 2016](http://qiita.com/advent-calendar/2016/android_second)
の5日目の記事です！

作成してるアプリで音楽のBPMを知りたかったので
音声ファイルから音楽のBPMを調べるプログラムを書いてみました。

記事として書いてみましたがわかってないところも多い為
おかしいところがあるかもしれません。
そこはご指摘いただけると嬉しいですm(_ _)m

## 手順

1. 音声データをデコードする
2. デコードしたデータをサンプルごとに格納
3. フレームごとの音量を求める
4. 隣り合うフレームの音量の増加分を求める
5. どのテンポがマッチするかを求める

って手順でやりました！

ここのサイトをほぼほぼ参考にさせていただきました。
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;
            }
        }

```

下の図は音量の増加分の一部をグラフにしたものです
横が時間軸で縦が音量の増加分
![図1.png](https://qiita-image-store.s3.amazonaws.com/0/64452/02f9f8d8-8643-ce47-d352-ed631d87c5d1.png)




### どのテンポがマッチするかを求める

増加量の時間変化の周波数成分を求めて

```
		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/