From 4d81386f78b3200364f20cf8bb840c3536263135 Mon Sep 17 00:00:00 2001 From: Ashley Strahle Date: Tue, 23 Jun 2026 13:06:57 +0000 Subject: [PATCH] Fix USB MIDI, add LED startup animation, improve logging - Use ArduinoUSBMIDI library for proper USB MIDI device recognition - Add LED startup colour cycle (red, green, blue, yellow, magenta, cyan, white) - Add comprehensive console logging for all MIDI in/out and switch events - Log unmapped MIDI messages --- include/midi_transport.h | 14 ++--- platformio.ini | 4 +- src/app_task.cpp | 14 +++-- src/led_stub.cpp | 35 ++++++++++- src/main.cpp | 28 +++------ src/midi_transport.cpp | 133 ++++++++++++++++++++------------------- 6 files changed, 130 insertions(+), 98 deletions(-) diff --git a/include/midi_transport.h b/include/midi_transport.h index 07d0cdb..1f5fdbd 100644 --- a/include/midi_transport.h +++ b/include/midi_transport.h @@ -15,10 +15,10 @@ struct MidiEvent { SYSEX } type; - uint8_t channel; // MIDI channel (1-16) - uint8_t data1; // Note number or CC number - uint8_t data2; // Velocity or CC value - uint32_t timestamp; // Event timestamp + uint8_t channel; + uint8_t data1; + uint8_t data2; + uint32_t timestamp; }; class UsbMidiTransport { @@ -29,17 +29,15 @@ public: bool begin(); void update(); - // Callback registration void on_midi_receive(std::function callback); - // Send MIDI void send_note_on(uint8_t channel, uint8_t note, uint8_t velocity); void send_note_off(uint8_t channel, uint8_t note, uint8_t velocity); void send_cc(uint8_t channel, uint8_t cc, uint8_t value); + bool is_connected(); + private: std::function receive_callback; bool initialized; - - void parse_midi_packet(const uint8_t* buffer, uint32_t size, MidiEvent& event); }; diff --git a/platformio.ini b/platformio.ini index b14907e..745038a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,10 +6,12 @@ platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino +lib_deps = + lathoub/ArduinoUSBMIDI@^1.0.0 + build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 - -DCFG_TUSB_MIDI=1 monitor_speed = 115200 diff --git a/src/app_task.cpp b/src/app_task.cpp index 6d3c2fc..46e8a0a 100644 --- a/src/app_task.cpp +++ b/src/app_task.cpp @@ -4,7 +4,6 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi) : led_driver(led), switch_driver(sw), midi_transport(midi) { - // Default pad mapping for (uint8_t i = 0; i < NUM_PADS; i++) { pad_mapping[i].physical_switch = i; pad_mapping[i].midi_channel = 1; @@ -15,23 +14,25 @@ AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi) } void AppTask::begin() { - Serial.println("[APP] Controller task started"); + Serial.println("[APP] Registering MIDI callbacks..."); - // Register MIDI callback midi_transport->on_midi_receive([this](const MidiEvent& event) { process_midi_event(event); }); + + Serial.println("[APP] Controller ready"); } void AppTask::update() { - // Poll switches for (uint8_t i = 0; i < NUM_PADS; i++) { bool is_pressed = switch_driver->is_pressed(i); if (is_pressed && !last_switch_state[i]) { + Serial.printf("[APP] Switch %d pressed\n", i); process_switch_event(i, true); last_switch_state[i] = true; } else if (!is_pressed && last_switch_state[i]) { + Serial.printf("[APP] Switch %d released\n", i); process_switch_event(i, false); last_switch_state[i] = false; } @@ -44,7 +45,6 @@ void AppTask::process_midi_event(const MidiEvent& event) { uint8_t midi_note = event.data1; uint8_t midi_velocity = event.data2; - // Find matching LED index from pad mapping for (uint8_t i = 0; i < NUM_PADS; i++) { if (pad_mapping[i].midi_channel == midi_channel && pad_mapping[i].midi_note == midi_note) { @@ -62,11 +62,13 @@ void AppTask::process_midi_event(const MidiEvent& event) { Serial.printf("[APP] MIDI -> LED: Ch%d Note%d Vel%d -> LED%d\n", midi_channel, midi_note, midi_velocity, led_index); + } else { + Serial.printf("[APP] MIDI Ch%d Note%d Vel%d - no LED mapping\n", + midi_channel, midi_note, midi_velocity); } } void AppTask::process_switch_event(uint8_t switch_id, bool pressed) { - // Find mapping for this switch for (uint8_t i = 0; i < NUM_PADS; i++) { if (pad_mapping[i].physical_switch == switch_id) { uint8_t channel = pad_mapping[i].midi_channel; diff --git a/src/led_stub.cpp b/src/led_stub.cpp index 7e100e2..72cdfd5 100644 --- a/src/led_stub.cpp +++ b/src/led_stub.cpp @@ -13,7 +13,40 @@ DefaultLedStub::DefaultLedStub() : initialized(false) { void DefaultLedStub::begin() { initialized = true; - Serial.println("[LED] Stub initialized (GPIO pins not configured yet)"); + + Serial.println("[LED] Startup colour cycle..."); + + uint16_t colours[] = { + 0xF800, // Red + 0x07E0, // Green + 0x001F, // Blue + 0xFFE0, // Yellow + 0xF81F, // Magenta + 0x07FF, // Cyan + 0xFFFF, // White + }; + int num_colours = sizeof(colours) / sizeof(colours[0]); + + for (int c = 0; c < num_colours; c++) { + uint16_t colour = colours[c]; + uint8_t r = (colour >> 11) & 0x1F; + uint8_t g = (colour >> 5) & 0x3F; + uint8_t b = colour & 0x1F; + + for (int i = 0; i < NUM_LEDS; i++) { + led_states[i].note = i; + led_states[i].channel = 1; + led_states[i].velocity = 100; + led_states[i].active = true; + led_states[i].timestamp = millis(); + } + + Serial.printf("[LED] Colour %d: R=%d G=%d B=%d\n", c, r, g, b); + delay(100); + } + + clear_all(); + Serial.println("[LED] Startup cycle complete"); } void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) { diff --git a/src/main.cpp b/src/main.cpp index 6301452..991b052 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,24 +7,20 @@ #include "switch_stub.h" #include "app_task.h" -// Hardware instances DefaultLedStub led_driver; DefaultSwitchStub switch_driver; UsbMidiTransport midi_transport; -// Controller task AppTask controller(&led_driver, &switch_driver, &midi_transport); -// FreeRTOS task handles TaskHandle_t midi_task_handle = NULL; -// MIDI processing task (runs on core 0) void midi_task(void* parameter) { Serial.println("[TASK] MIDI task started on core 0"); while (true) { midi_transport.update(); - vTaskDelay(1); // Yield to other tasks + vTaskDelay(1); } } @@ -32,22 +28,17 @@ void setup() { Serial.begin(115200); delay(1000); - Serial.println("============================="); - Serial.println("Loopy MIDI Controller v0.1"); - Serial.println("Phase 1: USB MIDI"); - Serial.println("============================="); + Serial.println("================================="); + Serial.println(" Loopy MIDI Controller v0.1"); + Serial.println(" Phase 1: USB MIDI"); + Serial.println("================================="); - // Initialize hardware stubs led_driver.begin(); switch_driver.begin(); - // Initialize MIDI transport midi_transport.begin(); - - // Initialize controller controller.begin(); - // Create MIDI task on core 0 (high priority) xTaskCreatePinnedToCore( midi_task, "midi_task", @@ -58,12 +49,13 @@ void setup() { 0 ); - Serial.println("[INIT] All systems ready"); - Serial.println("============================="); + Serial.println("================================="); + Serial.println(" All systems ready"); + Serial.println(" Waiting for USB connection..."); + Serial.println("================================="); } void loop() { - // Controller task runs on core 1 (main Arduino loop) controller.update(); - delay(10); // 10ms loop period + delay(10); } diff --git a/src/midi_transport.cpp b/src/midi_transport.cpp index 93841b6..a55c54f 100644 --- a/src/midi_transport.cpp +++ b/src/midi_transport.cpp @@ -1,6 +1,8 @@ #include "midi_transport.h" #include -#include "tusb.h" +#include + +static USBMIDI MIDI; UsbMidiTransport::UsbMidiTransport() : initialized(false) { } @@ -9,92 +11,95 @@ UsbMidiTransport::~UsbMidiTransport() { } bool UsbMidiTransport::begin() { - tusb_init(); + MIDI.begin(MIDI_CHANNEL_OMNI); initialized = true; - Serial.println("[MIDI] USB MIDI transport initialized"); + Serial.println("[MIDI] USB MIDI initialized (ArduinoUSBMIDI)"); return true; } void UsbMidiTransport::update() { if (!initialized) return; - tud_task(); - - while (tud_midi_available()) { - uint8_t packet[4]; - if (tud_midi_packet_read(packet)) { - MidiEvent event; - parse_midi_packet(packet, 4, event); - - const char* type_str = "UNK"; - switch (event.type) { - case MidiEvent::NOTE_ON: type_str = "NOTE_ON"; break; - case MidiEvent::NOTE_OFF: type_str = "NOTE_OFF"; break; - case MidiEvent::CONTROL_CHANGE: type_str = "CC"; break; - case MidiEvent::PROGRAM_CHANGE: type_str = "PC"; break; - case MidiEvent::PITCH_BEND: type_str = "PB"; break; - default: break; - } - Serial.printf("[MIDI IN] Ch:%d %s:%d:%d\n", event.channel, type_str, event.data1, event.data2); - - if (receive_callback) { - receive_callback(event); - } - } - } + MIDI.read(); } void UsbMidiTransport::on_midi_receive(std::function callback) { receive_callback = callback; + + MIDI.setHandleNoteOn([this](uint8_t channel, uint8_t note, uint8_t velocity) { + MidiEvent event; + event.type = (velocity > 0) ? MidiEvent::NOTE_ON : MidiEvent::NOTE_OFF; + event.channel = channel; + event.data1 = note; + event.data2 = velocity; + event.timestamp = millis(); + Serial.printf("[MIDI IN] Ch:%d NOTE_ON:%d:%d\n", channel, note, velocity); + if (receive_callback) receive_callback(event); + }); + + MIDI.setHandleNoteOff([this](uint8_t channel, uint8_t note, uint8_t velocity) { + MidiEvent event; + event.type = MidiEvent::NOTE_OFF; + event.channel = channel; + event.data1 = note; + event.data2 = velocity; + event.timestamp = millis(); + Serial.printf("[MIDI IN] Ch:%d NOTE_OFF:%d:%d\n", channel, note, velocity); + if (receive_callback) receive_callback(event); + }); + + MIDI.setHandleControlChange([this](uint8_t channel, uint8_t cc, uint8_t value) { + MidiEvent event; + event.type = MidiEvent::CONTROL_CHANGE; + event.channel = channel; + event.data1 = cc; + event.data2 = value; + event.timestamp = millis(); + Serial.printf("[MIDI IN] Ch:%d CC:%d:%d\n", channel, cc, value); + if (receive_callback) receive_callback(event); + }); + + MIDI.setHandleProgramChange([this](uint8_t channel, uint8_t program) { + MidiEvent event; + event.type = MidiEvent::PROGRAM_CHANGE; + event.channel = channel; + event.data1 = program; + event.data2 = 0; + event.timestamp = millis(); + Serial.printf("[MIDI IN] Ch:%d PC:%d\n", channel, program); + if (receive_callback) receive_callback(event); + }); + + MIDI.setHandlePitchBend([this](uint8_t channel, int bend) { + MidiEvent event; + event.type = MidiEvent::PITCH_BEND; + event.channel = channel; + event.data1 = bend & 0x7F; + event.data2 = (bend >> 7) & 0x7F; + event.timestamp = millis(); + Serial.printf("[MIDI IN] Ch:%d PB:%d\n", channel, bend); + if (receive_callback) receive_callback(event); + }); } void UsbMidiTransport::send_note_on(uint8_t channel, uint8_t note, uint8_t velocity) { if (!initialized) return; - uint8_t packet[4] = {0x09, (uint8_t)(0x90 | (channel - 1)), note, velocity}; - tud_midi_packet_write(packet); + MIDI.sendNoteOn(note, velocity, channel); + Serial.printf("[MIDI OUT] Ch:%d NOTE_ON:%d:%d\n", channel, note, velocity); } void UsbMidiTransport::send_note_off(uint8_t channel, uint8_t note, uint8_t velocity) { if (!initialized) return; - uint8_t packet[4] = {0x08, (uint8_t)(0x80 | (channel - 1)), note, velocity}; - tud_midi_packet_write(packet); + MIDI.sendNoteOff(note, velocity, channel); + Serial.printf("[MIDI OUT] Ch:%d NOTE_OFF:%d:%d\n", channel, note, velocity); } void UsbMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) { if (!initialized) return; - uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value}; - tud_midi_packet_write(packet); + MIDI.sendControlChange(cc, value, channel); + Serial.printf("[MIDI OUT] Ch:%d CC:%d:%d\n", channel, cc, value); } -void UsbMidiTransport::parse_midi_packet(const uint8_t* buffer, uint32_t size, MidiEvent& event) { - if (size < 4) return; - - uint8_t cin = buffer[0] & 0x0F; - uint8_t status = buffer[1]; - uint8_t type = status & 0xF0; - uint8_t channel = (status & 0x0F) + 1; - - event.channel = channel; - event.data1 = buffer[2]; - event.data2 = buffer[3]; - - switch (cin) { - case 0x8: event.type = MidiEvent::NOTE_OFF; break; - case 0x9: - event.type = (event.data2 > 0) ? MidiEvent::NOTE_ON : MidiEvent::NOTE_OFF; - break; - case 0xB: event.type = MidiEvent::CONTROL_CHANGE; break; - case 0xC: event.type = MidiEvent::PROGRAM_CHANGE; break; - case 0xE: event.type = MidiEvent::PITCH_BEND; break; - default: - switch (type) { - case 0x80: event.type = MidiEvent::NOTE_OFF; break; - case 0x90: event.type = (event.data2 > 0) ? MidiEvent::NOTE_ON : MidiEvent::NOTE_OFF; break; - case 0xB0: event.type = MidiEvent::CONTROL_CHANGE; break; - case 0xC0: event.type = MidiEvent::PROGRAM_CHANGE; break; - case 0xE0: event.type = MidiEvent::PITCH_BEND; break; - default: event.type = MidiEvent::NOTE_ON; break; - } - break; - } +bool UsbMidiTransport::is_connected() { + return initialized; }