From 6d55c2315a40372ae4a878dc3f427f29cca49105 Mon Sep 17 00:00:00 2001 From: Jose Date: Sat, 12 Apr 2025 21:23:24 +0200 Subject: [PATCH] Changes committed: new file: .gitignore modified: README.md new file: data/www/dashboard.html new file: data/www/index.html new file: data/www/wifi.html new file: include/README new file: lib/README new file: notes.ods new file: platformio.ini new file: src/NoteMappings copy.h new file: src/NoteMappings.h new file: src/audio_input.cpp new file: src/audio_input.h new file: src/ble.cpp new file: src/ble.h new file: src/config.h new file: src/esp_info.cpp new file: src/esp_info.h new file: src/fft_processing.cpp new file: src/fft_processing.h new file: src/led_control.cpp new file: src/led_control.h new file: src/main.cpp new file: src/midi.cpp new file: src/midi.h new file: src/web_server.cpp new file: src/web_server.h new file: test/README --- .gitignore | 34 +++++++++ README.md | 102 +++++++++++++++++++++++++- data/www/dashboard.html | 41 +++++++++++ data/www/index.html | 122 +++++++++++++++++++++++++++++++ data/www/wifi.html | 61 ++++++++++++++++ include/README | 37 ++++++++++ lib/README | 46 ++++++++++++ notes.ods | Bin 0 -> 26522 bytes platformio.ini | 40 +++++++++++ src/NoteMappings copy.h | 43 +++++++++++ src/NoteMappings.h | 43 +++++++++++ src/audio_input.cpp | 75 +++++++++++++++++++ src/audio_input.h | 14 ++++ src/ble.cpp | 70 ++++++++++++++++++ src/ble.h | 16 +++++ src/config.h | 50 +++++++++++++ src/esp_info.cpp | 154 ++++++++++++++++++++++++++++++++++++++++ src/esp_info.h | 18 +++++ src/fft_processing.cpp | 48 +++++++++++++ src/fft_processing.h | 13 ++++ src/led_control.cpp | 23 ++++++ src/led_control.h | 7 ++ src/main.cpp | 79 +++++++++++++++++++++ src/midi.cpp | 43 +++++++++++ src/midi.h | 12 ++++ src/web_server.cpp | 106 +++++++++++++++++++++++++++ src/web_server.h | 14 ++++ test/README | 11 +++ 28 files changed, 1321 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 data/www/dashboard.html create mode 100644 data/www/index.html create mode 100644 data/www/wifi.html create mode 100644 include/README create mode 100644 lib/README create mode 100644 notes.ods create mode 100644 platformio.ini create mode 100644 src/NoteMappings copy.h create mode 100644 src/NoteMappings.h create mode 100644 src/audio_input.cpp create mode 100644 src/audio_input.h create mode 100644 src/ble.cpp create mode 100644 src/ble.h create mode 100644 src/config.h create mode 100644 src/esp_info.cpp create mode 100644 src/esp_info.h create mode 100644 src/fft_processing.cpp create mode 100644 src/fft_processing.h create mode 100644 src/led_control.cpp create mode 100644 src/led_control.h create mode 100644 src/main.cpp create mode 100644 src/midi.cpp create mode 100644 src/midi.h create mode 100644 src/web_server.cpp create mode 100644 src/web_server.h create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63b3af4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# PlatformIO build files +.pio/ +.pioenvs/ +.piolibdeps/ + +# VS Code settings +.vscode/ + +# PlatformIO package cache +.platformio/ + +# Python virtual environment +venv/ +env/ + +# macOS system files +.DS_Store + +# Windows system files +Thumbs.db + +# Compiled object files +*.o +*.obj + +# Compiled Dynamic libraries +*.dll +*.so +*.dylib + +# Firmware binaries +*.bin +*.elf +*.hex diff --git a/README.md b/README.md index 98b7aaa..b0f17df 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,102 @@ -# audio2midi +# Audio2MIDI +This project is an Arduino-based application that manages WiFi connectivity, LED control, and ESP device information. It uses the [WiFiManager](https://github.com/tzapu/WiFiManager) library to handle WiFi connections with fallback to Access Point (AP) mode and a captive portal for configuration. + +## Features + +- **WiFi Management**: Automatically connects to saved WiFi credentials or starts an AP with a captive portal for configuration if no connection is possible. +- **ESP Information**: Displays ESP device information such as chip ID and flash size. +- **LED Control**: Provides functionality for controlling LEDs (e.g., cycling through colors). +- **Web Server**: (Placeholder for potential web server functionality). + +## Project Structure + +The project is organized as follows: + +``` +src/ +├── esp_info.cpp # Handles ESP device information +├── esp_info.h +├── index.h # Placeholder for web server content +├── led_control.cpp # Handles LED control logic +├── led_control.h +├── main.cpp # Main entry point for the application +├── web_server.cpp # Placeholder for web server functionality +├── web_server.h +├── wifi_manager.cpp # Manages WiFi connectivity +├── wifi_manager.h +``` + +### Key Components + +#### `main.cpp` +The main entry point of the application. It initializes the WiFi manager, prints ESP information, and sets up LED control. + +#### `wifi_manager.cpp` and `wifi_manager.h` +Manages WiFi connectivity using the `WiFiManager` library. It attempts to connect to saved WiFi credentials and falls back to AP mode with a captive portal if the connection fails. + +#### `esp_info.cpp` and `esp_info.h` +Provides functions to retrieve and display ESP device information such as chip ID and flash size. + +#### `led_control.cpp` and `led_control.h` +Handles LED control logic, such as cycling through colors or setting specific patterns. + +#### `web_server.cpp` and `web_server.h` +(Placeholder) Intended for implementing web server functionality. + +## Setup and Usage + +### Prerequisites + +- Arduino IDE or PlatformIO +- ESP8266 or ESP32 microcontroller +- [WiFiManager](https://github.com/tzapu/WiFiManager) library installed + +### Installation + +1. Clone this repository: + ```bash + git clone https://github.com/your-username/audio2midi.git + cd audio2midi + ``` + +2. Open the project in your preferred IDE (e.g., Arduino IDE or VS Code with PlatformIO). + +3. Install the required libraries: + - [WiFiManager](https://github.com/tzapu/WiFiManager) + +4. Configure your microcontroller board and upload the code. + +### Usage + +1. Power on the ESP device. +2. The device will attempt to connect to saved WiFi credentials. +3. If no connection is possible, the device will start an AP named `AudioToMIDI-AP` with the password `password123`. +4. Connect to the AP and configure the WiFi credentials through the captive portal. +5. Once connected, the device will proceed with its functionality (e.g., LED control, ESP info display). + +## Customization + +- **AP Name and Password**: Modify the AP name and password in `wifi_manager.cpp`: + ```cpp + wm.autoConnect("AudioToMIDI-AP", "password123"); + ``` +- **LED Behavior**: Customize LED patterns in `led_control.cpp`. + +## Troubleshooting + +- If the device fails to start the AP or connect to WiFi, ensure the `WiFiManager` library is installed and the ESP device is properly configured. +- Check the serial monitor for debug output. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- [WiFiManager](https://github.com/tzapu/WiFiManager) for simplifying WiFi configuration. + + +Visit http://[esp-ip]/list to see all files in SPIFFS +Verify that you see /www/index.html and /www/dashboard.html listed +If files are not listed, you need to upload them to SPIFFS using PlatformIO's "Upload Filesystem Image" task \ No newline at end of file diff --git a/data/www/dashboard.html b/data/www/dashboard.html new file mode 100644 index 0000000..f1a534d --- /dev/null +++ b/data/www/dashboard.html @@ -0,0 +1,41 @@ + + + + ESP32 Dashboard + + + +
+ + + diff --git a/data/www/index.html b/data/www/index.html new file mode 100644 index 0000000..6c44a6b --- /dev/null +++ b/data/www/index.html @@ -0,0 +1,122 @@ + + + + Audio2MIDI + + + +
+ + + + + +
+

