Use Git or checkout with SVN using the web URL.
Work fast with our official CLI. Learn more.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Failed to load latest commit information.
Bluetooth Audio for Arduino
The code exposes the A2DP profile (Bluetooth Audio) available in ESP32 boards using the Arduino interface. It assumes you have installed the ESP32 core for Arduino and have an ESP32 board. I quite like the TinyPico because it’s so powerful and so tiny! I also like the ESP32-PICO-KIT because it’s so powerful and cheap. Both have the same chips (i think) but the TinyPico is way smaller.
Table of contents
4. Within the Arduino IDE, select «Sketch» -> «Include Library» -> «Add .ZIP library», then select the downloaded zip file.
This should add the library. To use the library, you’ll have to include the relevant header in the Arduino sketch. You’ll see this in the following sketches.
Advertising the Connection
This section covers the advertiseBluetooth example. The first step to getting some Bluetooth audio up and running is to advertise your ESP32 board. You will need to include the btAudio header and declare a btAudio object.
The string that you supply to the btAudio object becomes the name of the ESP32 Bluetooth connection. However, this only initialises the object. It doesn’t actually start the Bluetooth. For that you’ll need to use the btAudio::begin method.
Yay, now you can connect to your ESP32 board and stream audio to it. You can connect with your phone, laptop, MP3 player, whatever you want. Sadly, this data is stuck on the ESP32 unless you have a DAC (Digital to Analogue Converter) that can actually send the audio somewhere (speaker, Hi-Fi system). I’ll cover that in the next section.
btAudio can remember and attempt to automatically connect to the last connected source device. Just call the audio.reconnect() function, and btAudio will connect to the last remembered device. You can also manually disconnect from the current source device using the audio.disconnect() function.
The whole script is below:
This section covers the minimalAudio example. Now that we have mastered the Bluetooth component of «Bluetooth Audio», let’s turn to the audio part. This requires some extra hardware. I like the Adafruit I2S Stereo decoder. It takes data from the ESP32 and converts it to a line out signal which can be plugged into a stereo or Hi-Fi system (instantly adding wireless audio to your audio system). But what is I2S and what extra hardware do you need?
- a Breadboard
- An ESP32 board. I like the TinyPico and the ESP32-PICO-KIT
- Adafruit I2S Stereo decoder.
- Micro USB cable
- 5 Jumper Wires (3 for I2S, 1 for power and 1 for ground)
- Aux cable/3.5mm headphones
- Speaker with Line in connection (if you’re not using headphones).
- Set up the breadboard. There’s a wiring guide for the I2S DAC over at adafruit. The connections to the DAC are the same. Just swap the outputs from the microcontroller in the adafruit example to the pins you selected for the ESP32 in the minimalAudio example. Mine looks like this. While making this project I didn’t have access to jumper wires or a soldering iron (they were at my locked-down workplace). For the jumper wires I just cut up one long blue wire into as many wires as I needed. To attach pins to the DAC I used Pimoroni push headers. This is not ideal but cheaper than buying a temporary soldering iron.
Upload the minimalAudio example using the micro USB cable.
Disconnect USB from computer and power the ESP32 board from a USB power supply near the speaker. Don’t worry about it losing power it will remember the code you uploaded previously.
Connect the Line out from the DAC to the the Line in on the stereo/Hi-Fi using your Aux cable. You could also used wired headphones instead of an Aux cable to a speaker. My setup looks like this.
On your laptop/phone connect to the ESP32 like you would any other bluetooth device.
Play some audio!
I2S is method of digitally transferring audio data between devices. It uses just 3 wires. This is particularly useful in transferring data to an external high performance Digital to Analog Converter (DAC). For instance most ESP32s have 2 8-bit DACs whereas music is usually played over 16-bit DACs (or better). However, you can get cheap 16 bit DACs that you can plug into your speakers/Hi-Fi systems. These DACs receive data from your microcontroller using I2S. The one I have been using is the Adafruit I2S Stereo decoder. Not all microcontrollers have I2S communication and Bluetooth Classic/ WIFI. This is why the ESP32 boards are key here. They have both. A simple bit of code to combine both the bluetooth and the I2S is given below. The only difference between this code and the advertising code is calling the btAudio::I2S method and specifying the 3 pins that you want to use. A cool feature about ESP32s is that you usually pick whatever pins to do whatever action you want. So feel free to change the pins.
The full contents of the minimalAudio example are below.
This section covers the changeVolume example. Volume is a tricky issue. Ideally, the sender should issue a request for volume to be changed at the receiver (using something like AVRCP). The sender should not change the data before it is transmitted. My MP3 player does not change the data before transmission. My laptop and phone do. One option is to digitally alter the data before it goes to the DAC but after it has been received by the ESP32. We can do this by using the btAudio::volume method. It accepts one argument: a floating point number between 0 and 1. It then scales the data by that number.
This section covers the serialControl example. If you want to control any of the features proposed here you may need to create a hardware interface (buttons, potentiometers, etc). In the meantime I’ve created a serial interface for you to play with. It’s pretty simple and not necessarily the best method but it works. The idea is to send a string to over the comm port (e.g. vol ). Follow this string with a terminator(e.g. # ) and then follow this with a number (e.g. 0.42 ). So the full command to cut the volume by half would be vol#0.5 . In the image below you can see waht this looks like in hte serial monitor.
Once the code in the next section is uploaded try and experiment by opening the serial monitor and changing the volume a bit. This general approach can be adapted for any method.
This section covers the highpassFilter example. High-Pass filters remove low frequency content from your data. You can either set the parameters in the void setup() section or you can interactively edit the parameters via the serial interface. The method is btAudio::createFilter . The implementation uses a cascade of biquad filters. The method takes three arguments. The first specifies the number of filter cascades. A higher number makes the filter sharper but increases the run-time of the method. For me a value of 3 is a good compromise between computation time and filter efficacy. The second argument is the filter cutoff. Below this frequency the signal starts to get suppressed. The third argument is the filter type. Setting the value to highpass makes the filter a high-pass filter. Calling the stopFilter() method stops the effects of the filter(send command stopFilt# ). Experiment with a few values for your high pass cut off. Most speakers struggle to produce sound below 100Hz so sending the command hp#100 should only make a very small difference to your listening. Send the command hp#600 and you will notice a very big difference! The sound will sound like it’s coming through a very old phone.
This section covers the lowpassFilter example. Low-Pass filters suppress high frequency signals in your audio. Creating one is very similar to the High-Pass filter example. It only requires you to change the third argument from highpass to lowpass in the btAudio::createFilter method. We can similarly use the serial interface to change the frequency cutoff ( lp#10000 ). Sets a 10000Hz Low-Pass Filter. Lowering this setting creates a more muffled sound (like listening to audio through a wall or under water). As a side note, if you send the hp#100 and lp#14000 commands you will create a Band-Pass Filter with cutoffs at 100Hz & 14000Hz. stopFilt# will once again turn off the filters.
Dynamic Range Compression
I’ll highlight terms throughout this section that will be parameters for the Dynamic range compression(DRC). DRC is a method that evens out the sound of audio. It compresses loud sounds to the level of quiet ones and then, optionally, amplifies( gain ) both. The result is a more even audio experience. It is nonlinear and has many, many settings. It is both complicated to implement and to use. Consider this feature experimental. There are two main types of compression: Hard Knee and Soft Knee. Basically, once the audio reaches a certain threshold it starts to be compressed by a certain Ratio . Hard knee compressors start compressing immediately at the threshold but Soft knee compressors start compressing sooner and in a more smooth fashion. Supposedly, this leads to a more natural sound but it is far more computationaly intensive to implement.
You can also specify the width of this soft knee (i.e. How gradual you want the compression to be). Setting this width to zero implements a hard knee. You can also set attack and release times. These are time constants that determine how quickly the compressor starts working. For instance if there was a sudden change in volume you might want to have a very quick attack time so as to compress that signal quickly. Having a long release time will leave the compressor running for a brief time after it has started compressing. I’ve got two examples on how to use the compressor. One has extreme parameters that you can just turn on or off and another with full serial control over all the parameters.
DRC: Partial Control
This section covers the partialDRC example. For turning the compressor on and off just send the compress# and decompress# commands over the serial port. This will call the btAudio::compress method and the btAudio::decompress method. Hopefully, you should hear a clear difference between compressed and uncompressed data. It can be quite useful for making sure audio will never clip a set of speakers and for watching TV shows that have very loud background music and very low vocals. I’m looking at you xxxxxxx!
DRC: Full Control
This section covers the fullDRC example. Examples of the commands to control the individual parameters are as follows:
The actual code that does the compression is the same but the serial interface is more verbose.
Approximate Dynamic Range Compression
Dynamic Range Compression is very computationally expensive. I found for my ESP32 the big stress was converting the computed gain (dB) back to a 16 bit integer. This required computing pow10f(x/20) which became a severe bottleneck. This operation took takes about 5 microseconds to compute. For a stereo system that’s 10 microseconds. Considering there is only 11.3 microseconds between stereo samples using 10 microseconds for one line of code is abhorrent.
I thought about precomputing the values and creating a lookup table but that would require > 130KB of memory. That would not be the polite thing to do. Program storage space is valuable, particularly as the ESP32 uses a lot of memory for WIFI and Bluetooth. The compromise was to use a lookup table for integral part of x and a polynomial approximation for the fractional part of x . As x only ranges between -90dB and 90dB for signed 16 bit integers we can create a lookup table for the integral part using less than 400 bytes. For the fractional part we use Newton’s Divided Difference method for polynomial interpolation between the integer parts. Long story short this method has an accuracy of 0.01% and runs in 0.2 microseconds. If you just use the integer lookup method the error is greater than 10% and takes 0.1 microseconds. In my opinion the extra 0.1 microseconds is worth the 1000 fold increase in accuracy. The code below is covered in the benchLookup example. This example isn’t crucial but useful if you want to see the difference in methodologies.
If you run the above code you should get results like this on the serial monitor.
This section covers the webInterface example The serial interface is useful for debugging but not very useful if you have the ESP32 hooked up behind a speaker. To edit the DSP parameters wirelessly I’ve created a very simple web server that you can access via any browser connected to the same network as the ESP32. It’s pretty straightforward to use. Simply create a webDSP object and pass it your SSID, password, and the btAudio object.
The first time you run this bit of code, you should have the serial monitor open so that you can see what the IP address assigned to your ESP32 is. Once you know this number, simply enter it in to your browser and you’ll be greeted by this webpage!
You can expand each of the three sections and edit any of the parameters of the filters, volume or dynamic range compression.
Not so simple audio
What if you hate classes/Object Oriented Programming and don’t want to use my code but still want Bluetooth audio. Well this section covers just how to do that with the underTheHood example. It uses the minimum amount of ESP32 code to get audio output on I2S. I ain’t gonna explain this. The reason I wrote the library is so that I wouldn’t have to explain low level ESP32 code. However, for developers it may be easier to work with the raw ESP32 code as you can see the all the moving parts and dependencies more easily. However, for an end user(not a developer) I think a class based approach that hides these details is the way to go. Whatever your thoughts, I think I’m unlikely to shift from the object-oriented programming to more functional programming for this library.
On a technical note this code adapts the ESP-IDF A2DP Sink example but uses the arduino-esp32 library . There are very subtle differences between these libraries. So this example will only work with the Arduino library, not the ESP-IDF library. You’ll need to use the much more complicated ESP-IDF example if you work outside the Arduino environment.