はじめに
pythonでMIDIを扱うパッケージである,pretty_midiの中にピアノロールを取得できるメソッドがあるので使ってみます.
pretty_midi.PrettyMIDI.get_piano_roll
example.midというファイルをピアノロール表示したい場合,
midi = pretty_midi.PrettyMIDI("example.mid")
piano_roll = midi.get_piano_roll()
print(piano_roll)
という感じで使えます.
pretty_midiのドキュメントによると
get_piano_rollの引数には,
サンプリング周波数,時間,ペダルの閾値を設定できます.
サンプリング周波数
これはpiano_rollの行(時間方向)の1秒間あたりの区切りの数を表しています.
デフォルトは100となっているので,テンポ60の曲なら,1拍が100分割で表され,テンポ120なら1泊が50分割で表されます.
もし,テンポ60の曲でサンプリング周波数を4に設定すると,16分音符で分割した時を表しています.
時間
これはサンプリングされた各要素がはじますタイミングを秒数で示した行列になります.
言葉で説明するのは難しいので例を出すと,
このデフォルト値はnp.arange(0, get_end_time(), 1./fs)
と書かれています.
get_end_time()もPrettyMIDIのメソッドで,曲が終わる時間を取得します.
fsは先ほど説明したサンプリング周波数です.
曲が終わる時間を100sとして,サンプリング周波数が10の場合,
[0, 0.1, 0.2, ・・・ 99.7, 99.8, 99.9]
のように表されます.
これは,piano_rollが曲の0秒の時点,0.1秒の時点,0.2秒の時点・・・で区切られているということを表しています.
なので,この時間をうまく変更すれば,
シャッフルビートの入っている曲を8分音符として表す,みたいなこともできそうです.
上記二つの引数の情報から,get_piano_rollは,秒数指定を元にしているので,
拍の情報からピアノロールを取得する訳ではないみたいです(テンポも計算しないとできない).
ペダルの閾値
これはサステインペダルがオンになっているかどうかを判断するためにあります.
ペダルのオンオフは,音の持続時間に影響してきます.
ペダルの情報はコントロールチェンジというMIDIの情報に含まれているようです.
デフォルトでは64となっているので
コントロールチェンジの情報が64以上でペダルオン,63以下でペダルオフとなります.
ドキュメントを見て少し混乱したのが,
コントロールチェンジにはコントロール番号というものがあり,
コントロール番号によってサステインペダルの他にもさまざまな情報を管理しています.
サステインペダルに関するコントロール番号は64となっています.
これは,デフォルトの閾値である64とは関係がないのですが,
最初はごっちゃになってしまって混乱しました.
出力(piano_roll)
出力である,np.ndarrayのシェイプは音高×曲の長さです.
音高は128で表されています.
曲の長さは上記で説明した時間の長さと等しいです.
複数楽器の場合は,全てまとめて表されているので楽器の区別はできないみたいですね.
内部の数値はおそらく音量が表されています.
ただ,実際データを使って見てみると,
楽譜でみると無いはずの音が,0ではないところもあったのでもう少し使い方について検討しようと思います.
※追記
もう一度確認してみたところ,↑の問題はなぜか直っていました.
おそらく,midiのテンポとサンプリング周波数のずれによってこの問題が起こっていたのではないかと思います.
pretty_midiにはestimate_tempoやget_tempo_changesというメソッドも用意されていて,midiのテンポ推定も行うことができたので,それも駆使すればテンポと絡めてピアノロールを得ることができそうです.
get_tempo_changesに関しては,テンポが変わる秒数と,テンポが返ってきます.
estimate_tempoは推定されたテンポから最も良いものを取ってきます.
ただ,このestimate_tempoの精度があまり良くない気がします・・・.
実際はテンポ120の曲ですが,このメソッドの結果では215となりました.
なのでget_tempo_changesのテンポの方の返り値を用いた方が良いと思います.
(余談) Pathlibやglobと併用した場合
余談ですが,PrettyMIDIオブジェクトの作成の際に,
私の場合はpathlib.Pathとglobと併用して使っていたのですが,
dir = pathllib.Path("./data/midi/")
files = dir.glob("*.mid")
for file in files:
midi = pretty_midi.PrettyMIDI(file) //ここでエラーが出る
piano_roll = midi.get_piano_roll()
print(piano_roll)
上記のように書くとエラーが出ました.
AttributeError: 'PosixPath' object has no attribute 'read'
下のように変更すると直りました.
dir = pathllib.Path("./data/midi/")
files = dir.glob("*.mid")
for file in files:
midi = pretty_midi.PrettyMIDI(str(file)) //文字列に変換してから
piano_roll = midi.get_piano_roll()
print(piano_roll)
PrettyMIDIがPathLikeオブジェクトに対応していないのが原因らしいです.
終わりに
個人的には,秒数指定ではなく,
拍や小節単位で使えたら良いなと思っていたので,少し残念です.