普段、DDJ-400 というPioneer社製のDJ機材を使ってDJをしています。
「PCとDDJ-400の間では、MIDIという規格で通信が行われているらしいが、MIDIってなんだろう?」
「DJのプレイを記録して、自分の癖を知りたいな」
そういった経緯から、MIDIに入門してDJプレイをざっくり監視します。
MIDI
MIDIとは
Musical Instrument Digital Interfaceの頭文字を組み合わせた言葉で、電子楽器やコンピュータ等のメーカーや機種に関わらず音楽の演奏情報を効率良く伝達するための統一規格1
だそうです。
機器ごとの差異を吸収してくれる感じですかね。
DDJ-400
今回使うDDJ-400にも、MIDI周りについての仕様書がありました。
この仕様書を読むと、本機→コンピューター と書かれた箇所が見つかります。
本機とはDDJ-400のことなので、DDJ-400からPCに対して下記の情報が送られてくるようです。
- Status (16進)
- Data1 (16進)
- Data2 (16進)
なるほど、機材の各ボタンを押すとそれに対応した16進数のメッセージが送られてくるので、それをこちらで解釈してあげればプレイが監視できそうですね。
実装
今回、MIDIメッセージを読み取るにあたって使うライブラリはpygameです。
ゲームを作る際に使うライブラリですが、音声を扱うモジュールも含まれているかつシンプルで使いやすいので採用しました。
$ pip install pygame
MIDIメッセージを受け取る
MIDIメッセージを受け取るには、最初にPCに機器が認識される必要があります。
PCに接続機器が認識されているか確認します。
import pygame.midi as m
m.init()
for i in range(m.get_count()):
print(m.get_device_info(i))
出力結果が下記になります。
$ python3 check_midi.py
2.1.2 (SDL 2.0.18, Python 3.7.8)Hello from the pygame community.
https://www.pygame.org/contribute.html
(b'CoreMIDI', b'IAC Driver my_port1', 1, 0, 0)
(b'CoreMIDI', b'DDJ-400', 1, 0, 0)
(b'CoreMIDI', b'IAC Driver my_port1', 0, 1, 0)
(b'CoreMIDI', b'DDJ-400', 0, 1, 0)
お、認識されていそうですね。
次は、実際に機器から信号を受け取ります。
from pygame import midi as m
def observer():
m.init()
i = m.Input(1)
while True:
try:
if i.poll(): #データがあればTrue
midi_event = i.read(1)
print(midi_event)
except KeyboardInterrupt:
print("\nTerminating Observation...")
break
i.close()
exit()
if __name__=="__main__":
observer()
5行目あたりで
i = m.Input(1)
としていますが、これは機器の接続確認で得た出力↓の1番目を指定しているという意味です。
0番目:(b'CoreMIDI', b'IAC Driver my_port1', 1, 0, 0)
1番目:(b'CoreMIDI', b'DDJ-400', 1, 0, 0)
2番目:(b'CoreMIDI', b'IAC Driver my_port1', 0, 1, 0)
3番目:(b'CoreMIDI', b'DDJ-400', 0, 1, 0)
1番目を指定する理由としては、Inputフラグ(末尾から3個目の要素)がTrue(1)になっているので、Inputデバイスとして認識されているからです。0番目と3番目は、関係ないMIDIデバイスになります。
DDJ-400からのMIDIメッセージを確認すると、
$ python3 observe.py
pygame 2.1.2 (SDL 2.0.18, Python 3.7.8)Hello from the pygame community.
https://www.pygame.org/contribute.html
[[[144, 11, 127, 0], 1327]]
[[[144, 11, 0, 0], 1467]]
[[[144, 32, 127, 0], 4467]]
[[[144, 32, 0, 0], 4582]]
[[[151, 34, 127, 0], 5614]]
[[[151, 34, 0, 0], 5696]]
[[[148, 71, 127, 0], 8257]]
[[[148, 71, 0, 0], 8419]]
[[[144, 83, 127, 0], 12292]]
[[[144, 83, 0, 0], 12452]]
[[[144, 16, 127, 0], 13884]]
[[[144, 16, 0, 0], 14036]]
[[[182, 13, 38, 0], 16593]]
[[[182, 45, 98, 0], 16593]]
[[[182, 13, 39, 0], 16647]]
[[[182, 45, 19, 0], 16647]]
...
いい感じに受信できてそうですね。
それぞれ何に対応しているかが分からなかったのですが、pygameのコードに書いてありました。
[[[status,data1,data2,data3],timestamp],
[[status,data1,data2,data3],timestamp],...]
なるほど、DDJ-400からは Status, Data1, Data2
しか送られてこないので、Data3
に該当する欄が空になる感じですね。
MIDIメッセージをざっくり解釈する
解釈と書きましたが、仰々しいかもしれません。
実際やることはJSONで、数字に対応する動作をベタ書きしただけです。
{
"0xb6": {
"0x40": "browser",
"0x1f": "crossfader",
"0x3f": "crossfader",
"0x08": "mixer master level"
},
...
}
しかも、数字の組み合わせが多すぎたので色んなところ省いていますし、かなり粗い粒度で書いています。(ここがざっくりの由縁)
送られてくるMIDIメッセージがJSON内にあるキーに該当すれば文字列を返すメソッドを追加しました。
from pygame import midi as m
import json
def converter(midi_event: list):
try:
status = str(hex(midi_event[0])) #メッセージは10進数なので16進に変換
data1 = str(hex(midi_event[1]))
play = ddj400[status][data1]
print(play)
except KeyError:
print(midi_event)
def observer():
m.init()
i = m.Input(1)
while True:
try:
if i.poll():
midi_event = i.read(1)
converter(midi_event[0][0]) #timestampは要らないので除外
except KeyboardInterrupt:
print("\nTerminating Observation...")
break
i.close()
exit()
if __name__=="__main__":
ddj400_midi_message = open("./midi.json", "r")
ddj400 = json.load(ddj400_midi_message)
ddj400_midi_message.close()
observer()
実行結果は下記です。
$ python3 observer.py
pygame 2.1.2 (SDL 2.0.18, Python 3.7.8)
Hello from the pygame community. https://www.pygame.org/contribute.html
crossfader
crossfader
crossfader
[144, 11, 127, 0]
[144, 11, 0, 0]
browser
browser push
browser push
[182, 8, 95, 0]
mixer master level
master cue
master cue
[144, 34, 127, 0]
[144, 34, 0, 0]
...
JSONに書いてあるメッセージに関してはちゃんと認識されてるようです。
総括
- timestampはバッサリ切り捨てたが、時系列データとして扱えば面白そうな気配がしている
- 文字列だと何しているか分かりにくいのでVisualizeしたい
最後まで読んでいただいてありがとうございました。