web-dev-qa-db-ja.com

Arduino-ピエゾブザーで同時に2つ以上のトーンを作成するにはどうすればよいですか?

私の高校の電子工学のクラスは、いくつかのarduinounoキットを購入することに決めました。これは非常にクールだと言わざるを得ません。それについては十分ですが、現在、クラスではピエゾブザーを実験しています( this のように見えます)。ピエゾブザーを使って曲を作ることを学びました。私たちの先生は私たちに「創造的」であると言いました。 KatyPerryの「Firework」を使用するよりもクリエイティブになるのに最適な方法です。

いくつかの創造的な自由を使用して、私はこの曲の素敵なピアノ曲を見つけました(リンク ここ )。今、私はピアノ奏者です(AP音楽理論を採用しました)。私が抱えている問題は、ピエゾブザーだけで1音しか演奏できないことです。ピアノで(または少なくとも近くで)再生されているように聞こえるように、ピエゾブザーで曲を再生することは可能ですか?低音部記号と高音部記号の音がブザーで同時に演奏されるようです。

位相シフトと音符の周波数の追加が含まれることは理解していますが、これをピエゾブザーのコードにどのように変換しますか?いくつかのサンプルコードを投稿していただければ幸いです。そうでない場合は、可能な限り明確な方法で説明していただけますか。私はプログラミングの達人ではありませんが、初心者でもありません。

9
Vishwa Iyer

Arduinoはデジタル出力のみを提供します。出力はオン(+ 5V)またはオフ(0V)のいずれかです。この時点で遭遇したと思われるtone()関数は、指定された周波数で方形波を出力します。

100Hzのトーンが必要だとします。 100Hzは、出力が1/100秒、つまり10msごとに繰り返されることを意味します。したがって、tone(PIN,100)は、5msごとに呼び出されるタイマー割り込みを設定します。割り込みが最初に呼び出されると、出力がLowに設定され、プログラムが実行していたものに戻ります。次回呼び出されると、出力が高く設定されます。したがって、5msごとに出力が変化し、50%のデューティサイクルで方形波が得られます。これは、出力が正確に半分の時間オンになることを意味します。

これはすべて非常にうまくいきますが、ほとんどのオーディオ波形は方形波ではありません。 2つの方形波トーンを同時に再生する場合、または1つの方形波トーンの音量を制御する場合でも、「オン」と「オフ」だけでなく、より多くの値を出力できる必要があります。

良いニュースは、パルス幅変調(一般にPWMと略される)と呼ばれる使用できるトリックがあることです。アイデアは、出力を2つの値のいずれかにしか設定できない可能性があるということですが、非常に高速に設定できます。人間は約20kHzまでの可聴周波数を聞くことができます。それよりも速く出力をディドルした場合、たとえば200kHz(16MHzでクロックされるArduinoの機能の範囲内)では、個々の出力遷移は聞こえませんが、平均長期間にわたる値。

tone()で200kHzのトーンを生成することを想像してみてください。聞くには高すぎますが、平均値はオンとオフの中間です(50%のデューティサイクル、覚えていますか?)。これで、3つの可能な出力値(オン、オフ、および中間)が得られました。これは、2つの方形波を同時に再生するのに十分です。 Diagram of summing square waves

高品質のオーディオには、これよりもはるかに多くの値が必要です。 CDは16ビットオーディオを保存します。つまり、65536の可能な値があります。また、ArduinoからCD品質のオーディオを取得することはできませんが、50%以外のデューティサイクルを選択することで、より多くの出力値を取得できます。実際、Arduinoにはこれに対応するハードウェアがあります。

analogWrite()に会います。これは、Arduinoの組み込みPWMハードウェアを使用してさまざまな出力レベルを偽造します。悪いニュースは、PWM周波数が通常500Hzであるということです。これは、LEDを調光するには問題ありませんが、オーディオには低すぎます。したがって、ハードウェアレジスタを自分でプログラムする必要があります。

Arduino PWMの秘密 いくつかの詳細情報があります。ここに 詳細なリファレンス ArduinoにPWMDACを実装する方法があります。

