Files
loopy_midi_controller/src/app_task.cpp
T

206 lines
7.0 KiB
C++

#include "app_task.h"
#include <Arduino.h>
extern volatile uint8_t beats_per_bar;
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};
const uint8_t cc_assignments[10] = {112, 22, 113, 25, 114, 24, 115, 26, 116, 117};
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;
cc_map[i] = cc_assignments[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 - CC mode");
for (uint8_t i = 0; i < NUM_PADS; i++) {
Serial.printf("[APP] Pad %d -> CC%d -> LED%d\n", i + 1, cc_map[i], i);
}
}
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]) {
process_switch_event(i, true);
last_switch_state[i] = true;
} else if (!is_pressed && last_switch_state[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);
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);
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,
led_index
);
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: look up which pad this CC belongs to
else if (event.type == MidiEvent::CONTROL_CHANGE) {
uint8_t cc_num = event.data1;
uint8_t cc_val = event.data2;
for (uint8_t i = 0; i < NUM_PADS; i++) {
if (cc_map[i] == cc_num) {
led_index = i;
break;
}
}
if (led_index < NUM_PADS) {
led_driver->set_led_state(
pad_mapping[led_index].midi_note,
pad_mapping[led_index].midi_channel,
cc_val,
led_index
);
Serial.printf("[APP] CC%d Val%d -> LED%d\n",
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) {
// Time signature combo: hold pad 10 (switch 9) + press pad 1/2/3
if (switch_id <= 2 && switch_driver->is_pressed(9)) {
if (pressed) {
switch (switch_id) {
case 0: beats_per_bar = 4; Serial.println("[APP] Time sig: 4/4"); break;
case 1: beats_per_bar = 3; Serial.println("[APP] Time sig: 3/4"); break;
case 2: beats_per_bar = 6; Serial.println("[APP] Time sig: 6/4"); break;
}
}
return; // suppress CC in combo mode
}
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;
uint8_t cc_num = cc_map[i];
// Use palette index 127 (magenta) for visible feedback
uint8_t value = pressed ? 127 : 0;
if (pressed) {
midi_transport->send_cc(channel, cc_num, value);
}
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);
}
}