Danger Zone

+ + + +
+
+
+ +
+ + + + diff --git a/data/www/wifi.html b/data/www/wifi.html new file mode 100644 index 0000000..48aa681 --- /dev/null +++ b/data/www/wifi.html @@ -0,0 +1,61 @@ + + + + WiFi Settings + + + +
+

WiFi Settings

+
+

Current Status

+
+
+
+ + + diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/notes.ods b/notes.ods new file mode 100644 index 0000000000000000000000000000000000000000..a121c2a12100b800dd89e0964ba343c80f45b675 GIT binary patch literal 26522 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 X8|HaKQZm9H8Nolo&RHH8!f*cv +#include + +// Frequency to Note Map (simplified for illustration) +std::map frequencyToNoteMap = { + {16.00, "C1"}, {32.00, "C#1"}, {33.08, "D1"}, {34.65, "D#1"}, {36.71, "E1"}, + {38.89, "F1"}, {41.20, "F#1"}, {43.65, "G1"}, {46.25, "G#1"}, {49.00, "A1"}, + {51.91, "A#1"}, {55.00, "B1"}, {58.27, "C2"}, {61.74, "C#2"}, {65.41, "D2"}, + {69.30, "D#2"}, {73.42, "E2"}, {77.78, "F2"}, {82.41, "F#2"}, {87.31, "G2"}, + {92.50, "G#2"}, {98.00, "A2"}, {103.83, "A#2"}, {110.00, "B2"}, + {116.54, "C3"}, {123.47, "C#3"}, {130.81, "D3"}, {138.59, "D#3"}, + {146.83, "E3"}, {155.56, "F3"}, {164.81, "F#3"}, {174.61, "G3"}, + {184.99, "G#3"}, {195.99, "A3"}, {207.65, "A#3"}, {220.00, "B3"}, + {233.08, "C4"}, {246.94, "C#4"}, {261.63, "D4"}, {277.18, "D#4"}, + {293.66, "E4"}, {311.13, "F4"}, {329.63, "F#4"}, {349.23, "G4"}, + {369.99, "G#4"}, {392.00, "A4"}, {415.30, "A#4"}, {440.00, "B4"}, + {466.16, "C5"}, {493.88, "C#5"}, {523.25, "D5"}, {554.37, "D#5"}, + {587.33, "E5"}, {622.25, "F5"}, {659.25, "F#5"}, {698.46, "G5"}, + {739.99, "G#5"}, {783.99, "A5"}, {830.61, "A#5"}, {880.00, "B5"} +}; + +// Note to MIDI Number Map +std::map noteToMidiMap = { + {"C1", 24}, {"C#1", 25}, {"D1", 26}, {"D#1", 27}, {"E1", 28}, + {"F1", 29}, {"F#1", 30}, {"G1", 31}, {"G#1", 32}, {"A1", 33}, + {"A#1", 34}, {"B1", 35}, {"C2", 36}, {"C#2", 37}, {"D2", 38}, + {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, + {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47}, {"C3", 48}, + {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, + {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, + {"B3", 59}, {"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, + {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, + {"A4", 69}, {"A#4", 70}, {"B4", 71}, {"C5", 72}, {"C#5", 73}, + {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, + {"G5", 79}, {"G#5", 80}, {"A5", 81} +}; + +#endif diff --git a/src/NoteMappings.h b/src/NoteMappings.h new file mode 100644 index 0000000..b4e8649 --- /dev/null +++ b/src/NoteMappings.h @@ -0,0 +1,43 @@ +// NoteMappings.h + +#ifndef NOTEMAPPINGS_H +#define NOTEMAPPINGS_H + +#include +#include + +// Frequency to Note Map (simplified for illustration) +std::map frequencyToNoteMap = { + {16.00, "C1"}, {32.00, "C#1"}, {33.08, "D1"}, {34.65, "D#1"}, {36.71, "E1"}, + {38.89, "F1"}, {41.20, "F#1"}, {43.65, "G1"}, {46.25, "G#1"}, {49.00, "A1"}, + {51.91, "A#1"}, {55.00, "B1"}, {58.27, "C2"}, {61.74, "C#2"}, {65.41, "D2"}, + {69.30, "D#2"}, {73.42, "E2"}, {77.78, "F2"}, {82.41, "F#2"}, {87.31, "G2"}, + {92.50, "G#2"}, {98.00, "A2"}, {103.83, "A#2"}, {110.00, "B2"}, + {116.54, "C3"}, {123.47, "C#3"}, {130.81, "D3"}, {138.59, "D#3"}, + {146.83, "E3"}, {155.56, "F3"}, {164.81, "F#3"}, {174.61, "G3"}, + {184.99, "G#3"}, {195.99, "A3"}, {207.65, "A#3"}, {220.00, "B3"}, + {233.08, "C4"}, {246.94, "C#4"}, {261.63, "D4"}, {277.18, "D#4"}, + {293.66, "E4"}, {311.13, "F4"}, {329.63, "F#4"}, {349.23, "G4"}, + {369.99, "G#4"}, {392.00, "A4"}, {415.30, "A#4"}, {440.00, "B4"}, + {466.16, "C5"}, {493.88, "C#5"}, {523.25, "D5"}, {554.37, "D#5"}, + {587.33, "E5"}, {622.25, "F5"}, {659.25, "F#5"}, {698.46, "G5"}, + {739.99, "G#5"}, {783.99, "A5"}, {830.61, "A#5"}, {880.00, "B5"} +}; + +// Note to MIDI Number Map +std::map noteToMidiMap = { + {"C1", 24}, {"C#1", 25}, {"D1", 26}, {"D#1", 27}, {"E1", 28}, + {"F1", 29}, {"F#1", 30}, {"G1", 31}, {"G#1", 32}, {"A1", 33}, + {"A#1", 34}, {"B1", 35}, {"C2", 36}, {"C#2", 37}, {"D2", 38}, + {"D#2", 39}, {"E2", 40}, {"F2", 41}, {"F#2", 42}, {"G2", 43}, + {"G#2", 44}, {"A2", 45}, {"A#2", 46}, {"B2", 47}, {"C3", 48}, + {"C#3", 49}, {"D3", 50}, {"D#3", 51}, {"E3", 52}, {"F3", 53}, + {"F#3", 54}, {"G3", 55}, {"G#3", 56}, {"A3", 57}, {"A#3", 58}, + {"B3", 59}, {"C4", 60}, {"C#4", 61}, {"D4", 62}, {"D#4", 63}, + {"E4", 64}, {"F4", 65}, {"F#4", 66}, {"G4", 67}, {"G#4", 68}, + {"A4", 69}, {"A#4", 70}, {"B4", 71}, {"C5", 72}, {"C#5", 73}, + {"D5", 74}, {"D#5", 75}, {"E5", 76}, {"F5", 77}, {"F#5", 78}, + {"G5", 79}, {"G#5", 80}, {"A5", 81} +}; + +#endif diff --git a/src/audio_input.cpp b/src/audio_input.cpp new file mode 100644 index 0000000..a7c54cd --- /dev/null +++ b/src/audio_input.cpp @@ -0,0 +1,75 @@ +#include +#include "audio_input.h" +#include "driver/i2s.h" + +float vReal[SAMPLES]; +float vImag[SAMPLES]; + +bool initI2S() { + i2s_config_t i2s_config = { + .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), + .sample_rate = SAMPLING_FREQUENCY, + .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, + .dma_buf_count = 8, + .dma_buf_len = 64, + .use_apll = false, + .tx_desc_auto_clear = false, + .fixed_mclk = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, + .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT + }; + + // Add proper error handling + esp_err_t result = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); + if (result != ESP_OK) { + Serial.printf("Error installing I2S driver: %d\n", result); + return false; + } + + i2s_pin_config_t pin_config = { + .mck_io_num = I2S_PIN_NO_CHANGE, + .bck_io_num = I2S_BCLK_PIN, + .ws_io_num = I2S_LRCLK_PIN, + .data_out_num = I2S_PIN_NO_CHANGE, + .data_in_num = I2S_DATA_IN_PIN + }; + + result = i2s_set_pin(I2S_NUM_0, &pin_config); + if (result != ESP_OK) { + Serial.printf("Error setting I2S pins: %d\n", result); + i2s_driver_uninstall(I2S_NUM_0); // Cleanup on error + return false; + } + + return true; +} + +void readAudioData() { + size_t bytesRead = 0; + esp_err_t result; + + for (int i = 0; i < SAMPLES; i++) { + int16_t sample; + int retries = 0; + do { + result = i2s_read(I2S_NUM_0, &sample, sizeof(sample), &bytesRead, portMAX_DELAY); + retries++; + if (result != ESP_OK || bytesRead != sizeof(sample)) { + Serial.println("Error reading from I2S, retrying..."); + delay(10); + } + } while ((result != ESP_OK || bytesRead != sizeof(sample)) && retries < MAX_RETRIES); + + if (result != ESP_OK || bytesRead != sizeof(sample)) { + Serial.print("Error reading from I2S after retries: "); + Serial.println(esp_err_to_name(result)); + break; + } + + vReal[i] = (float)sample; + vImag[i] = 0; + } +} diff --git a/src/audio_input.h b/src/audio_input.h new file mode 100644 index 0000000..f954293 --- /dev/null +++ b/src/audio_input.h @@ -0,0 +1,14 @@ +#ifndef AUDIO_INPUT_H +#define AUDIO_INPUT_H + +#include +#include "driver/i2s.h" +#include "config.h" + +extern float vReal[]; +extern float vImag[]; + +bool initI2S(); +void readAudioData(); + +#endif diff --git a/src/ble.cpp b/src/ble.cpp new file mode 100644 index 0000000..1f5bcf8 --- /dev/null +++ b/src/ble.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include +#include "ble.h" + +// Add static instance to prevent memory leaks +static MyServerCallbacks* callbacks = nullptr; +bool deviceConnected; + +void MyServerCallbacks::onConnect(BLEServer* pServer) { + deviceConnected = true; + Serial.println("Device connected"); +} + +void MyServerCallbacks::onDisconnect(BLEServer* pServer) { + deviceConnected = false; + Serial.println("Device disconnected"); + // Restart advertising to allow for reconnection + pServer->startAdvertising(); +} + +void setupBLE() { + if (callbacks == nullptr) { + callbacks = new MyServerCallbacks(); + } + + // Initialize BLE with explicit power settings for ESP32-S3 + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + + BLECharacteristic *pCharacteristic; + deviceConnected = false; + + // Set up BLE (optional for your project) + BLEDevice::init("ESP32 MIDI Device"); + BLEServer *pServer = BLEDevice::createServer(); + pServer->setCallbacks(callbacks); + // retry mechanism for BLE initialization + int retries = 0; + while (!BLEDevice::getInitialized() && retries < 3) { + Serial.println("Retrying BLE initialization..."); + delay(1000); + retries++; + } + + if (!BLEDevice::getInitialized()) { + Serial.println("Failed to initialize BLE"); + return; + } + + BLEService *pService = pServer->createService("12345678-1234-5678-1234-56789abcdef0"); + pCharacteristic = pService->createCharacteristic("87654321-1234-5678-1234-56789abcdef0", BLECharacteristic::PROPERTY_NOTIFY); + pService->start(); + BLEAdvertising *pAdvertising = pServer->getAdvertising(); + pAdvertising->start(); + + Serial.println("ESP32 MIDI Device started."); +} + + + +void cleanupBLE() { + if (callbacks != nullptr) { + delete callbacks; + callbacks = nullptr; + } + BLEDevice::deinit(false); +} \ No newline at end of file diff --git a/src/ble.h b/src/ble.h new file mode 100644 index 0000000..b9782b7 --- /dev/null +++ b/src/ble.h @@ -0,0 +1,16 @@ +#ifndef BLE_H +#define BLE_H + +#include + +extern bool deviceConnected; +void setupBLE(); +void cleanupBLE(); + +class MyServerCallbacks : public BLEServerCallbacks { +public: // Make methods public + void onConnect(BLEServer* pServer) override; + void onDisconnect(BLEServer* pServer) override; +}; + +#endif \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..2317a16 --- /dev/null +++ b/src/config.h @@ -0,0 +1,50 @@ +// config.h +#ifndef CONFIG_H +#define CONFIG_H + +#include + +// Pin Definitions +#define I2S_DATA_IN_PIN 41 // Data input pin for INMP441 +#define I2S_BCLK_PIN 42 // Bit clock pin +#define I2S_LRCLK_PIN 40 // Left-Right clock pin + +// Audio Configuration +#define SAMPLES 1024 // Must be a power of 2 +#define SAMPLING_FREQUENCY 16000 +#define MAX_RETRIES 3 +#define NOISE_THRESHOLD 50 + +// BLE Configuration +#define BLE_DEVICE_NAME "ESP32 MIDI Device" +#define BLE_SERVICE_UUID "12345678-1234-5678-1234-56789abcdef0" +#define BLE_CHARACTERISTIC_UUID "87654321-1234-5678-1234-56789abcdef0" + +// Web Server Configuration +#define WEB_SERVER_PORT 80 +#define MAX_LOG_SIZE 512 + +// Watchdog Configuration +#define WDT_TIMEOUT 10000 // Watchdog timeout in milliseconds + +// I2S Configuration +#define I2S_PORT I2S_NUM_0 +#define DMA_BUF_COUNT 8 +#define DMA_BUF_LEN 64 + +// Flash Configuration +#define PROGRAM_PARTITION_SIZE 1310720 // Default app partition size ~1.3MB +#define FLASH_SIZE 0x400000 // 4MB total flash size +#define PROGRAM_SPACE_OFFSET 0x10000 // Program space starts at 64KB offset + +// Debug Configuration +#define SERIAL_BAUD_RATE 115200 +#define SERIAL_INIT_DELAY 1000 // Delay after Serial.begin() in ms + +// Device Configuration +#define DEVICE_HOSTNAME "esp32-midi" // Add this line +// #define MDNS_SERVICE "_midi" // mDNS service type +// #define MDNS_PROTOCOL "_tcp" // mDNS protocol + + +#endif diff --git a/src/esp_info.cpp b/src/esp_info.cpp new file mode 100644 index 0000000..03f6464 --- /dev/null +++ b/src/esp_info.cpp @@ -0,0 +1,154 @@ +// esp_info.cpp +#include +#include "esp_info.h" +#include "driver/temp_sensor.h" + +static bool temp_sensor_initialized = false; + +float readInternalTemperature() { + esp_err_t err; + float temp = 0; + + if (!temp_sensor_initialized) { + Serial.println("Initializing temperature sensor..."); + + // Manual default configuration for ESP32-S3 + temp_sensor_config_t temp_config = { + .dac_offset = TSENS_DAC_L2, // Default offset + .clk_div = 6, // Default clock divider + }; + + err = temp_sensor_set_config(temp_config); + if (err != ESP_OK) { + Serial.printf("Failed to set temp sensor config: %d\n", err); + return NAN; + } + + err = temp_sensor_start(); + if (err != ESP_OK) { + Serial.printf("Failed to start temp sensor: %d\n", err); + return NAN; + } + + temp_sensor_initialized = true; + delay(100); // Allow sensor to stabilize + Serial.println("Temperature sensor initialized"); + } + + err = temp_sensor_read_celsius(&temp); + if (err != ESP_OK) { + Serial.printf("Failed to read temperature: %d\n", err); + return NAN; + } + + return temp; +} + +void prettyPrintBytes(size_t bytes) { + if (bytes < 1024) { + Serial.printf("%d b\n", bytes); + } else if (bytes < (1024 * 1024)) { + Serial.printf("%.2f kb\n", bytes / 1024.0); + } else { + Serial.printf("%.2f mb\n", bytes / (1024.0 * 1024)); + } +} + +String formatBytes(size_t bytes) { + if (bytes < 1024) return String(bytes) + " b"; + else if (bytes < (1024 * 1024)) return String(bytes / 1024.0, 2) + " kb"; + else return String(bytes / (1024.0 * 1024), 2) + " mb"; +} + +String getESPInfoHTML() { + String html = "

