Compare commits

36 Commits
Author SHA1 Message Date
ash 20e9d4a08d cal_max 950 2026-07-09 07:30:47 +00:00
ash 8aad6fdccd Reduce cal_max to 1000 2026-07-09 07:28:03 +00:00
ash 3558bb1050 Reduce cal_max to 1050 for full 0-127 MIDI range 2026-07-09 07:24:58 +00:00
ash cadb29f234 Reduce exp pedal cal_max from 1180 to 1100 to reach 100% MIDI 2026-07-04 05:20:20 +00:00
ash f055bad4ac Proportional catch-up: send midpoint per update (fast convergence, rejects ±1 jitter) 2026-07-04 05:17:48 +00:00
ash 1dec81e329 Rate-limit exp pedal: step ±1/update, stationary jitter ignored 2026-07-04 05:15:09 +00:00
ash 17def39f93 Remove hysteresis (smoothing handles noise); values now step ±1 per update 2026-07-04 05:11:47 +00:00
ash 4f58d4b8e6 Exponential smoothing on expression pedal ADC (factor 8) to filter noise 2026-07-04 05:05:35 +00:00
ash 33c9dd10c0 Send CC 0 on release (MIDI Learn fix); flash_latency=0 2026-07-03 22:32:24 +00:00
ash dfa339a9b9 Merge branch 'bugfix_tempo' 2026-07-03 08:43:01 +00:00
ash 4aec064f3c Startup animation iterates in pixel pairs (0+1, 2+3, ..., 8+9) 2026-07-03 08:41:36 +00:00
ash 39326e3247 Startup animation iterates in pixel pairs (0+1, 2+3, ..., 8+9) 2026-07-03 08:41:26 +00:00
ash 88fee54f96 Remove beat 1 differentiation - all beats flash white 50ms 2026-07-03 08:36:14 +00:00
ash 868c5eda82 Auto-detect time signature from SPP deltas (bar boundaries) 2026-07-03 08:32:03 +00:00
ash d7f9bb16fd Fix beats_per_bar linkage (remove static) 2026-07-03 08:26:48 +00:00
ash b3f4eb1a8b Add SPP (Song Position Pointer) detection - uses SPP on START to set correct tick position. Add beats_per_bar (default 4) with button combo: hold 10 + press 1=4/4, 2=3/4, 3=6/4 2026-07-03 08:22:47 +00:00
ash 3226986709 Capture tick 0 (last_beat=UINT32_MAX) so downbeat is not skipped 2026-07-03 08:13:08 +00:00
ash e1507d5d7c Beat 1 = % 4 == 1 (last untested offset) 2026-07-03 08:09:35 +00:00
ash 3c5e0b07ae Try % 4 == 2 for beat 1; latency 55ms 2026-07-03 08:07:24 +00:00
ash 9d1a2fdc87 Shift beat-1 detection to % 4 == 3 (was off by 3 beats); latency 60ms 2026-07-03 08:04:40 +00:00
ash 7c93839f94 Bump flash latency to 50ms 2026-07-03 08:00:03 +00:00
ash 907279e1ea Make flash latency tunable at runtime via 'latency N' serial command 2026-07-03 07:57:31 +00:00
ash df1f678845 Add 20ms flash latency to compensate for audio buffer (flash was ahead of beat) 2026-07-03 07:53:04 +00:00
ash b859527169 Beat 1 = RED (100ms), other beats = WHITE (50ms) 2026-07-03 07:50:16 +00:00
ash ad977d3b09 Beat 1 = yellow, other beats = white — visible color contrast 2026-07-03 07:46:26 +00:00
ash acd3cc51fe Beat 1 (every 4th) = full white 50ms snap, others = barely visible 6,6,6 — sharp contrast for phase detection 2026-07-03 07:43:26 +00:00
ash eb5d66d29b Clean baseline: skip tick-0 flash, first flash at tick 24 (first beat boundary) 2026-07-03 07:40:53 +00:00
ash 354f18efe7 Beat 0 (first beat after START) full white, subsequent beats dimmer — marks bar downbeat 2026-07-03 07:27:06 +00:00
ash eda2a959dc Remove time signature assumption — all beats equal brightness 2026-07-03 07:25:58 +00:00
ash 53752365b8 Debug: beat 1 (downbeat) full white, other beats dimmer (64) — visually identify phase 2026-07-03 07:25:21 +00:00
ash ea45c927e7 Fix clock latency: process one USB packet per update() — no more batch-processing 0xF8 bursts 2026-07-03 07:19:05 +00:00
ash 72496d2daf Fix MIDI clock phase: handle 0xFA START to reset tick counter to downbeat 2026-07-03 07:13:38 +00:00
ash 1a70cbe870 Fix duplicate/stray code in midi_task function 2026-07-03 07:05:56 +00:00
ash ec0cdc9438 Fix midi_tick_count declaration and 0xF8 detection: add proper extern + transport clock parsing 2026-07-03 07:04:40 +00:00
ash 9d2872a513 Fix pixel 6 beat timing: align with actual MIDI beats using tick-based counting (24 ticks per beat) 2026-07-03 07:00:02 +00:00
ash c83a500b3c Fix pixel 6 beat timing: align flashes with real-time beats, not just MIDI ticks 2026-07-03 06:55:50 +00:00
7 changed files with 151 additions and 39 deletions
+1 -1
View File
@@ -24,11 +24,11 @@ private:
uint8_t midi_channel; uint8_t midi_channel;
uint8_t midi_cc; uint8_t midi_cc;
static const uint8_t HYSTERESIS = 3;
static const uint16_t READ_INTERVAL_MS = 5; static const uint16_t READ_INTERVAL_MS = 5;
uint32_t last_read_time; uint32_t last_read_time;
uint16_t current_raw; uint16_t current_raw;
uint16_t smoothed_raw;
uint16_t cal_min; uint16_t cal_min;
uint16_t cal_max; uint16_t cal_max;
+4
View File
@@ -3,6 +3,10 @@
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
extern volatile uint32_t midi_tick_count;
extern volatile uint16_t last_spp_position;
extern volatile bool spp_valid;
struct MidiEvent { struct MidiEvent {
enum Type { enum Type {
NOTE_ON, NOTE_ON,
+15 -4
View File
@@ -1,6 +1,8 @@
#include "app_task.h" #include "app_task.h"
#include <Arduino.h> #include <Arduino.h>
extern volatile uint8_t beats_per_bar;
AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi) AppTask::AppTask(LedStub* led, SwitchStub* sw, UsbMidiTransport* midi)
: led_driver(led), switch_driver(sw), midi_transport(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) { 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++) { for (uint8_t i = 0; i < NUM_PADS; i++) {
if (pad_mapping[i].physical_switch == switch_id) { if (pad_mapping[i].physical_switch == switch_id) {
uint8_t channel = pad_mapping[i].midi_channel; uint8_t channel = pad_mapping[i].midi_channel;
uint8_t cc_num = cc_map[i]; uint8_t cc_num = cc_map[i];
// Use palette index 127 (magenta) for visible feedback // Use palette index 127 (magenta) for visible feedback
uint8_t value = pressed ? 127 : 0; uint8_t value = pressed ? 127 : 0;
midi_transport->send_cc(channel, cc_num, value);
if (pressed) {
midi_transport->send_cc(channel, cc_num, value);
}
break; break;
} }
} }
+16 -13
View File
@@ -10,8 +10,9 @@ ExpressionPedal::ExpressionPedal(uint8_t adc_pin)
, midi_cc(4) , midi_cc(4)
, last_read_time(0) , last_read_time(0)
, current_raw(0) , current_raw(0)
, smoothed_raw(0)
, cal_min(36) , cal_min(36)
, cal_max(1180) , cal_max(950)
{ {
} }
@@ -21,7 +22,8 @@ void ExpressionPedal::begin() {
pinMode(adc_pin, INPUT); pinMode(adc_pin, INPUT);
delay(10); delay(10);
current_raw = analogRead(adc_pin); 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; last_sent_value = current_value;
Serial.printf("[EXP] Starting: raw ADC=%d -> MIDI=%d (cal: %d-%d)\n", Serial.printf("[EXP] Starting: raw ADC=%d -> MIDI=%d (cal: %d-%d)\n",
current_raw, current_value, cal_min, cal_max); current_raw, current_value, cal_min, cal_max);
@@ -34,18 +36,19 @@ void ExpressionPedal::update(UsbMidiTransport& midi) {
last_read_time = now; last_read_time = now;
current_raw = analogRead(adc_pin); 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 || // Proportional catch-up: send midpoint rounded up each update.
new_value + HYSTERESIS < current_value || // Rejects ±1 stationary jitter but converges to exact value on movement
(new_value != current_value && (new_value == 0 || new_value == 127))) { // (e.g., 0→127 converges in ~35ms).
current_value = new_value; 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);
if (current_value != last_sent_value) { } else if (current_value < last_sent_value - 1) {
last_sent_value = current_value; last_sent_value = (last_sent_value + current_value) / 2;
midi.send_cc(midi_channel, midi_cc, current_value); midi.send_cc(midi_channel, midi_cc, last_sent_value);
Serial.printf("[EXP] CC%d: %d (raw ADC: %d)\n", midi_cc, current_value, current_raw);
} }
} }
+10 -6
View File
@@ -194,21 +194,25 @@ void DefaultLedStub::begin() {
return; 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 // Sweep each pixel-pair through palette, then off
for (int i = 0; i < NUM_LEDS; i++) { for (int pair = 0; pair < 5; pair++) {
int i1 = pair * 2;
int i2 = pair * 2 + 1;
for (int c = 1; c <= 127; c += 8) { for (int c = 1; c <= 127; c += 8) {
uint32_t color = launchpad_palette[c]; uint32_t color = launchpad_palette[c];
uint8_t r = (color >> 16) & 0xFF; uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF; uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 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(); mux_ptr->show();
delay(15); delay(15);
} }
// Turn off this LED before moving to next // Turn off this pair before moving to next
mux_ptr->set_led_color(i, 0, 0, 0); mux_ptr->set_led_color(i1, 0, 0, 0);
mux_ptr->set_led_color(i2, 0, 0, 0);
mux_ptr->show(); mux_ptr->show();
} }
+42
View File
@@ -21,14 +21,44 @@ UsbMidiTransport midi_transport;
AppTask controller(&led_driver, &switch_driver, &midi_transport); AppTask controller(&led_driver, &switch_driver, &midi_transport);
ExpressionPedal exp_pedal(4); 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; TaskHandle_t midi_task_handle = NULL;
void midi_task(void* parameter) { void midi_task(void* parameter) {
Serial.println("[TASK] MIDI task started on core 0"); 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) { while (true) {
midi_transport.update(); midi_transport.update();
exp_pedal.update(midi_transport); 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); vTaskDelay(1);
} }
} }
@@ -89,6 +119,16 @@ void handle_serial_command(const String& cmd) {
} else if (cmd == "exp") { } else if (cmd == "exp") {
Serial.printf("[CMD] EXP ADC=%d MIDI=%d\n", Serial.printf("[CMD] EXP ADC=%d MIDI=%d\n",
exp_pedal.get_raw(), exp_pedal.get_value()); 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") { } else if (cmd == "usb") {
Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO"); Serial.printf("[CMD] USB mounted: %s\n", TinyUSBDevice.mounted() ? "YES" : "NO");
Serial.printf("[CMD] USB ready: %s\n", TinyUSBDevice.ready() ? "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(" red/green/blue - solid colour");
Serial.println(" pixel0/pixel1 - single pixel test"); Serial.println(" pixel0/pixel1 - single pixel test");
Serial.println(" exp - expression pedal ADC/MIDI value"); 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(" usb - USB connection status and descriptor info");
Serial.println(" gpiotest - raw GPIO pin diagnostic"); Serial.println(" gpiotest - raw GPIO pin diagnostic");
Serial.println(" rawled - bit-bang WS2812 (no library)"); Serial.println(" rawled - bit-bang WS2812 (no library)");
+63 -15
View File
@@ -2,6 +2,11 @@
#include <Arduino.h> #include <Arduino.h>
#include "Adafruit_TinyUSB.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; static Adafruit_USBD_MIDI usb_midi;
UsbMidiTransport::UsbMidiTransport() : initialized(false) { UsbMidiTransport::UsbMidiTransport() : initialized(false) {
@@ -48,25 +53,68 @@ void UsbMidiTransport::update() {
TinyUSBDevice.mounted() ? "YES" : "NO"); TinyUSBDevice.mounted() ? "YES" : "NO");
} }
while (usb_midi.available()) { if (usb_midi.available()) {
uint8_t packet[4]; uint8_t packet[4];
if (usb_midi.readPacket(packet)) { if (usb_midi.readPacket(packet)) {
MidiEvent event; uint8_t cin = packet[0] & 0x0F;
parse_midi_packet(packet, 4, event); 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"; // Auto-detect time signature from SPP delta (sent at bar boundaries)
switch (event.type) { if (prev_spp > 0 && last_spp_position > prev_spp) {
case MidiEvent::NOTE_ON: type_str = "NOTE_ON"; break; uint16_t delta = last_spp_position - prev_spp;
case MidiEvent::NOTE_OFF: type_str = "NOTE_OFF"; break; if (delta >= 8 && delta <= 64 && delta % 4 == 0) {
case MidiEvent::CONTROL_CHANGE: type_str = "CC"; break; uint8_t detected = delta / 4;
case MidiEvent::PROGRAM_CHANGE: type_str = "PC"; break; if (detected >= 2 && detected <= 16 && detected != beats_per_bar) {
case MidiEvent::PITCH_BEND: type_str = "PB"; break; beats_per_bar = detected;
default: break; Serial.printf("[CLK] Auto-detected %d/4 time from SPP delta=%d\n", detected, delta);
} }
Serial.printf("[MIDI IN] Ch:%d %s:%d:%d\n", event.channel, type_str, event.data1, event.data2); }
}
} else {
MidiEvent event;
parse_midi_packet(packet, 4, event);
if (receive_callback) { const char* type_str = "UNK";
receive_callback(event); 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);
}
} }
} }
} }