#include "app_task.h" #include AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi) : led_driver(led), switch_driver(sw), midi_transport(midi) { // Launchpad X standard: bottom row = notes 36-45 (C2 to A2) on channel 1 const uint8_t launchpad_notes[10] = {36, 37, 38, 39, 40, 41, 42, 43, 44, 45}; for (uint8_t i = 0; i < NUM_PADS; i++) { pad_mapping[i].physical_switch = i; pad_mapping[i].midi_channel = 1; pad_mapping[i].midi_note = launchpad_notes[i]; pad_mapping[i].led_index = i; last_switch_state[i] = false; } } void AppTask::begin() { Serial.println("[APP] Registering MIDI callbacks..."); midi_transport->on_midi_receive([this](const MidiEvent& event) { process_midi_event(event); }); Serial.println("[APP] Controller ready - Launchpad X mode (notes 36-45, ch1)"); } void AppTask::update() { 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; } } } void AppTask::process_midi_event(const MidiEvent& event) { Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n", event.type, event.channel, event.data1, event.data2); // Flash LED 0 white briefly on ANY MIDI input - visual activity indicator // (visible without serial when connected to iPad) led_driver->flash_activity(); if (event.type == MidiEvent::SYSEX) { // Cin is encoded in channel for SYSEX packets uint8_t cin = event.channel; uint8_t packet[3] = {event.data1, event.data2, 0}; process_sysex_packet(packet, cin); led_driver->flash_sysex(); return; } uint8_t led_index = 0xFF; uint8_t midi_channel = event.channel; uint8_t midi_note = event.data1; uint8_t midi_velocity = event.data2; // Launchpad X: NOTE_ON/NOTE_OFF on channels 1-3 // ch1 = static, ch2 = flashing, ch3 = pulsing // Notes 36-45 (C2-A2) map to pads 0-9 // Velocity 1-127 = color palette index if (event.type == MidiEvent::NOTE_ON || event.type == MidiEvent::NOTE_OFF) { if (midi_channel >= 1 && midi_channel <= 3) { for (uint8_t i = 0; i < NUM_PADS; i++) { if (pad_mapping[i].midi_note == midi_note) { led_index = pad_mapping[i].led_index; break; } } if (led_index < NUM_PADS) { uint8_t color_vel = (event.type == MidiEvent::NOTE_ON) ? midi_velocity : 0; led_driver->set_led_state( midi_note, midi_channel, color_vel ); Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n", midi_channel, midi_note, color_vel, led_index); } else { Serial.printf("[APP] NOTE Ch%d Note%d Vel%d - no mapping\n", midi_channel, midi_note, midi_velocity); } } else { Serial.printf("[APP] NOTE Ch%d ignored (not Launchpad channel 1-3)\n", midi_channel); } } // CONTROL_CHANGE fallback for generic MIDI / Loopy Pro generic mode else if (event.type == MidiEvent::CONTROL_CHANGE) { uint8_t cc_num = event.data1; uint8_t cc_val = event.data2; // Map CC to pad: CC2-11 (Loopy Pro), CC0-9, CC36-45 if (cc_num >= 2 && cc_num < 2 + NUM_PADS) { led_index = cc_num - 2; } else if (cc_num < NUM_PADS) { led_index = cc_num; } else if (cc_num >= 36 && cc_num < 36 + NUM_PADS) { led_index = cc_num - 36; } if (led_index < NUM_PADS) { led_driver->set_led_state( pad_mapping[led_index].midi_note, pad_mapping[led_index].midi_channel, cc_val ); Serial.printf("[APP] CC -> LED: Ch%d CC%d Val%d -> LED%d\n", midi_channel, cc_num, cc_val, led_index); } else { Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n", midi_channel, cc_num, cc_val); } } } void AppTask::process_switch_event(uint8_t switch_id, bool pressed) { 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; // Loopy Pro Launchpad mode expects NOTE_ON/NOTE_OFF on notes 36-45 uint8_t note = pad_mapping[i].midi_note; uint8_t velocity = pressed ? 127 : 0; if (pressed) { midi_transport->send_note_on(channel, note, velocity); } else { midi_transport->send_note_off(channel, note, velocity); } Serial.printf("[APP] Switch %d -> Ch%d Note%d Vel%d (%s)\n", switch_id, channel, note, velocity, pressed ? "PRESS" : "RELEASE"); break; } } } void AppTask::process_sysex_packet(const uint8_t* packet, uint8_t cin) { // Cin values: 0x4=start/short, 0x5=continue, 0x6=end (2 bytes), 0x7=end (1 byte/3 bytes) if (cin == 0x4) { // SysEx start sysex_active = true; sysex_len = 0; } if (!sysex_active || sysex_len >= SYSEX_MAX_LEN) return; // Add data bytes (skip F0/F7 which are handled by Cin) sysex_buffer[sysex_len++] = packet[0]; if (cin == 0x4 || cin == 0x5 || cin == 0x6) { sysex_buffer[sysex_len++] = packet[1]; } if (cin == 0x6 || cin == 0x7) { // SysEx end sysex_active = false; handle_sysex(sysex_buffer, sysex_len); sysex_len = 0; } } void AppTask::handle_sysex(const uint8_t* data, uint8_t len) { if (len < 7) return; // Check Novation SysEx header: F0 00 20 29 02 0C/0D ... if (data[0] != 0x00 || data[1] != 0x20 || data[2] != 0x29 || data[3] != 0x02) { return; } uint8_t sub_id = data[4]; uint8_t command = data[5]; Serial.printf("[APP] SysEx: sub=%02X cmd=%02X len=%d\n", sub_id, command, len); // Command 0x00 = Layout select, 0x0E = Programmer/Live mode if (command == 0x00 && len >= 7) { // Layout select uint8_t layout = data[6]; Serial.printf("[APP] Layout select: %02X\n", layout); // 0x7F = Programmer mode if (layout == 0x7F) { Serial.println("[APP] Entered Programmer mode"); } } else if (command == 0x0E && len >= 7) { // Programmer/Live mode uint8_t mode = data[6]; Serial.printf("[APP] Programmer mode: %02X\n", mode); } }