Generating sounds on the Arduino

This is a small project to read wav files from a micro SD card via an Arduino Nano and then play them directly to a loudspeaker. I’ll state upfront that this isn’t the easiest way to do this (there are MP3 modules and libraries), but I wanted to:
- Understand sound generation using a microcontroller from first principles
- Have some tools / ideas about how to generate sounds for any future projects
As an idea of the sort of thing I was thinking of - I’ve been thinking of a DIY “ring” doorbell. I already have a camera that can see anyone approaching the front door. A 433.92MHz receiver could detect when the doorbell is pressed (so no new doorbell required). A Raspberry Pi (i.e. the one with the camera) controlling it all and sending and receiving to my mobile phone. A lot more work than just buying a Ring doorbell - but a lot more satisfying!
To drive the loudspeaker I used a power MOSFET. Ideally I would use two (or maybe four) MOSFETs in a push pull arrangement. I was more interested in getting the software right however, so I didn’t spend a lot of time getting the hardware correct. [Figure 3 on this page][Class D Audio Amplifiers] would seem be the ideal solution, but I suspect that if I went down this route it would need four independently controllable PWM inputs - and having given this a bit of thought - I think this would be best done with an FPGA. That’s a project for another day! (but it would be pretty awesome). …or maybe I’ll just buy an audio amplifier module from Aliexpress :)
Hardware:

…and an SD card module for the Arduino. They cost about 50p if you get them directly from China.
I realized early on that PWM was the easiest way to generate the sound - and after some investigation and calculations worked out that I could have 8 bit audio with a PWM running at 62.5KHz (16MHz clock divided by 256).
PWM code (62.5KHz):
1inline void fastWriteD3(int value)
2{
3 if (value) PORTD |= 1 << 3;
4 else PORTD &= ~(1 << 3);
5}
6
7// Using Timer 2 B = pin D3, 62.5KHz i.e. no prescaler
8void setupPWMTimer()
9{
10 pinMode(3, OUTPUT);
11 //pinMode(11, OUTPUT);
12 TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
13 TCCR2B = _BV(CS20);
14 OCR2B = 0x80;
15}
16
17void updatePWMTimer(uint8_t val)
18{
19 if (val == 0)
20 {
21 TCCR2A &= ~_BV(COM2B1);
22 fastWriteD3(0);
23 }
24 else if (val == 0xff)
25 {
26 TCCR2A &= ~_BV(COM2B1);
27 fastWriteD3(1);
28 }
29 else
30 {
31 TCCR2A |= _BV(COM2B1);
32 OCR2B = val;
33 }
34}
The next thing I needed was to play each sample. I probably could do one channel at 44KHz but decided to use 16KHz - which would mean the upper frequency limit would be 8KHz. Nowhere near HiFi, but easily good enough for what I was trying to achieve. So I created an interrupt routine that occurred 16,000 times a second with the following code:
Interrupt code (16KHz):
1ISR(TIMER1_COMPA_vect)
2{
3 if (dataFirst != dataLast)
4 {
5 uint8_t val = dataBuffer[dataFirst++];
6 if (dataFirst == DATA_BUFFER_SIZE)
7 {
8 dataFirst = 0;
9 }
10 updatePWMTimer(val);
11 }
12 else if (!paused)
13 {
14 noData = true;
15 }
16}
17
18// Set timer1 interrupt at 16kHz
19void setup16KHzTimer()
20{
21 TCCR1A = 0;
22 TCCR1B = 0;
23 TCNT1 = 0; //initialize counter value to 0
24 OCR1A = 1000; // = (16*10^6) / (1 * 16000) - 1 (must be <65536)
25 TCCR1B |= (1 << WGM12); // turn on CTC mode
26 TCCR1B |= (1 << CS10); // Set CS10 bit for no prescaler
27 TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
28 sei();
29}
Every time the interrupt occurs one sample of the WAV file should be sent to the PWM. You can see this in the ISR.
I haven’t included the SD card reading code here (because it’s not the reason for this article), but the entire sketch can be be found [on this github repo][Arduino Code Github Repo].
And this is what it sounds like:
There seems to be lots of noise in the background of both tracks - but it was raining very hard outside when I recorded them - so it might be that.
I created the WAV files using Audacity - 8 bit @ 16,000 samples per second - which works out to about 1 MByte / minute. I’ll be the first to admit it’s not HiFi quality - but it’s not bad considering how it’s being generated (well I think anyway :))
What I learnt
I did some tests and found that I could read the data from the SD card about 7 times faster than I needed it.
Therefore I should be able to do stereo (although I’m not sure what the point would be) but I would need another PWM channel.