簡易MIDIシーケンサ(Mido)とシグナルタワー(パトライト)で星野源をアゲアゲにする

# はじめに
画像処理の勉強のためにパトライトの点灯状況をカメラから取得するプログラムを作っているつもりが、いつの間にか星野源をアゲアゲにしていました。どうしてこうなった。
<img width="1918" alt="スクリーンショット 2020-05-24 14.54.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90313/29146702-3fa3-9a13-a8d9-2a1ce69cc75e.png">


## 作成した動画　
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ご家庭によくある <a href="https://twitter.com/hashtag/%E3%83%91%E3%83%88%E3%83%A9%E3%82%A4%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#パトライト</a> の <a href="https://twitter.com/hashtag/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB%E3%82%BF%E3%83%AF%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#シグナルタワー</a> に一緒に歌ってもらいました。<br><br> <a href="https://twitter.com/hashtag/%E3%81%86%E3%81%A1%E3%81%A7%E8%B8%8A%E3%82%8D%E3%81%86?src=hash&amp;ref_src=twsrc%5Etfw">#うちで踊ろう</a> <a href="https://twitter.com/hashtag/DancingOnTheInside?src=hash&amp;ref_src=twsrc%5Etfw">#DancingOnTheInside</a> <a href="https://t.co/lZC0mgzSzf">pic.twitter.com/lZC0mgzSzf</a></p>&mdash; 伊織 (@iori_ama) <a href="https://twitter.com/iori_ama/status/1264184427833253889?ref_src=twsrc%5Etfw">May 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 今回使用した楽譜
星野源「うちで踊ろう」をmusescoreで楽譜を書き起こしてmidi化。(単音)
- 動画はこちらから　　https://www.youtube.com/watch?v=b4DeMn_TtF4
- 楽譜はこちらから　　https://www.print-gakufu.com/scene/detail/2108/

## Midoとは
Midiファイルの読み取り、Midiメッセージの送信などを簡単に行うことができるpythonライブラリ。
https://mido.readthedocs.io/en/latest/
実装にあたっては、こちらを参考にしました。
https://qiita.com/tjsurume/items/75a96381fd57d5350971

## Midiシーケンサとは
(簡単に言うと)Midiメッセージ受付可能なデバイスに、Midiメッセージを送信して音楽を演奏させる機構を持つもの

## シグナルタワーとは
工場などで工作機械などの動作状況を信号等でお知らせするもの。遠くからでもよく見えます。今回はご家庭にもよくあるパトライト社のシグナルタワー「NHS-FB2」を使用しています。http://www.patlite.jp/product/nh-spl.html

# 技術概要
## 用語整理
- 鳴音/消音：MIDIデバイスに発音/消音させること
- 点灯/消灯：シグナルタワーに点灯/消灯させること

## MIDIシーケンサ部(Python,Mido)
- 鳴音(消音含む)単位で、MIDIメッセージ(msg)を作成する
- msg　note_onメッセージのみみることとしています。概要は以下のとおり。
    - channel→今回は考慮しない
    - note→音程
    - velocity→音量
    - time→前のメッセージから、次のメッセージを鳴動させるまでの時間
- 一度に送信するのではなく、時間で分割し、鳴音(消音)させたいタイミングでmsgをMIDIデバイスに送信している。

## シグナルタワー接続部
- パトライトにURLクエリパラメータを投げる
- msg.noteの値によって点灯/消灯するライトの種類を変更する

## シグナルタワーの仕組み
- ネットワークで接続。点灯させるパラメータをURLクエリパラメータ(alert)に含め、送信する
- 上位3ビットがそれぞれ三色のシグナルタワーに対応している。(下位3ビットはブザー。今回は未使用)
```
http://192.168.10.1/api/control?alert=000000
```

# 技術課題　
## シグナルタワーはネットワーク接続であるため、遅延が発生する
- そもそもこの製品は遅延があることを許容される製品である
- URLクエリを投げてからレスポンスを受け取るまでの時間を計算し、次回の待ち時間を調整

## MIDIメッセージに「消音」が毎回挟まれる
- スラーで繋いでいない音形なのだから毎回消音があるのは当然
- 「消音」=「消灯」として毎回リクエスト送信すると、消音の部分が間に合わない
- 基本は消音の場合、シグナルタワーにリクエストを送信しない設計にした
    - ただし、その場合に「消音」時でも常に点灯し続ける問題が発生
- ウォッチドッグタイマの機構を導入
    - 「消音」時にウォッチドッグタイマをスタートさせる、時間内にリセットされなければ「消灯」リクエストを送信
    - 「鳴音」時にウォッチドッグタイマをストップさせる。　
    - 一定時間「消音」のときのみ、「消灯」リクエストを送信できるようにした
   
## そもそも動画とMIDIのテンポが合わない
- 動画編集でどうにかしました(気合い)

# プログラム
https://github.com/iamakawa/mido-patlite

```python:mido-patlite.py
import mido
import time
import urllib.request
from threading import Timer

ports = mido.get_output_names()
url_base = 'http://192.168.10.1/api/control?alert='
url_val = ['000000','001000','010000','011000','100000','101000','110000','111000']
note_idx = [0,50,52,55,57,59,60,62]
watchdog_time = 0.1
elapsed_time = 0

class Watchdog:
  def __init__(self, timeout, userHandler=None):
    self.timeout = timeout
    self.handler = userHandler if userHandler is not None else self.defaultHandler
    self.timer = Timer(self.timeout, self.handler)
    self.timer.start()

  def reset(self):
    self.timer.cancel()
    self.timer = Timer(self.timeout, self.handler)
    self.timer.start()

  def stop(self):
    self.timer.cancel()

  def defaultHandler(self):
    raise self

def myHandler():
  global watchdog
  url = url_base + url_val[0]
  print(url) # Debug Code
  req = urllib.request.urlopen(url)
  watchdog.stop()

watchdog = Watchdog(watchdog_time, myHandler)
watchdog.stop()

with mido.open_output(ports[0]) as outport:
  for msg in mido.MidiFile('DanceOnTheInside.mid'):
    sleep_time = msg.time - elapsed_time
    if sleep_time <0:
      msg.time = msg.time + sleep_time
      elapsed_time = abs(sleep_time)
      sleep_time = 0
    else:
      elapsed_time = 0

    time.sleep(sleep_time)
    if not msg.is_meta:
      if(str(msg.type)=="note_on"):
        if(msg.velocity == 0):
          watchdog.reset()
        else:
          index = note_idx.index(msg.note)
          url = url_base + url_val[index]
          print(url) # Debug Code

          start = time.time()
          req = urllib.request.urlopen(url)
          elapsed_time = elapsed_time + time.time() - start
          watchdog.stop()

      outport.send(msg) 
```


# おわりに
- ネットワークの実遅延と、MIDIデバイスは(限界があるものの)ある程度歩み寄れることが分かりました
- 最初の自分の課題をなんとかしないと・・