Compare commits
36
Commits
a483eebd84
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e9d4a08d | ||
|
|
8aad6fdccd | ||
|
|
3558bb1050 | ||
|
|
cadb29f234 | ||
|
|
f055bad4ac | ||
|
|
1dec81e329 | ||
|
|
17def39f93 | ||
|
|
4f58d4b8e6 | ||
|
|
33c9dd10c0 | ||
|
|
dfa339a9b9 | ||
|
|
4aec064f3c | ||
|
|
39326e3247 | ||
|
|
88fee54f96 | ||
|
|
868c5eda82 | ||
|
|
d7f9bb16fd | ||
|
|
b3f4eb1a8b | ||
|
|
3226986709 | ||
|
|
e1507d5d7c | ||
|
|
3c5e0b07ae | ||
|
|
9d1a2fdc87 | ||
|
|
7c93839f94 | ||
|
|
907279e1ea | ||
|
|
df1f678845 | ||
|
|
b859527169 | ||
|
|
ad977d3b09 | ||
|
|
acd3cc51fe | ||
|
|
eb5d66d29b | ||
|
|
354f18efe7 | ||
|
|
eda2a959dc | ||
|
|
53752365b8 | ||
|
|
ea45c927e7 | ||
|
|
72496d2daf | ||
|
|
1a70cbe870 | ||
|
|
ec0cdc9438 | ||
|
|
9d2872a513 | ||
|
|
c83a500b3c |
@@ -24,11 +24,11 @@ private:
|
||||
uint8_t midi_channel;
|
||||
uint8_t midi_cc;
|
||||
|
||||
static const uint8_t HYSTERESIS = 3;
|
||||
static const uint16_t READ_INTERVAL_MS = 5;
|
||||
uint32_t last_read_time;
|
||||
|
||||
uint16_t current_raw;
|
||||
uint16_t smoothed_raw;
|
||||
uint16_t cal_min;
|
||||
uint16_t cal_max;
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
extern volatile uint32_t midi_tick_count;
|
||||
extern volatile uint16_t last_spp_position;
|
||||
extern volatile bool spp_valid;
|
||||
|
||||
struct MidiEvent {
|
||||
enum Type {
|
||||
NOTE_ON,
|
||||
|
||||
+15
-4
@@ -1,6 +1,8 @@
|
||||
#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) {
|
||||
|
||||
@@ -124,16 +126,25 @@ void AppTask::process_midi_event(const MidiEvent& event) {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
midi_transport->send_cc(channel, cc_num, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-13
@@ -10,8 +10,9 @@ ExpressionPedal::ExpressionPedal(uint8_t adc_pin)
|
||||
, midi_cc(4)
|
||||
, last_read_time(0)
|
||||
, current_raw(0)
|
||||
, smoothed_raw(0)
|
||||
, cal_min(36)
|
||||
, cal_max(1180)
|
||||
, cal_max(950)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -21,7 +22,8 @@ void ExpressionPedal::begin() {
|
||||
pinMode(adc_pin, INPUT);
|
||||
delay(10);
|
||||
current_raw = analogRead(adc_pin);
|
||||
current_value = adc_to_midi(current_raw);
|
||||
smoothed_raw = current_raw;
|
||||
current_value = adc_to_midi(smoothed_raw);
|
||||
last_sent_value = current_value;
|
||||
Serial.printf("[EXP] Starting: raw ADC=%d -> MIDI=%d (cal: %d-%d)\n",
|
||||
current_raw, current_value, cal_min, cal_max);
|
||||
@@ -34,18 +36,19 @@ void ExpressionPedal::update(UsbMidiTransport& midi) {
|
||||
last_read_time = now;
|
||||
|
||||
current_raw = analogRead(adc_pin);
|
||||
uint8_t new_value = adc_to_midi(current_raw);
|
||||
smoothed_raw = (smoothed_raw * 7 + current_raw + 4) / 8;
|
||||
uint8_t new_value = adc_to_midi(smoothed_raw);
|
||||
current_value = new_value;
|
||||
|
||||
if (new_value > current_value + HYSTERESIS ||
|
||||
new_value + HYSTERESIS < current_value ||
|
||||
(new_value != current_value && (new_value == 0 || new_value == 127))) {
|
||||
current_value = new_value;
|
||||
}
|
||||
|
||||
if (current_value != last_sent_value) {
|
||||
last_sent_value = current_value;
|
||||
midi.send_cc(midi_channel, midi_cc, current_value);
|
||||
Serial.printf("[EXP] CC%d: %d (raw ADC: %d)\n", midi_cc, current_value, current_raw);
|
||||
// Proportional catch-up: send midpoint rounded up each update.
|
||||
// Rejects ±1 stationary jitter but converges to exact value on movement
|
||||
// (e.g., 0→127 converges in ~35ms).
|
||||
if (current_value > last_sent_value + 1) {
|
||||
last_sent_value = (last_sent_value + current_value + 1) / 2;
|
||||
midi.send_cc(midi_channel, midi_cc, last_sent_value);
|
||||
} else if (current_value < last_sent_value - 1) {
|
||||
last_sent_value = (last_sent_value + current_value) / 2;
|
||||
midi.send_cc(midi_channel, midi_cc, last_sent_value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -194,21 +194,25 @@ void DefaultLedStub::begin() {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[LED] Launchpad-style startup animation...");
|
||||
Serial.println("[LED] Launchpad-style startup animation (paired)...");
|
||||
|
||||
// Launchpad X style: sweep each LED through palette, then all-off
|
||||
for (int i = 0; i < NUM_LEDS; i++) {
|
||||
// Sweep each pixel-pair through palette, then off
|
||||
for (int pair = 0; pair < 5; pair++) {
|
||||
int i1 = pair * 2;
|
||||
int i2 = pair * 2 + 1;
|
||||
for (int c = 1; c <= 127; c += 8) {
|
||||
uint32_t color = launchpad_palette[c];
|
||||
uint8_t r = (color >> 16) & 0xFF;
|
||||
uint8_t g = (color >> 8) & 0xFF;
|
||||
uint8_t b = color & 0xFF;
|
||||
mux_ptr->set_led_color(i, r, g, b);
|
||||
mux_ptr->set_led_color(i1, r, g, b);
|
||||
mux_ptr->set_led_color(i2, r, g, b);
|
||||
mux_ptr->show();
|
||||
delay(15);
|
||||
}
|
||||
// Turn off this LED before moving to next
|
||||
mux_ptr->set_led_color(i, 0, 0, 0);
|
||||
// Turn off this pair before moving to next
|
||||
mux_ptr->set_led_color(i1, 0, 0, 0);
|
||||
mux_ptr->set_led_color(i2, 0, 0, 0);
|
||||
mux_ptr->show();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,44 @@ UsbMidiTransport midi_transport;
|
||||
AppTask controller(&led_driver, &switch_driver, &midi_transport);
|
||||
ExpressionPedal exp_pedal(4);
|
||||
|
||||
volatile uint32_t flash_latency = 0;
|
||||
volatile uint8_t beats_per_bar = 4; // change to match your project's time signature
|
||||
|
||||
TaskHandle_t midi_task_handle = NULL;
|
||||
|
||||
void midi_task(void* parameter) {
|
||||
Serial.println("[TASK] MIDI task started on core 0");
|
||||
|
||||
// Beat timing fixes for precise beat alignment - sync to real MIDI time, not hardcoded tempo
|
||||
// UINT32_MAX ensures tick 0 (first 0xF8 after START) triggers a flash
|
||||
uint32_t last_beat = UINT32_MAX;
|
||||
uint32_t flash_start = 0;
|
||||
|
||||
while (true) {
|
||||
midi_transport.update();
|
||||
exp_pedal.update(midi_transport);
|
||||
|
||||
uint32_t tick = midi_tick_count;
|
||||
uint32_t now = millis();
|
||||
|
||||
uint32_t current_beat = tick / 24;
|
||||
|
||||
if (current_beat != last_beat) {
|
||||
last_beat = current_beat;
|
||||
flash_start = now + flash_latency;
|
||||
// All beats flash white
|
||||
mux.set_led_color(6, 255, 255, 255);
|
||||
}
|
||||
|
||||
if (flash_start > 0 && now >= flash_start) {
|
||||
mux.show();
|
||||
if (now - flash_start >= 50) {
|
||||
mux.set_led_color(6, 20, 20, 20);
|
||||
mux.show();
|
||||
flash_start = 0;
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
@@ -89,6 +119,16 @@ void handle_serial_command(const String& cmd) {
|
||||
} else if (cmd == "exp") {
|
||||
Serial.printf("[CMD] EXP ADC=%d MIDI=%d\n",
|
||||
exp_pedal.get_raw(), exp_pedal.get_value());
|
||||
} else if (cmd == "latency") {
|
||||
Serial.printf("[CMD] Current flash latency: %d ms\n", flash_latency);
|
||||
} else if (cmd.startsWith("latency ")) {
|
||||
int val = atoi(cmd.c_str() + 8);
|
||||
if (val >= 0 && val <= 500) {
|
||||
flash_latency = val;
|
||||
Serial.printf("[CMD] Flash latency set to %d ms\n", flash_latency);
|
||||
} else {
|
||||
Serial.println("[CMD] Latency must be 0-500 ms");
|
||||
}
|
||||
} else if (cmd == "usb") {
|
||||
Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO");
|
||||
Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "YES" : "NO");
|
||||
@@ -133,6 +173,8 @@ void handle_serial_command(const String& cmd) {
|
||||
Serial.println(" red/green/blue - solid colour");
|
||||
Serial.println(" pixel0/pixel1 - single pixel test");
|
||||
Serial.println(" exp - expression pedal ADC/MIDI value");
|
||||
Serial.println(" latency - show current flash latency");
|
||||
Serial.println(" latency N - set flash latency to N ms (0-500)");
|
||||
Serial.println(" usb - USB connection status and descriptor info");
|
||||
Serial.println(" gpiotest - raw GPIO pin diagnostic");
|
||||
Serial.println(" rawled - bit-bang WS2812 (no library)");
|
||||
|
||||
+63
-15
@@ -2,6 +2,11 @@
|
||||
#include <Arduino.h>
|
||||
#include "Adafruit_TinyUSB.h"
|
||||
|
||||
volatile uint32_t midi_tick_count = 0;
|
||||
volatile uint16_t last_spp_position = 0;
|
||||
volatile bool spp_valid = false;
|
||||
extern volatile uint8_t beats_per_bar;
|
||||
|
||||
static Adafruit_USBD_MIDI usb_midi;
|
||||
|
||||
UsbMidiTransport::UsbMidiTransport() : initialized(false) {
|
||||
@@ -48,25 +53,68 @@ void UsbMidiTransport::update() {
|
||||
TinyUSBDevice.mounted() ? "YES" : "NO");
|
||||
}
|
||||
|
||||
while (usb_midi.available()) {
|
||||
if (usb_midi.available()) {
|
||||
uint8_t packet[4];
|
||||
if (usb_midi.readPacket(packet)) {
|
||||
MidiEvent event;
|
||||
parse_midi_packet(packet, 4, event);
|
||||
uint8_t cin = packet[0] & 0x0F;
|
||||
if (cin == 0x0F) {
|
||||
if (packet[1] == 0xF8) {
|
||||
midi_tick_count++;
|
||||
} else if (packet[1] == 0xFA) {
|
||||
if (spp_valid) {
|
||||
midi_tick_count = last_spp_position * 6;
|
||||
spp_valid = false;
|
||||
Serial.printf("[CLK] START at SPP=%d -> tick %d\n", last_spp_position, midi_tick_count);
|
||||
} else {
|
||||
midi_tick_count = 0xFFFFFFFF;
|
||||
Serial.println("[CLK] START (no SPP) - next F8 = tick 0");
|
||||
}
|
||||
} else if (packet[1] == 0xFB) {
|
||||
// CONTINUE - same as START for our purposes
|
||||
if (spp_valid) {
|
||||
midi_tick_count = last_spp_position * 6;
|
||||
spp_valid = false;
|
||||
Serial.printf("[CLK] CONTINUE at SPP=%d -> tick %d\n", last_spp_position, midi_tick_count);
|
||||
} else {
|
||||
Serial.println("[CLK] CONTINUE (no SPP)");
|
||||
}
|
||||
}
|
||||
} else if (cin == 0x03 && packet[1] == 0xF2) {
|
||||
// Song Position Pointer
|
||||
uint16_t prev_spp = last_spp_position;
|
||||
last_spp_position = (packet[3] << 7) | packet[2];
|
||||
spp_valid = true;
|
||||
Serial.printf("[CLK] SPP=%d\n", last_spp_position);
|
||||
|
||||
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);
|
||||
// Auto-detect time signature from SPP delta (sent at bar boundaries)
|
||||
if (prev_spp > 0 && last_spp_position > prev_spp) {
|
||||
uint16_t delta = last_spp_position - prev_spp;
|
||||
if (delta >= 8 && delta <= 64 && delta % 4 == 0) {
|
||||
uint8_t detected = delta / 4;
|
||||
if (detected >= 2 && detected <= 16 && detected != beats_per_bar) {
|
||||
beats_per_bar = detected;
|
||||
Serial.printf("[CLK] Auto-detected %d/4 time from SPP delta=%d\n", detected, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MidiEvent event;
|
||||
parse_midi_packet(packet, 4, event);
|
||||
|
||||
if (receive_callback) {
|
||||
receive_callback(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user