Arduino - 如何在压电蜂鸣器上同时创建两个或更多音调?

时间:2015-03-11 00:37:59

标签: audio arduino frequency

我的高中电子班决定购买一些arduino uno套件,我必须说它非常酷。足够的,现在在课堂上我们正在试验压电蜂鸣器(它看起来像this)。我们学会了使用压电蜂鸣器创作歌曲。我们的老师告诉我们要“有创意”。有什么比使用Katy Perry的“Firework”更有创意的方式。

利用一些创作自由,我找到了这首歌的精美钢琴曲(链接here)。现在我是一名钢琴演奏家(我接受了AP音乐理论),而我遇到的问题是我只能用压电蜂鸣器播放一个音符。是否可以在压电蜂鸣器上播放这首歌,因此它听起来像是在钢琴上播放(或者至少接近)。我的意思是在蜂鸣器上同时播放低音和高音谱号音符。

我知道它涉及相移和添加音符的频率,但是如何将其转换为压电蜂鸣器的代码?如果您可以发布一些非常感谢的示例代码。如果没有,你能以最清晰的方式解释它吗?我不是编程大师,但我也不是初学者。

2 个答案:

答案 0 :(得分:16)

Arduinos仅提供数字输出:输出为(+ 5V)或关闭(0V)。我希望你在这一点上遇到的tone()函数输出一个指定频率的方波。

假设你想要一个100Hz的音调。 100Hz表示输出每1/100秒或10ms重复一次。因此tone(PIN,100)将设置每5ms调用一次定时器中断。第一次调用中断时,它会将输出设置为低电平,并返回到程序正在执行的任何操作。下次调用它会将输出设置为高电平。因此,每输出5ms,输出就会变为50%占空比的方波,这意味着输出正好只有一半的时间。

这一切都很好,但大多数音频波形不是方波。如果你想同时播放两个方波音调,甚至控制单个方波音调的音量,你需要能够输出更多的值而不仅仅是“开”和“关”。

好消息是你可以使用一种称为脉冲宽度调制(通常缩写为PWM)的技巧。我们的想法是,您可能只能将输出设置为两个值之一,但您可以非常快速地完成。人类可以听到高达约20kHz的音频。如果你输出的速度比那个快,比如说200kHz(在Arduino的功能范围内,时钟频率为16MHz),你就听不到单独的输出转换,但平均值超过了更长的时间。

想象一下用tone()产生200kHz的音调。这听起来太高了,但平均值是开关之间的中间值(占空比为50%,记得吗?)。所以我们现在有三个可能的输出值:on,off和halfway。这足以让我们同时玩两个方波: Diagram of summing square waves

高品质音频需要比此更多的价值。 CD存储16位音频,这意味着有65536个可能的值。虽然我们不会从Arduino中获得CD质量的音频,但我们可以通过选择50%以外的占空比来获得更多的输出值。事实上,Arduino为我们提供了硬件。

见面analogWrite()。这使用Arduino的内置PWM硬件伪装不同的输出电平。坏消息是PWM频率通常为500Hz,这对于调暗LED很好,但对于音频来说太低了。所以我们必须自己编写硬件寄存器。

Secrets of Arduino PWM有更多信息,这里有关于如何在Arduino上实现PWM DAC的detailed reference

我选择了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 */
    }
  }
}

这会播放四种语音。我只有一个Arduino Leonardo(好吧,Pro Micro)来测试它,所以你可能需要根据哪个引脚连接到TIMER1A来更改PIN(如果我正确读取它在Uno上的引脚9)和Mega上的引脚11)。遗憾的是,你无法选择使用哪个引脚。

我也只是用耳机测试过,所以我不知道压电蜂鸣器会发出什么声音...

希望它能让您了解可能性,并为您自己的曲调提供一个潜在的起点。很高兴解释任何不清楚的事情,也谢谢你给我借口写这个:)

答案 1 :(得分:2)

此第三方音色库可以在多个引脚上同时播放方波:Link

您可以在多个引脚和一个扬声器之间连接电阻,以便从一个扬声器中获取所有音调。