last updated: 23/12/19
Sometimes we only want to play a short sound sample on our microcontroller. With an ESP32 we can use the internal DAC. With other controller the analogWrite()
method gives us a PWM (PCM). In the following example I use a little speaker (one of those contained in birthday cards) connected do pin D7 (GPIO13) of an ESP8266 (Wemos D1 mini pro).
First we open the file in Audacity a free powerful open source software. Then we eliminate one channel if it is a stereo file. This can be done by clicking on the file name and choosing Split Stereo to Mono
and after this by deleting one of the channels.
Next we choose a sample rate of 22.050 kHz in the Project Rate
window at the left bottom of the window. We can't choose a lesser frequency because the PWM frequency also would be audible.
After this we export the file in an unsigned 8 bit format. Click on File > Export > Export as WAV
. In the new window we chose Other uncompressed files
as file format and Unsigned 8-bit PWM
in Encoding (no header).
The WAV file has a header of 44 byte. This has to be removed. The remaining bytes contain the 8 bit (0-255) values of our PWM. As we use Arduino (C, C++) we need an array variable to contain the data. As these variables are quite big, we will write them in a separate header file. For such tasks python is a cool programming language. The following script does the work:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# gpio_blink.py
import sys
from functools import partial
if len(sys.argv) < 2:
sys.exit('Usage: %s file' % sys.argv[0])
old_file = sys.argv[1]
variable_name = sys.argv[1].split('.')
variable_name = variable_name[0]
new_file = variable_name + ".h"
print (new_file)
with open(new_file, 'w') as outfile:
outfile.write("const byte ")
outfile.write(variable_name)
outfile.write("[] PROGMEM = {")
n = 0
with open(old_file, "rb") as in_file:
for c in iter(partial(in_file.read, 1), b''):
if n > 44:
outfile.write("0x%02X," % ord(c))
n += 1
if (n % 16 == 0) and (n > 44):
outfile.write("\n")
outfile.write("};")
Save the file as wav2c.py and run the program with (python instead of python3 on Windows).
python3 wav2c.py xmas_22050.wav
The argument is the wav file, and as result we get a header file (same name, but with extension .h
).
The new header file has to reside in the same folder than the sketch.
As the wav header file respectively our array variable can get very big, we save the data in flash by using PROGMEM
:
const byte xmas_22050[] PROGMEM = {0x7C,0x7F,0x83,0x83,0x7F,0x78,
...
...
0x84,0x83,0x83,0x80,0x83,0x82,0x7F,0x79,0x7C,0x7E,0x7D,0x7A,0x7C};
Here it would be a good idea to use the SPIFFS file system to store the file, but we want to keep the code simple for this tips and tricks example.
// play_wav.ino
// play wav files (8 bit, sr 22050Hz) on ESP8266
// weigu.lu
#include "xmas_22050.h"
#define wav xmas_22050
unsigned long now_micros, prev_micros, count;
void setup() {
analogWriteFreq(22050); // 22kHz PWM
analogWriteRange(255); // reduce to 8 bit
}
void loop() {
count = 0;
prev_micros = micros();
while (count != sizeof(wav)) {
now_micros = micros();
if ((now_micros - prev_micros) > 45) {
prev_micros = now_micros;
byte a_value = pgm_read_byte(wav+count);
analogWrite(D7,a_value);
count++;
yield(); // to avoid watchdog reset
}
}
delay(2000);
}
By default the PWM range for the ESP8266 is 0-1023, so we reduce it to 8 bit. We use the micros function to call analogWrite()
every 45µs (1/22050Hz). We can also use the ticker library or timer interrupts. The yield() function is necessary because we block the main loop and so the software watchdog gets nervous after a certain time.
That's it folks :)