言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

世界聴診器を作るその1

では、Arduino を使って世界聴診器 http://swikis.ddo.jp/WorldStethoscope/2 を作ってゆきます。世界聴診器とは、アナログ入力の電圧を元に周波数が変化するような物ですが、これだけだと実は Arduino で実現するのは超たやすいです。例えば http://todbot.com/blog/bionicarduino/ にある Theremin のサンプルが分かりやすいです。digitalWrite() 関数で入力された電圧を元に delayMicroseconds() でウエイトをかけながら出力信号を ON/OFF するだけです。

しかしこのままでは世界聴診器としては使えません。タイミングが厳密ではなく音がキタナイのと、周波数ではなく、波長が電圧に比例しているからです。周波数と電圧が比例するというのは、高い電圧で高い音が鳴った方が直感的だという阿部さんのこだわりなのだろうと思いますが、尊重してそのまま作ります。以下のプログラムは Analog 1 が入力、Digital 9 が音声出力になっています。5V - 光センサ - Analog1 - 抵抗 - GND のように接続すると、テルミンみたいに遊べますし、もちろん世界聴診器モーフでもかっちり動きます。

今回は Output Compare Register A というのを使って、プリスケール 32 のカウンタを 4 周期固定で回しています。するとカウンタ周期は 16MHz / 32 / 4 = 125KHz となり、それを基準周波数として、欲しい周波数を作り出します。125KHz じゃ説明しにくいので、もしも 基準周波数が 10KHz で欲しい周波数が 4KHz だとすると、

  • アルゴリズム
    • 初期値:
      • 余分周波数 := 基準周波数
      • 目的周波数 := 鳴らしたい周波数 * 2 (ON/OFF を切り替えるので)
    • タイマ発生: 余分周波数 := 余分周波数 - 目的周波数
    • 余分周波数 < 0 なら一回休み
    • 余分周波数 >= 0 なら
      • トグル
      • 余分周波数 := 基準周波数 + 余分周波数
  • 例: 4KHz が欲しい時は、4KHz の倍の 8KHz を目的周波数として
    • 10KHz + 0KHz - 8KHz = 2KHz 待ち
    • 2KHz - 8KHz = -6KHz ON
    • 10KHz + -6Hz - 8KHz = -4KHz OFF
    • 10KHz + -4Hz - 8KHz = -2KHz ON
    • 10KHz + -2Hz - 8KHz = 0KHz OFF
    • 10KHz + 0Hz - 8KHz = 2KHz 待ち
    • 2Hz - 8KHz = -6KHz ON

のようにすると、10Hz の 2/5 すなわち 4KHz の矩形波を足し算と引き算だけで作る事が出来ます。途中で波長を求めなくて良いのがミソです。最高でも 5KHz 出せれば良いので基準周波数として 10KHz あれば十分そうですが、なんか音が汚かったのであれこれ試して 125KHz にしました。それでもまだ変な音が混じりますがマルチメーターで測ると割とちゃんと思った通り周波数がとれるのでこれで良いという事にします。

どういうわけだか最新版の開発環境では動かず、Arduino 0010 を使いました。

// World Stethoscope Clone
// for Arduino 0010
#define PRESCALE 32
#define COUNTER_MAX 4
#define PRECISION 1024
#define FREQ_SIGNAL_MAX (5000L * 2)

#define PIN_OUTPUT 9
#define PIN_INPUT 1

long SAMPLING_RATE = F_CPU / PRESCALE / COUNTER_MAX;
long margin = SAMPLING_RATE;
long frequency = FREQ_SIGNAL_MAX;
long nextFrequency = FREQ_SIGNAL_MAX;
char signal = 0;

void setup() {
  pinMode(PIN_OUTPUT, OUTPUT);

  TCCR2A = 1 <<WGM21 | 0<<WGM20; // Clear Timer on Compare Match
  TCCR2B = 0<<WGM22 | 0<<CS22 | 1<<CS21 | 1<<CS20; // clk/32 prescale
  OCR2A = COUNTER_MAX - 1; // Output Compare Register A
  TIMSK2 = 1<<OCIE2A; // Timer/Counter2 Output Compare Match A Interrupt Enable
  TCNT2 = 0; // Reset Timer/Counter2
}

ISR(TIMER2_COMPA_vect) { // Timer/Counter2 Overflow

  margin -= frequency;
  if (margin <= 0) {
    margin += SAMPLING_RATE;
    digitalWrite(PIN_OUTPUT, signal); // Output signal
    signal ^= 1;
    frequency = nextFrequency;
  }
}

void loop() {
  int inputValue = analogRead(PIN_INPUT) + 1; // 1 ... 1024
  long freq = inputValue * FREQ_SIGNAL_MAX / PRECISION;
  nextFrequency = freq;
}

追記

nekosan さんのブログ http://brown.ap.teacup.com/nekosan0/382.html で紹介して頂きました。有り難うございました!で、ソースが切れる件ですが、はてなダイアリーのバグのようですが、再現方法が分かりません。とりあえず上では TCCR2A の行の 1<< を 1 << に変えるとなおりました。他にも使ってる部分あるのに、なんでかな???