Android에서 음성 파일에서 음악의 BPM을 살펴 보았습니다.

9158 단어 안드로이드audio
안드로이드 2 Advent Calendar 2016
5 일째 기사입니다!

만든 앱에서 음악의 BPM을 알고 싶었기 때문에
음성 파일에서 음악의 BPM을 조사하는 프로그램을 작성해 보았습니다.

기사로 써 보았지만 모르는 곳도 많기 때문에
이상한 곳이 있을지도 모릅니다.
거기에 지적해 주시면 기쁩니다 m(_ _)m

절차


  • 오디오 데이터 디코딩
  • 디코딩 된 데이터를 샘플 당 저장
  • 프레임 당 볼륨 얻기
  • 인접한 프레임의 음량의 증가분을 구한다
  • 어떤 템포가 매치할지를 찾는다

  • 라고 순서대로 했습니다!

    여기의 사이트를 거의 거의 참고로 했습니다.
    htp://hp.ゔ c와 r. 이. jp / 아우테 rs / ゔ 046927 / m포 / m포. 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이므로
    초당 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://魏Tub. 작은 m/벌레 C431 페r/벌레 cBpm

    마지막으로



    BPM을 살펴보십시오.
    딱 120이라는 값이 나와서 놀랐습니다.
    마찬가지로 템포 100 메트로놈의 음원에서도 시도해 보았습니다.
    딱 맞는 값이 나왔습니다.

    이번에는 메트로놈의 음원이라고합니다.
    템포가 알기 쉬운 음원을 이용했습니다.
    아직 음악 데이터로 실제로 BPM을 사용할 수 없습니다.

    채널 수가 다르기 때문에, 또 세세한 곳이 다른 것일까라고 생각합니다.
    음악에서도 실제로 BPM을 취할 수 있도록 가고 싶습니다.
    2채널의 경우 어떤 식으로 BPM을 조사하면 좋을지 등
    아는 분, 어드바이스 받을 수 있으면 기쁩니다.

    그럼!

    참고



    MediaCodec 문서
    htps : //에서 ゔぇぺぺr. 안 d로이 d. 코 m/레후오렌세/안 d로이 d/메아아/메아아코에서 c. HTML

    MediaCodec 문서 번역
    ぃ tp // 코 m / 지금 / ms / bd9d49cfb1f73383 또는 12

    디코드시에 참고로 한 코드
    htps : // 기주 b. 코 m / 타에 후 v / 메아 아코데세 mp ぇ
    htps : // 기주 b. 코 m / ょ ぇ 시오 / 메아 아코로 c에서도

    C/C++ 언어로 음성 파일의 템포 해석을 실시하는 샘플 프로그램
    htp://hp.ゔ c와 r. 이. jp / 아우테 rs / ゔ 046927 / m포 / m포. HTML

    메트로놈의 음원 획득
    htp : ///메 t로노메 r. 코m/

    좋은 웹페이지 즐겨찾기