My high school electronics class decided to buy some arduino uno kits, which I must say are extremely cool. Enough about that, right now in the class we're experimenting with the piezo buzzer (it looks like this). We learned about creating songs using the piezo buzzer. Our teacher told us to be "creative". What better way to be creative than to use "Firework" by Katy Perry.
Using some creative liberties, I found a nice piano piece of this song (link here). Now I'm a piano player (I took AP Music theory), and the problem I'm having is that I'm only able to play one note only the piezo buzzer. Is it possible to play the song on a piezo buzzer so it sounds like it's being played on a piano (or at least close to). I mean like the bass and treble clef notes are played simultaneously on the buzzer.
I understand that it involves phase shifts and adding frequencies of notes, but how do you translate this into code for a piezo buzzer? If you could post some example code that would be greatly appreciated. If not, could you explain it in the clearest way possible. I'm not a master at programming, but I'm not a beginner either.
Arduinos offer only digital output: the output is either on (+5V) or off (0V). The tone()
function, which I expect you've run into by this point, outputs a square wave at a specified frequency.
Say you want a 100Hz tone. 100Hz means the output repeats every 1/100 of a second, or 10ms. So tone(PIN,100)
will set a timer interrupt to be called every 5ms. The first time the interrupt is called, it sets the output low, and returns to whatever your program was doing. Next time it's called it sets the output high. Thus every 5ms the output changes, and you get a square wave at 50% duty cycle, which means the output is on for precisely half the time.
This is all very well, but most audio waveforms are not square waves. If you want to play two square wave tones simultaneously, or even to control the volume of a single square wave tone, you need to be able to output more values than just "on" and "off".
The good news is that there's a trick you can use called pulse width modulation (commonly abbreviated PWM). The idea is that you may only be able to set your output to one of two values, but you can do so really fast. Humans can hear audio frequencies up to about 20kHz. If you diddle your output faster than that, say at 200kHz (well within the capabilities of the Arduino, which is clocked at 16MHz), you don't hear the individual output transitions, but the average value over a longer period.
Imagine generating a 200kHz tone with tone()
. It's way too high to hear, but the average value is halfway between on and off (50% duty cycle, remember?). So we now have three possible output values: on, off, and halfway. This is enough to allow us to play two square waves simultaneously:
High-quality audio requires many more values than this. CDs store 16-bit audio, which means there are 65536 possible values. And while we will not get CD-quality audio out of an Arduino, we can get more output values by choosing a duty cycle other than 50%. In fact, the Arduino has hardware to to this for us.
Meet analogWrite()
. This fakes varying output levels using the Arduino's built-in PWM hardware. The bad news is that the PWM frequency is typically 500Hz, which is fine for dimming an LED but way too low for audio. So we have to program hardware registers ourself.
Secrets of Arduino PWM has some more information, and here's a detailed reference on how to implement a PWM DAC on an Arduino.
I picked 7-bit resolution, which means the output is a 16MHz/128=125kHz square wave with 128 possible duty cycles.
Of course, once you've got PWM output working the fun is just beginning. With multiple voices you can't rely on interrupts to set the frequency your waveforms, you have to stretch them yourself. Knowledge of basic digital signal processing (DSP) will come in jolly handy. You'll need tight code to generate audio data from within an interrupt handler, and then you'll need a playroutine to trigger the right notes at the right time. The sky's the limit!
Anyway, here's some code:
#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 */
}
}
}
This plays a four-voice tune. I have only an Arduino Leonardo (well, Pro Micro) to test it on, so you may need to change PIN
according to which pin is hooked up to TIMER1A (if I'm reading correctly it's pin 9 on an Uno and pin 11 on a Mega). You don't get a choice of which pin to use, sadly.
I've also only tested it with headphones, so I have no idea how it'll sound on a piezo buzzer...
Hopefully it gives you some idea of the possiblities, and a potential starting point for your own tune. Happy to explain anything that's unclear, and also thankyou for giving me an excuse to write this :)
This third-party Tone library can play simultaneous square waves on multiple pins:Link
You can connect resistors between the multiple pins and a single speaker to get all tones out of one speaker.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With