├── .gitattributes
├── README.md
├── examples
├── advertiseBluetooth
│ └── advertiseBluetooth.ino
├── allFeatures
│ └── allFeatures.ino
├── benchLookup
│ └── benchLookup.ino
├── biquadSpeedTest
│ └── biquadSpeedTest.ino
├── changeVolume
│ └── changeVolume.ino
├── customCallbacks
│ └── customCallbacks.ino
├── fullDRC
│ └── fullDRC.ino
├── highpassFilter
│ └── highpassFilter.ino
├── lowpassFilter
│ └── lowpassFilter.ino
├── minimalAudio
│ └── minimalAudio.ino
├── partialDRC
│ └── partialDRC.ino
├── queryMeta
│ └── queryMeta.ino
├── serialControl
│ └── serialControl.ino
├── underTheHood
│ └── underTheHood.ino
└── webInterface
│ └── webInterface.ino
├── keywords.txt
├── library.properties
├── readme
├── approximation.PNG
├── bread_rotate.jpg
├── download.png
├── includeLibrary.png
├── serial.PNG
├── speaker.jpg
└── webpage.PNG
└── src
├── DRC.cpp
├── DRC.h
├── btAudio.cpp
├── btAudio.h
├── filter.cpp
├── filter.h
├── webDSP.cpp
└── webDSP.h
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bluetooth Audio for Arduino
2 | The code exposes the A2DP profile (Bluetooth Audio) available in ESP32 boards using the Arduino interface. It assumes you have installed the [ESP32 core](https://github.com/espressif/arduino-esp32) for Arduino and have an ESP32 board. I quite like the [TinyPico](https://www.tinypico.com/) because it's so powerful and so tiny! I also like the [ESP32-PICO-KIT](https://www.mouser.co.uk/ProductDetail/Espressif-Systems/ESP32-PICO-KIT?qs=MLItCLRbWsyoLrlknFRqcQ%3D%3D&vip=1&gclid=EAIaIQobChMImN2EgKTG6QIVWbvVCh0zcAPBEAQYASABEgK0kfD_BwE) because it's so powerful and cheap. Both have the same chips (i think) but the TinyPico is way smaller.
3 |
4 |
5 | ## Table of contents
6 | 1. [Installation](#a)
7 | 2. [Advertising the Connection](#b)
8 | 3. [Simple Audio](#c)
9 | 1. [Hardware: Components](#c1)
10 | 1. [Hardware: Setup](#c2)
11 | 2. [I2S](#c3)
12 | 4. [Changing Volume](#d)
13 | 5. [Serial Control](#e)
14 | 6. [High-Pass Filtering](#f)
15 | 7. [Low-Pass Filtering](#g)
16 | 8. [Dynamic Range Compression](#h)
17 | 1. [DRC: Partial Control](#h1)
18 | 2. [DRC: Full Control](#h2)
19 | 3. [DRC: Approximation](#h3)
20 | 9. [WiFi Interface](#i)
21 |
22 | [Appendix I: Not so simple audio](#app)
23 |
24 |
25 |
26 | ## Installation
27 | 1. [Install the Arduino IDE](https://www.arduino.cc/en/main/software)
28 | 2. [Install the ESP32 core for Arduino](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html)
29 | 3. [Download this repository](https://github.com/tierneytim/btAudio/archive/refs/heads/master.zip)
30 |
31 |
32 |
33 | 4. Within the Arduino IDE, select "Sketch" -> "Include Library" -> "Add .ZIP library", then select the downloaded zip file.
34 |
35 |
36 |
37 | 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.
38 |
39 |
40 | ## Advertising the Connection
41 | This section covers the [advertiseBluetooth](examples/advertiseBluetooth/advertiseBluetooth.ino) example.
42 | 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.
43 |
44 | ```cpp
45 | #include
46 |
47 | // Sets the name of the audio device
48 | btAudio audio = btAudio("ESP_Speaker");
49 | ```
50 |
51 | 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.
52 |
53 | ```cpp
54 | void setup() {
55 |
56 | // Streams audio data to the ESP32
57 | audio.begin();
58 |
59 | }
60 |
61 | void loop() {
62 |
63 | }
64 | ```
65 |
66 | 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.
67 |
68 | 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.
69 |
70 | ```cpp
71 | void setup() {
72 |
73 | // Streams audio data to the ESP32
74 | audio.begin();
75 |
76 | // Re-connects to last connected device
77 | audio.reconnect();
78 |
79 | }
80 |
81 | void loop() {
82 |
83 | }
84 | ```
85 |
86 | The whole script is below:
87 |
88 | ```cpp
89 | #include
90 |
91 | // Sets the name of the audio device
92 | btAudio audio = btAudio("ESP_Speaker");
93 |
94 | void setup() {
95 |
96 | // Streams audio data to the ESP32
97 | audio.begin();
98 |
99 | // Re-connects to last connected device
100 | audio.reconnect();
101 |
102 | }
103 |
104 | void loop() {
105 |
106 | }
107 | ```
108 |
109 |
110 | ## Simple Audio
111 | This section covers the [minimalAudio](examples/minimalAudio/minimalAudio.ino) example.
112 | 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](https://www.adafruit.com/product/3678). 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?
113 |
114 |
115 | ### Hardware: Components
116 | You will need:
117 | * a Breadboard
118 | * An ESP32 board. I like the [TinyPico](https://www.tinypico.com/) and the [ESP32-PICO-KIT](https://www.mouser.co.uk/ProductDetail/Espressif-Systems/ESP32-PICO-KIT?qs=MLItCLRbWsyoLrlknFRqcQ%3D%3D&vip=1&gclid=EAIaIQobChMImN2EgKTG6QIVWbvVCh0zcAPBEAQYASABEgK0kfD_BwE)
119 | * Adafruit [I2S Stereo decoder](https://www.adafruit.com/product/3678).
120 | * Micro USB cable
121 | * 5 Jumper Wires (3 for I2S, 1 for power and 1 for ground)
122 | * Aux cable/3.5mm headphones
123 | * Speaker with Line in connection (if you're not using headphones).
124 |
125 |
126 | ### Hardware: Setup
127 | 1. Set up the breadboard. There's a wiring guide for the I2S DAC over at [adafruit](https://learn.adafruit.com/adafruit-i2s-stereo-decoder-uda1334a/circuitpython-wiring-test). 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](examples/minimalAudio/minimalAudio.ino) example.
128 | 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](https://shop.pimoroni.com/products/gpio-hammer-header?variant=35643241098). This is not ideal but cheaper than buying a temporary soldering iron.
129 |
130 |
131 |
132 |
133 |
134 | 2. Upload the [minimalAudio](examples/minimalAudio/minimalAudio.ino) example using the micro USB cable.
135 |
136 | 3. 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.
137 | 4. 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.
138 | My setup looks like this.
139 |
140 |
141 |
142 |
143 | 5. On your laptop/phone connect to the ESP32 like you would any other bluetooth device.
144 |
145 | 6. Play some audio!
146 |
147 |
148 | ### I2S
149 | [I2S](https://www.arduino.cc/en/Reference/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](https://www.adafruit.com/product/3678). 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.
150 |
151 | The full contents of the `minimalAudio` example are below.
152 |
153 | ```cpp
154 | #include
155 |
156 | // Sets the name of the audio device
157 | btAudio audio = btAudio("ESP_Speaker");
158 |
159 | void setup() {
160 |
161 | // Streams audio data to the ESP32
162 | audio.begin();
163 |
164 | // Re-connects to last connected device
165 | audio.reconnect();
166 |
167 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
168 | int bck = 26;
169 | int ws = 27;
170 | int dout = 25;
171 | audio.I2S(bck, dout, ws);
172 | }
173 |
174 | void loop() {
175 |
176 | }
177 | ```
178 |
179 |
180 | ## Changing Volume
181 | This section covers the [changeVolume](examples/changeVolume/changeVolume.ino) example.
182 | Volume is a tricky issue. Ideally, the sender should issue a request for volume to be changed at the receiver (using something like [AVRCP](https://en.wikipedia.org/wiki/List_of_Bluetooth_profiles#Audio/Video_Remote_Control_Profile_(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.
183 |
184 | ```cpp
185 | #include
186 |
187 | // Sets the name of the audio device
188 | btAudio audio = btAudio("ESP_Speaker");
189 |
190 | void setup() {
191 |
192 | // Streams audio data to the ESP32
193 | audio.begin();
194 |
195 | // Re-connects to last connected device
196 | audio.reconnect();
197 |
198 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
199 | int bck = 26;
200 | int ws = 27;
201 | int dout = 25;
202 | audio.I2S(bck, dout, ws);
203 | }
204 |
205 | void loop() {
206 | delay(3000);
207 | audio.volume(0.1);
208 | delay(3000);
209 | audio.volume(1.0);
210 | }
211 | ```
212 |
213 |
214 | ## Serial Control
215 | This section covers the [serialControl](examples/serialControl/serialControl.ino) example.
216 | 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.
217 |
218 |
219 |
220 |
221 |
222 | 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.
223 |
224 | ```cpp
225 | #include
226 |
227 | btAudio audio = btAudio("ESP_Speaker");
228 | String command;
229 |
230 | void setup() {
231 | // Streams audio data to the ESP32
232 | audio.begin();
233 | // Re-connects to last connected device
234 | audio.reconnect();
235 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
236 | int bck = 26;
237 | int ws = 27;
238 | int dout = 25;
239 | audio.I2S(bck, dout, ws);
240 | // Opens the serial port
241 | Serial.begin(115200);
242 | }
243 |
244 |
245 | void loop() {
246 | delay(300);
247 | // check if data is available
248 | if(Serial.available()){
249 | //read until a terminator. after this point there should only be numbers
250 | command = Serial.readStringUntil('#');
251 | if(command.equals("vol")){
252 | //read and set volume
253 | float vol =Serial.parseFloat();
254 | Serial.println("Changing Volume");
255 | audio.volume(vol);
256 | }
257 | }
258 | }
259 | ```
260 |
261 |
262 | ## High-Pass Filtering
263 | This section covers the [highpassFilter](examples/highpassFilter/highpassFilter.ino) example.
264 | 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](https://www.earlevel.com/main/2012/11/26/biquad-c-source-code/).
265 | 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...
266 |
267 | ```cpp
268 | #include
269 |
270 | btAudio audio = btAudio("ESP_Speaker");
271 | String command;
272 |
273 | void setup() {
274 | // Streams audio data to the ESP32
275 | audio.begin();
276 | // Re-connects to last connected device
277 | audio.reconnect();
278 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
279 | int bck = 26;
280 | int ws = 27;
281 | int dout = 25;
282 | audio.I2S(bck, dout, ws);
283 | // Opens the serial port
284 | Serial.begin(115200);
285 | }
286 |
287 | int fo=3;
288 | float fc=30;
289 |
290 | void loop() {
291 | delay(300);
292 | if(Serial.available()){
293 | command = Serial.readStringUntil('#');
294 | if(command.equals("hp")){
295 | Serial.println("Setting a High-Pass Filter...");
296 | fc =Serial.parseFloat();
297 | audio.createFilter(fo, fc, highpass);
298 | }
299 | else if(command.equals("stopFilt")){
300 | audio.stopFilter();
301 | Serial.println("Stopping Filter...");
302 | }
303 | }
304 | }
305 | ```
306 |
307 |
308 | ## Low-Pass Filtering
309 | This section covers the [lowpassFilter](examples/lowpassFilter/lowpassFilter.ino) 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.
310 |
311 | ```cpp
312 | #include
313 |
314 | btAudio audio = btAudio("ESP_Speaker");
315 | String command;
316 |
317 | void setup() {
318 | // Streams audio data to the ESP32
319 | audio.begin();
320 | // Re-connects to last connected device
321 | audio.reconnect();
322 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
323 | int bck = 26;
324 | int ws = 27;
325 | int dout = 25;
326 | audio.I2S(bck, dout, ws);
327 | // Opens the serial port
328 | Serial.begin(115200);
329 | }
330 |
331 | int fo=3;
332 | float fc;
333 |
334 | void loop() {
335 | delay(300);
336 | if(Serial.available()){
337 | command = Serial.readStringUntil('#');
338 | if(command.equals("hp")){
339 | Serial.println("Setting a High-Pass Filter...");
340 | fc =Serial.parseFloat();
341 | audio.createFilter(fo, fc, highpass);
342 | }
343 | else if(command.equals("stopFilt")){
344 | audio.stopFilter();
345 | Serial.println("Stopping Filter...");
346 | }
347 | else if(command.equals("lp")){
348 | Serial.println("Setting a Low-Pass Filter...");
349 | fc =Serial.parseFloat();
350 | audio.createFilter(fo, fc, lowpass);
351 | }
352 | }
353 | }
354 | ```
355 |
356 | ## Dynamic Range Compression
357 | I'll highlight terms throughout this section that will be parameters for the [Dynamic range compression(DRC)](https://en.wikipedia.org/wiki/Dynamic_range_compression). 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.
358 |
359 |
360 | 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.
361 |
362 |
363 | ### DRC: Partial Control
364 | This section covers the [partialDRC](examples/partialDRC/partialDRC.ino) example.
365 | 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!
366 | ```cpp
367 | #include
368 |
369 | btAudio audio = btAudio("ESP_Speaker");
370 | String command;
371 |
372 | void setup() {
373 | // Streams audio data to the ESP32
374 | audio.begin();
375 | // Re-connects to last connected device
376 | audio.reconnect();
377 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
378 | int bck = 26;
379 | int ws = 27;
380 | int dout = 25;
381 | audio.I2S(bck, dout, ws);
382 | // Opens the serial port
383 | Serial.begin(115200);
384 | }
385 |
386 | float thresh=30;
387 | float attack=0.1;
388 | float release= 0.2;
389 | float ratio = 10;
390 | float width= 10;
391 | float gain=0;
392 |
393 | void loop() {
394 | delay(300);
395 | if(Serial.available()){
396 | command = Serial.readStringUntil('#');
397 | if(command.equals("compress")){
398 | Serial.println("Applying Compression");
399 | audio.compress(thresh,attack,release,ratio,width,gain);
400 | }
401 | else if(command.equals("decompress")){
402 | Serial.println("Releasing Compression");
403 | audio.decompress();
404 | }
405 | }
406 | }
407 | ```
408 |
409 |
410 | ### DRC: Full Control
411 | This section covers the [fullDRC](examples/fullDRC/fullDRC.ino) example. Examples of the commands to control the individual parameters are as follows:
412 | * compress#
413 | * decompress#
414 | * gain#0
415 | * thresh#25
416 | * attack#0.1
417 | * release#0.1
418 | * ratio#10
419 | * width#10
420 |
421 | The actual code that does the compression is the same but the serial interface is more verbose.
422 |
423 | ```cpp
424 | #include
425 |
426 | btAudio audio = btAudio("ESP_Speaker");
427 | String command;
428 |
429 | void setup() {
430 | // Streams audio data to the ESP32
431 | audio.begin();
432 | // Re-connects to last connected device
433 | audio.reconnect();
434 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
435 | int bck = 26;
436 | int ws = 27;
437 | int dout = 25;
438 | audio.I2S(bck, dout, ws);
439 | // Opens the serial port
440 | Serial.begin(115200);
441 | }
442 |
443 | float thresh=30;
444 | float attack=0.1;
445 | float release= 0.2;
446 | float ratio = 10;
447 | float kneeWidth= 1;
448 | float gain=0;
449 |
450 | void loop() {
451 | delay(300);
452 | if(Serial.available()){
453 | command = Serial.readStringUntil('#');
454 | if(command.equals("compress")){
455 | Serial.println("Applying Compression");
456 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
457 | }
458 | else if(command.equals("decompress")){
459 | Serial.println("Releasing Compression");
460 | audio.decompress();
461 | }
462 | else if(command.equals("gain")){
463 | gain =Serial.parseInt();
464 | Serial.println((String)"Compressing with a gain of "+gain);
465 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
466 | }
467 | else if(command.equals("thresh")){
468 | thresh =Serial.parseFloat();
469 | Serial.println(thresh);
470 | Serial.println((String)"Compressing with a threshold of " + thresh + "dB");
471 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
472 | }
473 | else if(command.equals("attack")){
474 | attack =Serial.parseFloat();
475 | Serial.println((String)"Compressing with an attack time of " + attack*1000 + "ms");
476 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
477 | }
478 | else if(command.equals("release")){
479 | release =Serial.parseFloat();
480 | Serial.println((String)"Compressing with an release time of " + release*1000 + "ms");
481 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
482 | }
483 | else if(command.equals("ratio")){
484 | ratio =Serial.parseFloat();
485 | Serial.println((String)"Compressing with a Ratio of " + ratio);
486 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
487 | }
488 | else if(command.equals("width")){
489 | kneeWidth =Serial.parseInt();
490 | Serial.println((String)"Compressing with a knee width of " + kneeWidth + "dB");
491 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
492 | }
493 | }
494 | }
495 | ```
496 |
497 | ## Approximate Dynamic Range Compression
498 | 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.
499 |
500 | 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](https://en.wikipedia.org/wiki/Newton_polynomial) 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](examples/benchLookup/benchLookup.ino) example. This example isn't crucial but useful if you want to see the difference in methodologies.
501 |
502 | ```cpp
503 | void setup() {
504 | Serial.begin(115200);
505 |
506 | // Create lookup table
507 | float lu[199];
508 | for(int i = -99; i <100;i++){
509 | lu[i+99]=pow10f(i/20.0);
510 | }
511 |
512 | // test data ranges between -10 and 10 dB
513 | float temp=-10.0;
514 | float val[200];
515 | for(int i = 0; i <200;i++){
516 | temp+=.1;
517 | val[i]= temp;
518 | }
519 |
520 |
521 | // exact solution
522 | float outvalEx[200];
523 | unsigned long t1 =micros();
524 | for(int i = 0; i <200;i++){
525 | outvalEx[i]= pow10f(val[i]/20.0f);
526 | }
527 | unsigned long t2 =micros();
528 | Serial.print("Exact Method: ");
529 | Serial.print((t2-t1)/200.0);
530 | Serial.println(" Microseconds per transform");
531 |
532 |
533 | // approximate method
534 |
535 | float outvalAppi[200];
536 |
537 | t1 =micros();
538 | for(int i = 0; i <200;i++){
539 | outvalAppi[i]= lu[(int)val[i]+99];
540 | }
541 | t2=micros();
542 | Serial.print("Approximate Method(lookup): ");
543 | Serial.print((t2-t1)/200.0);
544 | Serial.println(" Microseconds per transform");
545 |
546 |
547 | // approximate method
548 | float frac;
549 | float outvalApp[200];
550 |
551 | t1 =micros();
552 | for(int i = 0; i <200;i++){
553 | float input =val[i];
554 | if(input<0){
555 | int integ= (int)input;
556 | integ-=1;
557 | frac= input-integ;
558 | outvalApp[i]= 1.0f+ (0.1146f+0.0074f*frac)*frac;
559 | outvalApp[i]*=lu[integ+99];
560 | }else{
561 | int integ= (int)input;
562 | frac= input-integ;
563 | outvalApp[i]= 1.0f+ (0.1146f+0.0074f*frac)*frac;
564 | outvalApp[i]*=lu[integ+99];
565 | }
566 | }
567 | t2=micros();
568 | Serial.print("Approximate Method: ");
569 | Serial.print((t2-t1)/200.0);
570 | Serial.println(" Microseconds per transform");
571 |
572 | float err=0.0;
573 | float maxerr=0.0;
574 |
575 |
576 | // Approximation error(lookup)
577 | for(int i = 0; i <200;i++){
578 | err= abs((outvalAppi[i]-outvalEx[i])/outvalEx[i]*100);
579 | if(err>maxerr){
580 | maxerr=err;
581 | }
582 | }
583 | Serial.print("Approximate method(lookup) max error is: ");
584 | Serial.print(maxerr);
585 | Serial.println("%");
586 |
587 | maxerr=0;
588 | // Approximation error(polynomial)
589 | for(int i = 0; i <200;i++){
590 | err= abs((outvalApp[i]-outvalEx[i])/outvalEx[i]*100);
591 | if(err>maxerr){
592 | maxerr=err;
593 | }
594 | }
595 | Serial.print("Approximate method (polynomial) max error is: ");
596 | Serial.print(maxerr);
597 | Serial.println("%");
598 |
599 | }
600 |
601 | void loop() {
602 | // put your main code here, to run repeatedly:
603 | delay(1000);
604 | }
605 | ```
606 | If you run the above code you should get results like this on the serial monitor.
607 |
608 |
609 |
610 |
611 |
612 |
613 | ## WiFi interface
614 | This section covers the [webInterface example](examples/webInterface/webInterface.ino)
615 | 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.
616 |
617 | ```cpp
618 | #include
619 | #include
620 |
621 | // create audio object
622 | btAudio audio = btAudio("ESP_Speaker");
623 |
624 | // create webserver object
625 | webDSP web;
626 |
627 | void setup() {
628 | // Streams audio data to the ESP32
629 | audio.begin();
630 | // Re-connects to last connected device
631 | audio.reconnect();
632 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
633 | int bck = 26;
634 | int ws = 27;
635 | int dout = 25;
636 | audio.I2S(bck, dout, ws);
637 | // Opens the serial port
638 | Serial.begin(115200);
639 |
640 | // Replace ssid and password with your details
641 | const char* ssid = "";
642 | const char* password = "";
643 | web.begin(ssid, password, &audio);
644 | }
645 |
646 | void loop() {
647 | // continually check on client
648 | web._server.handleClient();
649 | }
650 | ```
651 |
652 | 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!
653 |
654 |
655 |
656 |
657 |
658 | You can expand each of the three sections and edit any of the parameters of the filters, volume or dynamic range compression.
659 |
660 |
661 | ## Not so simple audio
662 | What if [you hate classes/Object Oriented Programming](https://medium.com/better-programming/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7 ) 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](examples/underTheHood/underTheHood.ino) 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.
663 |
664 | On a technical note this code adapts the [ESP-IDF A2DP Sink](https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink/main/main.c
665 | ) example but uses the [arduino-esp32 library](https://github.com/espressif/arduino-esp32) . 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](https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink/main/main.c
666 | ) if you work outside the Arduino environment.
667 |
668 | ```cpp
669 | // cricial audio bits taken from https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink/main/main.c
670 |
671 | // bluetooth, config, discover and audio
672 | #include "esp_bt_main.h"
673 | #include "esp_bt_device.h"
674 | #include "esp_gap_bt_api.h"
675 | #include "esp_a2dp_api.h"
676 |
677 |
678 | // the audio DAC and amp configuration.
679 | #include "driver/i2s.h"
680 |
681 | //globals
682 |
683 |
684 | // the callback(processes bluetooth data).
685 | // this is the most important function.
686 | void bt_data_cb(const uint8_t *data, uint32_t len){
687 | // number of 16 bit samples
688 | int n = len/2;
689 |
690 | // point to a 16bit sample
691 | int16_t* data16=(int16_t*)data;
692 |
693 | // create a variable (potentially processed) that we'll pass via I2S
694 | int16_t fy;
695 |
696 | // Records number of bytes written via I2S
697 | size_t i2s_bytes_write = 0;
698 |
699 | for(int i=0;i(I2S_MODE_MASTER | I2S_MODE_TX),
720 | .sample_rate = 44100,
721 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
722 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
723 | .communication_format = static_cast(I2S_COMM_FORMAT_I2S|I2S_COMM_FORMAT_I2S_MSB),
724 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // default interrupt priority
725 | .dma_buf_count = 8,
726 | .dma_buf_len = 1000,
727 | .use_apll = false,
728 | .tx_desc_auto_clear = true
729 | };
730 |
731 | // i2s pinout
732 | static const i2s_pin_config_t pin_config = {
733 | .bck_io_num = 26,//26
734 | .ws_io_num = 27,
735 | .data_out_num = 25, //
736 | .data_in_num = I2S_PIN_NO_CHANGE
737 | };
738 |
739 | // now configure i2s with constructed pinout and config
740 | i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
741 | i2s_set_pin(I2S_NUM_0, &pin_config);
742 | i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO);
743 | i2s_set_sample_rates(I2S_NUM_0, 44100);
744 |
745 | // set up bluetooth classic via bluedroid
746 | btStart();
747 | esp_bluedroid_init();
748 | esp_bluedroid_enable();
749 |
750 |
751 | // set up device name
752 | const char *dev_name = "ESP_SPEAKER";
753 | esp_bt_dev_set_device_name(dev_name);
754 |
755 | // initialize A2DP sink and set the data callback(A2DP is bluetooth audio)
756 | esp_a2d_sink_register_data_callback(bt_data_cb);
757 | esp_a2d_sink_init();
758 |
759 | // set discoverable and connectable mode, wait to be connected
760 | esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE);
761 |
762 | }
763 | void loop() {
764 | // put your main code here, to run repeatedly:
765 | delay(1000);
766 | }
767 | ```
768 |
--------------------------------------------------------------------------------
/examples/advertiseBluetooth/advertiseBluetooth.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | // Sets the name of the audio device
4 | btAudio audio = btAudio("ESP_Speaker");
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | }
15 |
16 | void loop() {
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/examples/allFeatures/allFeatures.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | float thresh=30;
25 | float attack=0.1;
26 | float release= 0.2;
27 | float ratio = 10;
28 | float kneeWidth= 1;
29 | float gain=0;
30 | int fo=3;
31 | float fc;
32 |
33 |
34 | void loop() {
35 | delay(300);
36 | if(Serial.available()){
37 | command = Serial.readStringUntil('#');
38 | if(command.equals("compress")){
39 | Serial.println("Applying Compression");
40 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
41 | }
42 | else if(command.equals("decompress")){
43 | Serial.println("Releasing Compression");
44 | audio.decompress();
45 | }
46 | else if(command.equals("gain")){
47 | gain =Serial.parseInt();
48 | Serial.println((String)"Compressing with a gain of "+gain);
49 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
50 | }
51 | else if(command.equals("thresh")){
52 | thresh =Serial.parseFloat();
53 | Serial.println(thresh);
54 | Serial.println((String)"Compressing with a threshold of " + thresh + "dB");
55 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
56 | }
57 | else if(command.equals("attack")){
58 | attack =Serial.parseFloat();
59 | Serial.println((String)"Compressing with an attack time of " + attack*1000 + "ms");
60 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
61 | }
62 | else if(command.equals("release")){
63 | release =Serial.parseFloat();
64 | Serial.println((String)"Compressing with an release time of " + release*1000 + "ms");
65 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
66 | }
67 | else if(command.equals("ratio")){
68 | ratio =Serial.parseFloat();
69 | Serial.println((String)"Compressing with a Ratio of " + ratio);
70 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
71 | }
72 | else if(command.equals("width")){
73 | kneeWidth =Serial.parseInt();
74 | Serial.println((String)"Compressing with a knee width of " + kneeWidth + "dB");
75 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
76 | }
77 | else if(command.equals("hp")){
78 | Serial.println("Setting a High-Pass Filter...");
79 | fc =Serial.parseFloat();
80 | audio.createFilter(fo, fc, highpass);
81 | }
82 | else if(command.equals("stopFilt")){
83 | audio.stopFilter();
84 | Serial.println("Stopping Filter...");
85 | }
86 | else if(command.equals("order")){
87 | fo =Serial.parseInt();
88 | Serial.println((String)"Filter order changed to " + fo + "for next filter ");
89 | }
90 | else if(command.equals("lp")){
91 | Serial.println("Setting a Low-Pass Filter...");
92 | fc =Serial.parseFloat();
93 | audio.createFilter(fo, fc, lowpass);
94 | }else{
95 | Serial.println("Command not recognised");
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/examples/benchLookup/benchLookup.ino:
--------------------------------------------------------------------------------
1 | void setup() {
2 | Serial.begin(115200);
3 |
4 | // Create lookup table
5 | float lu[199];
6 | for(int i = -99; i <100;i++){
7 | lu[i+99]=pow10f(i/20.0);
8 | }
9 |
10 | // test data ranges between -10 and 10 dB
11 | float temp=-10.0;
12 | float val[200];
13 | for(int i = 0; i <200;i++){
14 | temp+=.1;
15 | val[i]= temp;
16 | }
17 |
18 |
19 | // exact solution
20 | float outvalEx[200];
21 | unsigned long t1 =micros();
22 | for(int i = 0; i <200;i++){
23 | outvalEx[i]= pow10f(val[i]/20.0f);
24 | }
25 | unsigned long t2 =micros();
26 | Serial.print("Exact Method: ");
27 | Serial.print((t2-t1)/200.0);
28 | Serial.println(" Microseconds per transform");
29 |
30 |
31 | // approximate method
32 |
33 | float outvalAppi[200];
34 |
35 | t1 =micros();
36 | for(int i = 0; i <200;i++){
37 | outvalAppi[i]= lu[(int)val[i]+99];
38 | }
39 | t2=micros();
40 | Serial.print("Approximate Method(lookup): ");
41 | Serial.print((t2-t1)/200.0);
42 | Serial.println(" Microseconds per transform");
43 |
44 |
45 | // approximate method
46 | float frac;
47 | float outvalApp[200];
48 |
49 | t1 =micros();
50 | for(int i = 0; i <200;i++){
51 | float input =val[i];
52 | if(input<0){
53 | int integ= (int)input;
54 | integ-=1;
55 | frac= input-integ;
56 | outvalApp[i]= 1.0f+ (0.1146f+0.0074f*frac)*frac;
57 | outvalApp[i]*=lu[integ+99];
58 | }else{
59 | int integ= (int)input;
60 | frac= input-integ;
61 | outvalApp[i]= 1.0f+ (0.1146f+0.0074f*frac)*frac;
62 | outvalApp[i]*=lu[integ+99];
63 | }
64 | }
65 | t2=micros();
66 | Serial.print("Approximate Method: ");
67 | Serial.print((t2-t1)/200.0);
68 | Serial.println(" Microseconds per transform");
69 |
70 | float err=0.0;
71 | float maxerr=0.0;
72 |
73 |
74 | // Approximation error(lookup)
75 | for(int i = 0; i <200;i++){
76 | err= abs((outvalAppi[i]-outvalEx[i])/outvalEx[i]*100);
77 | if(err>maxerr){
78 | maxerr=err;
79 | }
80 | }
81 | Serial.print("Approximate method(lookup) max error is: ");
82 | Serial.print(maxerr);
83 | Serial.println("%");
84 |
85 | maxerr=0;
86 | // Approximation error(polynomial)
87 | for(int i = 0; i <200;i++){
88 | err= abs((outvalApp[i]-outvalEx[i])/outvalEx[i]*100);
89 | if(err>maxerr){
90 | maxerr=err;
91 | }
92 | }
93 | Serial.print("Approximate method (polynomial) max error is: ");
94 | Serial.print(maxerr);
95 | Serial.println("%");
96 |
97 | }
98 |
99 | void loop() {
100 | // put your main code here, to run repeatedly:
101 | delay(1000);
102 | }
103 |
--------------------------------------------------------------------------------
/examples/biquadSpeedTest/biquadSpeedTest.ino:
--------------------------------------------------------------------------------
1 | #include "filter.h"
2 | const uint16_t sampleRate = 48000;
3 | int16_t sig[sampleRate];
4 |
5 | void setup() {
6 | delay(3000);
7 |
8 | //begin serial after clocks set
9 | Serial.begin(115200);
10 | Serial.println("hello");
11 |
12 | // signal to be modulated
13 | for (int i = 1; i < (sampleRate + 1); i++) {
14 | sig[i - 1] = (int16_t)(sin(2.0 * PI * 1000 * i / sampleRate) * 32767 * 1);
15 | }
16 |
17 | volatile int16_t result;
18 | uint32_t beg;
19 |
20 |
21 | filter filt = filter(8000, 48000, 1, lowpass);
22 | beg = micros();
23 | for (uint16_t i = 0; i < sampleRate; i++) {
24 | result = filt.process(sig[i]);
25 | }
26 | Serial.print((micros() - beg) * 100.0 / 1000000.0);
27 | Serial.println("% CPU used for 1 Biquad");
28 | Serial.print("Ignore this number: ");
29 | Serial.println(result);
30 |
31 | filter filt2 = filter(8000, 48000, 2, lowpass);
32 | beg = micros();
33 | for (uint16_t i = 0; i < sampleRate; i++) {
34 | result = filt2.process(sig[i]);
35 | }
36 | Serial.print((micros() - beg) * 100.0 / 1000000.0);
37 | Serial.println("% CPU used for 2 Biquads");
38 | Serial.print("Ignore this number: ");
39 | Serial.println(result);
40 |
41 | filter filt3 = filter(200, 48000, 3, highpass);
42 | beg = micros();
43 | for (uint16_t i = 0; i < sampleRate; i++) {
44 | result = filt3.process(sig[i]);
45 | }
46 | Serial.print((micros() - beg) * 100.0 / 1000000.0);
47 | Serial.println("% CPU used for 3 Biquads");
48 | Serial.print("Ignore this number: ");
49 | Serial.println(result);
50 | }
51 |
52 | void loop() {
53 | }
--------------------------------------------------------------------------------
/examples/changeVolume/changeVolume.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | // Sets the name of the audio device
4 | btAudio audio = btAudio("ESP_Speaker");
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 | }
20 |
21 | void loop() {
22 | delay(3000);
23 | audio.volume(0.1);
24 | delay(3000);
25 | audio.volume(1.0);
26 | }
27 |
--------------------------------------------------------------------------------
/examples/customCallbacks/customCallbacks.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | // Sets the name of the audio device
4 | btAudio audio = btAudio("ESP_Speaker");
5 |
6 | // First callack
7 | void bt_data_cb1(const uint8_t *data, uint32_t len){
8 | // number of 16 bit samples
9 | int n = len/2;
10 |
11 | // point to a 16bit sample
12 | int16_t* data16=(int16_t*)data;
13 |
14 | // create a variable (potentially processed) that we'll pass via I2S
15 | int16_t fy;
16 |
17 | // Records number of bytes written via I2S
18 | size_t i2s_bytes_write = 0;
19 |
20 | for(int i=0;i
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | float thresh=30;
25 | float attack=0.1;
26 | float release= 0.2;
27 | float ratio = 10;
28 | float kneeWidth= 1;
29 | float gain=0;
30 |
31 | void loop() {
32 | delay(300);
33 | if(Serial.available()){
34 | command = Serial.readStringUntil('#');
35 | if(command.equals("compress")){
36 | Serial.println("Applying Compression");
37 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
38 | }
39 | else if(command.equals("decompress")){
40 | Serial.println("Releasing Compression");
41 | audio.decompress();
42 | }
43 | else if(command.equals("gain")){
44 | gain =Serial.parseInt();
45 | Serial.println((String)"Compressing with a gain of "+gain);
46 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
47 | }
48 | else if(command.equals("thresh")){
49 | thresh =Serial.parseFloat();
50 | Serial.println(thresh);
51 | Serial.println((String)"Compressing with a threshold of " + thresh + "dB");
52 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
53 | }
54 | else if(command.equals("attack")){
55 | attack =Serial.parseFloat();
56 | Serial.println((String)"Compressing with an attack time of " + attack*1000 + "ms");
57 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
58 | }
59 | else if(command.equals("release")){
60 | release =Serial.parseFloat();
61 | Serial.println((String)"Compressing with an release time of " + release*1000 + "ms");
62 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
63 | }
64 | else if(command.equals("ratio")){
65 | ratio =Serial.parseFloat();
66 | Serial.println((String)"Compressing with a Ratio of " + ratio);
67 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
68 | }
69 | else if(command.equals("width")){
70 | kneeWidth =Serial.parseInt();
71 | Serial.println((String)"Compressing with a knee width of " + kneeWidth + "dB");
72 | audio.compress(thresh,attack,release,ratio,kneeWidth,gain);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/examples/highpassFilter/highpassFilter.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | int fo=3;
25 | float fc=30;
26 |
27 | void loop() {
28 | delay(300);
29 | if(Serial.available()){
30 | command = Serial.readStringUntil('#');
31 | if(command.equals("hp")){
32 | Serial.println("Setting a High-Pass Filter...");
33 | fc =Serial.parseFloat();
34 | audio.createFilter(fo, fc, highpass);
35 | }
36 | else if(command.equals("stopFilt")){
37 | audio.stopFilter();
38 | Serial.println("Stopping Filter...");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/lowpassFilter/lowpassFilter.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | int fo=3;
25 | float fc;
26 |
27 | void loop() {
28 | delay(300);
29 | if(Serial.available()){
30 | command = Serial.readStringUntil('#');
31 | if(command.equals("hp")){
32 | Serial.println("Setting a High-Pass Filter...");
33 | fc =Serial.parseFloat();
34 | audio.createFilter(fo, fc, highpass);
35 | }
36 | else if(command.equals("stopFilt")){
37 | audio.stopFilter();
38 | Serial.println("Stopping Filter...");
39 | }
40 | else if(command.equals("lp")){
41 | Serial.println("Setting a Low-Pass Filter...");
42 | fc =Serial.parseFloat();
43 | audio.createFilter(fo, fc, lowpass);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/minimalAudio/minimalAudio.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | // Sets the name of the audio device
4 | btAudio audio = btAudio("ESP_Speaker");
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 | }
20 |
21 | void loop() {
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/examples/partialDRC/partialDRC.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | float thresh=30;
25 | float attack=0.1;
26 | float release= 0.2;
27 | float ratio = 10;
28 | float width= 10;
29 | float gain=0;
30 |
31 | void loop() {
32 | delay(300);
33 | if(Serial.available()){
34 | command = Serial.readStringUntil('#');
35 | if(command.equals("compress")){
36 | Serial.println("Applying Compression");
37 | audio.compress(thresh,attack,release,ratio,width,gain);
38 | }
39 | else if(command.equals("decompress")){
40 | Serial.println("Releasing Compression");
41 | audio.decompress();
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/queryMeta/queryMeta.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | // Sets the name of the audio device
4 | btAudio audio = btAudio("ESP_Speaker");
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 | void loop() {
25 | delay(5000);
26 | // updates metadata
27 | audio.updateMeta();
28 | Serial.print("Title: ");
29 | Serial.println(audio.title);
30 | Serial.print("Artist: ");
31 | Serial.println(audio.artist);
32 | Serial.print("Album: ");
33 | Serial.println(audio.album);
34 | Serial.print("Genre: ");
35 | Serial.println(audio.genre);
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/examples/serialControl/serialControl.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | btAudio audio = btAudio("ESP_Speaker");
4 | String command;
5 |
6 | void setup() {
7 |
8 | // Streams audio data to the ESP32
9 | audio.begin();
10 |
11 | // Re-connects to last connected device
12 | audio.reconnect();
13 |
14 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
15 | int bck = 26;
16 | int ws = 27;
17 | int dout = 25;
18 | audio.I2S(bck, dout, ws);
19 |
20 | // Opens the serial port
21 | Serial.begin(115200);
22 | }
23 |
24 |
25 | void loop() {
26 | delay(300);
27 | // check if data is available
28 | if(Serial.available()){
29 | //read until a terminator. after this point there should only be numbers
30 | command = Serial.readStringUntil('#');
31 | if(command.equals("vol")){
32 | //read and set volume
33 | float vol =Serial.parseFloat();
34 | Serial.println("Changing Volume");
35 | audio.volume(vol);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/underTheHood/underTheHood.ino:
--------------------------------------------------------------------------------
1 | // cricial audio bits taken from https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink/main/main.c
2 |
3 | // bluetooth, config, discover and audio
4 | #include "esp_bt_main.h"
5 | #include "esp_bt_device.h"
6 | #include "esp_gap_bt_api.h"
7 | #include "esp_a2dp_api.h"
8 |
9 |
10 | // the audio DAC and amp configuration.
11 | #include "driver/i2s.h"
12 |
13 |
14 | // the callback(processes bluetooth data).
15 | // this is the most important function.
16 | void bt_data_cb(const uint8_t *data, uint32_t len){
17 | // number of 16 bit samples
18 | int n = len/2;
19 |
20 | // point to a 16bit sample
21 | int16_t* data16=(int16_t*)data;
22 |
23 | // create a variable (potentially processed) that we'll pass via I2S
24 | int16_t fy;
25 |
26 | // Records number of bytes written via I2S
27 | size_t i2s_bytes_write = 0;
28 |
29 | for(int i=0;i(I2S_MODE_MASTER | I2S_MODE_TX),
50 | .sample_rate = 44100,
51 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
52 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
53 | .communication_format = static_cast(I2S_COMM_FORMAT_I2S|I2S_COMM_FORMAT_I2S_MSB),
54 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // default interrupt priority
55 | .dma_buf_count = 8,
56 | .dma_buf_len = 1000,
57 | .use_apll = false,
58 | .tx_desc_auto_clear = true
59 | };
60 |
61 | // i2s pinout
62 | static const i2s_pin_config_t pin_config = {
63 | .bck_io_num = 26,//26
64 | .ws_io_num = 27,
65 | .data_out_num = 25, //
66 | .data_in_num = I2S_PIN_NO_CHANGE
67 | };
68 |
69 | // now configure i2s with constructed pinout and config
70 | i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
71 | i2s_set_pin(I2S_NUM_0, &pin_config);
72 | i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO);
73 | i2s_set_sample_rates(I2S_NUM_0, 44100);
74 |
75 | // set up bluetooth classic via bluedroid
76 | btStart();
77 | esp_bluedroid_init();
78 | esp_bluedroid_enable();
79 |
80 |
81 | // set up device name
82 | const char *dev_name = "ESP_SPEAKER";
83 | esp_bt_dev_set_device_name(dev_name);
84 |
85 | // initialize A2DP sink and set the data callback(A2DP is bluetooth audio)
86 | esp_a2d_sink_register_data_callback(bt_data_cb);
87 | esp_a2d_sink_init();
88 |
89 | // set discoverable and connectable mode, wait to be connected
90 | esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE);
91 |
92 | }
93 | void loop() {
94 | // put your main code here, to run repeatedly:
95 | delay(1000);
96 | }
--------------------------------------------------------------------------------
/examples/webInterface/webInterface.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | // create audio object
5 | btAudio audio = btAudio("ESP_Speaker");
6 |
7 | // create webserver object
8 | webDSP web;
9 |
10 | void setup() {
11 |
12 | // Streams audio data to the ESP32
13 | audio.begin();
14 |
15 | // Re-connects to last connected device
16 | audio.reconnect();
17 |
18 | // Outputs the received data to an I2S DAC, e.g. https://www.adafruit.com/product/3678
19 | int bck = 26;
20 | int ws = 27;
21 | int dout = 25;
22 | audio.I2S(bck, dout, ws);
23 |
24 | // Opens the serial port
25 | Serial.begin(115200);
26 |
27 | // Replace ssid and password with your details
28 | const char* ssid = "";
29 | const char* password = "";
30 | web.begin(ssid, password, &audio);
31 | }
32 |
33 | void loop() {
34 | // continually check on client
35 | web._server.handleClient();
36 | }
37 |
--------------------------------------------------------------------------------
/keywords.txt:
--------------------------------------------------------------------------------
1 | #######################################
2 | # Syntax Coloring Map SPI
3 | #######################################
4 |
5 | #######################################
6 | # Datatypes (KEYWORD1)
7 | #######################################
8 |
9 | btAudio KEYWORD1
10 | DRC KEYWORD1
11 | filter KEYWORD1
12 |
13 | #######################################
14 | # Methods and Functions (KEYWORD2)
15 | #######################################
16 | begin KEYWORD2
17 | end KEYWORD2
18 | softKnee KEYWORD2
19 | filter KEYWORD2
20 | process KEYWORD2
21 | createFilter KEYWORD2
22 | stopFilter KEYWORD2
23 | compress KEYWORD2
24 | decompress KEYWORD2
25 | volume KEYWORD2
26 | record KEYWORD2
27 | stopRecord KEYWORD2
28 | postProc KEYWORD2
29 | btAudio KEYWORD2
30 | DRC KEYWORD2
31 |
32 | #######################################
33 | # Constants (LITERAL1)
34 | #######################################
35 | lowpass LITERAL1
36 | highpass LITERAL1
37 |
--------------------------------------------------------------------------------
/library.properties:
--------------------------------------------------------------------------------
1 | name=btAudio
2 | version=1.0
3 | author=Tim Tierney
4 | maintainer=Tim Tierney
5 | sentence= High level interface to the Advanced Audio Distribution Profile (A2DP) available in ESP32 boards.
6 | paragraph= This library provides an easy to use interface for developing bluetooth audio projects. It also provides some real-time signal processing(Filters and Dynamic Range Compression).
7 | category=Audio
8 | url=https://github.com/tierneytim/btAudio
9 | architectures=esp32
10 |
--------------------------------------------------------------------------------
/readme/approximation.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/approximation.PNG
--------------------------------------------------------------------------------
/readme/bread_rotate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/bread_rotate.jpg
--------------------------------------------------------------------------------
/readme/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/download.png
--------------------------------------------------------------------------------
/readme/includeLibrary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/includeLibrary.png
--------------------------------------------------------------------------------
/readme/serial.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/serial.PNG
--------------------------------------------------------------------------------
/readme/speaker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/speaker.jpg
--------------------------------------------------------------------------------
/readme/webpage.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tierneytim/btAudio/581c020e5625da8d7e703b9e273f01cc2c143558/readme/webpage.PNG
--------------------------------------------------------------------------------
/src/DRC.cpp:
--------------------------------------------------------------------------------
1 | #include "DRC.h"
2 |
3 | #define pow10f(x) expf(2.302585092994046f*x)
4 |
5 | DRC::DRC(float fs, float T,float tauAtt,float tauRel, float R,float w, float mu){
6 | _T= T;
7 | _alphAtt=exp(-1/(tauAtt*fs));
8 | _alphRel=exp(-1/(tauRel*fs));
9 | _slope=1/R-1;
10 | _yprev=0;
11 | _w = constrain(w,1,90);
12 | _mu = mu;
13 | _w4= 1/(4*_w);
14 | }
15 |
16 | // thanks you ! https://github.com/chipaudette/OpenAudio_ArduinoLibrary/blob/master/AudioEffectCompressor_F32.h
17 | float DRC::log10f_approx(float X){
18 | float Y, F;
19 | int E;
20 | F = frexpf(fabsf(X), &E);
21 | Y = 1.23149591368684f;
22 | Y *= F;
23 | Y += -4.11852516267426f;
24 | Y *= F;
25 | Y += 6.02197014179219f;
26 | Y *= F;
27 | Y += -3.13396450166353f;
28 | Y += E;
29 | Y *=0.3010299956639812f;
30 | return(Y);
31 | }
32 |
33 | int16_t DRC::softKnee(float x){
34 | // signal to dB
35 | float dBx = fabsf(x);
36 | dBx+=1;
37 | dBx= 20.0*log10f_approx(dBx);
38 |
39 | // how far above threshold are we?
40 | float delta = dBx-_T;
41 |
42 | // if not above do nothing
43 | float dBy = dBx;
44 |
45 | //if above soft kneee compress linearly
46 | if((delta-_w) >= 0){
47 | dBy += _slope*delta;
48 | } else if (delta> -_w && delta<_w){ //if within knee compress quadratically
49 | delta+= _w;
50 | dBy += _slope*(delta*delta)*_w4;
51 | }
52 |
53 | // smooth difference between compressed and uncompressed data
54 | dBy-=dBx;
55 | if(dBy<_yprev){
56 | dBy = _alphAtt*_yprev+ (1-_alphAtt)*dBy;
57 |
58 | }else{
59 | dBy = _alphRel*_yprev+ (1-_alphRel)*dBy;
60 | }
61 | // save current value for next iteration
62 | _yprev=dBy;
63 |
64 | // add in make up gain
65 | dBy+=_mu;
66 |
67 | /*polynomial approximation (newtons divided differences)
68 | for fractional part of pow10f(dB/20). The integral part is
69 | determined from a lookup table(G). Precision > 16bits.
70 | */
71 | if(dBy<0){
72 | int integ= (int)dBy;
73 | integ-=1;
74 | float frac= dBy-integ;
75 | dBy= frac*(frac*(0.0003027786f*frac+0.006535915f)+0.115179760f)+1.0f;
76 | dBy*=G[integ+96];
77 | dBy*=x;
78 | }else{
79 | int integ= (int)dBy;
80 | float frac= dBy-integ;
81 | //dBy= 1.0f+ (0.1146f+0.0074f*frac)*frac;
82 | dBy= frac*(frac*(0.0003027786f*frac+0.006535915f)+0.115179760f)+1.0f;
83 |
84 | dBy*=G[integ+96];
85 | dBy*=x;
86 | }
87 |
88 | // constrain to be in int16_t range
89 | if(dBy>32767){
90 | dBy=32767;
91 | }else if (dBy < -32767){
92 | dBy= -32767;
93 | }
94 | // and return 16-bit signal */
95 | return (int16_t)(dBy);
96 | }
97 |
--------------------------------------------------------------------------------
/src/DRC.h:
--------------------------------------------------------------------------------
1 | #ifndef DRC_H
2 | #define DRC_H
3 |
4 | #include "Arduino.h"
5 | class DRC {
6 | public:
7 | DRC(float fs, float T,float tauAtt,float tauRel, float R,float w, float mu);
8 | int16_t softKnee(float x);
9 |
10 | private:
11 | float log10f_approx(float X);
12 | float _T;
13 | float _alphAtt;
14 | float _alphRel;
15 | float _slope;
16 | float _yprev;
17 | float _w;
18 | float _mu;
19 | float _w4;
20 |
21 | };
22 | static const float G[193]={0.00001585f, 0.00001778f, 0.00001995f, 0.00002239f, 0.00002512f, 0.00002818f, 0.00003162f, 0.00003548f, 0.00003981f, 0.00004467f, 0.00005012f, 0.00005623f, 0.00006310f, 0.00007079f, 0.00007943f, 0.00008913f, 0.00010000f, 0.00011220f, 0.00012589f, 0.00014125f, 0.00015849f, 0.00017783f, 0.00019953f, 0.00022387f, 0.00025119f, 0.00028184f, 0.00031623f, 0.00035481f, 0.00039811f, 0.00044668f, 0.00050119f, 0.00056234f, 0.00063096f, 0.00070795f, 0.00079433f, 0.00089125f, 0.00100000f, 0.00112202f, 0.00125893f, 0.00141254f, 0.00158489f, 0.00177828f, 0.00199526f, 0.00223872f, 0.00251189f, 0.00281838f, 0.00316228f, 0.00354813f, 0.00398107f, 0.00446684f, 0.00501187f, 0.00562341f, 0.00630957f, 0.00707946f, 0.00794328f, 0.00891251f, 0.01000000f, 0.01122018f, 0.01258926f, 0.01412538f, 0.01584893f, 0.01778279f, 0.01995262f, 0.02238721f, 0.02511886f, 0.02818383f, 0.03162277f, 0.03548133f, 0.03981072f, 0.04466835f, 0.05011873f, 0.05623413f, 0.06309573f, 0.07079458f, 0.07943282f, 0.08912510f, 0.10000000f, 0.11220185f, 0.12589255f, 0.14125374f, 0.15848932f, 0.17782794f, 0.19952624f, 0.22387213f, 0.25118864f, 0.28183830f, 0.31622776f, 0.35481340f, 0.39810717f, 0.44668359f, 0.50118721f, 0.56234133f, 0.63095737f, 0.70794576f, 0.79432821f, 0.89125097f, 1.00000000f, 1.12201846f, 1.25892544f, 1.41253757f, 1.58489323f, 1.77827942f, 1.99526238f, 2.23872113f, 2.51188660f, 2.81838274f, 3.16227770f, 3.54813409f, 3.98107195f, 4.46683550f, 5.01187229f, 5.62341309f, 6.30957365f, 7.07945824f, 7.94328213f, 8.91250896f, 10.00000000f, 11.22018337f, 12.58925438f, 14.12537479f, 15.84893322f, 17.78279495f, 19.95262146f, 22.38721275f, 25.11886215f, 28.18383217f, 31.62277603f, 35.48133469f, 39.81071854f, 44.66835785f, 50.11872864f, 56.23413086f, 63.09572601f, 70.79458618f, 79.43281555f, 89.12510681f, 100.00000000f, 112.20183563f, 125.89251709f, 141.25378418f, 158.48933411f, 177.82794189f, 199.52621460f, 223.87207031f, 251.18870544f, 281.83831787f, 316.22778320f, 354.81335449f, 398.10708618f, 446.68368530f, 501.18728638f, 562.34130859f, 630.95727539f, 707.94561768f, 794.32843018f, 891.25103760f, 1000.00000000f, 1122.01831055f, 1258.92517090f, 1412.53784180f, 1584.89331055f, 1778.27941895f, 1995.26208496f, 2238.72070312f, 2511.88696289f, 2818.38330078f, 3162.27758789f, 3548.13354492f, 3981.07080078f, 4466.83691406f, 5011.87304688f, 5623.41308594f, 6309.57275391f, 7079.45605469f, 7943.28417969f, 8912.51074219f, 10000.00000000f, 11220.18945313f, 12589.25195313f, 14125.37890625f, 15848.92480469f, 17782.79492187f, 19952.63281250f, 22387.20703125f, 25118.86914062f, 28183.81640625f, 31622.77734375f, 35481.35546875f, 39810.70703125f, 44668.36718750f, 50118.70312500f, 56234.13281250f, 63095.76171875f};
23 | #endif
--------------------------------------------------------------------------------
/src/btAudio.cpp:
--------------------------------------------------------------------------------
1 | #include "btAudio.h"
2 | #include // For saving audio source BT addr for auto-reconnect
3 | ////////////////////////////////////////////////////////////////////
4 | ////////////// Nasty statics for i2sCallback ///////////////////////
5 | ////////////////////////////////////////////////////////////////////
6 | float btAudio::_vol=0.95;
7 | esp_bd_addr_t btAudio::_address;
8 | int32_t btAudio::_sampleRate=44100;
9 |
10 | String btAudio::title="";
11 | String btAudio::album="";
12 | String btAudio::genre="";
13 | String btAudio::artist="";
14 |
15 | int btAudio::_postprocess=0;
16 | filter btAudio::_filtLhp = filter(2,_sampleRate,3,highpass);
17 | filter btAudio::_filtRhp = filter(2,_sampleRate,3,highpass);
18 | filter btAudio::_filtLlp = filter(20000,_sampleRate,3,lowpass);
19 | filter btAudio::_filtRlp = filter(20000,_sampleRate,3,lowpass);
20 | DRC btAudio::_DRCL = DRC(_sampleRate,60.0,0.001,0.2,4.0,10.0,0.0);
21 | DRC btAudio::_DRCR = DRC(_sampleRate,60.0,0.001,0.2,4.0,10.0,0.0);
22 |
23 | Preferences preferences;
24 |
25 | ////////////////////////////////////////////////////////////////////
26 | ////////////////////////// Constructor /////////////////////////////
27 | ////////////////////////////////////////////////////////////////////
28 | btAudio::btAudio(const char *devName) {
29 | _devName=devName;
30 | }
31 |
32 | ////////////////////////////////////////////////////////////////////
33 | ////////////////// Bluetooth Functionality /////////////////////////
34 | ////////////////////////////////////////////////////////////////////
35 | void btAudio::begin() {
36 |
37 | //Arduino bluetooth initialisation
38 | btStart();
39 |
40 | // bluedroid allows for bluetooth classic
41 | esp_bluedroid_init();
42 | esp_bluedroid_enable();
43 |
44 | //set up device name
45 | esp_bt_dev_set_device_name(_devName);
46 |
47 | // initialize AVRCP controller
48 | esp_avrc_ct_init();
49 | esp_avrc_ct_register_callback(avrc_callback);
50 |
51 | // this sets up the audio receive
52 | esp_a2d_sink_init();
53 |
54 | esp_a2d_register_callback(a2d_cb);
55 |
56 | // set discoverable and connectable mode, wait to be connected
57 | #if ESP_IDF_VERSION_MAJOR > 3
58 | esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
59 | #else
60 | esp_bt_gap_set_scan_mode(ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE);
61 | #endif
62 | }
63 |
64 | void btAudio::end() {
65 | esp_a2d_sink_deinit();
66 | esp_bluedroid_disable();
67 | esp_bluedroid_deinit();
68 | btStop();
69 | }
70 |
71 | void btAudio::reconnect() {
72 | // Load rememebered device address from flash
73 | preferences.begin("btAudio", false);
74 | _address[0] = preferences.getUChar("btaddr0", 0);
75 | _address[1] = preferences.getUChar("btaddr1", 0);
76 | _address[2] = preferences.getUChar("btaddr2", 0);
77 | _address[3] = preferences.getUChar("btaddr3", 0);
78 | _address[4] = preferences.getUChar("btaddr4", 0);
79 | _address[5] = preferences.getUChar("btaddr5", 0);
80 | preferences.end();
81 |
82 | // Only attempt connection if address exists
83 | if (_address[0] + _address[1] +
84 | _address[2] + _address[3] +
85 | _address[4] + _address[5] != 0)
86 | {
87 | ESP_LOGI("btAudio", "Connecting to remembered BT device: %d %d %d %d %d %d",
88 | _address[0], _address[1],
89 | _address[2], _address[3],
90 | _address[4], _address[5]);
91 | // Connect to remembered device
92 | esp_a2d_sink_connect(_address);
93 | }
94 | }
95 |
96 | void btAudio::disconnect() {
97 | esp_a2d_sink_disconnect(_address);
98 | }
99 |
100 | void btAudio::a2d_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t*param){
101 | esp_a2d_cb_param_t *a2d = (esp_a2d_cb_param_t *)(param);
102 | switch (event) {
103 | case ESP_A2D_CONNECTION_STATE_EVT:
104 | {
105 | uint8_t* temp= a2d->conn_stat.remote_bda;
106 | if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED)
107 | {
108 | _address[0]= *temp; _address[1]= *(temp+1);
109 | _address[2]= *(temp+2); _address[3]= *(temp+3);
110 | _address[4]= *(temp+4); _address[5]= *(temp+5);
111 | ESP_LOGI("btAudio", "Connected to BT device: %d %d %d %d %d %d", _address[0], _address[1], _address[2], _address[3], _address[4], _address[5]);
112 |
113 | // Store connected BT address for use by reconnect()
114 | preferences.begin("btAudio", false);
115 | if (preferences.getUChar("btaddr0", 0) != _address[0]) { preferences.putUChar("btaddr0", _address[0]); ESP_LOGI("btAudio", "Writing BTaddr0"); }
116 | if (preferences.getUChar("btaddr1", 0) != _address[1]) { preferences.putUChar("btaddr1", _address[1]); ESP_LOGI("btAudio", "Writing BTaddr1"); }
117 | if (preferences.getUChar("btaddr2", 0) != _address[2]) { preferences.putUChar("btaddr2", _address[2]); ESP_LOGI("btAudio", "Writing BTaddr2"); }
118 | if (preferences.getUChar("btaddr3", 0) != _address[3]) { preferences.putUChar("btaddr3", _address[3]); ESP_LOGI("btAudio", "Writing BTaddr3"); }
119 | if (preferences.getUChar("btaddr4", 0) != _address[4]) { preferences.putUChar("btaddr4", _address[4]); ESP_LOGI("btAudio", "Writing BTaddr4"); }
120 | if (preferences.getUChar("btaddr5", 0) != _address[5]) { preferences.putUChar("btaddr5", _address[5]); ESP_LOGI("btAudio", "Writing BTaddr5"); }
121 | preferences.end();
122 | break;
123 | }
124 | }
125 | case ESP_A2D_AUDIO_CFG_EVT: {
126 | ESP_LOGI("BT_AV", "A2DP audio stream configuration, codec type %d", a2d->audio_cfg.mcc.type);
127 | // for now only SBC stream is supported
128 | if (a2d->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) {
129 | _sampleRate = 16000;
130 | char oct0 = a2d->audio_cfg.mcc.cie.sbc[0];
131 | if (oct0 & (0x01 << 6)) {
132 | _sampleRate = 32000;
133 | } else if (oct0 & (0x01 << 5)) {
134 | _sampleRate = 44100;
135 | } else if (oct0 & (0x01 << 4)) {
136 | _sampleRate = 48000;
137 | }
138 | ESP_LOGI("BT_AV", "Configure audio player %x-%x-%x-%x",
139 | a2d->audio_cfg.mcc.cie.sbc[0],
140 | a2d->audio_cfg.mcc.cie.sbc[1],
141 | a2d->audio_cfg.mcc.cie.sbc[2],
142 | a2d->audio_cfg.mcc.cie.sbc[3]);
143 | if(i2s_set_sample_rates(I2S_NUM_0, _sampleRate)==ESP_OK){
144 | ESP_LOGI("BT_AV", "Audio player configured, sample rate=%d", _sampleRate);
145 | }
146 | }
147 |
148 | break;
149 | }
150 | default:
151 | log_e("a2dp invalid cb event: %d", event);
152 | break;
153 | }
154 | }
155 | void btAudio::updateMeta() {
156 | uint8_t attr_mask = ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST | ESP_AVRC_MD_ATTR_ALBUM | ESP_AVRC_MD_ATTR_GENRE;
157 | esp_avrc_ct_send_metadata_cmd(1, attr_mask);
158 | }
159 | void btAudio::avrc_callback(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t *param) {
160 | esp_avrc_ct_cb_param_t *rc = (esp_avrc_ct_cb_param_t *)(param);
161 | char *attr_text;
162 | String mystr;
163 |
164 | switch (event) {
165 | case ESP_AVRC_CT_METADATA_RSP_EVT: {
166 | attr_text = (char *) malloc (rc->meta_rsp.attr_length + 1);
167 | memcpy(attr_text, rc->meta_rsp.attr_text, rc->meta_rsp.attr_length);
168 | attr_text[rc->meta_rsp.attr_length] = 0;
169 | mystr = String(attr_text);
170 |
171 | switch (rc->meta_rsp.attr_id) {
172 | case ESP_AVRC_MD_ATTR_TITLE:
173 | //Serial.print("Title: ");
174 | //Serial.println(mystr);
175 | title= mystr;
176 | break;
177 | case ESP_AVRC_MD_ATTR_ARTIST:
178 | //Serial.print("Artist: ");
179 | //Serial.println(mystr);
180 | artist= mystr;
181 | break;
182 | case ESP_AVRC_MD_ATTR_ALBUM:
183 | //Serial.print("Album: ");
184 | //Serial.println(mystr);
185 | album= mystr;
186 | break;
187 | case ESP_AVRC_MD_ATTR_GENRE:
188 | //Serial.print("Genre: ");
189 | //Serial.println(mystr);
190 | genre= mystr;
191 | break;
192 | }
193 | free(attr_text);
194 | }break;
195 | default:
196 | ESP_LOGE("RCCT", "unhandled AVRC event: %d", event);
197 | break;
198 | }
199 | }
200 | void btAudio::setSinkCallback(void (*sinkCallback)(const uint8_t *data, uint32_t len) ){
201 | esp_a2d_sink_register_data_callback(sinkCallback);
202 | }
203 | ////////////////////////////////////////////////////////////////////
204 | ////////////////// I2S Audio Functionality /////////////////////////
205 | ////////////////////////////////////////////////////////////////////
206 | void btAudio::I2S(int bck, int dout, int ws) {
207 | // i2s configuration
208 | uint32_t usamplerate = (uint32_t)_sampleRate;
209 | static const i2s_config_t i2s_config = {
210 | .mode = static_cast(I2S_MODE_MASTER | I2S_MODE_TX),
211 | .sample_rate = usamplerate,
212 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
213 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
214 | #if ESP_IDF_VERSION_MAJOR > 3
215 | .communication_format = static_cast(I2S_COMM_FORMAT_STAND_I2S|I2S_COMM_FORMAT_STAND_MSB),
216 | #else
217 | .communication_format = static_cast(I2S_COMM_FORMAT_I2S|I2S_COMM_FORMAT_I2S_MSB),
218 | #endif
219 | .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // default interrupt priority
220 | .dma_buf_count = 3,
221 | .dma_buf_len = 600,
222 | .use_apll = false,
223 | .tx_desc_auto_clear = true
224 | };
225 |
226 | // i2s pinout
227 | static const i2s_pin_config_t pin_config = {
228 | .bck_io_num =bck,//26
229 | .ws_io_num = ws, //27
230 | .data_out_num = dout, //25
231 | .data_in_num = I2S_PIN_NO_CHANGE
232 | };
233 |
234 | // now configure i2s with constructed pinout and config
235 | i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
236 | i2s_set_pin(I2S_NUM_0, &pin_config);
237 |
238 | // Sets the function that will handle data (i2sCallback)
239 | esp_a2d_sink_register_data_callback(i2sCallback);
240 | }
241 | void btAudio::i2sCallback(const uint8_t *data, uint32_t len){
242 | size_t i2s_bytes_write = 0;
243 | int16_t* data16=(int16_t*)data; //playData doesnt want const
244 | int16_t fy[2];
245 | float temp;
246 |
247 | int jump =4; //how many bytes at a time get sent to buffer
248 | int n = len/jump; // number of byte chunks
249 | switch (_postprocess) {
250 | case NOTHING:
251 | for(int i=0;i32767){
269 | temp=32767;
270 | }
271 | if(temp < -32767){
272 | temp= -32767;
273 | }
274 | fy[0] = (int16_t)(temp);
275 | data16++;
276 |
277 | // process right channel
278 | temp = _filtRlp.process(_filtRhp.process((*data16)*_vol));
279 | if(temp>32767){
280 | temp=32767;
281 | }
282 | if(temp < -32767){
283 | temp= -32767;
284 | }
285 | fy[1] =(int16_t) (temp);
286 | data16++;
287 | i2s_write(I2S_NUM_0, fy, jump, &i2s_bytes_write, 100 );
288 | }
289 | break;
290 | case COMPRESS:
291 | for(int i=0;i
5 | enum {
6 | lowpass = 0,
7 | highpass,
8 | bandpass,
9 | };
10 | class filter {
11 | public:
12 | filter(float Fc,float fs, int ncascs,int type);
13 |
14 | template
15 | T process(T in){
16 | float fout=0;
17 | float fprev= (float)in;
18 | for(int i=0; i<_ncascs;i++){
19 | fout= fprev * _b0[i] + _z1[i];
20 | _z1[i] = fprev * _b1[i] + _z2[i] - _a1[i] * fout;
21 | _z2[i] = fprev * _b2[i] - _a2[i] * fout;
22 | fprev=fout;
23 | }
24 | return (T)fout;
25 | }
26 |
27 |
28 | private:
29 | float _b0[5], _b1[5], _b2[5], _a1[5], _a2[5], _z1[5], _z2[5];
30 | int _ncascs;
31 | };
32 | #endif
--------------------------------------------------------------------------------
/src/webDSP.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | webDSP::webDSP() {
3 |
4 | }
5 | void webDSP::begin(const char* ssid, const char* password, btAudio* audio){
6 | WiFi.mode(WIFI_STA);
7 | while(WiFi.status() != WL_CONNECTED) {
8 | WiFi.begin(ssid, password);
9 | delay(1000);
10 | Serial.print(".");
11 | }
12 | _audio=audio;
13 | Serial.print("IP Address: ");
14 | Serial.println(WiFi.localIP());
15 |
16 | _server.on("/", [this]() { this->handleRoot(); });
17 | _server.on("/workStyle.css", [this]() { this->handleCss(); });
18 | _server.on("/get", [this]() { this->handleGet(); });
19 | _server.begin();
20 | }
21 |
22 | void webDSP::handleRoot() {
23 | _server.send(200, "text/html", html2);
24 | }
25 | void webDSP::handleCss() {
26 | _server.send_P(200, "text/css", css2);
27 | }
28 | void webDSP::handleGet(){
29 | String inputName = _server.argName(0);
30 | String inputVal = _server.arg(0);
31 |
32 | char sw = inputName.charAt(0);
33 | float cT= _audio->_T;
34 | float caa= _audio->_alphAtt;
35 | float car= _audio->_alphRel;
36 | float cR= _audio->_R;
37 | float cw= _audio->_w;
38 | float cm= _audio->_mu;
39 |
40 | _server.send_P(200, "text/html", html2);
41 | Serial.println(inputName);
42 | switch(sw) {
43 | case 'h':
44 | _audio->createFilter(3, inputVal.toFloat(), highpass);
45 | break;
46 | case 'l':
47 | _audio->createFilter(3, inputVal.toFloat(), lowpass);
48 | break;
49 | case 'c':
50 | _audio->decompress();
51 | break;
52 | case 'g':
53 | _audio->compress(cT,caa,car,cR,cw,inputVal.toFloat());
54 | break;
55 | case 't':
56 | _audio->compress(inputVal.toFloat(),caa,car,cR,cw,cm);
57 | break;
58 | case 'a':
59 | _audio->compress(cT,inputVal.toFloat(),car,cR,cw,cm);
60 | break;
61 | case 'r':
62 | _audio->compress(cT,caa,inputVal.toFloat(),cR,cw,cm);
63 | break;
64 | case 'w':
65 | _audio->compress(cT,caa,car,cR,inputVal.toFloat(),cm);
66 | break;
67 | case 'f':
68 | _audio->stopFilter();
69 | break;
70 | case 'R':
71 | _audio->compress(cT,caa,car,inputVal.toFloat(),cw,cm);
72 | break;
73 | case 'v':
74 | _audio->volume(inputVal.toFloat());
75 | break;
76 | default:
77 | break;
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/src/webDSP.h:
--------------------------------------------------------------------------------
1 | #ifndef WEBDSP_H
2 | #define WEBDSP_H
3 |
4 | #include "Arduino.h"
5 | #include
6 | #include
7 | #include
8 |
9 |
10 | class webDSP {
11 | public:
12 | webDSP();
13 | void begin(const char* ssid, const char* password, btAudio* audio);
14 | WebServer _server;
15 |
16 | private:
17 | btAudio* _audio;
18 | void handleRoot();
19 | void handleCss();
20 | void handleGet();
21 |
22 |
23 | };
24 |
25 | const char PROGMEM html2[2866]= R"rawliteral(
26 | DSP