├── .editorconfig ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── aggregate_bin.sh ├── include └── README ├── lib └── README ├── platformio.ini ├── post_extra_script.py ├── src ├── icons.h ├── main.cpp └── sounds.h └── test └── README /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | max_line_length = 100 14 | curly_bracket_next_line = false 15 | 16 | [*.{html,php}] 17 | max_line_length = 160 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThingPulse Icon64 Bluetooth Speaker 2 | 3 | ESP32 based Bluetooth loudspeaker with 8 band spectrum analyzer. This is the stock firmware for the [ThingPulse Icon64](https://thingpulse.com/product/icon64/) devices. 4 | 5 | [![ThingPulse Icon64](https://thingpulse.com/wp-content/uploads/2020/11/Whitebox_Heart.jpg)](https://thingpulse.com/product/icon64/) 6 | 7 | ## Demo video 8 | 9 | [![YouTube demo](http://img.youtube.com/vi/1UpbtE98OBA/0.jpg)](http://www.youtube.com/watch?v=1UpbtE98OBA "Icon64 Bluetooth Speaker") 10 | 11 | ## LED matrix state machine 12 | 13 | Below list briefly explains what is displayed on the "GUI" (i.e. the LED matrix) in which state. Note 14 | that all icons are rendered in a pulsing manner. 15 | 16 | - **no** BLE audio device connected to Icon64: heart icon (see image above) 17 | - BLE audio device connected: Bluetooth icon 18 | - BLE audio device connected and audio playback suspended (when once started): pause icon 19 | - audio playing: spectrum analyzer 20 | 21 | -------------------------------------------------------------------------------- /aggregate_bin.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Packages the 4 binaries into a single .bin for address 0x0000 as per the list below. Inspired by 6 | # https://github.com/marcelstoer/docker-nodemcu-build/blob/master/build-esp32#L85 7 | # 8 | # 0x1000 bootloader 9 | # 0x8000 partitions 10 | # 0xe000 boot_app0 11 | # 0x10000 firmware 12 | # -> https://gitter.im/espressif/arduino-esp32?at=5c9395c0dfc69a1454cf3323 -> "probably boot_app0.bin is a stub for OTA that tells it that app0 is the active partition." 13 | # 14 | # srec_cat doesn't like relative file paths (current version on macOS) -> build absolute ones 15 | 16 | home_dir=$(echo ~) 17 | current_dir=$(pwd) 18 | build_dir="$current_dir/.pio/build/esp-wrover-kit" 19 | 20 | bootloader="$home_dir/.platformio/packages/framework-arduinoespressif32/tools/sdk/bin/bootloader_dio_40m.bin" 21 | partitions="$build_dir/partitions.bin" 22 | boot_app="$home_dir/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin" 23 | firmware="$build_dir/firmware.bin" 24 | app="$build_dir/app.bin" 25 | 26 | echo "Aggregating binaries." 27 | srec_cat -output "$app" -binary "$bootloader" -binary -offset 0x1000 -fill 0xff 0x0000 0x8000 "$partitions" -binary -offset 0x8000 -fill 0xff 0x8000 0xe000 "$boot_app" -binary -offset 0xe000 -fill 0xff 0xe000 0x10000 "$firmware" -binary -offset 0x10000 28 | echo "Done, created '$app'." 29 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp-wrover-kit] 12 | extra_scripts = post:post_extra_script.py 13 | platform = espressif32@3.2.0 14 | board = esp-wrover-kit 15 | framework = arduino 16 | upload_port = /dev/cu.SLAB_USBtoUART 17 | monitor_port = /dev/cu.SLAB_USBtoUART 18 | monitor_speed = 115200 19 | upload_speed = 921600 20 | monitor_filters = esp32_exception_decoder 21 | board_build.partitions = no_ota.csv 22 | ; Don't activate PSRAM, consumes +1500ms on boot up 23 | ;build_flags = -DCORE_DEBUG_LEVEL=5 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue 24 | build_flags =-DASYNCWEBSERVER_REGEX -DCORE_DEBUG_LEVEL=3 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue 25 | lib_ldf_mode=deep 26 | lib_deps = 27 | ; not available in PIO library repo :( -> pin to Git tag 28 | https://github.com/pschatzmann/ESP32-A2DP#v1.6.0 29 | https://github.com/kosme/arduinoFFT @ ^1.5.6 30 | fastled/FastLED @ ^3.4.0 31 | earlephilhower/ESP8266Audio @ 1.9.5 32 | ;roboticsbrno/SmartLeds @ 1.2.1 33 | -------------------------------------------------------------------------------- /post_extra_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | Import("env", "projenv") 3 | 4 | def after_build(source, target, env): 5 | print("Calling aggregate shell script") 6 | os.system("./aggregate_bin.sh") 7 | 8 | env.AddPostAction("buildprog", after_build) 9 | -------------------------------------------------------------------------------- /src/icons.h: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 ThingPulse GmbH 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include 26 | 27 | // https://javl.github.io/image2cpp/ 28 | // 'Heart, 8x8px 29 | const uint32_t HEART [] PROGMEM = { 30 | 0x00222222, 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00222222, 0x00222222, 0x00222222, 31 | 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00222222, 0x00222222, 32 | 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00222222, 33 | 0x00222222, 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 34 | 0x00222222, 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 35 | 0x00222222, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff0000, 0x00ff9c85, 0x00222222, 36 | 0x00222222, 0x00ff0000, 0x00fff1df, 0x00ff0000, 0x00ff0000, 0x00ff9c85, 0x00222222, 0x00222222, 37 | 0x00222222, 0x00222222, 0x00fff1df, 0x00fff1df, 0x00ff9c85, 0x00222222, 0x00222222, 0x00222222 38 | }; 39 | 40 | const uint32_t BLE [] PROGMEM = { 41 | 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 42 | 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 43 | 0x000000ff, 0x000000ff, 0x00ffffff, 0x00ffffff, 0x000000ff, 0x00ffffff, 0x00ffffff, 0x000000ff, 44 | 0x000000ff, 0x00ffffff, 0x000000ff, 0x000000ff, 0x00ffffff, 0x000000ff, 0x000000ff, 0x00ffffff, 45 | 0x000000ff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 46 | 0x000000ff, 0x000000ff, 0x000000ff, 0x00ffffff, 0x000000ff, 0x00ffffff, 0x000000ff, 0x000000ff, 47 | 0x000000ff, 0x000000ff, 0x00ffffff, 0x000000ff, 0x000000ff, 0x000000ff, 0x00ffffff, 0x000000ff, 48 | 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff 49 | }; 50 | 51 | const uint32_t PAUSE [] PROGMEM = { 52 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 53 | 0x00000000, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00000000, 54 | 0x00000000, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00000000, 55 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 56 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 57 | 0x00000000, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00000000, 58 | 0x00000000, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00ffffff, 0x00000000, 59 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000 60 | }; 61 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 ThingPulse GmbH 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #include "AudioFileSourcePROGMEM.h" 26 | #include "AudioGeneratorAAC.h" 27 | #include "AudioOutputI2S.h" 28 | #include "BluetoothA2DPSink.h" 29 | #include "icons.h" 30 | #include "sounds.h" 31 | #include 32 | #include 33 | 34 | // Audio Settings 35 | #define I2S_DOUT 25 36 | #define I2S_BCLK 26 37 | #define I2S_LRC 22 38 | #define MODE_PIN 33 39 | 40 | // LED Settings 41 | #define LED_COUNT 64 42 | #define LED_PIN 32 43 | #define CHANNEL 0 44 | 45 | #define DATA_PIN 32 46 | 47 | // FFT Settings 48 | #define NUM_BANDS 8 49 | #define SAMPLES 512 50 | #define SAMPLING_FREQUENCY 44100 51 | 52 | #define BRIGHTNESS 100 53 | 54 | #define DEVICE_NAME "ThingPulse-Icon64" 55 | 56 | arduinoFFT FFT = arduinoFFT(); 57 | BluetoothA2DPSink a2dp_sink; 58 | CRGB leds[LED_COUNT]; 59 | 60 | int pushButton = 39; 61 | 62 | float amplitude = 200.0; 63 | 64 | int32_t peak[] = {0, 0, 0, 0, 0, 0, 0, 0}; 65 | double vReal[SAMPLES]; 66 | double vImag[SAMPLES]; 67 | 68 | double brightness = 0.25; 69 | 70 | QueueHandle_t queue; 71 | 72 | int16_t sample_l_int; 73 | int16_t sample_r_int; 74 | 75 | int visualizationCounter = 0; 76 | int32_t lastVisualizationUpdate = 0; 77 | 78 | static const i2s_pin_config_t pin_config = {.bck_io_num = I2S_BCLK, 79 | .ws_io_num = I2S_LRC, 80 | .data_out_num = I2S_DOUT, 81 | .data_in_num = I2S_PIN_NO_CHANGE}; 82 | 83 | uint8_t hueOffset = 0; 84 | 85 | // audio state management 86 | bool devicePlayedAudio = false; 87 | esp_a2d_audio_state_t currentAudioState = ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND; 88 | 89 | // device connection management 90 | bool bleDeviceConnected = false; 91 | 92 | uint8_t getLedIndex(uint8_t x, uint8_t y) { 93 | // x = 7 - x; 94 | if (y % 2 == 0) { 95 | return y * 8 + x; 96 | } else { 97 | return y * 8 + (7 - x); 98 | } 99 | } 100 | 101 | void createBands(int i, int dsize) { 102 | uint8_t band = 0; 103 | if (i <= 2) { 104 | band = 0; // 125Hz 105 | } else if (i <= 5) { 106 | band = 1; // 250Hz 107 | } else if (i <= 7) { 108 | band = 2; // 500Hz 109 | } else if (i <= 15) { 110 | band = 3; // 1000Hz 111 | } else if (i <= 30) { 112 | band = 4; // 2000Hz 113 | } else if (i <= 53) { 114 | band = 5; // 4000Hz 115 | } else if (i <= 106) { 116 | band = 6; // 8000Hz 117 | } else { 118 | band = 7; 119 | } 120 | int dmax = amplitude; 121 | if (dsize > dmax) 122 | dsize = dmax; 123 | if (dsize > peak[band]) { 124 | peak[band] = dsize; 125 | } 126 | } 127 | 128 | void renderFFT(void *parameter) { 129 | int item = 0; 130 | for (;;) { 131 | if (uxQueueMessagesWaiting(queue) > 0) { 132 | 133 | FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD); 134 | FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD); 135 | FFT.ComplexToMagnitude(vReal, vImag, SAMPLES); 136 | 137 | for (uint8_t band = 0; band < NUM_BANDS; band++) { 138 | peak[band] = 0; 139 | } 140 | 141 | // Don't use sample 0 and only first SAMPLES/2 are usable. Each array eleement represents a frequency and its value the amplitude. 142 | for (int i = 2; i < (SAMPLES / 2); i++) { 143 | if (vReal[i] > 2000) { // Add a crude noise filter, 10 x amplitude or more 144 | createBands(i, (int)vReal[i] / amplitude); 145 | } 146 | } 147 | 148 | // Release handle 149 | xQueueReceive(queue, &item, 0); 150 | 151 | uint8_t intensity; 152 | 153 | FastLED.clear(); 154 | FastLED.setBrightness(BRIGHTNESS); 155 | for (byte band = 0; band < NUM_BANDS; band++) { 156 | intensity = map(peak[band], 1, amplitude, 0, 8); 157 | 158 | for (int i = 0; i < 8; i++) { 159 | leds[getLedIndex(7 - i, 7 - band)] = (i >= intensity) ? CHSV(0, 0, 0) : CHSV(i * 16, 255, 255); 160 | } 161 | } 162 | 163 | FastLED.show(); 164 | 165 | if ((millis() - lastVisualizationUpdate) > 1000) { 166 | log_e("Fps: %f", visualizationCounter / ((millis() - lastVisualizationUpdate) / 1000.0)); 167 | visualizationCounter = 0; 168 | lastVisualizationUpdate = millis(); 169 | hueOffset += 5; 170 | } 171 | visualizationCounter++; 172 | } 173 | } 174 | } 175 | 176 | void drawIcon(const uint32_t *icon) { 177 | // As this is invoked from loop() we can achieve a pulsing effect by 178 | // changing the brightness with every invocation. Goal: make it look 179 | // like a human breathing pattern (or like a contracting heart). 180 | // 181 | // The human breathing algorithm appears to be e^sin(x). 182 | // Plotted: https://www.wolframalpha.com/input/?i=e%5Esin%28x%29 183 | // Minimum & maximum amplitude are as follows: 184 | // min(e^sin(x)) = e^-1 = 1/e = 0.36787944 185 | // max(e^sin(x)) = e^1 = e = 2.71828182 186 | // 187 | // Hence, in order for the minimum to be 0 you need to offset the 188 | // function by 1/e: e^sin(x) - 1/e. The maximum of this function thus 189 | // becomes e - 1/e = 2.35040238 190 | // 191 | // To map this to a (brightness) range of [0, 100]: 192 | // -> multiply each value with 100/(e - 1/e): 42.54590641 193 | // Plotted: https://www.wolframalpha.com/input/?i=%28e%5Esin%28x%29+-+1%2Fe%29+*+100%2F%28e+-+1%2Fe%29 194 | // 195 | // Multiply x (i.e. the current time) by any value to adjust the frequency. 196 | // 197 | // Inspiration: https://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ 198 | uint8_t brightness = (exp(sin(millis() / 2000.0 * PI)) - 0.36787944) * 42.54590641; 199 | FastLED.setBrightness(brightness); 200 | for (int i = 0; i < LED_COUNT; i++) { 201 | uint32_t pixel = pgm_read_dword(icon + i); 202 | uint8_t red = (pixel >> 16) & 0xFF; 203 | uint8_t green = (pixel >> 8) & 0xFF; 204 | uint8_t blue = pixel & 0xFF; 205 | log_v("%d. %08X, %02X %02X %02X", i, pixel, red, green, blue); 206 | leds[getLedIndex(i % 8, i / 8)] = CRGB(green, red, blue); 207 | } 208 | delay(1); 209 | FastLED.show(); 210 | } 211 | 212 | void audio_data_callback(const uint8_t *data, uint32_t len) { 213 | int item = 0; 214 | // Only prepare new samples if the queue is empty 215 | if (uxQueueMessagesWaiting(queue) == 0) { 216 | // log_e("Queue is empty, adding new item"); 217 | int byteOffset = 0; 218 | for (int i = 0; i < SAMPLES; i++) { 219 | sample_l_int = (int16_t)(((*(data + byteOffset + 1) << 8) | *(data + byteOffset))); 220 | sample_r_int = (int16_t)(((*(data + byteOffset + 3) << 8) | *(data + byteOffset + 2))); 221 | vReal[i] = (sample_l_int + sample_r_int) / 2.0f; 222 | vImag[i] = 0; 223 | byteOffset = byteOffset + 4; 224 | } 225 | 226 | // Tell the task in core 1 that the processing can start 227 | xQueueSend(queue, &item, portMAX_DELAY); 228 | } 229 | } 230 | 231 | void connection_state_changed(esp_a2d_connection_state_t state, void *) { 232 | log_i("Connection state changed, new state: %d", state); 233 | if (ESP_A2D_CONNECTION_STATE_CONNECTED == state) { 234 | bleDeviceConnected = true; 235 | } else { 236 | bleDeviceConnected = false; 237 | } 238 | } 239 | 240 | void playBootupSound() { 241 | AudioFileSourcePROGMEM *in = new AudioFileSourcePROGMEM(sound, sizeof(sound)); 242 | AudioGeneratorAAC *aac = new AudioGeneratorAAC(); 243 | AudioOutputI2S *out = new AudioOutputI2S(); 244 | out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); 245 | 246 | aac->begin(in, out); 247 | 248 | log_i("Playing bootup sound..."); 249 | while (aac->isRunning()) { 250 | drawIcon(HEART); 251 | aac->loop(); 252 | } 253 | aac->stop(); 254 | log_i("...done"); 255 | } 256 | 257 | void setup() { 258 | pinMode(MODE_PIN, OUTPUT); 259 | pinMode(pushButton, INPUT); 260 | digitalWrite(MODE_PIN, HIGH); 261 | 262 | FastLED.addLeds(leds, LED_COUNT); 263 | playBootupSound(); 264 | 265 | // The queue is used for communication between A2DP callback and the FFT 266 | // processor 267 | queue = xQueueCreate(1, sizeof(int)); 268 | if (queue == NULL) { 269 | log_i("Error creating the A2DP->FFT queue"); 270 | } 271 | 272 | // This task will process the data acquired by the Bluetooth audio stream 273 | xTaskCreatePinnedToCore(renderFFT, // Function that should be called 274 | "FFT Renderer", // Name of the task (for debugging) 275 | 10000, // Stack size (bytes) 276 | NULL, // Parameter to pass 277 | 1, // Task priority 278 | NULL, // Task handle 279 | 1 // Core you want to run the task on (0 or 1) 280 | ); 281 | 282 | a2dp_sink.set_pin_config(pin_config); 283 | a2dp_sink.start((char *)DEVICE_NAME); 284 | // redirecting audio data to do FFT 285 | a2dp_sink.set_stream_reader(audio_data_callback); 286 | a2dp_sink.set_on_connection_state_changed(connection_state_changed); 287 | } 288 | 289 | void loop() { 290 | // For some reason the audio state changed callback doesn't work properly -> need to fetch the state here. 291 | // 292 | // Otherwise you could hook this up in setup() as sketched below. 293 | // 294 | // void audio_state_changed(esp_a2d_audio_state_t state, void *){ 295 | // log_i("audio state: %d", state); 296 | // } 297 | // a2dp_sink.set_on_audio_state_changed(audio_state_changed); 298 | 299 | esp_a2d_audio_state_t state = a2dp_sink.get_audio_state(); 300 | if (currentAudioState != state) { 301 | log_i("Audio state changed; new state: %d", state); 302 | currentAudioState = state; 303 | } 304 | switch (state) { 305 | // Unclear how stopped and remote suspend really differ from one another. In 306 | // ESP32-A2DP >= v1.6 we seem to be getting the later when the client stops 307 | // audio playback. 308 | case ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND: 309 | case ESP_A2D_AUDIO_STATE_STOPPED: 310 | if (bleDeviceConnected) { 311 | if (devicePlayedAudio) { 312 | drawIcon(PAUSE); 313 | } else { 314 | drawIcon(BLE); 315 | } 316 | } else { 317 | drawIcon(HEART); 318 | } 319 | break; 320 | case ESP_A2D_AUDIO_STATE_STARTED: 321 | devicePlayedAudio = true; 322 | break; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/sounds.h: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 ThingPulse GmbH 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | 26 | #include 27 | 28 | const unsigned char sound[] PROGMEM = { 29 | 0xff, 0xf1, 0x50, 0x40, 0x08, 0xdf, 0xfc, 0xde, 0x04, 0x00, 0x4c, 0x61, 30 | 0x76, 0x63, 0x35, 0x38, 0x2e, 0x31, 0x30, 0x38, 0x2e, 0x31, 0x30, 0x30, 31 | 0x00, 0x02, 0x38, 0x96, 0x50, 0x99, 0xe2, 0x10, 0x23, 0xaf, 0xe9, 0xe8, 32 | 0x00, 0x01, 0x60, 0x8c, 0x5b, 0xc5, 0xdd, 0x94, 0x46, 0x59, 0x65, 0x96, 33 | 0x59, 0x65, 0x96, 0x59, 0x65, 0x11, 0x11, 0x11, 0x11, 0x11, 0x17, 0x65, 34 | 0x94, 0xf1, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1a, 0x0e, 0xff, 0xf1, 35 | 0x50, 0x40, 0x08, 0xbf, 0xfc, 0x01, 0x54, 0x8a, 0xd9, 0x68, 0x9a, 0x2a, 36 | 0xd6, 0xbb, 0x64, 0x09, 0xf1, 0x01, 0xf5, 0xf1, 0xfd, 0x8f, 0xf7, 0x3d, 37 | 0xf7, 0x6f, 0x80, 0x00, 0x0b, 0xf9, 0x26, 0x82, 0x47, 0x28, 0x95, 0xc5, 38 | 0xca, 0xba, 0x3f, 0xcf, 0xe9, 0xd1, 0xd2, 0x40, 0x50, 0x20, 0x57, 0xca, 39 | 0x3e, 0xc8, 0xfb, 0xa5, 0x8a, 0x83, 0x77, 0x1b, 0xa4, 0xf6, 0x30, 0xf2, 40 | 0xab, 0xee, 0xed, 0x3e, 0x18, 0x1e, 0x1c, 0xff, 0xf1, 0x50, 0x40, 0x06, 41 | 0xff, 0xfc, 0x01, 0x1a, 0xeb, 0x19, 0x36, 0x34, 0x38, 0x0d, 0xfa, 0x7c, 42 | 0xeb, 0x90, 0x00, 0x05, 0x9f, 0x92, 0xa1, 0x8c, 0x51, 0x21, 0x0d, 0x44, 43 | 0x5f, 0x78, 0x09, 0x1c, 0xe7, 0x40, 0xc0, 0x91, 0x36, 0x94, 0x9a, 0x18, 44 | 0xd6, 0xd2, 0x18, 0xbf, 0x9a, 0xd5, 0xb8, 0xaa, 0x7b, 0x0c, 0x07, 0xc0, 45 | 0xc3, 0xbf, 0xff, 0xf1, 0x50, 0x40, 0x07, 0x1f, 0xfc, 0x01, 0x22, 0x2b, 46 | 0x19, 0x56, 0x25, 0x38, 0x0d, 0xf9, 0xfb, 0x60, 0x00, 0x01, 0x4f, 0x27, 47 | 0x13, 0x40, 0x70, 0x03, 0x66, 0x02, 0xae, 0x46, 0x9b, 0xaf, 0xa4, 0xeb, 48 | 0xf5, 0x2f, 0x4d, 0xb1, 0xd7, 0x4e, 0xac, 0xa9, 0x7d, 0xd1, 0x78, 0xc0, 49 | 0x15, 0x91, 0x86, 0x62, 0x3b, 0xf9, 0x67, 0x31, 0x38, 0x0e, 0xff, 0xf1, 50 | 0x50, 0x40, 0x07, 0x7f, 0xfc, 0x01, 0x12, 0x2b, 0x20, 0x28, 0x8b, 0x1a, 51 | 0x9c, 0x06, 0xfe, 0xfd, 0x80, 0x00, 0x07, 0xb5, 0x3e, 0x64, 0xa9, 0xb0, 52 | 0xa2, 0x8b, 0xa1, 0x7f, 0xa8, 0x6e, 0x3a, 0x0c, 0x85, 0x14, 0xfd, 0x83, 53 | 0x5b, 0xc0, 0x25, 0x3e, 0x25, 0x34, 0xa4, 0x7b, 0xba, 0xe0, 0x90, 0x5d, 54 | 0xee, 0xe9, 0x75, 0x8f, 0xf0, 0xad, 0x22, 0x01, 0xc0, 0xff, 0xf1, 0x50, 55 | 0x40, 0x06, 0x9f, 0xfc, 0x01, 0x10, 0x2b, 0x0d, 0x36, 0x35, 0x30, 0x11, 56 | 0xff, 0x60, 0x00, 0x01, 0xfe, 0xa6, 0xd9, 0x56, 0xe1, 0x39, 0x42, 0xe7, 57 | 0xc2, 0x31, 0x60, 0xeb, 0xdc, 0xcb, 0x9d, 0x5e, 0x42, 0xa2, 0x2a, 0x97, 58 | 0xca, 0x4b, 0x99, 0x95, 0x80, 0xa5, 0x2b, 0x39, 0xfb, 0xef, 0x13, 0x8a, 59 | 0xe0, 0xff, 0xf1, 0x50, 0x40, 0x06, 0xff, 0xfc, 0x01, 0x0a, 0x2b, 0x20, 60 | 0x24, 0x8b, 0x1a, 0x9c, 0x06, 0xfe, 0xbe, 0x80, 0x00, 0x05, 0xaa, 0x5d, 61 | 0x1a, 0x3b, 0x8d, 0xca, 0xb6, 0xb5, 0x22, 0xd1, 0xc4, 0xa9, 0x05, 0x89, 62 | 0x62, 0x4c, 0x49, 0x1c, 0x33, 0xbc, 0x34, 0xd3, 0xd9, 0xa8, 0x10, 0x32, 63 | 0xcb, 0x08, 0xa5, 0xff, 0x3c, 0xd8, 0x80, 0x70, 0xff, 0xf1, 0x50, 0x40, 64 | 0x06, 0xff, 0xfc, 0x01, 0x06, 0x2b, 0x20, 0x84, 0x5b, 0x1b, 0x98, 0x08, 65 | 0xf9, 0xf7, 0xf6, 0xfd, 0x80, 0x00, 0x35, 0xba, 0xb4, 0x6a, 0x40, 0x81, 66 | 0x53, 0x61, 0x25, 0x17, 0xa3, 0x3e, 0x91, 0xa9, 0x21, 0x05, 0xc5, 0x6f, 67 | 0x46, 0x26, 0x87, 0x48, 0x67, 0x8d, 0xb0, 0xa0, 0x37, 0xc4, 0x14, 0x8f, 68 | 0x15, 0x63, 0x80, 0xff, 0xf1, 0x50, 0x40, 0x07, 0x3f, 0xfc, 0x01, 0x06, 69 | 0x2b, 0x28, 0x82, 0x5b, 0x1a, 0x9c, 0x06, 0xaf, 0x7a, 0xfd, 0x80, 0x00, 70 | 0x1a, 0xc4, 0x27, 0x34, 0x41, 0x2c, 0x63, 0x26, 0x64, 0x7a, 0x83, 0x2d, 71 | 0x7f, 0x4e, 0x0a, 0x8b, 0x5b, 0x63, 0xe9, 0x5a, 0x88, 0xf0, 0xf1, 0x61, 72 | 0x00, 0xc2, 0xd4, 0x75, 0xff, 0xcf, 0xdb, 0xd3, 0x8d, 0x40, 0x27, 0x80, 73 | 0xff, 0xf1, 0x50, 0x40, 0x07, 0x3f, 0xfc, 0x01, 0x06, 0x2b, 0x28, 0x82, 74 | 0x5b, 0x1b, 0x98, 0x48, 0xf1, 0xe2, 0x7e, 0x80, 0x00, 0x01, 0xa8, 0x5b, 75 | 0xad, 0xcb, 0xd3, 0xc3, 0xc9, 0x9b, 0xe5, 0x2e, 0xd4, 0x1a, 0xf7, 0x91, 76 | 0x15, 0xdb, 0xf3, 0xfa, 0xc8, 0x29, 0xb9, 0x70, 0x41, 0xa8, 0x00, 0x17, 77 | 0x0a, 0xba, 0xf5, 0xfb, 0x79, 0xa0, 0xb8, 0x17, 0x1c, 0xff, 0xf1, 0x50, 78 | 0x40, 0x08, 0x5f, 0xfc, 0x01, 0x04, 0x2b, 0x28, 0x82, 0x6b, 0x14, 0xa0, 79 | 0x04, 0xfd, 0xbe, 0xdf, 0xcf, 0xf0, 0x00, 0x01, 0xa1, 0x19, 0xfc, 0x61, 80 | 0xc2, 0x36, 0x81, 0xe0, 0xb9, 0x6d, 0x46, 0x0a, 0x58, 0x43, 0xbb, 0xa7, 81 | 0x43, 0x8f, 0xa5, 0x75, 0x35, 0x83, 0x9b, 0x16, 0xb1, 0xc2, 0xf4, 0x6a, 82 | 0x41, 0xb2, 0xa2, 0xe2, 0xe3, 0x2a, 0x28, 0x91, 0x8f, 0xb7, 0x77, 0xf2, 83 | 0x80, 0x0a, 0x0e, 0xff, 0xf1, 0x50, 0x40, 0x06, 0xff, 0xfc, 0x01, 0x02, 84 | 0x2b, 0x28, 0x86, 0x6b, 0x12, 0x9c, 0x06, 0xfb, 0x7e, 0xde, 0x7c, 0x00, 85 | 0x00, 0x3b, 0x2c, 0x25, 0x0c, 0x11, 0x6e, 0xd6, 0xa4, 0xdc, 0x35, 0x33, 86 | 0x3b, 0xe9, 0x14, 0x19, 0x56, 0xca, 0x27, 0x0a, 0x45, 0x2a, 0x31, 0xb8, 87 | 0x40, 0x24, 0x19, 0x0d, 0xfe, 0xae, 0x10, 0x24, 0x27, 0x80, 0xff, 0xf1, 88 | 0x50, 0x40, 0x07, 0xdf, 0xfc, 0x01, 0x02, 0x2b, 0x28, 0x88, 0x6b, 0x13, 89 | 0x98, 0x48, 0xfc, 0xf1, 0xfd, 0x40, 0x00, 0x06, 0x84, 0x64, 0x8f, 0xf8, 90 | 0x08, 0xd9, 0xf8, 0x00, 0xe4, 0x2a, 0x5e, 0xd8, 0xdc, 0x08, 0xba, 0x2b, 91 | 0x78, 0x3f, 0x14, 0xb4, 0x14, 0x75, 0x10, 0xfc, 0x8e, 0x48, 0xd1, 0xf4, 92 | 0x70, 0xc5, 0x91, 0x61, 0x20, 0x03, 0xfc, 0xd8, 0x10, 0x17, 0x2a, 0x07, 93 | 0xff, 0xf1, 0x50, 0x40, 0x06, 0x9f, 0xfc, 0x01, 0x02, 0x2b, 0x20, 0x84, 94 | 0x6b, 0x12, 0xa0, 0x04, 0xf9, 0xfd, 0xb5, 0xb0, 0x00, 0x03, 0xee, 0xdb, 95 | 0x58, 0x4d, 0x02, 0x4f, 0x2a, 0x9f, 0x21, 0xa8, 0xcb, 0x4c, 0x30, 0xef, 96 | 0x82, 0x27, 0x34, 0x44, 0x42, 0xd9, 0x4a, 0x02, 0xca, 0x14, 0x9f, 0xed, 97 | 0xc9, 0x60, 0x00, 0xbe, 0xff, 0xf1, 0x50, 0x40, 0x06, 0x9f, 0xfc, 0x00, 98 | 0xfe, 0x2b, 0x20, 0x44, 0x8a, 0x10, 0xa9, 0xfa, 0x7e, 0x40, 0x00, 0x00, 99 | 0xba, 0xa6, 0x89, 0xb2, 0x7c, 0x7c, 0x3b, 0x0a, 0x61, 0xcc, 0x29, 0x29, 100 | 0xb9, 0x57, 0xdf, 0x74, 0x29, 0x62, 0xff, 0x3f, 0x86, 0x0c, 0x3e, 0x2c, 101 | 0x7e, 0x62, 0x92, 0xc1, 0x8d, 0x88, 0x88, 0x07 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------