I got interested in how an Arduino can be used to play a WAV file and there is so much stuff in the TMRpcm library to deal with different situations that I could not figure it out.
I eventually cobbled together the program below and was quite suprised that it works on my Uno. I am posting it here in case anyone is curious about the nuts and bolts of playing a WAV file. I have included my test WAV file - it is a snippet of the sound of a steam railway locomotive. NOTE that it is attached as .txt file but you must change the extension to .wav.
This link has a nice clear explanation of the structure of a WAV file. My program prints out the data in the three sections so you can explore your own sound file.
To make things as simple as possible I have created a short WAV file with a sample rate of 11,025Hz and 8-bit samples and I have "hard-wired" those details into the program. The header data in the WAV file includes this data so you could write a program to work with different data formats. I used Audacity to create a small file with the sample rate and data size that I wanted from a much larger sound file.
Playing a WAV file is actually very simple. A WAV file contains a series of numbers that represent the amplitude of the signal at the instant that the sample was taken. To reproduce the sound it is necessary to output the samples at the correct frequency and the correct amplitude.
I am assuming that the reader will supplement this short note by studying the Atmel datasheet for the Atmega 328 and the Arduino Uno pin mapping.
In my demo I have used fast PWM mode on Timer2. I have set the Timer clock to 2MHz (0.5µsecs per step) and have set the upper limit for the count to 179 (OCR2A) as 179 counts at 0.5µsecs = 89.5µsecs. A frequency of 11,025Hz would have an interval of about 91µsecs but I tweaked the number with the aid of an oscilloscope - probably my Uno does not run at precisely 16MHz.
In fast PWM mode the timer overflow interrupt will be called every time the counter reaches the upper limit (set by OCR2A) and I use that interrupt to put the next data value in OCR2B. I have also set the timer so that it will change OC2B (Arduino Pin 3) to 0 when the count matches 0CR2B - and it is set back to 1 when the count goes back to 0. This means that (for example) if OCR2B has the value 90 (and OCR2A has the value 179) the pin OC2B (Arduino Pin 3) will be HIGH for about half of the time. In other words, the width of the pulse is proportionate to the amplitude of each sample. Note that I should have ensured that the WAV data only varies between 0 and the value in OCR2A.
To hear the sound I used a couple of pieces of wire to connect Pin 3 and GND to the input 3mm jack plug for a small amplified speaker that I have.
There are probably 100 ways in which this program could be improved and extended. But I am leaving it simple so that it is more easily understood (I hope).
As usual comments and corrections are welcome. But please keep in mind the limited scope of the demo.
// simple Uno program to play sound data from an SD Card using Timer2
// surprisingly it works !
// it is assumed that the user will study the Atmel Atmega328 datasheet
// and the Arduino Pin Mapping for the Uno/
/*
The circuit: (based directly on the SD Card library examples)
* SD card attached to SPI bus as follows:
** MOSI - pin 11
** MISO - pin 12
** CLK - pin 13
** CS - pin 4
* the sound output is on Pin3 which is OC2B
* Note that OC2A is pin 11 which is needed by the SD Card.
*/
#include <SPI.h>
#include <SD.h>
#define SD_ChipSelectPin 4
File sFile;
char filename[] = "steam8b.wav";
const byte soundPin = 3; // OC2B
int dataCount = 0;
unsigned int numBytes;
void setup() {
Serial.begin(9600);
Serial.println("Starting simple WAV demo\n");
pinMode(soundPin, OUTPUT);
if (!SD.begin(SD_ChipSelectPin)) {
Serial.println("SD fail");
return;
}
sFile = SD.open(filename);
if( !sFile ){
Serial.println("File not opened");
}
else {
// first show the data at the start of the WAV file
Serial.println("RIFF section");
for (byte i = 0; i < 12; i++){
Serial.print(sFile.read(), HEX);
Serial.print(' ');
}
Serial.println();
Serial.println();
Serial.println("Format section");
for (byte i = 12; i < 36; i++){
Serial.print(sFile.read(), HEX);
Serial.print(' ');
}
Serial.println();
Serial.println();
Serial.println("Data Header");
for (byte i = 36; i < 44; i++){
Serial.print(sFile.read(), HEX);
Serial.print(' ');
}
Serial.println();
Serial.println();
Serial.println("First 32 data bytes");
for (int i = 0; i < 32; i++){
Serial.print("0x");
Serial.print(sFile.read(), HEX);
Serial.print(',');
}
Serial.println();
Serial.println();
// Now prepare to play the sound
// move the pointer back to the start of the data size
sFile.seek(40);
// this assumes the data size fits in 2 bytes. There are 4 bytes to read if necessary
byte firstB = sFile.read();
byte nextB = sFile.read();
numBytes = firstB + (nextB << 8);
Serial.print("Num Bytes ");
Serial.print(numBytes);
Serial.println();
// move the pointer back to the start of the data
sFile.seek(44);
// set up Timer2 to run at 11025 Hz (or near it)
TCCR2A = 0b00100011; // fast PWM mode with OC2B changed
TCCR2B = 0b00000010; // last 3 bits 010 select clock/8 or 16MHz/8 = 2MHz
OCR2A = 179; // should give 11025 Hz, might need tweaking
OCR2B = 80;
// set the interrupt last thing so other stuff is not affected
TIMSK2 = 0b00000001; // Overflow interrupt enabled
}
}
void loop() {
}
ISR (TIMER2_OVF_vect) {
OCR2B = sFile.read(); // this represents the amplitude of each sample
dataCount ++;
if (dataCount >= numBytes) { // go back to the start of the file
dataCount = 0;
sFile.seek(44);
}
}
I like to read about Arduino & Audio (even though most Audio stuff is over my head still) LOL..
So you're just outputting to PIN3? NO DAC being used or anything? NO AMP? (how is the volume?)
Have you tested this to see if you get gapless/seamless playback of a .wav file? (ie: no gap/dead air when the clip loops/starts over again?)
I have been using (more or less) the Adafruit Waveshield.. and havent really found a way to get seanless/gapless playback. (heck I was willing to pay someone to make a small edit to the WaveHC lib to allow such a thing)...
I never got back on that project... but am wondering if it was a serial.print() that prevented seamless playback or if it just wasnt possible.
what speaker are you using for this?
edit: I see you used a powered/amplified speaker now...
There is a noticeable (but very short) gap when the code goes back to be beginning of the file. I have not investigated what causes that. It seems likely to be something to do with reading the SD Card. I have no idea how well the SD Card library is written.
I don't know how much I am going to pursue this. Ideally I would like to be able to switch between WAV files seemlessly to get suitable sound effects. But creating the sound files is, in itself, a lot of work and I don't know if I have the patience.