Maker Faire Tokyo 2016 森のタクトについての詳細記事です。
概要
今回のシステムを作るにあたって最も重要な部分が、楽器毎に複数のスピーカーを鳴らす仕組みでした。
その為にはまず、曲の演奏情報が楽器ごとに分かれている必要があるのでソースファイルとしてはMIDI(.midi)を使用しました。
また、複数のスピーカーに振り分けて音を鳴らすために、スピーカーひとつにつきRaspberryPiをひとつ割り当て、曲のソースファイルをもつRaspberryPiから音の情報を各RaspberryPiに送信し受け取ったらすぐに音を鳴らす、という仕様にしました。
RaspberryPi間のデータの送受信には有線LANを使用しています。本来MIDIのデータのやりとりにはMIDIの仕様に則ったMIDIケーブルを使うのが筋ですが、当然ながらMIDIポートなどはないのでそれより高速ならなんでもいいだろうということでMIDIは使用していません。
詳細
これを実現する方法を3つの要素に分解してそれぞれについて説明します。
MIDIファイルを読み込む
MIDIファイル(.mid拡張子)はSMFというフォーマットに従って音の情報が格納されています。
SMFフォーマットは、”デルタタイム”と"イベント"の2つをひとつのかたまりとして、それらを羅列して表現されていてます。デルタタイムはそのイベントの継続時間(次のイベントまでの時間)を示しており、イベントはその名の通り何かしらのアクション情報です。
イベントは3つに分かれていますが、そのイベントの中にもいくつかの細かい分類があります。
今回の開発で関係があるイベントは下記です。(実際はもっといろいろあります)
- MIDI Event
- NoteON
- NoteOFF
- Program Change
- Meta Event
- Set tempo
- SysEx Event
この中で最も重要なのが、MIDI Eventの中にあるNote ON、Note OFFのイベントです。具体的には下記の3byteのデータになっています。これは純粋に音を鳴らす、音を止めるというイベントです。
情報 | データサイズ | 説明 |
---|---|---|
ON/OFF | 4bit | 0x8の場合はNoteOFF(音を止める)、 0x9の場合はNoteON(音を鳴らす) |
Channel | 4bit | どのチャンネルの音を鳴らすか。 予めProgram Changeイベントによってチャンネルと楽器(音色)のひも付けが行われているので、 実質どの楽器(音色)を使うのかを示している。 |
Note Number | 8bit | 音階 |
Velocity | 8bit | 音の強さ。 NoteONイベントでもこれを0にするとNoteOFFと同義になる。 |
従って、このイベントに合わせた構造体を定義し、SMFを読み込みながらその構造体をリスト化することにしました。具体的にはsendbuf[]に送信する情報をセットしていきます。全てのイベントに対応しようとすると3byteでは足りないのですが、基本的な音を鳴らすだけであれば、3byteに収まってしまうのでこのように定義しています。また、wait_timeにはデルタタイムから計算した実時間(単位はusec)をセットします。
この構造体をリスト化するにあたっては、曲によってイベントの数が異なっているということがあるのでリンクリスト構造としSMFを頭から読み込みながらイベントのたびにメモリを確保、変数のセット、連結を行うようにしています。(注:このやり方はフォーマット0にしか対応していないので、フォーマット1は動きません。)
struct MidiEvent{
unsigned char sendbuf[3];
unsigned int wait_time; // usec
struct MidiEvent *next;
};
C言語でSMFの読み込むコードは下記のサイトを参考にさせていただきました。
http://torasukenote.blog120.fc2.com/blog-entry-104.html
音を鳴らす
まずはデータの送信は考えず1台のRaspberryPiの中でデータの読み込みから音の出力までをテストすることに。
最初にMIDI音源、Playerとして有名なtimidityをインストールします。
$ sudo apt-get install timidity
timidityを使ってmidiファイルを再生するには、
$ timidity hogehoge.mid
で済むのですが、今回は自前に読み込んだデータを利用して音を出力したいのでこれではいけません。
この場合はまずtimidityを使ってmidi portを開きます。
$ timidity -iA &
midi portが開いたかどうかはaplaymidiというコマンドを使用します。
$ aplaymidi -l
下図のようにtimidity -iAを行った後では128:0 ~ 128:3というPortが開いていることがわかります。
このPortに対して音の情報を流すプログラムを書きます。
プログラムにはalsa-libraryというものを使うので、ライブラリのインストールをします。
$ sudo apt-get install libasound2 libasound2-dev
プログラムの大まかな流れ(必要な部分だけを抜粋)は下記の通り。
まずは、seq_open()で事前にTimidityで開いておいたポート128:0 ~ 128:3のどれかに接続します。
接続したあとは、note_on()もしくはnote_off()を呼べば音がなったり音を止めたりできます。
具体的なプログラムの中身については下記を参考にしました。
http://www.nurs.or.jp/~sug/soft/binbo/binbo5.htm
#include <alsa/asoundlib.h> //alsaライブラリ
int seq_open(){
//自分自身のポートのオープンとTimidityポートへの接続
}
int seq_close(){
//ポートを閉じる
}
void note_on(unsigned char ch, unsigned char note, unsigned char velocity){
//引数に応じた音を鳴らすよう、接続したポートに送信する
}
void note_off(unsigned char ch, unsigned char note, unsigned char velocity){
//引数に応じた音を止めるよう、接続したポートに送信する
}
makeするときには-lasoundの指定してください。
ではこれを使って先ほど読み込んでおいたデータを鳴らすには下記のように記述します。
実際にはProgram Changeというイベントにも対応しないと音は鳴らせないですが、実装の雰囲気をわかりやすくするためにあえて省略しています。
int seq_open(){
//中身省略
}
int seq_close(){
//中身省略
}
void note_on(unsigned char ch, unsigned char note, unsigned char velocity){
//中身省略
}
void note_off(unsigned char ch, unsigned char note, unsigned char velocity){
//中身省略
}
struct MidiEvent *smf_parse(){
//SMFを読み込んでリンクリストの先頭を返す(中身省略)
struct MidiEvent head;
return &head;
}
int main(){
struct MidiEvent *datahead;
struct MidiEvent *p;
unsigned char ch;
seq_open();
datahead = smf_parse();
for(p = datahead->next; p != NULL; p = p->next){
switch(0xf0 & p->sendbuf[0]){
case 0x80:
//Note OFF
note_off(0x0f & p->sendbuf[0], p->sendbuf[1], p->sendbuf[2]);
break;
case 0x90:
//Note ON
note_on(0x0f & p->sendbuf[0], p->sendbuf[1], p->sendbuf[2]);
break;
default:
break;
}
usleep(p->wait_time);
}
seq_close();
return 0;
}
MIDI情報を送信する
これで音を鳴らすための下準備はできたので、あとは出力ポートを自分自身のRaspberryPiで開いたポートではなく、別のRaspberryPiで開いたポートを指定してあげればよいということになります。
問題は、先ほど書いたように物理的なMIDIインターフェースを使用するわけではないので何でデータ通信するか...です。最初はSerialを使用してこの3byteのデータをバイナリ送信し、受信側で受け取った3byteを各ポートに出力するようにしていました。
が、RaspberryPiのSerialが不安定(取りこぼしをする、物理層レベルで正しいデータが送信されていないetc.)で数日かけても原因を取り除くことができませんでした。
そこでSerialは捨ててその代わりにLANを使用しました。midiをLANでデータ通信する試み(midi over lanと呼ばれる)は先人たちが既に行っていたため利用できるツールもあり非常に簡単に実現できました。どのようにやるかというと、
(今後データを受信するRaspberryPiをサーバー、データを送信するRaspberryPiをクライアントと呼ぶことにします。)
1.サーバー側でサーバーを立ち上げる
(ip address:192.168.11.10とする)
$ timidity -iA &
$ aseqnet &
$ aconnect 128:0 129:0
2.クライアント側
$ aseqnet 192.168.11.10 &
$ aplaymidi -l
これでクライアント側で”128:0 aseqnet”というmidi portが見えれば成功。
あとは先ほどと同じように128:0というポートに向けて音情報を送信すれば、ネットワークを介してサーバー側のtimidityまで情報が伝わるようになります。(バケツリレーのようなイメージ)
今回は複数のスピーカーから音を鳴らす必要があるので、各RaspberryPiで同じようにサーバーを立てていき(ipはそれぞれ固定しておく)、そのアドレスに対してクライアント側からaseqnetで接続してあげます。
クライアント側にはmidi portが128:0から順に複数見えるようになるので、その全てのポートに対して接続し、
SMFのチャンネル情報を見て振り分け先を決めてデータを出力するようにプログラムを修正します。