From 799b6c36bcca3b468407cf0e2c55d1108be4fd4f Mon Sep 17 00:00:00 2001 From: Wilson Daniel Peres de Oliveira Date: Sun, 4 Oct 2026 18:08:03 -0300 Subject: [PATCH] feat: add optional BLE MIDI support for ESP32 --- edrumulus.ino | 425 ++++++++++++++++++++++++++++++++++++++++++++++++- platformio.ini | 38 ++--- 2 files changed, 443 insertions(+), 20 deletions(-) diff --git a/edrumulus.ino b/edrumulus.ino index 9cfa07c..52a92f2 100644 --- a/edrumulus.ino +++ b/edrumulus.ino @@ -28,7 +28,8 @@ static int analog_pins_rimshot4[] = { 35, -1, 26, -1, 14, // if you want to use less number of pads, simply adjust number_pads4 value // const int number_pads4 = sizeof ( analog_pins4 ) / sizeof ( int ); // example: use all inputs defined in analog_pins4 -const int number_pads4 = 8; // example: do not use tom3 and shrink number of pads from 9 to 8 +// const int number_pads4 = 8; // example: do not use tom3 and shrink number of pads from 9 to 8 +const int number_pads4 = 3; // const int number_pads4 = 1; // example: just one single pad #include "edrumulus.h" @@ -47,6 +48,12 @@ MIDI_CREATE_DEFAULT_INSTANCE(); # define MIDI_CONTROL_CHANGE_TYPE midi::ControlChange # define MIDI_SEND_AFTER_TOUCH sendAfterTouch # define MIDI_SERIAL 38400 +# ifdef USE_BLE_MIDI +# include +# include +# include +# include +# endif # endif # ifdef TEENSYDUINO # define MYMIDI usbMIDI @@ -65,6 +72,323 @@ int status_LED_pin = 0; // initialization value, will be set bool is_status_LED_on = false; // initialization value int selected_pad = 0; // initialization value +#ifdef USE_BLE_MIDI +void confirm_setting(const int controller, const int value, const bool send_all); +void preset_settings(); +void write_all_settings(); +#endif + +#ifdef USE_BLE_MIDI +# define MIDI_SERVICE_UUID "03b80e5a-ede8-4b33-a751-6ce34ec4c700" +# define MIDI_CHARACTERISTIC_UUID "7772e5db-3868-4112-a1a9-f2669d106bf3" + +BLECharacteristic *p_ble_midi_characteristic = nullptr; +bool ble_midi_device_connected = false; + +class BleMidiServerCallbacks : public BLEServerCallbacks +{ + void onConnect(BLEServer *p_server) + { + ble_midi_device_connected = true; + } + + void onDisconnect(BLEServer *p_server) + { + ble_midi_device_connected = false; + BLEDevice::startAdvertising(); + } +}; + +class BleMidiCharacteristicCallbacks : public BLECharacteristicCallbacks +{ + void onWrite(BLECharacteristic *p_characteristic) + { + std::string rx_value = p_characteristic->getValue(); + if (rx_value.empty()) + { + return; + } + + int byte_index = 0; + uint8_t running_status = 0; + + while (byte_index < static_cast(rx_value.size())) + { + const uint8_t byte_value = static_cast(rx_value[byte_index]); + + if (byte_value == 0x80) + { + byte_index++; + if (byte_index >= static_cast(rx_value.size())) + { + break; + } + const uint8_t timestamp_byte = static_cast(rx_value[byte_index]); + if ((timestamp_byte & 0x80) == 0) + { + byte_index--; + break; + } + byte_index++; + continue; + } + + if ((byte_value & 0x80) != 0) + { + running_status = byte_value; + byte_index++; + } + + if (byte_index >= static_cast(rx_value.size())) + { + break; + } + + const uint8_t data_byte1 = static_cast(rx_value[byte_index++]); + uint8_t data_byte2 = 0; + const uint8_t status_nibble = running_status >> 4; + + if (status_nibble != 0xC && status_nibble != 0xD) + { + if (byte_index >= static_cast(rx_value.size())) + { + break; + } + data_byte2 = static_cast(rx_value[byte_index++]); + } + + const int controller = data_byte1; + const int value = data_byte2; + if (running_status == 0xB0) + { + if (controller == 102) + { + edrumulus.set_pad_type(selected_pad, static_cast(value)); + edrumulus.write_setting(selected_pad, 0, value); + confirm_setting(controller, value, true); + } + else if (controller == 103) + { + edrumulus.set_velocity_threshold(selected_pad, value); + edrumulus.write_setting(selected_pad, 1, value); + confirm_setting(controller, value, false); + } + else if (controller == 104) + { + edrumulus.set_velocity_sensitivity(selected_pad, value); + edrumulus.write_setting(selected_pad, 2, value); + confirm_setting(controller, value, false); + } + else if (controller == 105) + { + edrumulus.set_pos_threshold(selected_pad, value); + edrumulus.write_setting(selected_pad, 3, value); + confirm_setting(controller, value, false); + } + else if (controller == 106) + { + edrumulus.set_pos_sensitivity(selected_pad, value); + edrumulus.write_setting(selected_pad, 4, value); + confirm_setting(controller, value, false); + } + else if (controller == 107) + { + edrumulus.set_rim_shot_threshold(selected_pad, value); + edrumulus.write_setting(selected_pad, 5, value); + confirm_setting(controller, value, false); + } + else if (controller == 108 && value < MAX_NUM_PADS) + { + selected_pad = value; + confirm_setting(controller, value, true); + } + else if (controller == 109) + { + edrumulus.set_curve(selected_pad, static_cast(value)); + edrumulus.write_setting(selected_pad, 6, value); + confirm_setting(controller, value, false); + } + else if (controller == 110) + { + edrumulus.set_spike_cancel_level(value); + edrumulus.write_setting(number_pads, 0, value); + confirm_setting(controller, value, false); + } + else if (controller == 111) + { + switch (value) + { + case 0: + edrumulus.set_rim_shot_is_used(selected_pad, false); + edrumulus.write_setting(selected_pad, 7, false); + edrumulus.set_pos_sense_is_used(selected_pad, false); + edrumulus.write_setting(selected_pad, 8, false); + break; + case 1: + edrumulus.set_rim_shot_is_used(selected_pad, true); + edrumulus.write_setting(selected_pad, 7, true); + edrumulus.set_pos_sense_is_used(selected_pad, false); + edrumulus.write_setting(selected_pad, 8, false); + break; + case 2: + edrumulus.set_rim_shot_is_used(selected_pad, false); + edrumulus.write_setting(selected_pad, 7, false); + edrumulus.set_pos_sense_is_used(selected_pad, true); + edrumulus.write_setting(selected_pad, 8, true); + break; + case 3: + edrumulus.set_rim_shot_is_used(selected_pad, true); + edrumulus.write_setting(selected_pad, 7, true); + edrumulus.set_pos_sense_is_used(selected_pad, true); + edrumulus.write_setting(selected_pad, 8, true); + break; + } + confirm_setting(controller, value, false); + } + else if (controller == 112) + { + edrumulus.set_midi_note_norm(selected_pad, value); + edrumulus.write_setting(selected_pad, 9, value); + confirm_setting(controller, value, false); + } + else if (controller == 113) + { + edrumulus.set_midi_note_rim(selected_pad, value); + edrumulus.write_setting(selected_pad, 10, value); + confirm_setting(controller, value, false); + } + else if (controller == 114) + { + edrumulus.set_cancellation(selected_pad, value); + edrumulus.write_setting(selected_pad, 11, value); + confirm_setting(controller, value, false); + } + else if (controller == 115) + { + preset_settings(); + write_all_settings(); + confirm_setting(controller, value, false); + } + else if (controller == 116) + { + edrumulus.set_midi_note_open_norm(selected_pad, value); + edrumulus.write_setting(selected_pad, 12, value); + confirm_setting(controller, value, false); + } + else if (controller == 117) + { + edrumulus.set_midi_note_open_rim(selected_pad, value); + edrumulus.write_setting(selected_pad, 13, value); + confirm_setting(controller, value, false); + } + else if (controller == 118) + { + edrumulus.set_mask_time(selected_pad, value); + edrumulus.write_setting(selected_pad, 14, value); + confirm_setting(controller, value, false); + } + else if (controller == 119) + { + edrumulus.set_rim_shot_boost(selected_pad, value); + edrumulus.write_setting(selected_pad, 15, value); + confirm_setting(controller, value, false); + } + else if (controller == 120) + { + edrumulus.set_coupled_pad_idx(selected_pad, value); + edrumulus.write_setting(selected_pad, 16, value); + confirm_setting(controller, value, false); + } + else if (controller == 121) + { + edrumulus.set_rim_pos_threshold(selected_pad, value); + edrumulus.write_setting(selected_pad, 17, value); + confirm_setting(controller, value, false); + } + else if (controller == 122) + { + edrumulus.set_rim_pos_sensitivity(selected_pad, value); + edrumulus.write_setting(selected_pad, 18, value); + confirm_setting(controller, value, false); + } + } + } + } +}; + +void ble_midi_setup() +{ + BLEDevice::init("Edrumulus"); + + BLEServer *p_server = BLEDevice::createServer(); + p_server->setCallbacks(new BleMidiServerCallbacks()); + + BLEService *p_service = p_server->createService(BLEUUID(MIDI_SERVICE_UUID)); + p_ble_midi_characteristic = p_service->createCharacteristic( + BLEUUID(MIDI_CHARACTERISTIC_UUID), + BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE_NR); + p_ble_midi_characteristic->addDescriptor(new BLE2902()); + p_ble_midi_characteristic->setCallbacks(new BleMidiCharacteristicCallbacks()); + + p_service->start(); + p_server->getAdvertising()->addServiceUUID(MIDI_SERVICE_UUID); + p_server->getAdvertising()->start(); +} + +void ble_midi_send_packet(const uint8_t *packet, const int length) +{ + if (ble_midi_device_connected && p_ble_midi_characteristic != nullptr) + { + p_ble_midi_characteristic->setValue(const_cast(packet), static_cast(length)); + p_ble_midi_characteristic->notify(); + } +} + +void ble_midi_send_note_on(const int note, const int velocity, const int channel) +{ + uint8_t packet[5]; + packet[0] = 0x80; + packet[1] = 0x80; + packet[2] = static_cast(0x90 | (channel & 0x0F)); + packet[3] = static_cast(note & 0x7F); + packet[4] = static_cast(velocity & 0x7F); + ble_midi_send_packet(packet, 5); +} + +void ble_midi_send_note_off(const int note, const int velocity, const int channel) +{ + uint8_t packet[5]; + packet[0] = 0x80; + packet[1] = 0x80; + packet[2] = static_cast(0x80 | (channel & 0x0F)); + packet[3] = static_cast(note & 0x7F); + packet[4] = static_cast(velocity & 0x7F); + ble_midi_send_packet(packet, 5); +} + +void ble_midi_send_control_change(const int controller, const int value, const int channel) +{ + uint8_t packet[5]; + packet[0] = 0x80; + packet[1] = 0x80; + packet[2] = static_cast(0xB0 | (channel & 0x0F)); + packet[3] = static_cast(controller & 0x7F); + packet[4] = static_cast(value & 0x7F); + ble_midi_send_packet(packet, 5); +} + +void ble_midi_send_after_touch(const int note, const int pressure, const int channel) +{ + uint8_t packet[5]; + packet[0] = 0x80; + packet[1] = 0x80; + packet[2] = static_cast(0xA0 | (channel & 0x0F)); + packet[3] = static_cast(note & 0x7F); + packet[4] = static_cast(pressure & 0x7F); + ble_midi_send_packet(packet, 5); +} +#endif + void setup() { // get the pin-to-pad assignments @@ -103,6 +427,9 @@ void setup() edrumulus.setup(number_pads, analog_pins, analog_pins_rimshot); digitalWrite(status_LED_pin, LOW); // set board LED to low right after setup is done +#ifdef USE_BLE_MIDI + ble_midi_setup(); +#endif #ifdef ESP_PLATFORM preset_settings(); // for ESP32, the load/save of settings is not supported, preset instead #else @@ -156,11 +483,23 @@ void loop() { // > 63 means DC offset error and pad/input index is coded in one value MYMIDI.sendNoteOff(125, 64 + dc_offset_error_channel, 1); +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_note_off(125, 64 + dc_offset_error_channel, 1); + } +#endif } else { // 1 means to set error state MYMIDI.sendNoteOff(125, 1, 1); +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_note_off(125, 1, 1); + } +#endif } } #endif @@ -174,6 +513,12 @@ void loop() is_status_LED_on = false; #ifdef USE_MIDI MYMIDI.sendNoteOff(125, 0, 1); // 0 means that all errors are cleared +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_note_off(125, 0, 1); + } +#endif #endif } } @@ -193,6 +538,12 @@ void loop() { const int midi_pos = edrumulus.get_midi_pos(pad_idx); MYMIDI.sendControlChange(16, midi_pos, midi_channel); // positional sensing +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_control_change(16, midi_pos, midi_channel); + } +#endif } // send Hi-Hat control message right before each Hi-Hat pad hit @@ -212,6 +563,13 @@ void loop() MYMIDI.sendNoteOn(midi_note, midi_velocity, midi_channel); // (note, velocity, channel) MYMIDI.sendNoteOff(midi_note, 0, midi_channel); // we need a note off +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_note_on(midi_note, midi_velocity, midi_channel); + ble_midi_send_note_off(midi_note, 0, midi_channel); + } +#endif } if (edrumulus.get_control_found(pad_idx)) @@ -219,6 +577,12 @@ void loop() const int midi_ctrl_ch = edrumulus.get_midi_ctrl_ch(pad_idx); const int midi_ctrl_value = edrumulus.get_midi_ctrl_value(pad_idx); MYMIDI.sendControlChange(midi_ctrl_ch, midi_ctrl_value, midi_channel); +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_control_change(midi_ctrl_ch, midi_ctrl_value, midi_channel); + } +#endif } if (edrumulus.get_choke_on_found(pad_idx)) @@ -231,6 +595,13 @@ void loop() const int midi_choke_noteon = edrumulus.get_midi_note_open_norm(pad_idx); MYMIDI.sendNoteOn(midi_choke_noteon, 127, midi_channel); MYMIDI.sendNoteOff(midi_choke_noteon, 0, midi_channel); // we need a note off +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_note_on(midi_choke_noteon, 127, midi_channel); + ble_midi_send_note_off(midi_choke_noteon, 0, midi_channel); + } +#endif } else { @@ -239,6 +610,15 @@ void loop() MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_rim(pad_idx), 127, midi_channel); MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_open_norm(pad_idx), 127, midi_channel); MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_open_rim(pad_idx), 127, midi_channel); +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_after_touch(edrumulus.get_midi_note_norm(pad_idx), 127, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_rim(pad_idx), 127, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_open_norm(pad_idx), 127, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_open_rim(pad_idx), 127, midi_channel); + } +#endif } } else if (edrumulus.get_choke_off_found(pad_idx)) @@ -248,6 +628,15 @@ void loop() MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_rim(pad_idx), 0, midi_channel); MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_open_norm(pad_idx), 0, midi_channel); MYMIDI.MIDI_SEND_AFTER_TOUCH(edrumulus.get_midi_note_open_rim(pad_idx), 0, midi_channel); +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + ble_midi_send_after_touch(edrumulus.get_midi_note_norm(pad_idx), 0, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_rim(pad_idx), 0, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_open_norm(pad_idx), 0, midi_channel); + ble_midi_send_after_touch(edrumulus.get_midi_note_open_rim(pad_idx), 0, midi_channel); + } +#endif } } @@ -497,6 +886,40 @@ void confirm_setting(const int controller, // return only the given parameter MYMIDI.sendNoteOff(controller, value, 1); // can be checked, e.g., in the log file } +#ifdef USE_BLE_MIDI + if (ble_midi_device_connected) + { + if (send_all) + { + ble_midi_send_control_change(102, static_cast(edrumulus.get_pad_type(selected_pad)), midi_channel); + ble_midi_send_control_change(103, edrumulus.get_velocity_threshold(selected_pad), midi_channel); + ble_midi_send_control_change(104, edrumulus.get_velocity_sensitivity(selected_pad), midi_channel); + ble_midi_send_control_change(105, edrumulus.get_pos_threshold(selected_pad), midi_channel); + ble_midi_send_control_change(106, edrumulus.get_pos_sensitivity(selected_pad), midi_channel); + ble_midi_send_control_change(107, edrumulus.get_rim_shot_threshold(selected_pad), midi_channel); + ble_midi_send_control_change(108, selected_pad, midi_channel); + ble_midi_send_control_change(109, static_cast(edrumulus.get_curve(selected_pad)), midi_channel); + ble_midi_send_control_change(110, edrumulus.get_spike_cancel_level(), midi_channel); + ble_midi_send_control_change(111, edrumulus.get_rim_shot_is_used(selected_pad) + 2 * edrumulus.get_pos_sense_is_used(selected_pad), midi_channel); + ble_midi_send_control_change(112, edrumulus.get_midi_note_norm(selected_pad), midi_channel); + ble_midi_send_control_change(113, edrumulus.get_midi_note_rim(selected_pad), midi_channel); + ble_midi_send_control_change(114, edrumulus.get_cancellation(selected_pad), midi_channel); + ble_midi_send_control_change(116, edrumulus.get_midi_note_open_norm(selected_pad), midi_channel); + ble_midi_send_control_change(117, edrumulus.get_midi_note_open_rim(selected_pad), midi_channel); + ble_midi_send_control_change(118, edrumulus.get_mask_time(selected_pad), midi_channel); + ble_midi_send_control_change(119, edrumulus.get_rim_shot_boost(selected_pad), midi_channel); + ble_midi_send_control_change(120, edrumulus.get_coupled_pad_idx(selected_pad), midi_channel); + ble_midi_send_control_change(121, edrumulus.get_rim_pos_threshold(selected_pad), midi_channel); + ble_midi_send_control_change(122, edrumulus.get_rim_pos_sensitivity(selected_pad), midi_channel); + ble_midi_send_note_off(126, VERSION_MINOR, midi_channel); + ble_midi_send_note_off(127, VERSION_MAJOR, midi_channel); + } + else + { + ble_midi_send_control_change(controller, value, midi_channel); + } + } +#endif } #endif diff --git a/platformio.ini b/platformio.ini index 698977a..d343225 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,18 +9,18 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -src_dir=. +src_dir = . +default_envs = esp32doit-devkit-v1 [env] framework = arduino lib_deps = fortyseveneffects/MIDI Library -build_src_filter = - +<*.c> - +<*.cpp> - +<*.h> - +<*.ino> +build_src_filter = + +<*.c> + +<*.cpp> + +<*.h> + +<*.ino> -; see https://docs.platformio.org/en/latest/platforms/teensy.html for more details [env:teensy36] platform = teensy board = teensy36 @@ -36,33 +36,33 @@ platform = teensy board = teensy41 build_flags = -D USB_MIDI_SERIAL -; see https://docs.platformio.org/en/latest/platforms/espressif32.html for more details [env:esp32doit-devkit-v1] platform = espressif32 board = esp32doit-devkit-v1 +build_flags = -D USE_BLE_MIDI=1 [env:az-delivery-devkit-v4] platform = espressif32 board = az-delivery-devkit-v4 +build_flags = -D USE_BLE_MIDI=1 [env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 build_unflags = -D ARDUINO_USB_MODE=1 build_flags = - -D USE_TINYUSB=1 - -D ARDUINO_USB_MODE=0 - -D ARDUINO_USB_CDC_ON_BOOT=1 - '-D CFG_TUSB_CONFIG_FILE="${platformio.core_dir}/packages/framework-arduinoespressif32/tools/sdk/esp32s3/include/arduino_tinyusb/include/tusb_config.h"' + -D USE_TINYUSB=1 + -D ARDUINO_USB_MODE=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 + '-D CFG_TUSB_CONFIG_FILE="${platformio.core_dir}/packages/framework-arduinoespressif32/tools/sdk/esp32s3/include/arduino_tinyusb/include/tusb_config.h"' lib_archive = no -lib_deps = - ${env.lib_deps} - adafruit/Adafruit TinyUSB Library +lib_deps = + ${env.lib_deps} + adafruit/Adafruit TinyUSB Library -; see https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#how-to-update-to-the-latest-code [env:arduino-latest] platform = https://github.com/sgryphon/platform-espressif32.git#sgryphon/add-esp32-arduino-libs board = az-delivery-devkit-v4 -platform_packages = - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#master - platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 +platform_packages = + platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#master + platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1