Blenderでmidiから自動演奏ピアノっぽいアニメーションを作成するPythonスクリプト

## 1. はじめに
blenderでmidoを使用してmidiを読み込み、ピアノっぽいオブジェクトと自動演奏っぽいアニメーションを生成するPythonスクリプトの解説です。
![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2694201/17226350-e559-af44-ae06-6b4e01a65f23.png)
今回はスクリプトを簡略化するため1つのchannelのnote_on情報のみを使用していますが、正確にする場合すべてのチャンネルとnote_offやvelocityの値も使用する必要があります。

ピアノ生成部分は急ごしらえなので、あまりじろじろ見ないでください。

## 2. 準備

### Pythonライブラリmidoのインストール
pip install midoでmidiファイルを読み込むためにblenderのPythonにmidoをインストールします。
公式ドキュメントはこちらです。

https://mido.readthedocs.io/en/latest/index.html#

jupyter notebook を使用する方法もおすすめですよ！！

https://qiita.com/odu_beyond/items/8bfb73bc24e8014e0903


### midiファイルの用意

http://www.asahi-net.or.jp/~YW4Y-OOb/j-folk.html

こちらのダニーボーイ（ロンドンデリーの歌）daniy.midを使用させて頂きました。


## 3. スクリプト全体
**動きがよくわからないかもしれないので[記事下におまけスクリプト](#6-おまけ)をつけました。**

実行するにはblenderのPythonで**midoを使える必要**があります。
**6行目"daniy.midのパス"部分の""内にmidiファイルのパスを入力**してください。
```:Windowsでの例
mid = MidiFile(r"C:\Users\Owner\Documents\daniy.mid")
```
Scriptingタブの+新規をクリックして画像下の「スクリプト全体」の内容を張り付けて実行します。
![script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2694201/911aaea0-7c5b-7000-07fa-765d63cf4597.png)

```:スクリプト全体
import bpy
from mido import MidiFile
import mido

# midiファイルの読み込み
mid = MidiFile("daniy.midのパス")

# BPMの計算
midi_tempo = mid.tracks[0][0].tempo
midi_bpm = mido.tempo2bpm( midi_tempo )

# blenderに設定されているfpsの取得
bfps = bpy.context.scene.render.fps * bpy.context.scene.render.fps_base

# ピアノの生成
wkl = [0,  2,  4,  5,  7,  9,  11,  12,  14,  16,  17,  19,  21,  23,  24,  26,  28,  29,  31,  33,  35,  36,  38,  40,  41,  43,  45,  47,  48,  50,  52,  53,  55,  57,  59,  60,  62,  64,  65,  67,  69,  71,  72,  74,  76,  77,  79,  81,  83,  84,  86,  88,  89,  91,  93,  95,  96,  98,  100,  101,  103,  105,  107,  108,  110,  112,  113,  115,  117,  119,  120,  122,  124,  125,  127]
kl  = ['C',  'C#1',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#0',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#1',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#2',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#3',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#5',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#6',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#7',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#8',  'D',  'D#',  'E',  'F',  'F#',  'G',  'G#',  'A',  'A#',  'B',  'C',  'C#9',  'D',  'D#',  'E',  'F',  'F#',  'G']
kkl = [1,  3,  6,  8,  10,  13,  15,  18,  20,  22,  25,  27,  30,  32,  34,  37,  39,  42,  44,  46,  49,  51,  54,  56,  58,  61,  63,  66,  68,  70,  73,  75,  78,  80,  82,  85,  87,  90,  92,  94,  97,  99,  102,  104,  106,  109,  111,  114,  116,  118,  121,  123,  126]

if not "Black" in [_.name for _ in bpy.data.materials]:
    matk = bpy.data.materials.new("Black")
    matk.diffuse_color = (0,0,0,1)

if not "White" in [_.name for _ in bpy.data.materials]:
    matw = bpy.data.materials.new("White")
    matw.diffuse_color = (255,255,255,1)

for i in wkl:
    bpy.ops.mesh.primitive_cube_add(size=1, location=(wkl.index(i)*1.1-len(wkl)/2*1.1, 0, 0), scale=(1, 6, 1))
    bpy.context.object.name = 'CUBE'+str(i).zfill(3)
    bpy.context.object.active_material = bpy.data.materials['White']

kl_count=0
kkl_count=0
for i in kl:
    if '#' in i:
        bpy.ops.mesh.primitive_cube_add(size=1, location=((kl_count-0.5)*1.1-len(wkl)/2*1.1, 1.5, 0.5), scale=(1, 3, 1))
        bpy.context.object.name = 'CUBE'+str(kkl[kkl_count]).zfill(3)
        kkl_count+=1
        bpy.context.object.active_material = bpy.data.materials['Black']
    else:
        kl_count+=1

# アニメーションの生成
midi_ticksperbeat = mid.ticks_per_beat
i=0
for msg in mid.tracks[1]:
    if msg.type == 'note_on':
        # 対応するオブジェクトを選択
        obj = bpy.context.scene.objects["CUBE"+str(msg.note).zfill(3)]
        
        # 音が鳴る5フレーム前後のx軸回転を0.1
        obj.rotation_euler.x = 0.1
        obj.keyframe_insert( data_path = "rotation_euler", frame = i )

        # 音が鳴る5フレーム前後のx軸回転を0
        obj.rotation_euler.x = 0
        obj.keyframe_insert( data_path = "rotation_euler", frame = i-5 )
        obj.keyframe_insert( data_path = "rotation_euler", frame = i+5 )
        
        i += round( mido.tick2second( msg.time, midi_ticksperbeat, midi_tempo ) * bfps )
```
## 4. 再生してみよう
### とりあえず再生
blenderのレイアウト画面タブ下のタイムラインを開きスペースキーか再生ボタンを押して再生します。
![timeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2694201/4daf7ef2-a1d6-0d27-e517-499906eb9c92.png)
ちゃんと動けば成功しているはずです。

#### コマ落とし再生設定
この設定はやらなくても問題ないです。
スペックの高くないPCだと再生時フレームレートが下がり音がずれてしまうので、コマ落とし再生を設定すると多少改善されます。
タイムライン→再生→「毎フレーム再生」を「コマ落とし」に変更

### 音を追加する
#### 1. midiファイルをmp3に変換
blenderでmidiの音を再生出来ないのでVLC等でmp3に変換したdaniy.mp3を用意しておきます。

#### 2. mp3の読み込み
シークバーをアニメーション開始位置に戻してから、ビデオシーケンサー→追加→音声でdaniy.midを変換したmp3ファイルdaniy.mp3を追加します。
![video.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2694201/b72806eb-b37d-6b7d-970b-80b1b08e8268.png)
再生ボタンをクリックします。
音と動きが一致していれば成功です。

レンダリングして動画編集ソフトで音を後付けする方法もあります。


## 5. スクリプトの解説
今回はmidiを読み込むことがメインなのでピアノの生成についての説明は省略します。

### ライブラリのインポート
必要なライブラリをインポートします。
```
import bpy
import mido
from mido import MidiFile
```
blenderでPythonを使用するためのbpyとmidiファイルを扱うためのmidoをインポートします。

### midiを読み込む
```
mid = MidiFile("daniy.midのパス")
```
daniy.midを読み込みます。mp3ではなくmidiファイルです。

試しにmidiファイルdaniy.midの情報をみてみましょう。
```
MidiFile("ファイルのパス")
```
でmidiファイルを読み込むことができます。

```:Windowsでの例
mid = MidiFile(r"C:\Users\Owner\Documents\daniy.mid")
print(mid)
```
と入力すると、
```
MidiFile(type=1, ticks_per_beat=480, tracks=[
  MidiTrack([
    MetaMessage('set_tempo', tempo=666666, time=0),
    MetaMessage('key_signature', key='C', time=0),
    MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8, time=0),
    MetaMessage('end_of_track', time=69120)]),
  MidiTrack([
    Message('control_change', channel=0, control=0, value=0, time=0),
    Message('control_change', channel=0, control=32, value=0, time=0),
    Message('program_change', channel=0, program=10, time=0),
    Message('control_change', channel=0, control=7, value=100, time=0),
    Message('control_change', channel=0, control=10, value=64, time=0),
    Message('note_on', channel=0, note=90, velocity=120, time=0),
    Message('note_on', channel=0, note=86, velocity=120, time=0),
    Message('note_on', channel=0, note=90, velocity=0, time=576),
...以下略
```
のようにdaniy.midの情報が表示されます。

ノートオン : どの鍵盤を押すか
ノートオフ : どの鍵盤を離すか
ノートナンバー : どの鍵盤か
ベロシティ : 音の強さ
ゲートタイム : 鍵盤を押している時間

等の情報が取得出来ます。

少しだけ説明すると、2行目の'set_tempo'が曲のテンポ、下のほうのMessage('note_on', channel=0, note=90, velocity=0, time=576)の'note_on'が音を鳴らす、noteが鳴らす音、timeが時間の情報です。
timeの単位は秒でなく、4分音符を分割した長さの単位tickで入っていました。
ゲートタイムは低い音0から高い音127まであり、88鍵のピアノの場合は21から108まで使います。

tempoの情報だけを取得したい場合は、
```
mid.tracks[0][0].tempo
```
で取得することができます。
上記でも述べましたが、**他のmidiファイルだと情報が格納されている場所が異なるので注意が必要**です。
例えばギター・ベース・ドラム・キーボードの情報が入っているmidiの場合、各楽器の演奏情報がレイヤー分けのように入っているので、どの音の情報を使用するか選択する必要があります。
今回はchannel0の情報だけ使用しました。

midiやmidoについて書くと長くなるので、詳しくは公式ライブラリや他の記事を参考にしてください。

これらの情報からアニメーションを生成します。


### BPMの計算
```
midi_tempo = mid.tracks[0][0].tempo
midi_bpm = mido.tempo2bpm( midi_tempo )
```
midoのtempo2bpmを使用してtempoをBPMに変換することができます。

tempo2bpmのコードはこちらです。
```
def tempo2bpm(tempo):
    return (60 * 1000000) / tempo
```
midoで取得できるtempo情報はmicroseconds per beatなので、beat per microsecondsは1/mtempoで、それに1000000をかけてbeat per secondsを算出します。
さらに1分は60秒なので最後に60倍してBPMを算出しています。


### blenderのフレームレートを取得
```
bfps = bpy.context.scene.render.fps * bpy.context.scene.render.fps_base
```
blenderに設定されているアニメーションのフレームレートを取得します。
blenderが初期設定の場合、フレームレートは24になっているので、
```
bfps = 24
```
でも問題ありません。

BPMとゲートタイムとfpsから無理やりフレーム間隔を計算するので、フレームレートが低いとアニメーションと音にずれが発生することが多くなります。

### アニメーションの生成
```
midi_ticksperbeat = mid.ticks_per_beat
i=0
for msg in mid.tracks[1]:
    if msg.type == 'note_on':
        # 対応するオブジェクトを選択
        obj = bpy.context.scene.objects["CUBE"+str(msg.note - mmin ).zfill(2)]

        # 音が鳴る5フレーム前後のx軸回転を0.1
        obj.rotation_euler.x = 0.1
        obj.keyframe_insert( data_path = "rotation_euler", frame = i )
        
        # 音が鳴る5フレーム前後のx軸回転を0
        obj.rotation_euler.x = 0
        obj.keyframe_insert( data_path = "rotation_euler", frame = i-5 )
        obj.keyframe_insert( data_path = "rotation_euler", frame = i+5 )
        
    i += round( mido.tick2second( msg.time, midi_ticksperbeat, midi_tempo ) * bfps )
```

細かく解説していきます。
```
midi_ticksperbeat = mid.ticks_per_beat
```
daniy.midのticks_per_beatを取得します。
daniy.midの音が鳴るnote_onのゲートタイム（時間の情報）が秒ではなくtickで入っているので、後ほどtickを秒に変換する際に使用します。

```
i=0
```
iはキーを打つフレームを指定する変数です。
初期値を0にしておきます。

```
for msg in mid.tracks[1]:
    if msg.type == 'note_on':
```
でmid.tracks[1]に入っている'note_on'の情報のみを取得します。
他のchannelや'note_off'、velocityの値を使用したい場合は、else文などで追記する必要があります。
```
obj = bpy.context.scene.objects["CUBE"+str( msg.note ).zfill(3)]
```
でノートナンバーに対応したキューブを選択します。
.zfill(3)で数字部が一桁の場合、頭文字に0を追加して生成したキューブの名前と一致するようにしています。

```
obj.rotation_euler.x = 0.1
obj.keyframe_insert( data_path = "rotation_euler", frame = i )
```
midiの音情報に対応したキューブに対してx軸回転0.1でフレームiにキーを打ちます。
```
import math
obj.rotation_euler.x = math.radians(5)
```
角度の指定部は、こう書いたほうが設定しやすいですね。

```
obj.rotation_euler.x = 0
obj.keyframe_insert( data_path = "rotation_euler", frame = i-5 )
obj.keyframe_insert( data_path = "rotation_euler", frame = i+5 )
```
動かした前後ずっとx軸回転0.1のままになってしまうので、x軸回転0.1のキーフレームを打った5フレーム前後にx軸回転0でキーフレームを打ちます。
```:変更例1
obj.keyframe_insert( data_path = "rotation_euler", frame = i-1 )
obj.keyframe_insert( data_path = "rotation_euler", frame = i+1 )
```
```:変更例2
obj.location.x = 1
obj.keyframe_insert( data_path = "location", frame = i )

obj.location.x = 0
obj.keyframe_insert( data_path = "location", frame = i-5 )
obj.keyframe_insert( data_path = "location", frame = i+5 )
```
などに変えてみると何をしているかわかりやすいかと思います。

上記でも記載していますが、今回はスクリプトを簡単にするため**channel0のnote_on情報のみを使用**しています。
**アニメーションを正確にするためには、他のチャンネル（mid.tracks[2]等）やnote_offやvelocityの値（msg.velocity）も使用する必要**があります。

また、同じ音が連続して鳴る場合の処理や、音が鳴っている途中で別の音が鳴り始める場合など、キーフレームを打つ際に前後で使用するノートナンバーを確認する必要が出てくる場合もあります。

```
i += round( mido.tick2second( msg.time, midi_ticksperbeat, midi_tempo ) * bfps )
```
最後にmidoのmido.tick2secondを使って、tickを秒に変換します。
各ゲートタイムmsg.timeと上記で取得しておいたmidi_ticksperbeatとmidi_tempoを使って音が鳴り続ける時間を秒で取得して、bfpsをかけて次にキーフレームを打つ時間を計算します。
フレームを選択する際、小数点以下は使用できないのでroundで丸め、算出した数値をiに加算します。

tick2secondのコードはこちらです。
```
def tick2second(tick, ticks_per_beat, tempo):
    scale = tempo * 1e-6 / ticks_per_beat
    return tick * scale
```
こちらは解説すると長くなるので、興味のある方はご自身で調べてみてください。

## 6. おまけ
![hoge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2694201/4913bf71-1c65-9d2f-35e1-05933d5eff84.gif)

必要最低限のキューブオブジェクトを生成して音に合わせて上下に動かすスクリプトです。
```:おまけスクリプト全体
import bpy
from mido import MidiFile
import mido

mid = MidiFile("daniy.midのパス")

mmin = min([_.note for _ in mid.tracks[1] if _.type == 'note_on'])
mmax = max([_.note for _ in mid.tracks[1] if _.type == 'note_on'])
mdiff = mmax - mmin

for i in range(mdiff+1):
    bpy.ops.mesh.primitive_cube_add(size=1, location=(i*1.5-mdiff/2*1.5, 0, 0))
    bpy.context.object.name = 'CUBE'+str(i).zfill(2)

midi_tempo = mid.tracks[0][0].tempo
midi_bpm = mido.tempo2bpm( midi_tempo )
bfps = bpy.context.scene.render.fps * bpy.context.scene.render.fps_base
midi_ticksperbeat = mid.ticks_per_beat

i=0
for msg in mid.tracks[1]:
    if msg.type == 'note_on':
        obj = bpy.context.scene.objects["CUBE"+str(msg.note - mmin ).zfill(2)]
        
        obj.location[2] = 0
        obj.keyframe_insert( data_path = "location", frame = i-5 )
        obj.keyframe_insert( data_path = "location", frame = i+5 )
        
        obj.location[2] = 1
        obj.keyframe_insert( data_path = "location", frame = i )

        i += round( mido.tick2second( msg.time, midi_ticksperbeat, midi_tempo ) * bfps )
```

## 7. 終わりに
指のボーンを操作して演奏モーションを作成するなどできそうですね。
おしまい。