##はじめに
librosaを使ってbpm解析をしたので備忘録。
BPM解析のアルゴリズム?は以下のサイトに詳しく書かれています。
C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム
##やったこと
ディレクトリかファイルのパスを指定して.wavと.mp3のデータのbpmを測定する。
##実行環境
Ubuntu16.04
Python3.5
##使用したライブラリ
・pydub
・librosa
の2つを使用しました。(理由は後述します。)
##実装の流れ
[pydubを使用]
・mp3,wavの両方を同時に扱いたいのでmp3はwavに変換後に一時ディレクトリに保存してファイルの形式を整えます。
(wavの場合はそのまま一時ディレクトリにコピーします。)
[librosaを使用]
・一時ディレクトリ内のwavファイルに対してbpmの解析を行う。
一度wav形式に変換している理由は、librosaでbpmを解析するには(たしか)wav形式しか対応していないためです。
pydubかlibrosaのどちらかでもできると思いますが、個人的に2つ使用したほうが楽だったので併用しています。
##実装
import
import sys
import os
import pydub as dub
import shutil
import tempfile as temp
import librosa
tmp directoryを作成する
#明示的に閉じなくてもガベージコレクションが掃除してくれます
tmp = temp.TemporaryDirectory()
#明示的に閉じたい場合は
#tmp.cleanup()
#を最後に書くといいです
mp3をwavに変換してtmpに保存(wavはtmpにコピー)
保存されているdirectoryのpathとファイル名を引数にとっています。
class TransToWav:
def __init__(self,dir_path,_path):
self.path = os.path.join(dir_path,_path)
self.split_path = os.path.splitext(self.path)
self.ext = self.split_path[-1]
self.file_name = _path.split(".")[0]
self.save_file = tmp.name +"/"+ self.file_name + ".wav"
def trans_wav(self):
self.music = dub.AudioSegment.from_mp3(self.path)
self.music.export(self.save_file,format="wav")
def save_wav(self):
if self.ext == ".mp3":
self.trans_wav()
elif self.ext == ".wav":
shutil.copyfile(self.path,self.save_file)
else:
pass
指定されたpathがディレクトリかファイルかを判別する
pathはプログラム実行時のコマンドライン引数で受けとる仕様にしています。
先ほどのクラスを実行するハンドラのようなクラスを実装します。
(受け取ったpathがディレクトリなのかファイルなのかを判断するため)
class WavSaveTmp:
def __init__(self,path):
self.path = path
if os.path.isdir(self.path) is True:
self.dir_path = self.path
self.file_name = [f for f in os.listdir(self.path) if os.path.isfile(os.path.join(self.path,f))]
elif os.path.isfile(path) is True:
self.dir_path = os.path.dirname(self.path)
self.file_name = os.path.basename(self.path)
else:
pass
def save_tmp(self):
if type(self.file_name) is list:
for i in self.file_name:
handle_wav = TransToWav(self.path,i)
handle_wav.save_wav()
else:
handle_wav = TransToWav(self.dir_path,self.file_name)
handle_wav.save_wav()
tmpに保存されたwavに対してbpm解析を行う
bpmの解析自体は
y, sr = librosa.load(path)
bpm = librosa.beat.tempo(y,sr)
で行うことができます。
実行した際に、タプルを使用してから配列にしてね(かなり適当な訳)みたいな警告が出ますが何回か使用した感じ特に問題はなさそうでした。
おそらくlibrosaの問題だと思います。
bpmを解析するコードを書きます。
曲名をキー値とした辞書に解析したbpmを入れていきます。
class BpmAnalyse:
def __init__(self):
self.dir_path = tmp.name
self.file_names = os.listdir(self.dir_path)
self.file_path = [self.dir_path + "/" + i for i in self.file_names]
self.bpm = {}
def analyse_bpm(self):
for file_name,path in zip(self.file_names,self.file_path):
if file_name not in self.bpm:
self.music, self.sr = librosa.load(path)
self._bpm = librosa.beat.tempo(self.music,self.sr)
self.bpm[file_name] = int(self._bpm[0])
else:
continue
return self.bpm
最後に、実際に使用したコードの全文を載せます。
コード全文
import sys
import os
import pydub as dub
from pydub.playback import play
import shutil
import tempfile as temp
import librosa
class TransToWav:
def __init__(self,dir_path,_path):
self.path = os.path.join(dir_path,_path)
self.split_path = os.path.splitext(self.path)
self.ext = self.split_path[-1]
self.file_name = _path
self.save_file = tmp.name +"/"+ self.file_name + ".wav"
def trans_wav(self):
self.music = dub.AudioSegment.from_mp3(self.path)
self.music.export(self.save_file,format="wav")
def save_wav(self):
if self.ext == ".mp3":
self.trans_wav()
elif self.ext == ".wav":
shutil.copyfile(self.path,self.save_file)
else:
pass
class WavSaveTmp:
def __init__(self,path):
self.path = path
if os.path.isdir(self.path) is True:
self.dir_path = self.path
self.file_name = [f for f in os.listdir(self.path) if os.path.isfile(os.path.join(self.path,f))]
elif os.path.isfile(path) is True:
self.dir_path = os.path.dirname(self.path)
self.file_name = os.path.basename(self.path)
else:
pass
def save_tmp(self):
if type(self.file_name) is list:
for i in self.file_name:
handle_wav = TransToWav(self.path,i)
handle_wav.save_wav()
else:
handle_wav = TransToWav(self.dir_path,self.file_name)
handle_wav.save_wav()
class play_music:
def __init__(self):
self.wav_name = os.listdir(tmp.name)
self.path = [tmp.name + "/" + i for i in self.wav_name]
def play(self):
for i in self.path:
self.sound = dub.AudioSegment.from_wav(i)
print(type(self.sound))
play(self.sound)
self.play()
class BpmAnalyse:
def __init__(self):
self.dir_path = tmp.name
self.file_names = os.listdir(self.dir_path)
self.file_path = [self.dir_path + "/" + i for i in self.file_names]
self.bpm = {}
def analyse_bpm(self):
for file_name,path in zip(self.file_names,self.file_path):
if file_name not in self.bpm:
self.music, self.sr = librosa.load(path)
self._bpm = librosa.beat.tempo(self.music,self.sr)
self.bpm[file_name] = int(self._bpm[0])
else:
continue
return self.bpm
#path
path = sys.argv[1]
#make temp directory
tmp = temp.TemporaryDirectory()
#mp3,wav save to temp file
save_ = WavSaveTmp(path)
save_.save_tmp()
#bpm analyse
analyser = BpmAnalyse()
bpm_list = analyser.analyse_bpm()
print(bpm_list)
#clean up temp directory
tmp.cleanup()
##実際に動かしてみる
テストするディレクトリ
mp3の曲が1つ,wavの曲が2つあるディレクトリを用意しました
test@ubuntu:~/Musics$ ls
music_1.wav music_2.wav music_3.mp3
実行した結果
test@ubuntu:~/bpm_analyse$ python bpm_analyser.py ../Musics/
{'music_2.wav': 129, 'music_1.wav': 95, 'music_3.wav': 112}
一曲だけ選択してみる
test@ubuntu:~/bpm_analyse$ python bpm_analyser.py ../Musics/test/music_2.wav
{'music_2.wav': 129}
見た感じ上手く動いてそうですね。
music_1.wav の実際のbpmは190なのですが半分のbpmが出てくるのはよくあることなので割り切ります。
警告について
librosaを使うと下の警告が出てきますが、librosaがscipyを使ってfftするときにの配列の持ち方で何か言われているっぽい
/usr/local/lib/python3.5/dist-packages/scipy/fftpack/basic.py:160: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
##最後に
今回はlibrosaを使ってbpm解析を行いました。
はじめて、このようなアウトプット記事を書きましたが案外楽しかったのでこれからも続けていこうと思います。
あと数ヶ月でプログラムを書き始めて1年経ちますがプログラミングの難しさに悪戦苦闘しています。。
なにか間違っているところやアドバイス等あればコメントで指摘してくだされば嬉しいです。