はじめに
画像処理の勉強のためにパトライトの点灯状況をカメラから取得するプログラムを作っているつもりが、いつの間にか星野源をアゲアゲにしていました。どうしてこうなった。
作成した動画
ご家庭によくある #パトライト の #シグナルタワー に一緒に歌ってもらいました。
— 伊織 (@iori_ama) May 23, 2020
#うちで踊ろう #DancingOnTheInside pic.twitter.com/lZC0mgzSzf
今回使用した楽譜
星野源「うちで踊ろう」を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のテンポが合わない
- 動画編集でどうにかしました(気合い)
プログラム
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デバイスは(限界があるものの)ある程度歩み寄れることが分かりました
- 最初の自分の課題をなんとかしないと・・