LoginSignup
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-05

Androidその2 Advent Calendar 2016
の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

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

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

        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/

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13