Maker Faire Tokyo 2016 森のタクトについての詳細記事です。
#概要
この作品の大きなフィーチャーのひとつとして音に合わせたアニメーションがあります。
スピーカー(サーバ)1台につき1台のモニタを配置し、スピーカーから音が出てない場合はアニメーションを止めます。
また、再生速度に応じてアニメーションの速度も変更します。
ブロック図はこのようになります。
前回までのところでクライアントで再生速度を計算する実装までは終了しています。
よって次は、その再生速度情報の送信とサーバーでの受信を実装します。受信にはaseqnetのソースコードに手を加えます。
その後、サーバーからAndroidに再生速度を伝えるためのPFM波形を生成します。PFMなので速度に応じてパルスON時間を変化させます(周波数も変化)。
また、音がなっていない場合(NoteOnが継続していない時)はPFMを停止させます。
これらの実装によってAndroid側でのアニメーションの停止・再生、アニメーション速度の変更を実現します。
#詳細
再生速度をクライアントからサーバーに伝送する
そもそもMIDIにおいては再生速度という概念はありません。全てデルタタイムによって管理されていて、それはクライアント側だけがデルタタイムに合わせて音情報を出力(NoteONとNoteOFF)し、サーバーはただ受け取った情報を元に音を鳴らすだけです。
ですので、当然ながらクライアントで利用しているalsaライブラリにも速度情報を送信するという機能はありません。
そこで今回はMIDIのSysExというイベントを利用することにしました。
SysExについて:http://www.geocities.jp/kuri_zill/memorandum/midi1.htm
SysExは本来音源固有の情報についてやりとりするものなのでこれを使っても害はないだろうという判断です。
SysExのデータは可変長となっていますが、SysExの流儀に従い0xF0ではじめ、0xF7で終わるデータとしその間に1byteの速度情報を挟むトータル3byteの情報としました。
速度情報は0 ~ 10までの整数で、0が最も早く10が最も遅いことを表すように定義しました。
まずクライアント側のソフトウェアを変更します。
前回のソースコードから引数numが増えていますが、これは送信先のサーバーを指定する引数です。
#include <alsa/asoundlib.h>
static snd_seq_t *seq_handle[6];
static snd_seq_event_t ev[6];
float ref_value[11];
void note_on(unsigned char num, unsigned char ch, unsigned char note, unsigned char velocity){
//省略
}
void note_off(unsigned char num, unsigned char ch, unsigned char note, unsigned char velocity){
//省略
}
unsigned char find_nearest_idx(float coeff){
//ref_value[]の中身とcoeffを比較してもっとも近いインデックスのものを返す(コード省略)
return nearest_idx;
}
void speed_msg(unsigned char num, unsigned char speed){
static unsigned char dataptr[3] = {0xf0, 0x00, 0xf7};
dataptr[1] = speed;
snd_seq_ev_set_sysex(&ev[num], (unsigned int)sizeof(dataptr), dataptr);
send_event(num, 1);
}
void calc_speed(){
//途中省略
//基本のテンポ周期からどれだけズレているか係数を計算
coeff = (float) dur / (BASE_PERIOD * 1000000)
//速度係数から11段階の速度インデックスに変換
unsigned char idx = find_nearest_idx(coeff);
//サーバーへ送信
for(i = 0; i < NUM_SERVER; i++){
speed_msg(i, idx);
}
}
int main(){
//速度係数coeffと11段階の速度を対応付けるための配列を予め作成
for (i = 0; i < 11 ; i++){
ref_value[i] = (float) pow(2.0, -1+0.2*i);
}
//曲の再生
for(p = datahead->next; p != NULL; p = p->next){
//省略
}
return 0;
}
次にサーバー側です。
aseqnetのソースコードをダウンロードしてきて改変します。
https://fossies.org/dox/alsa-utils-1.1.1/aseqnet_8c_source.html
//新しく追加
static void set_gpio(snd_seq_event_t *ev){
static unsigned char *dataptr;
switch (ev->type) {
case SND_SEQ_EVENT_SYSEX:
dataptr = (unsigned char*)ev->data.ext.ptr;
//ここでデータの受け取り
speed_idx = dataptr[1];
printf("Sys EX, datalen:%d, data:%d \n", ev->data.ext.len, speed_idx);
break;
case SND_SEQ_EVENT_NOTEON:
if(ev->data.note.velocity == 0){
//Note off
}else{
//Note on
}
break;
case SND_SEQ_EVENT_NOTEOFF:
//Note off
break;
}
}
//この部分を改変
static int copy_remote_to_local(int fd)
{
int count;
char *buf;
snd_seq_event_t *ev;
count = read(fd, readbuf, MAX_BUF_EVENTS * sizeof(snd_seq_event_t));
buf = readbuf;
if (count == 0) {
if (verbose)
fprintf(stderr, _("disconnected\n"));
return 1;
}
while (count > 0) {
ev = (snd_seq_event_t*)buf;
buf += EVENT_PACKET_SIZE;
count -= EVENT_PACKET_SIZE;
if (snd_seq_ev_is_variable(ev) && ev->data.ext.len > 0) {
ev->data.ext.ptr = buf;
buf += ev->data.ext.len;
count -= ev->data.ext.len;
}
snd_seq_ev_set_direct(ev);
snd_seq_ev_set_source(ev, seq_port);
snd_seq_ev_set_subs(ev);
if (info)
print_event(ev);
snd_seq_event_output(handle, ev);
//ここに追加
set_gpio(ev);
}
snd_seq_drain_output(handle);
return 0;
}
タッチパネルを使ったデータ伝送
次にRaspberryPiからAndroidに対してデータの転送方法(GPIOのON/OFF仕様)を決めます。
Androidは既にUSBの口をHDMI出力のために使用しているので、USB以外の方法で行わなければなりません。
そこで、タッチパネル(タッチの間隔)を利用して、速度の情報を送るという方法を取ることにしました。
まずは、回路的にタッチを行うようにします。
方法はこちらを参考にしました。
http://hackaday.com/2014/07/26/pwning-timberman-with-electronically-simulated-touchscreen-presses/
実際に作成した回路は以下の写真の通りです。銅箔テープをタッチしたい位置に貼り付け、タッチしたいときは銅箔テープをGNDに落とし(ON/OFFラインをHigh)、タッチしないときは銅箔テープを不定(ON/OFFラインをLow)にします。
注意点は銅箔テープは小さめに、銅箔テープからリレーの入り口までは細めのエナメルで短く配線します。そうしないとGNDに落とさなくても銅箔テープと導線だけで静電容量を変化させてしまいタッチ状態と認識させてしまうからです。
また、リレーの代わりにFETを使用しましたがうまく行きませんでした。FET内部の寄生容量のせいかもしれません。
トランジスタもだめでした。ただ、トランジスタでは成功している事例もあるので何か私の方に問題があるようです。
タッチパネル通信用PFM波形の生成
電気的にタッチができるようになったので、あとは通信仕様を決めます。
これはAndroidアプリを担当しているエンジニアと相談し、以下のテーブルのようにしました。
Android側がタッチを検出できる速度を実験した結果から算出したものです。
PWMではなくPFMを使用しているのは、PWMだとPFMと比較して周波数を下げる必要がありそれがデータ転送の遅延になるからです。
インデックス | 速度 | タッチの仕様(PFM) |
---|---|---|
0 | 2^1.0 (2倍速) | on:32ms, off:32ms |
1 | 2^0.8 | on:48ms, off:32ms |
2 | 2^0.6 | on:64ms, off:32ms |
3 | 2^0.4 | on:80ms, off:32ms |
4 | 2^0.2 | on:96ms, off:32ms |
5 | 2^0 (通常速度) | on:112ms, off:32ms |
6 | 2^-0.2 | on:128ms, off:32ms |
7 | 2^-0.4 | on:144ms, off:32ms |
8 | 2^-0.6 | on:160ms, off:32ms |
9 | 2^-0.8 | on:176ms, off:32ms |
10 | 2^-1.0 (0.5倍速) | on:192ms, off:32ms |
先ほど受け取った速度情報(インデックス)からこのテーブルに従ってPFM出力します。
PFMを生成するにあたって気をつけなければならない点は、ハードウェアPWMではなくソフトウェアPWMの機能を使うという点です。
RaspberryPiはAudio出力とハードウェアPWMが排他になっており、このサーバーでは既にAudio出力を使用しているためハードウェアPWMは使用できません。
大変ありがたいことにwiringPiはソフトウェアPWMも対応しているのでそちらを使用させてもらいました。
https://github.com/WiringPi/WiringPi/blob/master/wiringPi/wiringPi.c