500720dadf
Flash LED 0 white for 30ms on any MIDI input. Visible without serial - helps diagnose whether MIDI is arriving when connected to iPad with Loopy Pro.
140 lines
5.1 KiB
C++
140 lines
5.1 KiB
C++
#include "app_task.h"
|
|
#include <Arduino.h>
|
|
|
|
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();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|