Introduction: Bluetooth Audio & Digital Signal Processing: an Arduino Framework

About: Researcher with a passion for microcontrollers.

Summary

When I think of Bluetooth I think of music but sadly most microcontrollers can't play music via Bluetooth. The Raspberry Pi can but that is a computer. I want to develop an Arduino based framework for microcontrollers to play audio via Bluetooth. To fully flex my microcontroller's muscles I'm going to add real-time Digital Signal Processing (DSP) to the audio (high-pass filtering, low-pass filtering and dynamic range compression). For the cherry on top, I will add a webserver that can be used to configure the DSP wirelessly. The embedded video shows the basics of Bluetooth audio in action. It also shows me using the webserver to perform some high-pass filtering, low-pass filtering and dynamic range compression. The first use of Dynamic range compression purposefully causes distortion as an example of poor parameter choices. The second example eliminates this distortion.

For this project, the ESP32 is the microcontroller of choice. It costs less than £10 and is feature-packed with ADCs, DACs, Wifi, Bluetooth Low Energy, Bluetooth Classic and a 240MHz dual-core processor. The onboard DAC can technically play audio but it won't sound great. Instead, I'll use the Adafruit I2S stereo decoder to produce a line-out signal. This signal can easily be sent to any HiFi system to instantly add wireless audio to your existing HiFi system.

Supplies

Hopefully, most makers will have breadboards, jumpers, USB cables, power supply soldering irons and will only have to spend £15 on the ESP32 and the stereo decoder. If not, all the parts required are list below.

Step 1: Construction - the Breadboard

If you bought the ESP32-PICO-KIT you won't have to solder any pins as it comes pre-soldered. Simply place it on the breadboard.

Step 2: Construction - Push Headers/soldering

If you have a soldering iron, solder the pins to the stereo decoder according to the instructions on the Adafruit website. At the time of writing my soldering iron was at work which was locked down. I didn't want to pay for a temporary soldering iron so I cut up some push headers from pimoroni. I cut them up so they would fit to the stereo decoder. This is not the best solution (and not how the headers were intended to be used) but it is the cheapest alternative to a soldering iron. Slot the cut-up header on to the breadboard. You should only need 1 line of 6 pins for the decoder. You can add another six to the other side for stability but this isn't necessary for this prototype system. The pins to slot the headers into are vin, 3vo, gnd, wsel, din and bclk.

Step 3: Construction - Wire the Power Pins

Place the Stereo decoder on the push headers (vin, 3vo, gnd, wsel, din and bclk pins) and firmly push them together. Again, this ideally should be done with a soldering iron but I had to improvise. You will notice that all the wires in this instructable are blue. That's because I didn't have any jumper wires so I cut 1 long wire into smaller pieces. Also, I'm colourblind and don't really care about the wire colour. The power pins are attached as follows:

3v3 (ESP32) -> to vin on stereo decoder

gnd (ESP32) -> to gnd on stereo decoder

Step 4: Construction - I2S Wiring

To send the Bluetooth audio from the ESP32 to the stereo decoder we are going to use a method of digital communication called I2S. The stereo decoder will take this digital signal and turn it into an analogue signal that can be plugged into a speaker or HiFi. I2S only requires 3 wires and is reasonably straightforward to understand. The bit clock (bclk) line turns high and low to indicate a new bit is transmitted. The data-out line (dout) turns high or low to indicate whether that bit has a value of 0 or 1 and the word select line (wsel) turns high or low to indicate whether the left or right channel is being transmitted. Not every microcontroller supports I2S but the ESP32 has 2 I2S lines. This makes it an obvious choice for this project.

The wiring is as follows:

27 (ESP32) -> wsel (Stereo decoder)

25 (ESP32) -> din (Stereo decoder)

26 (ESP32) -> bclk (Stereo decoder)

Step 5: Installing the BtAudio Library

If you don't already have them installed install the Arduino IDE and the Arduino core for ESP32. Once you have them installed visit my Github page and download the repository. Within the Arduino IDE under Sketch>>Include Library>> select "Add .ZIP library". Then select the downloaded zip file. This should add my btAudio library to your Arduino libraries. To use the library you'll have to include the relevant header in the Arduino sketch. You'll see this in the next step.

Step 6: Using the BtAudio Library

Once installed, connect your ESP32 to your computer via micro USB and then connect your stereo decoder to your speaker with your 3.5mm wire. Before you upload the sketch you will need to change some things in the Arduino editor. After you have selected your board you will need to edit the partition scheme under Tools >> Partition Scheme and select either "No OTA (Large APP)" or "Minimal SPIFFS(Large APPS with OTA)". This is necessary because this project uses both WiFi and Bluetooth which are both very memory heavy libraries. Once you've done this upload the following sketch to the ESP32.