ESP32 Information

"; + + html += "

Internal RAM

"; + html += "

Heap Size: " + formatBytes(ESP.getHeapSize()) + "

"; + html += "

Free Heap: " + formatBytes(ESP.getFreeHeap()) + "

"; + + html += "

PSRAM

"; + html += "

PSRAM Size: " + formatBytes(ESP.getPsramSize()) + "

"; + html += "

Free PSRAM: " + formatBytes(ESP.getFreePsram()) + "

"; + + html += "

Flash

"; + size_t usedBytes = ESP.getSketchSize(); + size_t totalBytes = ESP.getFlashChipSize(); + float usagePercent = (usedBytes * 100.0) / totalBytes; + html += "

Flash Size: " + formatBytes(totalBytes) + "

"; + html += "

Used: " + formatBytes(usedBytes) + " (" + String(usagePercent, 1) + "%)

"; + html += "

Flash Speed: " + String(ESP.getFlashChipSpeed() / 1000000) + " MHz

"; + + html += "

CPU

"; + html += "

Model: " + String(ESP.getChipModel()) + "

"; + html += "

Cores: " + String(ESP.getChipCores()) + "

"; + html += "

Frequency: " + String(ESP.getCpuFreqMHz()) + " MHz

"; + + float temperature = readInternalTemperature(); + if (!isnan(temperature)) { + html += "

