MIDIのnote編集に関する基本tips【Python, mido】

※この記事ではmidiファイルの編集について、あくまでPythonのmidi編集ライブラリであるmidoを通して見たときの話をしています。厳密なmidi規格については音楽電子事業協会が公開しているmidi規格書を閲覧ください。（筆者もまだ未履修）

https://amei.or.jp/midistandardcommittee/MIDI1.0.pdf

※Cメジャーコードのつもりで生成してたものをよく見るとEメジャーコードだったので訂正しました。（ミソ#シ）というなんともわかりにくい感じになってしまいましたが本質はそこじゃないのでセーフ

## midoを使ったmidiのnote情報の閲覧・編集

当方はPythonで音楽制作ソフトを作れないかと試行錯誤を始めたプログラミング初心者かつ楽曲制作初心者です。最終的にはDAWを作りたいと思っています。

https://note.com/wangoroge333/n/n31d4d0041201

まずはmidiを編集できないと話にならんと思い、midoというライブラリを触ってみています。

midiファイルとは簡単に言えばコンピュータ上で楽器を演奏するための楽譜のようなものです。楽譜でいう音符がmidiでいうnoteに当たります。

midiのnoteはmidoを通して見ると基本的には以下の情報を持っています。

~~~
1. note_on or note_off （鍵盤を押すか鍵盤を離すか）
2. note = notenumber （音程、0～127でC-1からG9までを表す）
3. velocity （ベロシティ、音の強弱、0～127）
4. time （noteの長さ、何分音符か、単位はtickのためbpm依存で時間ではない、1920で1小節）
~~~

ベロシティなんかはDTMerにとっては聞き慣れた概念ですよね。

もっと詳しいのはこちらから。

https://mido.readthedocs.io/en/latest/message_types.html

基本的にこれらのパラメータを扱えれば思い通りのmidiファイルを生成したり、あるいは既に存在するmidiファイルの中身を覗いてみたりできるっぽいです。

後述しますが、note_onとnote_offの違いがちょっとややこしく、しかし音を単に鳴らす程度の用途ならばこの部分はおそらく省略できます。（note_onに統一できます。）

## 実際にmidiファイルを作ってDAWで見てみよう！

実際にmidiファイルを生成してみましょう。
サンプルコードは以下のブログから拝借させていただきました。

https://tattyamm.blog.jp/archives/5761686.html

### ド（C4）の生成

~~~py
# まずは1小節のnote（ド、C4）を1つ生成してみる

import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(120))) # bpm120

# timeはnote_onを基準（0）とし、note_offをどこで行うかと考える。
# 1920 = 1小節, 960 = 2分音符, 480 = 4部音符
# ちゃんと計算すれば付点音符や連符なんかも表現できる
track.append(Message('note_on', note=60, velocity=100, time=0))
track.append(Message('note_off', note=60, velocity=64, time = 1920))

mid.save('test.mid') # MidiFileを保存
~~~

### 結果をDAWで開いて確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/8d1f5f1f-6f73-1df9-b4d1-772856f3aa7c.png)

ド（C4）の音が1小節分出力されている。

### ドレミ（C4D4E4）の生成
~~~py
# note情報をこうする（ドレミ）
# お察しの通りnoteに+1すると1半音上、+2すると1全音上になる（+12でオクターブ上）

track.append(Message('note_on', note=60, velocity=100, time=0))
track.append(Message('note_off', note=60, velocity=64, time = 1920))
track.append(Message('note_on', note=60+2, velocity=100, time=0)) 
track.append(Message('note_off', note=60+2, velocity=64, time = 1920)) 
track.append(Message('note_on', note=60+4, velocity=100, time=0))
track.append(Message('note_off', note=60+4, velocity=64, time = 1920))
~~~

### 結果をDAWで開いて確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/749e4e9e-9d4b-21bb-a431-ebde4b4a2ca7.png)

きちんとドレミで三小節分生成されている。

### 和音（Eメジャーコード）の生成
~~~py
# ここら辺からちとややこしい
# メジャーコードはルート音+4と+7で表現できる（マイナーコードなら+4を+3に変更）
# 同時に鳴らす音は同時にnote_on
# 同時に音が鳴り終わる複数の音ではtimeを記述するのは1個でいい

track.append(Message('note_on', note=64, velocity=100, time=0))
track.append(Message('note_on', note=64+4, velocity=100, time=0))
track.append(Message('note_on', note=64+7, velocity=100, time=0))
track.append(Message('note_off', note=64, velocity=64, time = 1920)) #ここだけ1920
track.append(Message('note_off', note=64+4, velocity=64, time=0))
track.append(Message('note_off', note=64+7, velocity=64, time=0))
~~~

### 結果をDAWで開いて確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/41ea43d1-39ec-e64a-6928-d617a797c52a.png)

Eメジャーコード（ミソ#シ）が1小節分生成された。