7ビットの分解能を選択しました。これは、出力が16MHz/128 = 125kHzの方形波で、128のデューティサイクルが可能なことを意味します。

もちろん、PWM出力が機能するようになったら、楽しみは始まったばかりです。複数のボイスがある場合、波形の周波数を設定するために割り込みに頼ることはできません。自分でそれらを伸ばす必要があります。基本的なデジタル信号処理(DSP)の知識は非常に便利です。割り込みハンドラー内からオーディオデータを生成するにはタイトなコードが必要です。次に、適切なノートを適切なタイミングでトリガーするためのプレイルーチンが必要です。空が限界!

とにかく、ここにいくつかのコードがあります:

#define PIN 9

/* these magic constants were generated by the following Perl script:
   #!/usr/bin/Perl -lw
   my $freq = 16000000/256;
   my $A4 = 440;
   print int(128*$freq/$A4*exp(-log(2)*$_/12)) for (-9..2);
*/
const uint16_t frtab[] = {
  30578, 28861, 27241, 25712,
  24269, 22907, 21621, 20408,
  19262, 18181, 17161, 16198
};

#define VOICES 4

struct voice { 
  uint16_t freq;
  int16_t frac;
  uint8_t octave;
  uint8_t off;
  int8_t vol;
  const uint8_t *waveform;
} voice[VOICES];

#define PITCH 50 /* global pitch adjustment */

/* some waveforms. 16 samples each */
const uint8_t square_50[] = {
  0, 0, 0, 0, 0, 0, 0, 0,15,15,15,15,15,15,15,15
};
const uint8_t square_25[] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15,15,15,15
};
const uint8_t square_12[] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15,15
};
const uint8_t square_6[] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,15
};
const uint8_t sawtooth[] = {
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15
};
const uint8_t triangle[] = {
  0, 2, 4, 6, 8,10,12,14,15,13,11, 9, 7, 5, 3, 1
};
const uint8_t nicebass[] = {
  0, 8,14,18,22,23,24,25,26,25,24,23,22,18,14, 8
};

void setup() {
  /* TIMER0 is used by the Arduino environment for millis() etc.
   So we use TIMER1.
 */
  pinMode(PIN, OUTPUT);
  /* fast PWM, no prescaler */
  TCCR1A = 0x80;
  TCCR1B = 0x11;
  /* 7-bit precision => 125kHz PWM frequency */
  ICR1H = 0;
  ICR1L = 0x7f;
  /* enable interrupts on TIMER1 overflow */
  TIMSK1 = 1;
  OCR1AH = 0; /* hi-byte is unused */
  for (uint8_t i=0; i<VOICES; i++)
    clear_voice(i);
}

void set_voice(uint8_t v, uint8_t note, uint8_t volume, const uint8_t *waveform) {
  note += PITCH;
  voice[v].octave = note/12;
  voice[v].freq = frtab[note%12];
  voice[v].frac = 0;
  voice[v].off = 0;
  voice[v].waveform = waveform;
  voice[v].vol = volume;
}

void clear_voice (uint8_t v) {
  voice[v].freq = 0;
}

uint8_t s = 0;

ISR(TIMER1_OVF_vect) {
  /* Calculate new data every 4 pulses, i.e. at 125/4 = 31.25kHz.
     Being interrupted unnecessarily is kinda wasteful, but using another timer is messy.
  */
  if (s++ & 3)
    return;

  int8_t i;
  int8_t out = 0;
  for (i=0; i<VOICES; i++) {
    if (voice[i].freq) {
      voice[i].frac -= 128<<voice[i].octave;
      if (voice[i].frac < 0) { /* overflow */
        voice[i].frac += voice[i].freq;
        voice[i].off++;
      }
      /* warning: vol isn't a real volume control, only for square waves */
       out += (voice[i].waveform[voice[i].off & 15]) & voice[i].vol;
    }
  }

  /* out is in the range 0..127. With 4-bit samples this gives us headroom for 8 voices.
     Or we could use more than 4-bit samples (see nicebass).
   */
  OCR1AL = out;
}