Temperature Sensor

"; + html += "

Internal Temperature: " + String(temperature, 2) + " °C

"; +} + + + html += "
"; + return html; +} + +String getRAMInfoHTML() { + String html = "

RAM Information

"; + html += "

Heap Size: " + formatBytes(ESP.getHeapSize()) + "

"; + html += "

Free Heap: " + formatBytes(ESP.getFreeHeap()) + "

"; + html += "

Min Free Heap: " + formatBytes(ESP.getMinFreeHeap()) + "

"; + html += "

Max Alloc Heap: " + formatBytes(ESP.getMaxAllocHeap()) + "

"; + html += "

PSRAM

"; + html += "

PSRAM Size: " + formatBytes(ESP.getPsramSize()) + "

"; + html += "

Free PSRAM: " + formatBytes(ESP.getFreePsram()) + "

"; + html += "

Min Free PSRAM: " + formatBytes(ESP.getMinFreePsram()) + "

"; + html += "

Max Alloc PSRAM: " + formatBytes(ESP.getMaxAllocPsram()) + "

"; + html += "
"; + return html; +} +void printESPInfo() { + Serial.println("ESP32-S3 info"); + Serial.println("Internal RAM"); + Serial.print(" getHeapSize: "); prettyPrintBytes(ESP.getHeapSize()); + Serial.print(" getFreeHeap: "); prettyPrintBytes(ESP.getFreeHeap()); + Serial.print(" getMinFreeHeap: "); prettyPrintBytes(ESP.getMinFreeHeap()); + Serial.print(" getMaxAllocHeap: "); prettyPrintBytes(ESP.getMaxAllocHeap()); + Serial.println("PSRAM"); + Serial.print(" getPsramSize: "); prettyPrintBytes(ESP.getPsramSize()); + Serial.print(" getFreePsram: "); prettyPrintBytes(ESP.getFreePsram()); + Serial.print(" getMinFreePsram: "); prettyPrintBytes(ESP.getMinFreePsram()); + Serial.print(" getMaxAllocPsram: "); prettyPrintBytes(ESP.getMaxAllocPsram()); + Serial.println("Flash"); + Serial.print(" getFlashChipSize: "); prettyPrintBytes(ESP.getFlashChipSize()); + Serial.printf(" getFlashChipSpeed: %d MHz\n", ESP.getFlashChipSpeed() / 1000 / 1000); + Serial.print(" getFlashChipMode: "); Serial.println("SKIPPED"); + Serial.println("CPU"); + Serial.printf(" getChipRevision: %d\n", ESP.getChipRevision()); + Serial.printf(" getChipModel: %s\n", ESP.getChipModel()); + Serial.printf(" getChipCores: %d\n", ESP.getChipCores()); + Serial.printf(" getCpuFreqMHz: %d\n", ESP.getCpuFreqMHz()); + Serial.printf(" getEfuseMac: %llX\n", ESP.getEfuseMac()); + float temperature = readInternalTemperature(); + if (!isnan(temperature)) { + Serial.printf(" Internal Temperature: %.2f °C\n", temperature); + } else { + Serial.println(" Internal Temperature: Read failed"); + } + Serial.println("Software"); + Serial.printf(" getSdkVersion: %s\n", ESP.getSdkVersion()); + Serial.print(" getSketchSize: "); prettyPrintBytes(ESP.getSketchSize()); + Serial.printf(" getFreeSketchSpace: %d kb\n", ESP.getFreeSketchSpace() / 1024); + Serial.printf(" getSketchMD5: %s\n", ESP.getSketchMD5().c_str()); + + Serial.print("Internal Temperature: "); + float temp = readInternalTemperature(); + if (isnan(temp)) { + Serial.println("Error reading temperature"); + } else { + Serial.print(temp); + Serial.println("°C"); + } +} diff --git a/src/esp_info.h b/src/esp_info.h new file mode 100644 index 0000000..9e0495f --- /dev/null +++ b/src/esp_info.h @@ -0,0 +1,18 @@ +// esp_info.h +#ifndef ESP_INFO_H +#define ESP_INFO_H + +#pragma once +#include + +void prettyPrintBytes(size_t bytes); +void printESPInfo(); +String getESPInfoHTML(); +float getFlashUsagePercent(); +size_t getProgramFlashSize(); // Add this +size_t getAvailableFlashSize(); // Add this +String getRAMInfoHTML(); +float readInternalTemperature(); // Add this to esp_info.h + + +#endif diff --git a/src/fft_processing.cpp b/src/fft_processing.cpp new file mode 100644 index 0000000..097cc39 --- /dev/null +++ b/src/fft_processing.cpp @@ -0,0 +1,48 @@ +#include +#include "config.h" +#include "fft_processing.h" + +// extern const int SAMPLES; // Declare SAMPLES as external + +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQUENCY); + +void handleFFT() { + // Check for invalid data + bool hasValidData = false; + for (int i = 0; i < SAMPLES; i++) { + if (vReal[i] != 0) { + hasValidData = true; + break; + } + } + + if (!hasValidData) { + Serial.println("No valid audio data for FFT"); + return; + } + + FFT.windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD); + FFT.compute(vReal, vImag, SAMPLES, FFT_FORWARD); + FFT.complexToMagnitude(vReal, vImag, SAMPLES); +} + +void processFFTData() { + float maxMag = 0; + int maxIndex = 0; + + for (int i = 2; i < SAMPLES / 2; i++) { // Start from 2 to skip DC + if (vReal[i] > maxMag) { + maxMag = vReal[i]; + maxIndex = i; + } + } + + if (maxMag < NOISE_THRESHOLD) { + return; // Skip if signal is too weak + } + + float frequency = (maxIndex * SAMPLING_FREQUENCY) / (float)SAMPLES; + if (frequency > 20 && frequency < 20000) { // Valid audio range + Serial.printf("Detected frequency: %.2f Hz, Magnitude: %.2f\n", frequency, maxMag); + } +} diff --git a/src/fft_processing.h b/src/fft_processing.h new file mode 100644 index 0000000..3bc6188 --- /dev/null +++ b/src/fft_processing.h @@ -0,0 +1,13 @@ +#ifndef FFT_PROCESSING_H +#define FFT_PROCESSING_H + +#include + +extern ArduinoFFT FFT; +extern float vReal[]; +extern float vImag[]; + +void handleFFT(); +void processFFTData(); + +#endif diff --git a/src/led_control.cpp b/src/led_control.cpp new file mode 100644 index 0000000..d594fbf --- /dev/null +++ b/src/led_control.cpp @@ -0,0 +1,23 @@ +#include "led_control.h" +#include + +const uint8_t NUM_LEDS = 1; +const gpio_num_t DATA_PIN = GPIO_NUM_48; + +CRGB leds[NUM_LEDS]; + +void setupLED() { + CFastLED::addLeds(leds, NUM_LEDS); + FastLED.setBrightness(32); +} + +void ledCycle() { + leds[0] = CRGB::Red; + FastLED.delay(500); + leds[0] = CRGB::Green; + FastLED.delay(500); + leds[0] = CRGB::Blue; + FastLED.delay(500); + leds[0] = CRGB::Black; + FastLED.delay(500); +} diff --git a/src/led_control.h b/src/led_control.h new file mode 100644 index 0000000..397596d --- /dev/null +++ b/src/led_control.h @@ -0,0 +1,7 @@ +#ifndef LED_CONTROL_H +#define LED_CONTROL_H + +void setupLED(); +void ledCycle(); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1ec32ac --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,79 @@ +#include "esp_task_wdt.h" +#include +#include +#include +#include +#include "config.h" +#include "esp_info.h" +#include "led_control.h" +#include "web_server.h" + +WiFiManager wm; // Global WiFiManager instance + +void configModeCallback(WiFiManager *myWiFiManager) { + Serial.println("Entered config mode"); + Serial.println("AP IP: " + WiFi.softAPIP().toString()); + Serial.println("AP SSID: " + myWiFiManager->getConfigPortalSSID()); + // Start web server in AP mode + startAccessPoint(); +} + +bool setupWiFi() { + // Set callback that gets called when connecting to previous WiFi fails, and enters Access Point mode + wm.setAPCallback(configModeCallback); + + // Set config portal timeout (optional) + wm.setConfigPortalTimeout(180); // 3 minutes + + // Set custom hostname (optional) + wm.setHostname("Audio2MIDI"); + + // Automatically connect using saved credentials if they exist + // If connection fails it will start an access point with the name "Audio2MIDI_AP" + if (wm.autoConnect("Audio2MIDI_AP")) { // Removed password parameter + Serial.println("WiFi connected"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + // Initialize web server in station mode + setupWebServer(); + startWebServer(); + return true; + } + + Serial.println("Failed to connect and hit timeout"); + return false; +} + +void setup() { + Serial.begin(SERIAL_BAUD_RATE); + delay(SERIAL_INIT_DELAY); + + // Initialize SPIFFS + if(!SPIFFS.begin(true)) { + Serial.println("SPIFFS Mount Failed"); + return; + } + + // List files in SPIFFS for debugging + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + Serial.println("Files in SPIFFS:"); + while(file) { + Serial.printf(" - %s (%d bytes)\n", file.name(), file.size()); + file = root.openNextFile(); + } + + printESPInfo(); + setupLED(); + + if (!setupWiFi()) { + ESP.restart(); + } +} + +void loop() { + handleDNS(); // Process DNS requests for captive portal + ledCycle(); + delay(10); // Small delay to prevent watchdog triggers +} \ No newline at end of file diff --git a/src/midi.cpp b/src/midi.cpp new file mode 100644 index 0000000..568d1eb --- /dev/null +++ b/src/midi.cpp @@ -0,0 +1,43 @@ +#include +#include "midi.h" +#include "ble.h" +#include +#include +#include +#include + +#define MIDI_NOTE_ON 0x90 +void sendMIDI(uint8_t note, uint8_t velocity) { + if (!pCharacteristic) { + Serial.println("BLE characteristic not initialized"); + return; + } + if (deviceConnected) { + uint8_t midiMessage[3]; + midiMessage[0] = MIDI_NOTE_ON; + midiMessage[1] = note; + midiMessage[2] = velocity; + + pCharacteristic->setValue(midiMessage, sizeof(midiMessage)); + pCharacteristic->notify(); + Serial.println("MIDI message sent."); + } else { + Serial.println("No BLE device connected, MIDI message not sent."); + } +} + +void connectToBLE() { + BLEDevice::init("ESP32 MIDI Device"); + BLEServer *pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + BLEService *pService = pServer->createService("12345678-1234-5678-1234-56789abcdef0"); + pCharacteristic = pService->createCharacteristic( + "87654321-1234-5678-1234-56789abcdef0", + BLECharacteristic::PROPERTY_NOTIFY + ); + pCharacteristic->addDescriptor(new BLE2902()); + pService->start(); + BLEAdvertising *pAdvertising = pServer->getAdvertising(); + pAdvertising->start(); + Serial.println("ESP32 MIDI Device started."); +} diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 0000000..9509036 --- /dev/null +++ b/src/midi.h @@ -0,0 +1,12 @@ +#ifndef MIDI_H +#define MIDI_H + +#include + +extern BLECharacteristic *pCharacteristic; +extern bool deviceConnected; + +void sendMIDI(uint8_t note, uint8_t velocity); +void connectToBLE(); + +#endif diff --git a/src/web_server.cpp b/src/web_server.cpp new file mode 100644 index 0000000..7ce3f8e --- /dev/null +++ b/src/web_server.cpp @@ -0,0 +1,106 @@ +#include "web_server.h" +#include "esp_info.h" +#include "SPIFFS.h" + +AsyncWebServer server(80); +DNSServer dnsServer; +bool isAPMode = false; + +void startAccessPoint() { + WiFi.softAP("Audio2MIDI_AP"); + IPAddress IP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(IP); + + // Start DNS server for captive portal + dnsServer.start(53, "*", IP); + isAPMode = true; + + // Start the web server + setupWebServer(); + startWebServer(); +} + +void setupWebServer() { + // Add CORS headers to all responses + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + // Root route with debug logging + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.print("Handling root request from IP: "); + Serial.println(request->client()->remoteIP()); + + if (SPIFFS.exists("/www/index.html")) { + Serial.println("Serving /www/index.html"); + request->send(SPIFFS, "/www/index.html", "text/html"); + } else { + Serial.println("index.html not found!"); + request->send(200, "text/plain", "Welcome to Audio2MIDI!"); + } + }); + + // Handle static files with detailed error checking + server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("index.html").setFilter([](AsyncWebServerRequest *request) { + String path = request->url(); + Serial.printf("Static request for: %s\n", path.c_str()); + if (SPIFFS.exists("/www" + path)) { + Serial.println("File exists"); + return true; + } + Serial.println("File does not exist"); + return false; + }); + + // Debug - list files endpoint + server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request) { + String output = "Files in SPIFFS:\n"; + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + while(file) { + output += " - "; + output += file.name(); + output += " ("; + output += file.size(); + output += " bytes)\n"; + file = root.openNextFile(); + } + request->send(200, "text/plain", output); + }); + + // API endpoints + server.on("/api/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request) { + String json; + if (WiFi.status() == WL_CONNECTED) { + json = "{\"connected\":true,\"ssid\":\"" + WiFi.SSID() + + "\",\"ip\":\"" + WiFi.localIP().toString() + + "\",\"rssi\":" + String(WiFi.RSSI()) + "}"; + } else { + json = "{\"connected\":false}"; + } + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", json); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + }); + + // Catch-all handler for captive portal in AP mode + server.onNotFound([](AsyncWebServerRequest *request) { + if (isAPMode) { + request->redirect("/"); + } else { + request->send(404, "text/plain", "Not Found"); + } + }); +} + +void startWebServer() { + server.begin(); + Serial.println("Web server started on port 80"); +} + +void handleDNS() { + if (isAPMode) { + dnsServer.processNextRequest(); + } +} diff --git a/src/web_server.h b/src/web_server.h new file mode 100644 index 0000000..e65ce7e --- /dev/null +++ b/src/web_server.h @@ -0,0 +1,14 @@ +#ifndef WEB_SERVER_H +#define WEB_SERVER_H + +#include +#include + +void startAccessPoint(); +void setupWebServer(); +void startWebServer(); +void handleDNS(); +extern AsyncWebServer server; +extern DNSServer dnsServer; + +#endif diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html