### 全部のnote_offにtimeを記述するとどうなる？
~~~py
track.append(Message('note_on', note=64, velocity=100, time=0))
track.append(Message('note_on', note=64+4, velocity=100, time=0))
track.append(Message('note_on', note=64+7, velocity=100, time=0))
track.append(Message('note_off', note=64, velocity=64, time = 1920))
track.append(Message('note_off', note=64+4, velocity=64, time=1920)) #ここも1920
track.append(Message('note_off', note=64+7, velocity=64, time=1920)) #ここにも1920
~~~

### 結果をDAWで開いて確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/6639c6b6-52f8-55f6-4a08-1e37f4177f46.png)

三小節分のアルペジオになっちゃった。

これは全てのnoteがtime = 0で始まり、ミが1920で終了（一小節）、ソ#がその次の1920で終了（合計3840、二小節）、さらにシがその次の1920で終了している（合計5760、三小節）からであると考えられます。

この仕様を利用して複雑なアルペジオやコード演奏も作れちゃうわけですが、スクリプト自体の可読性や書き心地はバリしんどいと思われです。

## （小技）note_onとnote_offの互換性

基本的にはnote_onで始まり、note_offで終わるというのが型なのですが、note_onのベロシティを0にすることでnote_offの代わりに使うことができます。
（筆者はこの使い分けは正直まだよくわかっていません）

先ほどのEメジャーコード（ミソ#シ）を1小節分鳴らすスクリプトを書き換えてみましょう。

~~~py
# note_offをnote_onのvelocity=0に置き換える

track.append(Message('note_on', note=64, velocity=100, time=0))
track.append(Message('note_on', note=64+4, velocity=100, time=0))
track.append(Message('note_on', note=64+7, velocity=100, time=0))

#以下もnote_onでvelocity=0

track.append(Message('note_on', note=64, velocity=0, time = 1920)) 
track.append(Message('note_on', note=64+4, velocity=0, time=0))
track.append(Message('note_on', note=64+7, velocity=0, time=0))
~~~

### 結果をDAWで開いて確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/be0d0258-e865-8061-83ed-71d3a6a33ee1.png)

やはり一小節分のEメジャーコードです。

## バグ？ DAW側で作ったmidiを読み込むと……

以下は触っていて困った仕様？バグ？のメモになります。

他の環境ではどうなのかがわからないのでなんとも言えないのですが、midiの生成ではなく、DAW（cakewalk by bandlab）で作ったmidiの読み込みをしてみたところ、なぜかtimeが2倍になって表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632347/b1d6ca58-abec-be4c-e3d1-f375e98fff08.png)

いたって普通な1小節分のメジャーコードをDAWで打ち込む。

以下の方法で読み込む。

~~~py
# midiファイルを読み込み、再生する
import mido
import time

# midiファイルを読み込む
mid = mido.MidiFile(input("MIDIファイル名を入力してください: "))
# トラックを選択、候補を表示
print("トラックを選択してください")
for i, track in enumerate(mid.tracks):
    print(i, track.name)
# トラックを選択
track_num = int(input("トラック番号を入力してください: "))
track = mid.tracks[track_num]
# 全てのnoteを表示
for msg in track:
    print(msg)
~~~

すると結果はこちら。

~~~
MIDIファイル名を入力してください: testcode.mid
トラックを選択してください
0
1 gbN1
トラック番号を入力してください: 1
MetaMessage('track_name', name='\x83g\x83\x89\x83b\x83N1', time=0)
note_on channel=0 note=67 velocity=100 time=0
note_on channel=0 note=64 velocity=100 time=0
note_on channel=0 note=60 velocity=100 time=0
note_on channel=0 note=60 velocity=0 time=3840 ←??????????
note_on channel=0 note=64 velocity=0 time=0
note_on channel=0 note=67 velocity=0 time=0
MetaMessage('end_of_track', time=0)
~~~

1小節のコードのはずなのに表示されているデータ上では2小節になっています。

なお、前述の方法で生成したmidiを読み込んだ場合は正常なtick数が表示されたため、どうやらスクリプトの問題ではなく、DAW（cakewalk）で作ったmidiファイルに問題があるようでした。

別環境では正常な可能性もあるので、似たような状況の方や別のDAWを使っている方などいましたらコメントいただけると幸いです。

## おわりに

これでpythonでmidiの打ち込みができるようになりましたね。
ついでに読み込み方法も最後にご紹介しました。

音源を使った再生も既にあるスクリプトをコピペすれば簡単にできるのですが、ポートという概念が出てくるので内容を理解するという意味では若干難しいです。

どうでもいいけどプログラミングの意味でのコードと、音楽理論的な意味でのコードが混ざってややこしいので前者はスクリプトという表現を用いたのですが合っていたでしょうか。

みんなもpythonで音楽をやってみよう！
地味に音楽理論の数学的な理解に繋がるのが面白いですよ。

それではさいなら。