MIDIに入門してDJプレイをざっくり監視してみる

普段、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周りについての仕様書がありました。

https://www.pioneerdj.com/-/media/pioneerdj/software-info/controller/ddj-400/ddj-400_midi_message_list_j1.pdf



この仕様書を読むと、<b>本機→コンピューター</b> と書かれた箇所が見つかります。

本機とはDDJ-400のことなので、DDJ-400からPCに対して下記の情報が送られてくるようです。

- Status (16進)
- Data1 (16進)
- Data2 (16進)


なるほど、機材の各ボタンを押すとそれに対応した１６進数のメッセージが送られてくるので、それをこちらで解釈してあげればプレイが監視できそうですね。


# 実装
今回、MIDIメッセージを読み取るにあたって使うライブラリは[pygame](https://www.pygame.org/docs/ref/midi.html)です。
ゲームを作る際に使うライブラリですが、音声を扱うモジュールも含まれているかつシンプルで使いやすいので採用しました。

```terminal
$ pip install pygame
```

### MIDIメッセージを受け取る

MIDIメッセージを受け取るには、最初にPCに機器が認識される必要があります。
PCに接続機器が認識されているか確認します。

```check.py
import pygame.midi as m

m.init()
for i in range(m.get_count()):
  print(m.get_device_info(i))
```

出力結果が下記になります。

```terminal
$ 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)
```
お、認識されていそうですね。


次は、実際に機器から信号を受け取ります。

```observer.py
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()
```
５行目あたりで
```
i = m.Input(1)
```
としていますが、これは機器の接続確認で得た出力↓の１番目を指定しているという意味です。
```
０番目：(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)
```

１番目を指定する理由としては、Inputフラグ（末尾から３個目の要素）がTrue（１）になっているので、Inputデバイスとして認識されているからです。０番目と３番目は、関係ないMIDIデバイスになります。

DDJ-400からのMIDIメッセージを確認すると、
```terminal
$ 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` しか送られてこないので、`Data３`に該当する欄が空になる感じですね。


### MIDIメッセージをざっくり解釈する
解釈と書きましたが、仰々しいかもしれません。
実際やることはJSONで、数字に対応する動作をベタ書きしただけです。


```midi.json
{
  "0xb6": {
    "0x40": "browser",
    "0x1f": "crossfader",
    "0x3f": "crossfader",
    "0x08": "mixer master level"
  },
　　　　　...
}
```
しかも、数字の組み合わせが多すぎたので色んなところ省いていますし、かなり粗い粒度で書いています。（ここがざっくりの由縁）


送られてくるMIDIメッセージがJSON内にあるキーに該当すれば文字列を返すメソッドを追加しました。
```observer.py
from pygame import midi as m
import json

def converter(midi_event: list):
  try:
    status = str(hex(midi_event[0]))　#メッセージは10進数なので１６進に変換
    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()
```
 実行結果は下記です。
```terminal
$ 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したい

最後まで読んでいただいてありがとうございました。


[^1]:[一般社団法人音楽電子事業協会](https://www.amei.or.jp/midi2020.html)