ブラウザとMIDIコントローラを繋げてみる。

今回はMIDIコントローラでブラウザで生成する矩形波を弾いてみる。


準備するもの
chromeブラウザと、pythonとライブラリpygameとAutobahnPythonと、なんらかのMIDI機器。
firefoxも可能なのですが音声部分のタイプ量が増えるので読みやすさ重視で今回は見送ります。
また残念ながらnode.jsは使ってません。


作るもの
ファイルは2つで、MIDI受信サーバーのpythonファイルとシンセエンジンのhtml。


おおまかな仕組み
サーバーは、MIDIコントローラから送られるデータを待ち、受け取ったらブラウザに渡します。
ブラウザは、受け取ったデータに従い矩形波の音をオンオフしたりピッチや音量を変えます。


MIDI入力
PythonでリアルタイムにMIDI機器と通信するには、一番手っ取り早いのPygameだと思います。
ちなみに1.9以上が必要なので古いの使ってる人はバージョン上げてください。
(僕の環境だと多少不安定で、気になるならpyoやpysndobjあたり使ってはどうでしょうか)

import pygame.midi
pygame.midi.init()
out = pygame.midi.Input(1)
out.read(1)

必要なのはこの4行です。
3行目のInputの引数が1になっていますが、これはデバイスの番号です。
よって環境によって異なります、それを調べる方法が

for i in range(pygame.midi.get_count()):
    print pygame.midi.get_devie_info(i)

これで使用できるデバイスが全て表示されます。
ここから例えばKORGのnanoとかスタインバーグのCMCとか自分が持ってるMIDI機器を選びます。
持ってなくても仮想MIDIケーブルなどで音楽ソフトウェアからブラウザにアクセスしてみてもいいでしょう。


次に4行目のreadの引数は1になっています。
入力されたデータを1つだけ取り出すということです。
今回は鍵盤が押されたらなにかアクションをするという単純なものなのでヒトツデジュウブンデスヨ。


このreadはループの中で常に入力を待つような使い方をします。例えば

import time
while 1:
    print output.read(1)
    time.sleep(1)

これだと無限ループですが雰囲気は伝わるでしょうか?。
入力がない場合は空の配列が返されます。
キーが入力された場合は、

[[[144,60,100],4753]]

144は鍵盤が押されたということを表します。
逆に鍵盤を離した場合は128になるはずです。
60は周波数(ノートナンバー)で100は音量、4753は初期化されてからの時間(タイムスタンプ)。
配列は環境によっていろいろあるでしょうからご自身のMIDI機器とにらめっこしてください。


サーバー

さて次はサーバーです。AutobahnPythonを使ってみます。
あまり有名ではないかもしれませんが名前が気に入りました。
わずか10行でエコーサーバーが決め手です。
本当はwindowsのバイナリがあるのが決め手だったかも。


さてこのAutobahnPythonはTwistedに依存しているので使用するためには

Zope Interface
Twisted
AutobahnPython

これらをインストールしてください。winユーザも安心のバイナリありで、ちょちょいです。


さてコードです。10行でエコーサーバーを少し改良した程度のものです

from twisted.internet import reactor
from autobahn.websocket import WebSocketServerFactory, WebSocketServerProtocol, listenWS
import pygame.midi

class EchoServerProtocol(WebSocketServerProtocol):
   def sendMIDI(self):
      temp = self.out.read(1)
      if len(temp): # if self.out.poll():でもよい。入力があるとTrueを返すので。
         self.sendMessage(str(temp))
      reactor.callLater(1/30.0, self.sendMIDI)
   def onOpen(self):
      pygame.midi.init()
      self.out = pygame.midi.Input(1)
      self.sendMIDI()

if __name__ == '__main__':
   factory = WebSocketServerFactory("ws://localhost:9000")
   factory.protocol = EchoServerProtocol
   listenWS(factory)
   reactor.run()

11行目のonOpenが最初に呼ばれるのでそこでpygame.midiを初期化し、sendMIDIを呼びます。
sendMIDIを再帰的にcallLaterで1/30秒ごとにMIDIの入力をチェックします。
次にブラウザにメッセージを送信するのは9行目のsendMessage。


ブラウザ

最後はブラウザです。

<html>
<head>
<script type="text/javascript">
function $(id){ return document.getElementById(id) }
var sampleRate = 48000;
var bufferSize = 2048;
var freq = 440;
var amp = 0.3;
var audiocontext = new webkitAudioContext();
audiocontext.sampleRate = sampleRate;
var node = audiocontext.createJavaScriptNode(bufferSize, 0, 1); 
var phase = 0;
node.onaudioprocess = function (event) {
    var data = event.outputBuffer.getChannelData(0);
    // 以下は矩形波を生成する。
    for (var i = 0; i < data.length; i++) {
        phase += freq/sampleRate;
	phase = phase >= 1 ? -1 : phase;
        data[i] = phase < 0 ? -1*amp : 1*amp;
    }
};
//ノートナンバーを周波数に
function midi2freq(x){
    return 440*Math.pow(2, (x-69)/12);
}
window.onload = function() {
    $("console").innerHTML = "hoge";
    node.connect(audiocontext.destination); //ここで音が出る.
    var ws_uri = "ws://localhost:9000";
    if ("WebSocket" in window) {
        webSocket = new WebSocket(ws_uri);
    }else{
        webSocket = new MozWebSocket(ws_uri);
    }
    webSocket.onmessage = function(e) {
        temp = eval(e.data);
        $("console").innerHTML = " freq: " + String(midi2freq(temp[0][0][1]));
        freq = midi2freq(temp[0][0][1]); //周波数を取り出す。
        if(temp[0][0][0] !== 128){
            $("console").innerHTML += " amp: " + String(temp[0][0][2]);
            amp = parseInt(temp[0][0][2])/127.0;
            $("console").innerHTML += " state: on ";
        }else{
            $("console").innerHTML += " amp: 0 ";
            amp = 0;
            $("console").innerHTML += " state: off ";
        }
    }
}
</script>
</head>
<body>
<h1>midi -> browser test</h1>
<div id="console"></div>
</body>
</html>

関数midi2freqはその名の通りMidiのノートナンバーを周波数にしている。
音声処理の部分の説明は過去に何度も行なっているので省略します。