#include <btAudio.h>

// Sets the name of the audio device
btAudio audio = btAudio("ESP_Speaker");

void setup() {
 
 // streams audio data to the ESP32   
 audio.begin();
 
 //  outputs the received data to an I2S DAC 
 int bck = 26; 
 int ws = 27;
 int dout = 25;
 audio.I2S(bck, dout, ws);
}

void loop() {

}

The sketch can broadly be divided into 3 steps:

  1. Create a global btAudio object that sets the "Bluetooth name" of your ESP32
  2. Configure the ESP32 to receive audio with the btAudio::begin method
  3. Set the I2S pins with the btAudio::I2S method.

That's it on the software side! Now all you need to do is initiate the Bluetooth connection to your ESP32. Just scan for new devices on your phone/laptop/MP3 player and "ESP_Speaker" will appear. Once you are happy that everything is working (music plays) you can disconnect the ESP32 from your computer. Power it with the USB power supply and it will remember the last code that you uploaded to it. This way, you can leave your ESP32 hidden behind your HiFi system forever.

Step 7: DSP - Filtering

Extending the Receiver with Digital Signal Processing

If you followed all the steps (and I didn't leave anything out) you now have a fully functioning Bluetooth receiver for your HiFi system. While this is cool it doesn't really push the microcontroller to its limits. The ESP32 has two cores operating at 240MHz. That means this project is far more than just a receiver. It has the capacity to be a Bluetooth receiver with a Digital Signal Processor (DSP). DSPs essentially perform mathematical operations on the signal in real-time. One useful operation is called Digital Filtering. This process attenuates frequencies in a signal below or above a certain cutoff frequency, depending on whether you are using a high-pass or low pass filter.

High-pass filters

High-Pass filters attenuate frequencies below a certain band. I've built a filter library for Arduino systems based on code from the earlevel.com. The main difference is that I've changed the class structure to allow for the construction of higher-order filters more easily. Higher order filters suppress frequencies beyond your cutoff more effectively but they require much more computation. However, with the current implementation, you can even use 6th order filters for real-time audio!

The sketch is the same as the one found in the previous step except that we have changed the main loop. To enable the filters we use the btAudio::createFilter method. This method accepts 3 arguments. The first is the number of filter cascades. The number of filter cascades is half the order of the filter. For a 6th order filter, the first argument should be 3. For an 8th order filter, it would be 4. The second argument is the filter cutoff. I've set this to 1000Hz to have a really dramatic effect on the data. Finally, we specify the type of filer with the third argument. This should be highpass for a high-pass filter and lowpass for a low-pass filter. The script below switches the cutoff of this frequency between 1000Hz and 2Hz. You should hear a dramatic effect on the data.

#include <btAudio.h>

btAudio audio = btAudio("ESP_Speaker");

void setup() { 
 audio.begin();
 int bck = 26; 
 int ws = 27;
 int dout = 25;
 audio.I2S(bck, dout, ws);

}

void loop() {
 delay(5000);
 audio.createFilter(3, 1000, highpass);
 delay(5000);
 audio.createFilter(3, 2, highpass);
}

Low-pass filters

Low pass filters do the opposite of high pass filters and suppress frequencies above a certain frequency. They can be implemented in the same way as high pass filters except that they require changing the third argument to lowpass. For the sketch below I alternate the low-pass cutoff between 2000Hz and 20000Hz. Hopefully, you'll hear the difference. It should sound quite muffled when the low-pass filter is at 2000Hz.

#include <btAudio.h>

btAudio audio = btAudio("ESP_Speaker");

void setup() { 
 audio.begin();
 int bck = 26; 
 int ws = 27;
 int dout = 25;
 audio.I2S(bck, dout, ws);

}


void loop() {
 delay(5000);
 audio.createFilter(3, 2000, lowpass);
 delay(5000);
 audio.createFilter(3, 20000, lowpass);
}

Step 8: DSP - Dynamic Range Compression

Background

Dynamic range compression is a signal processing method that tries to even out the loudness of the audio. It compresses loud sounds, that rise above a certain threshold, to the level of quiet ones and then, optionally amplifies both. The result is a much more even listening experience. This came in really useful while I was watching a show with very loud background music and very quiet vocals. In this case, just increasing the volume didn't help as this only amplified the background music. With dynamic range compression, I could reduce the loud background music to the level of the vocals and hear everything properly again.

The Code

Dynamic range compression does not just involve lowering volume or thresholding the signal. It's a bit more clever than that. If you lower the volume quiet sounds will be reduced as well as the loud ones. One way around this is to threshold the signal but this results in severe distortion. Dynamic range compression involves a combination of soft thresholding and filtering to minimize the distortion one would get if you were to threshold/clip the signal. The result is a signal where the loud sounds are "clipped" without distortion and the quiet ones are left as they are. The code below switches between three different levels of compression.

  1. Compression with distortion

  2. Compression without distortion

  3. No Compression
#include <btAudio.h>
btAudio audio = btAudio("ESP_Speaker");

void setup() { 
 audio.begin();
 int bck = 26; 
 int ws = 27;
 int dout = 25;
 audio.I2S(bck, dout, ws);
}


void loop() {
 delay(5000);
 audio.compress(30,0.0001,0.0001,10,10,0);
 delay(5000);
 audio.compress(30, 0.0001,0.1,10,10,0);
 delay(5000);
 audio.decompress();
}

Dynamic range compression is complicated and the btAudio::compress methods has many parameters. I'll try and explain them (in order) here:

  1. Threshold - The level at which the audio gets reduced (measured in decibels)
  2. Attack time - The time it takes for the compressor to start working once the threshold has been exceeded
  3. Release time - The time it takes for the compressor to stop working.
  4. Reduction Ratio - the factor by which the audio is compressed.
  5. Knee Width - The width (in decibels) around the threshold at which the compressor partially works(more natural sound).
  6. The gain (decibels) added to the signal after compression (increase/decrease volume)

The very audible distortion in the first use of compression is because the threshold is very low and both the attack time and release time are very short effectively resulting in a hard thresholding behaviour. This is clearly solved in the second case by increasing the release time. This essentially causes the compressor to act in a much smoother way. Here, I've only shown how changing 1 parameter can have a dramatic effect on the audio. Now it's your turn to experiment with different parameters.

The Implementation (the magic mathematics - optional)

I found that naively implementing the Dynamic range compression to be challenging. The algorithm requires converting a 16-bit integer to decibels and then transforming it back to a 16-bit integer once you have processed the signal. I noticed that one line of code was taking 10 microseconds to process stereo data. As stereo audio sampled at 44.1 KHz leaves only 11.3 microseconds for the DSP this is unacceptably slow... However, by combining a small lookup table (400 bytes) and an interpolation procedure based on Netwon's divided differences we can obtain nearly 17 bits precision in 0.2 microseconds. I've attached a pdf document with all the maths for the truly interested. It's complicated, you've been warned!

Step 9: The Wifi Interface

Now you have a Bluetooth receiver capable of running real-time DSP. Sadly, if you want to change any of the DSP parameters you will need to disconnect from your HiFi, upload a new sketch and then reconnect. This is clunky. To fix this I developed a webserver that you can use to edit all the DSP parameters without reconnecting to your computer. The sketch to use the webserver is below.

#include<webDSP.h>
#include<btAudio.h>

btAudio audio = btAudio("ESP_Speaker");
webDSP web;

void setup() {
  Serial.begin(115200);  
  audio.begin();
  int bck = 26; 
  int ws = 27;
  int dout = 25;  
  audio.I2S(bck, dout, ws);
  
  // replace with your WiFi ID and password
  const char* ssid = "SSID";
  const char* password = "PASSWORD";
  web.begin(ssid,password ,&audio); 
}

void loop() {
  web._server.handleClient();
}

The code assigns an IP address to your ESP32 which you can use to access the webpage. The first time you run this code you should have it attached to your computer. That way you can see the IP address assigned to your ESP32 on your serial monitor. If you want to access this webpage simply enter this IP address into any web browser (tested on chrome).

By now we should be familiar with the method of enabling the Bluetooth and I2S. The key difference is the use of a webDSP object. This object takes your Wifi SSID and password as arguments as well as a pointer to the btAudio object. In the main loop, we continually get the webDSP object to listen for incoming data from the webpage and then update the DSP parameters. As a closing point, it should be noted that both Bluetooth and Wifi use the same radio on the ESP32. This means that you might have to wait for up to 10 seconds from when you enter parameters on the webpage to when the information actually reaches the ESP32.

Step 10: Future Plans

Hopefully, you've enjoyed this instructable and now have Bluetooth Audio and DSP added to your HiFi. However, I think there's a lot of room for growth in this project and I just wanted to point out some future directions I might take.

  • Enable Wifi streaming of audio (for the best audio quality)
  • Use an I2S microphone to enable voice commands
  • develop a WiFi controlled equaliser
  • Make it pretty (breadboard doesn't scream great product design)

When I do get around to implementing these ideas I'll make more instructables. Or maybe someone else will get these features implemented. That's the joy of making everything open source!