Edited at

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


はじめに

「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」「Androidで音声ファイルから音楽のBPMを調べてみた」や、「C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム」を参考にSwiftコードで作成しました。


手順


  1. オーディオファイルを読み込み、各チャネルの音源データをアクセスできるようにする

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

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

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

オーディオファイルの読み込みは、「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」を参考にしました。

また、その他の部分については「Androidで音声ファイルから音楽のBPMを調べてみた」と、「C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム」を参考に作成しました。


音源

テストにあたりいくつかの音源をメトロノーム音源集より利用しました。


コードの紹介

作成したサンプルプロジェクトはGithubで公開しています。

紹介します。


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

「[AVFoundation][Swift] Swiftでオーディオ・音声分析への道 1 オーディオデータ読み込み、書き出し」 を参考にコーディングしました。

        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 としました。

        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としています。

        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型となってしまい小数点以下の値が切り捨てされてしまいます。

注意が必要です。

        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型で数値計算するので整数型の変数などがある場合にはキャストをしないと小数点以下が切り捨てされてしまい狙いの計算ができなくなることに気づくのに数時間消費してしまいました。

間違えなどありましたら指摘コメントお願いします。

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


参考サイトなど