/* tune data */
const uint8_t bass[8][4] = {
  { 12, 19, 23, 24 },
  {  5, 12, 19, 21 },
  { 12, 19, 23, 24 },
  {  5, 12, 19, 21 },
  { 14, 16, 17, 21 },
  {  7, 19, 14, 19 },
  { 14, 16, 17, 21 },
  {  7, 19, 14, 19 }
};

const uint8_t melody[2][8][16] = {
  {/* first voice */
    {31, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 0,28,26,24 },
    { 0, 0, 0, 0, 0, 1, 2, 3,53,54,53,54, 0, 1, 2, 3 },
    {31, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5,28, 5,26 },
    { 5,28,24, 0, 0, 1, 2, 3,53,54,56,54, 0, 1, 2, 3 },

    {29, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5, 0,28, 5 },
    {28, 5, 0,26, 0, 1, 2, 3,54,56,58,56, 0, 1, 2, 3 },
    {29, 0, 0, 0, 0, 1, 2, 3,31,29,28,29, 5, 0,28, 5 },
    {28, 5, 0,26, 0, 1, 2, 3, 0,19,21,23,24,26,28,29 },
  },

  {/* second voice */
    {24, 0, 0, 0, 0, 1, 2, 3,24,24,24,24, 0,24,24,21 },
    { 0, 0, 0, 0, 0, 1, 2, 3,49,51,49,51, 0, 1, 2, 3 },
    {24, 0, 0, 0, 0, 1, 2, 3,24,24,24,24, 5,24, 5,24 },
    { 5,23,21, 0, 0, 1, 2, 3,49,51,53,51, 0, 1, 2, 3 },

    {26, 0, 0, 0, 0, 1, 2, 3,24,26,24,24, 5, 0,24, 5 },
    {24, 5, 0,24, 0, 0, 0, 0,51,51,54,54, 0, 1, 2, 3 },
    {26, 0, 0, 0, 0, 1, 2, 3,24,26,24,24, 5, 0,24, 5 },
    {24, 5, 0,23, 0, 1, 2, 3, 0, 5, 0,19,21,23,24,26 },  
  }
};

void loop() {
  uint8_t pos, i, j;

  for (pos=0; pos<8; pos++) {
    for (i=0; i<16; i++) {
      /* melody: voices 0 and 1 */
      for (j=0; j<=1; j++) {
        uint8_t m = melody[j][pos][i];
        if (m>10) {
           /* new note */
           if (m > 40) /* hack: new note, keep volume */
             set_voice(j, m-30, voice[j].vol, square_50);
           else /* new note, full volume */
             set_voice(j, m, 15, square_50);
        } else {
          voice[j].vol--; /* fade existing note */
          switch(m) { /* apply effect */
            case 1: voice[j].waveform = square_25; break;
            case 2: voice[j].waveform = square_12; break;
            case 3: voice[j].waveform = square_6; break;
            case 4: clear_voice(j); break; /* unused */
            case 5: voice[j].vol -= 8; break;
          }
          if (voice[j].vol < 0)
            voice[j].vol = 0; /* just in case */
        }
      }

      /* bass: voices 2 and 3 */
      set_voice(2, bass[pos][i%4], 31, nicebass);
      set_voice(3, bass[pos][0]-12, 15-i, sawtooth);

      delay(120); /* time per event */
    }
  }
}

これは4声の曲を演奏します。私はそれをテストするためのArduinoLeonardo(まあ、Pro Micro)しか持っていないので、どのピンがTIMER1Aに接続されているかに応じてPINを変更する必要があるかもしれません(正しく読んでいる場合は、宇野とメガのピン11)。残念ながら、使用するピンを選択することはできません。

また、ヘッドホンでしかテストしていないので、ピエゾブザーでどのように聞こえるかわかりません...

うまくいけば、それはあなたに可能性のいくつかのアイデアとあなた自身の曲の潜在的な出発点を与えるでしょう。不明な点があれば喜んで説明します。また、これを書くための言い訳をしてくれてありがとう:)

17
hexwab

このサードパーティのトーンライブラリは、複数のピンで同時に方形波を再生できます。 リンク

複数のピンと単一のスピーカーの間に抵抗を接続して、1つのスピーカーからすべてのトーンを引き出すことができます。

3
Fortran