Swiftで音楽のBPMを算出するコードを作ってみた

# はじめに
[「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」](https://qiita.com/programanx1/items/8912e60843bd824d74ce)、[「Androidで音声ファイルから音楽のBPMを調べてみた」](https://qiita.com/music431per/items/8d687b49afee0d7ccfdf)や、[「C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム」](http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html)を参考にSwiftコードで作成しました。

# 手順

1. オーディオファイルを読み込み、各チャネルの音源データをアクセスできるようにする
2. フレーム毎の音量を求める
3. 隣り合うフレームの音量の増加分を求める
4. どのテンポにマッチするか求める

オーディオファイルの読み込みは、[「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」](https://qiita.com/programanx1/items/8912e60843bd824d74ce)を参考にしました。

また、その他の部分については[「Androidで音声ファイルから音楽のBPMを調べてみた」](https://qiita.com/music431per/items/8d687b49afee0d7ccfdf)と、[「C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム」](http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html)を参考に作成しました。

# 音源

テストにあたりいくつかの音源を[メトロノーム音源集](http://yo-kan.com/mtronome/)より利用しました。

# コードの紹介

作成したサンプルプロジェクトは[Github](https://github.com/FromF/MusicBPM)で公開しています。
紹介します。

## オーディオファイルを読み込み、音源データをアクセスできるようにする

[「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」](https://qiita.com/programanx1/items/8912e60843bd824d74ce) を参考にコーディングしました。 

```swift
        do {
            audioFile = try AVAudioFile(forReading: fileURL)
            samplingRate = audioFile?.fileFormat.sampleRate
            nChannel = Int(audioFile?.fileFormat.channelCount ?? 0)
        } catch {
            print("Error : loading audio file failed.")
        }
        
        guard let audioFile = audioFile else {
            return false
        }
        
        guard let nChannel = nChannel else {
            return false
        }
        
        nframe = Int(audioFile.length)
        
        guard let nframe = nframe else {
            return false
        }
        
        PCMBuffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: AVAudioFrameCount(nframe))
        
        guard let floatChannelData = PCMBuffer.floatChannelData else {
            return false
        }
        
        do {
            try audioFile.read(into: PCMBuffer)
            
            buffer.removeAll()
            for i in 0 ..< nChannel {
                let buf:[Float] = Array(UnsafeMutableBufferPointer(start: floatChannelData[i], count: nframe))
                buffer.append(buf)
            }
        }catch{
            print("loading audio data failed.")
        }
```

## フレーム毎の音量を求める

今回は、1チャネルのみフレーム毎に音量を求めました。
参考サイト同様に `frameLength = 512` としました。

```swift
        guard let nframe = nframe else {
            return false
        }
        
        // フレームの数
        let n = nframe / frameLength
        
        vols.removeAll()
        
        for i in 0 ..< n {
            var vol:Double = 0
            for j in 0 ..< frameLength {
                let idx = i * frameLength + j
                let sound = Double(buffer[0][idx])
                vol += pow(sound, 2)
            }
            let vol2 = sqrt((1.0 / Double(frameLength)) * vol)
            vols.append(vol2)
        }
```

## 隣り合うフレームの音量の増加分を求める

配列 `diff` に増加分を格納しています。
参考サイト同様に、三項演算子を用いて負の値は0としています。

```swift
        guard let nframe = nframe else {
            return false
        }
        
        // フレームの数
        let n = nframe / frameLength
        
        diffs.removeAll()
        
        for i in 0 ..< n - 1 {
            let value = vols[i] - vols[ i + 1]
            let diff = value > 0 ? value : 0
            diffs.append(diff)
        }
        diffs.append(0)
```

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

参考サイトと同様なのですが、Double型に型を合わせる必要があります。
したがって `Double(i)` 、 `Double(n)` としないと、計算過程がInt型となってしまい小数点以下の値が切り捨てされてしまいます。
注意が必要です。

```swift
        guard let nframe = nframe else {
            return errorBPM
        }
        guard let samplingRate = samplingRate else {
            return errorBPM
        }
        
        // 最大最小テンポ
        let minBPM = 60
        let maxBPM = 240
        
        // フレームの数
        let n = nframe / frameLength
        
        let s = samplingRate / Double(frameLength)
        
        var a:[Double] = []
        var b:[Double] = []
        var r:[Double] = []
        
        for bpm in minBPM ... maxBPM {
            var aSum:Double = 0
            var bSum:Double = 0
            let f = Double(bpm) / Double(60)
            for i in 0 ..< n {
                aSum += diffs[i] * cos(2.0 * Double.pi * f * Double(i) / s)
                bSum += diffs[i] * sin(2.0 * Double.pi * f * Double(i) / s)
            }
            let aTMP = aSum / Double(n)
            let bTMP = bSum / Double(n)
            a.append(aTMP)
            b.append(bTMP)
            r.append(sqrt(pow(aTMP, 2) + pow(bTMP, 2)))
        }
        
        var maxIndex = errorBPM
        
        // 一番マッチするインデックスを求める
        var dy:Double = 0
        for i in 1 ..< (maxBPM - minBPM + 1) {
            let dyPre = dy
            dy = r[i] - r[i - 1]
            if dyPre > 0 && dy <= 0 {
                if maxIndex < 0 || r[i - 1] > r[maxIndex] {
                    maxIndex = i - 1
                }
            }
        }
        
        if maxIndex < 0 {
            return errorBPM
        }
```

# 最後に

参考サイトを見ながらSwift化してみました。
Double型で数値計算するので整数型の変数などがある場合にはキャストをしないと小数点以下が切り捨てされてしまい狙いの計算ができなくなることに気づくのに数時間消費してしまいました。
間違えなどありましたら指摘コメントお願いします。

ありがとうございました。

# 参考サイトなど

* [「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」](https://qiita.com/programanx1/items/8912e60843bd824d74ce)
* [「Androidで音声ファイルから音楽のBPMを調べてみた」](https://qiita.com/music431per/items/8d687b49afee0d7ccfdf)
* [「C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム」](http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html)
* [メトロノーム音源集](http://yo-kan.com/mtronome/)