Maker Faire Tokyo 2016 森のタクトについての詳細記事です。
概要
森のタクトでは、手を上下に振りその速度によって曲全体の再生速度を変えるようにしました。
今回手の振りを検出するために(普通であれば加速度センサーを使うところでしょうか)テルミンをハックして使うことにしました。
テルミンはアンテナ部に手を近づけると高い音がなり、離れると低い音がなります。
従って、その音の周波数をソフトウェアで計測することができるようになれば、手が近づいたのか、または手が遠くなったのかをソフトウェアからも判別することができるようになります。
また、手の上下運動を検出したいわけですから、アンテナはテーブルと接触するように配置し、手の上下運動を検出できるようにしました。(写真参照)
写真においてアルミの板をアンテナに取り付けているのはアンテナの感応面積をひろくとるためです。
ハードウェアのブロック図としては下記のようになります。
この記事での最終目標は手がテルミンに近づくとLow、テルミンから遠ざかるとHighを出すテンポクロックを生成しRaspberryPiに入力することです。
そのためのオーディオ波形の整形と周波数をカウントする回路を作成します。
詳細
##テルミンを分解してオーディオ信号を取り出す
この写真のように、分解しスピーカーの根本から直接ケーブルを引き出しました。
また、このテルミン回路のオーディオアンプがいけていないのかオシロスコープで波形を見ていると周波数が高くなるにつれて出力が著しく減衰していたので、スピーカーとの接続は切断しました。(出力が減衰すると次に説明する回路で都合が悪くなる)
##オーディオ信号をソフトウェアが読みやすい形に整形する
テルミンから出力されるオーディオ信号をArduinoで周波数カウントしたいのですが、オーディオ信号はサイン波なのでソフトウェアにとっては読み込みにくくなっています。
従ってこれをHighかLowの2値である矩形波に整形します。
具体的にはコンパレータを用いて、あるしきい値電圧以上になるとHigh,しきい値電圧以下Lowを出力させ、その出力をArudinoで周波数カウントします。
回路はこんなかんじです。
こちらが実際に作成した回路をオシロスコープで見た波形です。
ch1(黄)がコンパレータの+入力でテルミンから入力されてきたアナログ波形、ch2(青)がコンパレータの-入力でReference電圧、ch3(紫)がコンパレータの出力です。このch3の矩形波の周波数をArduinoで計測します。
##周波数をカウントし、手の振りに合わせたクロックを出力する
先ほどの矩形波の立ち上がりエッジ検出するためにArduinoへの入力先は割り込み検出が可能なD2ピンを使用しています。
周波数のカウントは、エッジを検出したらタイムスタンプをとり保持、次のエッジを検出したら同様にタイムスタンプをとり、その2つのタイムスタンプの差分によって1周期(逆数をとれば周波数)を計測します。
その周期が一定以下(周波数でいうと一定以上)であればArduinoのGPIOからHighを出力し、一定値以上であればLowを出力します。
これで手が下げられるとLEDが点灯し、上がるとLEDが消灯するようになりました。
ただし、ArduinoのGPIOは5V電圧、RaspberryPiのGPIOは3VなのでFET(Q1)を一段いれて電圧レベル変換をしています。レベル変換は抵抗分圧でもいいんですが、以前作った回路を流用している都合上FETになっています。
volatile unsigned long pre_duration = 0;
volatile unsigned long cur_duration = 0;
volatile unsigned int pulsecount = 0;
volatile unsigned long pre_micro = 0;
volatile unsigned long freq = 0;
unsigned long cnt = 0;
boolean irqflg = false;
unsigned int T_TH = 1200; //usec (100usec = 10kHz, 200usec = 5kHz)
boolean revflg = false;
boolean down_period = false;
boolean up_period = false;
boolean plsflg = false;
#define TEMPO_OUT 4
void setup() {
// Interrupt
attachInterrupt(0, periodIrq, RISING);
//GPIO
pinMode(TEMPO_OUT,OUTPUT);
digitalWrite(TEMPO_OUT, LOW);
// Serial
Serial.begin(9600);
Serial.println("");
Serial.println("Hello, I'm tempo counter!");
Serial.println("");
}
void loop() {
if (irqflg == true){
irqflg = false;
noInterrupts();
//Serial.print(cur_duration, DEC);
//Serial.println("usec");
if(cur_duration < T_TH){
//plsflg = true;
PORTD |= _BV(TEMPO_OUT);
//digitalWrite(TEMPO_OUT, HIGH);
}else{
//plsflg = false;
PORTD &= ~_BV(TEMPO_OUT);
//digitalWrite(TEMPO_OUT, LOW);
}
//Serial.print(1e3/cur_duration, 2);
//Serial.println("kHz");
//Serial.print("Plus: ");
//Serial.println(plsflg);
interrupts();
}
delay(100);
}
void periodIrq() {
unsigned long cur_micro = micros();
cur_duration = cur_micro - pre_micro;
pre_micro = cur_micro;
pre_duration = cur_duration;
irqflg = true;
}
##生成したクロック信号を使って再生速度を変更する
この回路で生成したテンポクロック信号をクライアントのRaspberryPiに入力します。
RaspberryPiはWiringPiを使用してどのGPIOであっても割り込み検出できますので、好きなGPIOに入力して構いません。
ソースコードはこんな感じです。
wiringPiをインストールしていない方はまずwiringPiをインストールしてください。
また、makeする際は-lwiringPiを指定してください。
注意:色々と省略しているのでこのままでは動きません。
#include <wiringPi.h>
#include <time.h>
#define BASE_PERIOD 1.2 //基本テンポ周期(sec)
float coeff = 1.0;
void calc_speed(){
struct timeval new_tv;
struct timeval old_tv;
unsigned long dur = 0;
unsigned long new = 0;
unsigned long old =0;
old = new;
gettimeofday(&new_tv, NULL);
new = (unsigned long)(new_tv.tv_sec*1000000 + new_tv.tv_usec);
dur = new - old;
//基本のテンポ周期からどれだけズレているか係数を計算
coeff = (float) dur / (BASE_PERIOD * 1000000)
}
int main(){
//GPIOの設定
wiringPiSetupGpio();
wiringPiISR(2, INT_EDGE_FALLING, calc_speed);
//曲の再生
for(p = datahead->next; p != NULL; p = p->next){
switch(0xf0 & p->sendbuf[0]){
//省略
}
//待ち時間に係数をかけて再生速度を調整する
usleep(p->wait_time*coeff);
}
return 0;
}