librosaを使ってBPM解析をしたので備忘録

##はじめに
librosaを使ってbpm解析をしたので備忘録。
BPM解析のアルゴリズム?は以下のサイトに詳しく書かれています。
[C/C++言語で音声ファイルのテンポ解析を行うサンプルプログラム](http://hp.vector.co.jp/authors/VA046927/tempo/tempo.html)

##やったこと
ディレクトリかファイルのパスを指定して.wavと.mp3のデータのbpmを測定する。

##実行環境
Ubuntu16.04
Python3.5

##使用したライブラリ
・pydub
・librosa
の２つを使用しました。(理由は後述します。)

##実装の流れ
[pydubを使用]
・mp3,wavの両方を同時に扱いたいのでmp3はwavに変換後に一時ディレクトリに保存してファイルの形式を整えます。
(wavの場合はそのまま一時ディレクトリにコピーします。)

[librosaを使用]
・一時ディレクトリ内のwavファイルに対してbpmの解析を行う。

一度wav形式に変換している理由は、librosaでbpmを解析するには(たしか)wav形式しか対応していないためです。
pydubかlibrosaのどちらかでもできると思いますが、個人的に2つ使用したほうが楽だったので併用しています。

##実装
__import__

```py:import
import sys
import os
import pydub as dub
import shutil
import tempfile as temp
import librosa
```

__tmp directoryを作成する__

```py:make_TmpDirectory
#明示的に閉じなくてもガベージコレクションが掃除してくれます
tmp = temp.TemporaryDirectory()

#明示的に閉じたい場合は
#tmp.cleanup()
#を最後に書くといいです
```

__mp3をwavに変換してtmpに保存(wavはtmpにコピー)__
保存されているdirectoryのpathとファイル名を引数にとっています。

```py:TransToWav
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がディレクトリなのかファイルなのかを判断するため)

```py:WavSaveTmp
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の解析自体は

```py
y, sr = librosa.load(path)
bpm =  librosa.beat.tempo(y,sr)
```

で行うことができます。
実行した際に、タプルを使用してから配列にしてね(かなり適当な訳)みたいな警告が出ますが何回か使用した感じ特に問題はなさそうでした。
おそらくlibrosaの問題だと思います。

bpmを解析するコードを書きます。
曲名をキー値とした辞書に解析したbpmを入れていきます。

```py:BpmAnalyse
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
```

最後に、実際に使用したコードの全文を載せます。

__コード全文__

```py:bpm_analyser.py
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年経ちますがプログラミングの難しさに悪戦苦闘しています。。
なにか間違っているところやアドバイス等あればコメントで指摘してくだされば嬉しいです。