Compare commits

1 Commits
Author SHA1 Message Date
ash 31cd488488 Remove heartbeat on pixel 9 2026-06-30 03:30:15 +00:00
15 changed files with 136 additions and 874 deletions
-5
View File
@@ -1,5 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
-10
View File
@@ -1,10 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
-467
View File
@@ -1,467 +0,0 @@
# Loopy MIDI Controller - USB MIDI Foot Controller
A compact ESP32-S3 based USB MIDI foot controller with hardware acceleration, designed to work seamlessly with Loopy Pro.
## Overview
This project implements a USB MIDI device that accepts foot pedal inputs and converts them to MIDI Continuous Controller (CC) messages. It's designed to be a reliable replacement for the MIDI-to-USB adapters used with Loopy Pro, offering low latency (1-5ms) and USB compatibility without Bluetooth interference.
**Key Features:**
- **USB MIDI Interface**: Uses ESP32-S3's native USB MIDI support for stable, reliable connectivity
- **Expression Pedal Support**: Read analog input with configurable calibration
- **Launchpad X Compatible**: Per-pad LED colors matching Novation Launchpad hardware
- **Low Latency Design**: 1-5ms response time for responsive control
- **MIDI Clock Sync**: Pixel 6 pulses in time with Loopy Pro tempo
- **Hardware Acceleration**: Uses multiple cores efficiently without cross-core LED issues
## Hardware Requirements
### PCB/Layout Notes
- **ESP32-S3-WROOM-1** microcontroller
- **Daisy-chained 74HC165** shift registers for button inputs (16 buttons total)
- **2x RMT WS2812C** LED drivers for 10 programmable RGB pixels
- **ADC on GPIO4** for expression pedal input
- **WS2812 external LED** for boot animation (GPIO12)
### Required Components
- ESP32-S3 development board (ESP32-S3-WROOM-1 preferred)
- 74HC165 shift register (2x for 16 buttons)
- WS2812C LED strip (10 pixels)
- PWM/ DAC capable for expression pedal
- Level shifters if needed for input voltages
## Software Architecture
### Core Design
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ USB MIDI │ │ MIDI Task │ │ LED Driver │
│ (Core 1) │ │ (Core 0) │ │ (Core 0) │
│ - Loopy Pro │◄──►│ - Clock Parity │◄──►│ - Pixel Pulse │
│ recognition │ │ Detection │ │ on beats │
│ - Note/CC │ │ - MIDI update │◄──►│ - Button LED │
│ processing │ │ processing │ │ feedback │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌─────────────────┐
│ Expression │
│ Pedal (Core 0)│
│ - ADC Read │
│ - CC 4 output │
└─────────────────┘
```
### Key Subsystems
#### 1. USB MIDI Transport (Core 1)
- Adafruit TinyUSB library integration
- Proper VID/PID spoofing (Novation Launchpad X: 0x1235/0x0103)
- MIDI event parsing and routing to Core 0
- Re-enumeration support for stable device connection
#### 2. MIDI Task (Core 0)
- Core 0 exclusive for all FastLED operations (prevents crashes)
- MIDI clock (0xF8) detection and counting
- Beat detection using tick count / 24 (24 PPQN)
- Pixel 6 pulsing on every MIDI beat
- Button-to-CC message conversion
#### 3. LED Driver (Core 0)
- Per-pad color configuration (Launchpad X compatibility)
- Velocity-based color mapping
- Dim off-state for power efficiency
- Launchpad-style startup animation
#### 4. Expression Pedal (Core 0)
- ADC input on GPIO4 (calibrated min/max)
- CC 4 output on MIDI channel 1
- Hysteresis filtering for stable readings
- 5ms update interval for responsive control
#### 5. Switch Driver (Core 0)
- 16 buttons via daisy-chained 74HC165
- 5ms debounce for reliable input
- Direct button-to-CC mapping
- Per-pad LED feedback
## Configuration Options
### MIDI Mapping
The controller uses a Launchpad X-style mapping for intuitive control:
| Pad | Hardware Button | MIDI Channel | Note/Pitch | CC | Default Color |
|-----|-----------------|--------------|------------|----|---------------|
| 0-1 | 0-1 | 1 | 36-45 | - | Blue (0x0000FF) |
| 2-4 | 2-4 | 1 | 36-45 | - | Orange (0xFF6600) |
| 5 | 5 | 1 | 36 | - | Red (0xFF0000) |
| 6 | 6 | 1 | 36 | - | White (0xFFFFFF) |
| 7 | 7 | 1 | 36 | - | Amber (0xFFBF00) |
| 8-9 | 8-9 | 1 | 36-37 | - | Purple (0x9900FF) |
**For Loopy Pro Users:**
- Buttons generate CC messages (not Note On/Off)
- CC values: 2-11 for buttons 0-9
- CC 4 for expression pedal
- Pixel colors provide visual feedback
### MIDI Clock Synchronization
Pixel 6 provides visual feedback synchronized to Loopy Pro's tempo:
1. **Configure Loopy Pro**: Enable MIDI Clock output targeting JOC Midi
2. **Device Recognition**: Loopy Pro detects the VID/PID (0x1235/0x0103)
3. **Visual Feedback**: Pixel 6 pulses white on each MIDI Clock beat (0xF8)
4. **Pulse Animation**: Quadratic fade (80ms duration) between beats
**Troubleshooting**: If pixel 6 doesn't pulse:
- Verify Loopy Pro has MIDI Clock enabled
- Ensure JOC Midi is the clock target
- Check MIDI device permissions
- Verify MIDI input in Loopy Pro shows "JOC Midi"
### PlatformIO Configuration
#### Build Options (`platformio.ini`)
```ini
[env]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
build_type = release
monitor_speed = 115200
upload_speed = 921600
build_flags =
-DCORE_DEBUG_LEVEL=0
-DARDUINO_USB_MODE=0
-DARDUINO_USB_LAUNCHER_MODE=0
lib_deps =
adafruit/[email protected]
https://github.com/FastLED/FastLED/archive/refs/tags/3.6.0.zip
extra_scripts =
pre_build.py
extra_script.py
```
#### Build Flags Explanation
- `-DARDUINO_USB_MODE=0`: USB in CDC/MIDI mode (no virtual serial)
- `-DARDUINO_USB_LAUNCHER_MODE=0`: No USB launcher mode
- `-DCORE_DEBUG_LEVEL=0`: Disable debug output
- `--allow-multiple-definition`: Required for Adafruit TinyUSB compatibility
#### Build Scripts
**`pre_build.py`**: Patches board definitions with Launchpad X VID/PID
**`extra_script.py`**: Ensures proper TinyUSB linking order
## Usage
### Initial Setup
1. **Flash Firmware**
```bash
platformio run --target upload
```
2. **Open Serial Monitor** (115200 baud)
- Shows startup sequence
- MIDI activity diagnostics
- System status
3. **Available Commands** (type in serial)
- `help` - Show all commands
- `dump` - Display button states
- `probe` - Hardware diagnostics
- `ledon`/`ledoff` - Turn all LEDs on/off
- `ledtest` - Color cycle test
- `exp` - Show expression pedal ADC/value
- `usb` - USB status
- `gpiotest` - Raw GPIO diagnostics
- `miditest` - Simulate MIDI input
- `padtest` - Test individual pads
- `mapping` - Show current pad mapping
### MIDI Configuration in Loopy Pro
1. **System Settings**
- Name: "JOC Midi"
- Manufacturer: "JOC"
- Model: "JOC Midi"
2. **MIDI Setup**
- Port 1: Enabled
- Input Channel: All
- Output Channel: 1 (or preferred)
3. **Sync Configuration**
- Sync Master: LOOPY (if Loopy Pro is master)
- Clock Output: Enabled
- Clock Targets: JOC Midi
### Operation
1. **Button Presses**
- Press any button to send corresponding CC message
- Pixel color changes to match button state
2. **Expression Pedal**
- Connect foot pedal to ADC input
- Calibrate min (heel) and max (toe) positions
- Watch pixel 5 for pedal value feedback
3. **Visual Feedback**
- Buttons: Color indicates CC state
- Expression pedal: Pixel 5 brightness reflects CC value
- Sync: Pixel 6 pulses with MIDI Clock
## Customization
### Adding New MIDI Functions
1. **Add New CC Mappings**
```cpp
// In switch_stub.h or app_task.cpp
static const uint8_t BUTTONS_TO_CC[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
```
2. **Custom LED Patterns**
```cpp
// In led_stub.cpp
void apply_custom_color(uint8_t index, uint8_t velocity) {
if (index == 6) { // Example: Custom color for expression pedal
mux_ptr->set_led_color(index, 0, 255, 0); // Green
return;
}
apply_pad_color(index, velocity);
}
```
### Changing MIDI Clock Behavior
1. **Modify Beat Detection**
```cpp
// In main.cpp, midi_task
uint32_t beat_interval = 24; // PPQN for MIDI Clock
if (tick / beat_interval != last_beat) {
last_beat = tick / beat_interval;
// Trigger pulse
}
```
2. **Change Pulse Animation**
```cpp
// In main.cpp, midi_task
float pulse_speed = 1.0f / 1.0; // Adjust pulse speed
mux.set_led_color(6, v * pulse_speed, v * pulse_speed, v * pulse_speed);
```
### Modifying Expression Pedal
1. **Change ADC Pin**
```cpp
// In expression_pedal.h/expression_pedal.cpp
static const uint8_t EXP_PEDAL_PIN = 4; // or another GPIO
```
2. **Adjust Calibration**
```cpp
// In main.cpp
default exp_pedal.cal_min(0); // Set based on testing
default exp_pedal.cal_max(1023); // Set based on testing
```
3. **Change MIDI CC**
```cpp
// In expression_pedal.cpp
send_cc(1, 4, value); // Keep existing
// Or change:
send_cc(1, 7, value); // Volume instead of expression
```
### Custom Pad Layout
1. **Map Pads Differently**
```cpp
// In app_task.h/app_task.cpp
#define PAD_NOTE_MAPPING {36, 37, 38, 39, 40, 41, 42, 43, 44, 45}
#define PAD_CC_MAPPING {2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
```
2. **Add Special Functions**
```cpp
// In app_task.cpp
void process_special_button(uint8_t led_index) {
if (led_index == 5) { // Expression pedal button
// Toggle pedal mode
}
if (led_index == 6) { // MIDI Clock toggle
// Toggle clock visualization
}
}
```
## Testing and Diagnostics
### Serial Commands for Debugging
| Command | Description |
|---------|-------------|
| `probe` | Test all buttons and LEDs |
| `gpiotest` | Check GPIO pin states |
| `ledtest` | Cycle through all LED colors |
| `miditest` | Simulate MIDI input for testing |
| `dump` | Show current button states |
### Troubleshooting Common Issues
#### Issue: Pixel 6 doesn't pulse
**Cause**: MIDI Clock not arriving
**Solution**:
1. Check Loopy Pro sync settings
2. Verify MIDI Clock target is JOC Midi
3. Examine `[CLK] !! NO MIDI CLOCK RECEIVED !!` in serial
#### Issue: Buttons not sending MIDI
**Cause**: Shift register issues
**Solution**:
1. Run `probe` command
2. Check wiring and pin connections
3. Verify shift register operation
#### Issue: Expression pedal unresponsive
**Cause**: ADC calibration incorrect
**Solution**:
1. Use `exp` command to see raw ADC values
2. Adjust calibration min/max values
3. Check voltage range and connection
#### Issue: USB connection drops
**Cause**: Re-enumeration issues
**Solution**:
1. Check mounted() status in `[MIDI] USB mounted:` messages
2. Ensure proper VID/PID values in build
3. Verify TinyUSB initialization
## Build Instructions
### Prerequisites
1. **PlatformIO IDE** or
2. **Arduino CLI** with ESP32 core support
### Quick Build
```bash
# Using PlatformIO
platformio run
# Using Arduino CLI
cd your-project
arduino-cli compile --builder chitrak/micropython-builder --fqbn esp32-s3-devkitc-1 .
```
### Upload
```bash
# Using PlatformIO
platformio upload
# Using Arduino CLI
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32-s3-devkitc-1 .
```
### Troubleshooting Build Issues
#### Common Build Errors
1. **Adafruit TinyUSB conflicts**
- Solution: Use the provided `extra_script.py` with `--allow-multiple-definition`
- Ensure build_flags match exactly
2. **Memory Issues**
- Solution: Reduce debug output, use release build type
- Check stack sizes in platformio.ini
3. **Pin Conflicts**
- Solution: Adjust GPIO pins in configuration files
- Verify all components use different pins
#### Hardware Issues
1. **LED Driver Problems**
- Solution: Test with simple color output commands
- Check wiring and power supply
2. **Button Issues**
- Solution: Use `probe` command regularly
- Ensure proper pull-up/pull-down configurations
## Future Enhancements
### Planned Features
1. **USB MIDI Sysex Support**
- Launchpad X programmer mode
- Bank select and patch change
2. **Advanced Expression Pedal**
- Rotary encoding support
- Multiple pedal modes (CC1/CC4)
3. **Enhanced LED Effects**
- Breathing animations
- SOS patterns for diagnostics
- Battery level indicator
4. **Additional MIDI Functions**
- Pitch Bend support
- Aftertouch
- Poly Pressure
### Custom Configuration Examples
#### For Loopy Pro DJs
```cpp
// Loopy-specific CC mappings
static const uint8_t LOOPY_CC_MAP[10] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
```
#### For External Controllers
```cpp
// CC-based interaction
static const uint8_t EXTERNAL_CC_TARGET = 1;
send_cc(EXTERNAL_CC_TARGET, 7, value); // Volume
```
#### For Recording Studios
```cpp
// Note-based for drum triggers
static const uint8_t DRUM_NOTES[10] = {36, 38, 40, 42, 44, 45, 47, 48, 50, 52};
```
## License
This project is provided as-is with no explicit license. The code is intended for educational and personal use. Modifications and distributions should respect original authors' intentions where specified.
## Acknowledgments
- **Novation Launchpad**: Inspiration for layout and color scheme
- **ESP32-S3**: Powerful microcontroller with native USB MIDI
- **Adafruit TinyUSB**: Reliable USB MIDI stack
- **FastLED**: Efficient LED control library
- **Contributors**: All who tested and provided feedback
## Contact
For issues or questions:
1. Check the project documentation
2. Review serial output for diagnostic messages
3. Use available testing commands
4. Submit issues with complete build logs if encountering problems
-1
View File
@@ -28,7 +28,6 @@ private:
static const uint8_t NUM_PADS = 10; static const uint8_t NUM_PADS = 10;
PadMapping pad_mapping[NUM_PADS]; PadMapping pad_mapping[NUM_PADS];
bool last_switch_state[NUM_PADS]; bool last_switch_state[NUM_PADS];
uint8_t cc_map[NUM_PADS];
// SysEx reassembly buffer // SysEx reassembly buffer
static const uint8_t SYSEX_MAX_LEN = 64; static const uint8_t SYSEX_MAX_LEN = 64;
-36
View File
@@ -1,36 +0,0 @@
#pragma once
#include <cstdint>
class UsbMidiTransport;
class ExpressionPedal {
public:
ExpressionPedal(uint8_t adc_pin = 5);
void begin();
void update(UsbMidiTransport& midi);
uint8_t get_value() const { return current_value; }
uint16_t get_raw() const { return current_raw; }
void set_cal_min();
void set_cal_max();
uint16_t get_cal_min() const { return cal_min; }
uint16_t get_cal_max() const { return cal_max; }
private:
uint8_t adc_pin;
uint8_t current_value;
uint8_t last_sent_value;
uint8_t midi_channel;
uint8_t midi_cc;
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;
uint8_t adc_to_midi(uint16_t adc_value);
};
+6 -2
View File
@@ -7,10 +7,11 @@ public:
virtual ~LedStub() {} virtual ~LedStub() {}
virtual void begin() = 0; virtual void begin() = 0;
virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) = 0; virtual void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) = 0;
virtual void clear_all() = 0; virtual void clear_all() = 0;
virtual void set_led_brightness(uint8_t brightness) = 0; virtual void set_led_brightness(uint8_t brightness) = 0;
virtual void flash_activity() {} virtual void flash_activity() {}
virtual void flash_sysex() {}
virtual void update() {} virtual void update() {}
virtual uint8_t note_to_index(uint8_t note) { virtual uint8_t note_to_index(uint8_t note) {
@@ -35,14 +36,17 @@ private:
bool initialized; bool initialized;
uint32_t activity_off_time = 0; uint32_t activity_off_time = 0;
uint8_t saved_r = 0, saved_g = 0, saved_b = 0; uint8_t saved_r = 0, saved_g = 0, saved_b = 0;
uint8_t sysex_saved_r[10] = {0}, sysex_saved_g[10] = {0}, sysex_saved_b[10] = {0};
bool sysex_flash_active = false;
public: public:
DefaultLedStub(); DefaultLedStub();
void begin() override; void begin() override;
void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index = -1) override; void set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) override;
void clear_all() override; void clear_all() override;
void set_led_brightness(uint8_t brightness) override; void set_led_brightness(uint8_t brightness) override;
void flash_activity() override; void flash_activity() override;
void flash_sysex() override;
void update() override; void update() override;
void set_mux(PixelStompMux* mux); void set_mux(PixelStompMux* mux);
-4
View File
@@ -3,10 +3,6 @@
#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,
-2
View File
@@ -21,5 +21,3 @@ monitor_speed = 115200
board_build.partitions = default_8MB.csv board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi board_build.arduino.memory_type = qio_opi
extra_scripts = pre:pre_build.py
-49
View File
@@ -1,49 +0,0 @@
import os
import fileinput
def patch_usb_ids():
# Find the core's pins_arduino.h for ESP32-S3 DevKitC-1
core_variants = os.path.expanduser("~/.platformio/packages/framework-arduinoespressif32/variants")
if not os.path.exists(core_variants):
# Try PlatformIO default location
core_variants = os.path.join(os.environ.get("PLATFORMIO_PACKAGES_DIR", ""), "framework-arduinoespressif32", "variants")
pins_file = os.path.join(core_variants, "esp32s3_devkitc", "pins_arduino.h")
if not os.path.exists(pins_file):
# Try alternative variant name
pins_file = os.path.join(core_variants, "esp32s3", "pins_arduino.h")
if os.path.exists(pins_file):
print(f"Patching {pins_file} with Launchpad X VID/PID")
# Read and replace
with open(pins_file, 'r') as f:
content = f.read()
# Replace USB_VID and USB_PID
content = content.replace(
'#define USB_VID 0x303a',
'#define USB_VID 0x1235'
)
content = content.replace(
'#define USB_PID 0x1001',
'#define USB_PID 0x0103'
)
# Add or replace manufacturer/product
if 'USB_MANUFACTURER' not in content:
content = content.replace(
'#define USB_PID 0x0103',
'#define USB_PID 0x0103\n#define USB_MANUFACTURER "JOC"\n#define USB_PRODUCT "JOC Midi"'
)
else:
content = content.replace('USB_MANUFACTURER "Novation"', 'USB_MANUFACTURER "JOC"')
content = content.replace('USB_PRODUCT "Launchpad X"', 'USB_PRODUCT "JOC Midi"')
with open(pins_file, 'w') as f:
f.write(content)
print("USB VID/PID patched successfully")
else:
print(f"WARNING: Could not find pins_arduino.h at {pins_file}")
patch_usb_ids()
+33 -36
View File
@@ -1,21 +1,17 @@
#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) {
// Launchpad X standard: bottom row = notes 36-45 (C2 to A2) on channel 1 // 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 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++) { for (uint8_t i = 0; i < NUM_PADS; i++) {
pad_mapping[i].physical_switch = i; pad_mapping[i].physical_switch = i;
pad_mapping[i].midi_channel = 1; pad_mapping[i].midi_channel = 1;
pad_mapping[i].midi_note = launchpad_notes[i]; pad_mapping[i].midi_note = launchpad_notes[i];
pad_mapping[i].led_index = i; pad_mapping[i].led_index = i;
cc_map[i] = cc_assignments[i];
last_switch_state[i] = false; last_switch_state[i] = false;
} }
} }
@@ -27,10 +23,7 @@ void AppTask::begin() {
process_midi_event(event); process_midi_event(event);
}); });
Serial.println("[APP] Controller ready - CC mode"); Serial.println("[APP] Controller ready - Launchpad X mode (notes 36-45, ch1)");
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() { void AppTask::update() {
@@ -38,9 +31,11 @@ void AppTask::update() {
bool is_pressed = switch_driver->is_pressed(i); bool is_pressed = switch_driver->is_pressed(i);
if (is_pressed && !last_switch_state[i]) { if (is_pressed && !last_switch_state[i]) {
Serial.printf("[APP] Switch %d pressed\n", i);
process_switch_event(i, true); process_switch_event(i, true);
last_switch_state[i] = true; last_switch_state[i] = true;
} else if (!is_pressed && last_switch_state[i]) { } else if (!is_pressed && last_switch_state[i]) {
Serial.printf("[APP] Switch %d released\n", i);
process_switch_event(i, false); process_switch_event(i, false);
last_switch_state[i] = false; last_switch_state[i] = false;
} }
@@ -51,11 +46,16 @@ void AppTask::process_midi_event(const MidiEvent& event) {
Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n", Serial.printf("[APP] MIDI IN: Type=%d Ch=%d Data1=%d Data2=%d\n",
event.type, event.channel, event.data1, event.data2); 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) { if (event.type == MidiEvent::SYSEX) {
// Cin is encoded in channel for SYSEX packets // Cin is encoded in channel for SYSEX packets
uint8_t cin = event.channel; uint8_t cin = event.channel;
uint8_t packet[3] = {event.data1, event.data2, 0}; uint8_t packet[3] = {event.data1, event.data2, 0};
process_sysex_packet(packet, cin); process_sysex_packet(packet, cin);
led_driver->flash_sysex();
return; return;
} }
@@ -83,8 +83,7 @@ void AppTask::process_midi_event(const MidiEvent& event) {
led_driver->set_led_state( led_driver->set_led_state(
midi_note, midi_note,
midi_channel, midi_channel,
color_vel, color_vel
led_index
); );
Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n", Serial.printf("[APP] NOTE -> LED: Ch%d Note%d Vel%d -> LED%d\n",
@@ -97,27 +96,28 @@ void AppTask::process_midi_event(const MidiEvent& event) {
Serial.printf("[APP] NOTE Ch%d ignored (not Launchpad channel 1-3)\n", midi_channel); 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 // CONTROL_CHANGE fallback for generic MIDI / Loopy Pro generic mode
else if (event.type == MidiEvent::CONTROL_CHANGE) { else if (event.type == MidiEvent::CONTROL_CHANGE) {
uint8_t cc_num = event.data1; uint8_t cc_num = event.data1;
uint8_t cc_val = event.data2; uint8_t cc_val = event.data2;
for (uint8_t i = 0; i < NUM_PADS; i++) { // Map CC to pad: CC2-11 (Loopy Pro), CC0-9, CC36-45
if (cc_map[i] == cc_num) { if (cc_num >= 2 && cc_num < 2 + NUM_PADS) {
led_index = i; led_index = cc_num - 2;
break; } 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) { if (led_index < NUM_PADS) {
led_driver->set_led_state( led_driver->set_led_state(
pad_mapping[led_index].midi_note, pad_mapping[led_index].midi_note,
pad_mapping[led_index].midi_channel, pad_mapping[led_index].midi_channel,
cc_val, cc_val
led_index
); );
Serial.printf("[APP] CC%d Val%d -> LED%d\n", Serial.printf("[APP] CC -> LED: Ch%d CC%d Val%d -> LED%d\n",
cc_num, cc_val, led_index); midi_channel, cc_num, cc_val, led_index);
} else { } else {
Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n", Serial.printf("[APP] CC Ch%d CC%d Val%d - no mapping\n",
midi_channel, cc_num, cc_val); midi_channel, cc_num, cc_val);
@@ -126,25 +126,22 @@ 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]; // Loopy Pro Launchpad mode expects NOTE_ON/NOTE_OFF on notes 36-45
// Use palette index 127 (magenta) for visible feedback uint8_t note = pad_mapping[i].midi_note;
uint8_t value = pressed ? 127 : 0; uint8_t velocity = pressed ? 127 : 0;
midi_transport->send_cc(channel, cc_num, value);
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; break;
} }
} }
-73
View File
@@ -1,73 +0,0 @@
#include "expression_pedal.h"
#include "midi_transport.h"
#include <Arduino.h>
ExpressionPedal::ExpressionPedal(uint8_t adc_pin)
: adc_pin(adc_pin)
, current_value(0)
, last_sent_value(255)
, midi_channel(1)
, midi_cc(4)
, last_read_time(0)
, current_raw(0)
, smoothed_raw(0)
, cal_min(36)
, cal_max(950)
{
}
void ExpressionPedal::begin() {
Serial.printf("[EXP] Expression pedal on ADC1_CH3 (GPIO%d)\n", adc_pin);
analogReadResolution(12);
pinMode(adc_pin, INPUT);
delay(10);
current_raw = analogRead(adc_pin);
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);
Serial.printf("[EXP] Move pedal full range to see raw values\n");
}
void ExpressionPedal::update(UsbMidiTransport& midi) {
uint32_t now = millis();
if (now - last_read_time < READ_INTERVAL_MS) return;
last_read_time = now;
current_raw = analogRead(adc_pin);
smoothed_raw = (smoothed_raw * 7 + current_raw + 4) / 8;
uint8_t new_value = adc_to_midi(smoothed_raw);
current_value = new_value;
// 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);
}
}
void ExpressionPedal::set_cal_min() {
cal_min = current_raw;
if (cal_min >= cal_max) cal_max = cal_min + 1;
Serial.printf("[EXP] cal_min set to %d (ADC raw)\n", cal_min);
}
void ExpressionPedal::set_cal_max() {
cal_max = current_raw;
if (cal_max <= cal_min) cal_min = cal_max - 1;
Serial.printf("[EXP] cal_max set to %d (ADC raw)\n", cal_max);
}
uint8_t ExpressionPedal::adc_to_midi(uint16_t adc_value) {
if (adc_value <= cal_min) return 0;
if (adc_value >= cal_max) return 127;
uint32_t span = cal_max - cal_min;
uint32_t offset = adc_value - cal_min;
return (uint8_t)(offset * 127 / span);
}
+73 -66
View File
@@ -129,48 +129,19 @@ static const uint32_t launchpad_palette[128] = {
0x1E002F, // 118 0x1E002F, // 118
0x1B0029, // 119 0x1B0029, // 119
0x170023, // 120 0x170023, // 120
0x190033, // 121: dark purple-red (was near-black) 0x14001E, // 121
0x3D0047, // 122: medium purple-red 0x110018, // 122
0x61005C, // 123: bright purple 0x0D0013, // 123
0x850070, // 124: brighter purple 0x0A000D, // 124
0x9900CC, // 125: bright magenta 0x070008, // 125
0xCC00FF, // 126: full magenta 0x030002, // 126
0xFF00FF, // 127: full magenta (Maximum velocity = visible) 0x000000, // 127: Off
}; };
static uint32_t velocity_to_color(uint8_t velocity) { static uint32_t velocity_to_color(uint8_t velocity) {
return launchpad_palette[velocity]; return launchpad_palette[velocity];
} }
static const uint32_t pad_base_colors[10] = {
0x0000FF, // 0: Blue
0x0000FF, // 1: Blue
0xFF6600, // 2: Orange
0xFF6600, // 3: Orange
0xFF6600, // 4: Orange
0xFF0000, // 5: Red
0xFFFFFF, // 6: White
0xFFBF00, // 7: Amber
0x9900FF, // 8: Purple
0x9900FF, // 9: Purple
};
static void apply_pad_color(uint8_t index, uint8_t velocity) {
if (index >= 10) return;
uint32_t base = pad_base_colors[index];
uint8_t r = (base >> 16) & 0xFF;
uint8_t g = (base >> 8) & 0xFF;
uint8_t b = base & 0xFF;
if (velocity == 0) {
r = (uint16_t)r * 20 / 255;
g = (uint16_t)g * 20 / 255;
b = (uint16_t)b * 20 / 255;
}
mux_ptr->set_led_color(index, r, g, b);
}
DefaultLedStub::DefaultLedStub() : initialized(false) { DefaultLedStub::DefaultLedStub() : initialized(false) {
for (int i = 0; i < NUM_LEDS; i++) { for (int i = 0; i < NUM_LEDS; i++) {
led_states[i].active = false; led_states[i].active = false;
@@ -194,25 +165,21 @@ void DefaultLedStub::begin() {
return; return;
} }
Serial.println("[LED] Launchpad-style startup animation (paired)..."); Serial.println("[LED] Launchpad-style startup animation...");
// Sweep each pixel-pair through palette, then off // Launchpad X style: sweep each LED through palette, then all-off
for (int pair = 0; pair < 5; pair++) { for (int i = 0; i < NUM_LEDS; i++) {
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(i1, r, g, b); mux_ptr->set_led_color(i, 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 pair before moving to next // Turn off this LED before moving to next
mux_ptr->set_led_color(i1, 0, 0, 0); mux_ptr->set_led_color(i, 0, 0, 0);
mux_ptr->set_led_color(i2, 0, 0, 0);
mux_ptr->show(); mux_ptr->show();
} }
@@ -233,23 +200,9 @@ void DefaultLedStub::begin() {
Serial.println("[LED] Startup complete"); Serial.println("[LED] Startup complete");
} }
void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity, int8_t led_index) { void DefaultLedStub::set_led_state(uint8_t note, uint8_t channel, uint8_t velocity) {
if (!initialized || !mux_ptr) return; if (!initialized || !mux_ptr) return;
// Direct index mode (used by CC feedback / pad colors)
if (led_index >= 0 && led_index < NUM_LEDS) {
led_states[led_index].active = (velocity > 0);
led_states[led_index].note = note;
led_states[led_index].channel = channel;
led_states[led_index].velocity = velocity;
led_states[led_index].timestamp = millis();
apply_pad_color(led_index, velocity);
mux_ptr->show();
Serial.printf("[LED] Set LED %d: note=%d ch=%d vel=%d\n", led_index, note, channel, velocity);
return;
}
// Note-match mode (used by NOTE_ON/OFF)
for (int i = 0; i < NUM_LEDS; i++) { for (int i = 0; i < NUM_LEDS; i++) {
if (led_states[i].active && led_states[i].note == note) { if (led_states[i].active && led_states[i].note == note) {
led_states[i].channel = channel; led_states[i].channel = channel;
@@ -294,7 +247,7 @@ void DefaultLedStub::clear_all() {
led_states[i].note = 0; led_states[i].note = 0;
led_states[i].channel = 0; led_states[i].channel = 0;
led_states[i].timestamp = 0; led_states[i].timestamp = 0;
apply_pad_color(i, 0); mux_ptr->set_led_color(i, 0, 0, 0);
} }
mux_ptr->show(); mux_ptr->show();
Serial.println("[LED] All cleared"); Serial.println("[LED] All cleared");
@@ -307,8 +260,53 @@ void DefaultLedStub::set_led_brightness(uint8_t brightness) {
} }
void DefaultLedStub::flash_activity() { void DefaultLedStub::flash_activity() {
// No-op: activity flash on all MIDI input was confusing. if (!initialized || !mux_ptr) return;
// LED feedback is handled directly by set_led_state().
uint32_t now = millis();
// Save current LED 0 color if we just started flashing
if (now >= activity_off_time) {
saved_r = 0;
saved_g = 0;
saved_b = 0;
if (led_states[0].active) {
uint32_t color = launchpad_palette[led_states[0].velocity];
saved_r = (color >> 16) & 0xFF;
saved_g = (color >> 8) & 0xFF;
saved_b = color & 0xFF;
}
}
// Flash LED 0 white
mux_ptr->set_led_color(0, 255, 255, 255);
mux_ptr->show();
activity_off_time = now + 50;
}
void DefaultLedStub::flash_sysex() {
if (!initialized || !mux_ptr) return;
// Save all LED states
for (int i = 0; i < NUM_LEDS; i++) {
if (led_states[i].active) {
uint32_t color = launchpad_palette[led_states[i].velocity];
sysex_saved_r[i] = (color >> 16) & 0xFF;
sysex_saved_g[i] = (color >> 8) & 0xFF;
sysex_saved_b[i] = color & 0xFF;
} else {
sysex_saved_r[i] = 0;
sysex_saved_g[i] = 0;
sysex_saved_b[i] = 0;
}
}
// Flash ALL LEDs white
for (int i = 0; i < NUM_LEDS; i++) {
mux_ptr->set_led_color(i, 255, 255, 255);
}
mux_ptr->show();
activity_off_time = now + 200;
sysex_flash_active = true;
} }
void DefaultLedStub::update() { void DefaultLedStub::update() {
@@ -316,10 +314,19 @@ void DefaultLedStub::update() {
uint32_t now = millis(); uint32_t now = millis();
// Turn off activity flash // Turn off activity/SysEx flash
if (activity_off_time > 0 && now >= activity_off_time) { if (activity_off_time > 0 && now >= activity_off_time) {
mux_ptr->set_led_color(0, saved_r, saved_g, saved_b); if (sysex_flash_active) {
for (int i = 0; i < NUM_LEDS; i++) {
mux_ptr->set_led_color(i, sysex_saved_r[i], sysex_saved_g[i], sysex_saved_b[i]);
}
sysex_flash_active = false;
} else {
mux_ptr->set_led_color(0, saved_r, saved_g, saved_b);
}
mux_ptr->show(); mux_ptr->show();
activity_off_time = 0; activity_off_time = 0;
} }
} }
+2 -54
View File
@@ -10,7 +10,6 @@
#include "led_stub.h" #include "led_stub.h"
#include "switch_stub.h" #include "switch_stub.h"
#include "app_task.h" #include "app_task.h"
#include "expression_pedal.h"
PixelStompMux mux(12, 10, 11, 9); PixelStompMux mux(12, 10, 11, 9);
@@ -19,46 +18,14 @@ DefaultSwitchStub switch_driver;
UsbMidiTransport midi_transport; UsbMidiTransport midi_transport;
AppTask controller(&led_driver, &switch_driver, &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; 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);
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);
} }
} }
@@ -116,19 +83,6 @@ void handle_serial_command(const String& cmd) {
mux.set_led_color(1, 255, 255, 255); mux.set_led_color(1, 255, 255, 255);
mux.show(); mux.show();
Serial.println("[CMD] Pixel 1 WHITE (max brightness)"); Serial.println("[CMD] Pixel 1 WHITE (max brightness)");
} 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") { } 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");
@@ -172,9 +126,6 @@ void handle_serial_command(const String& cmd) {
Serial.println(" read - raw button read"); Serial.println(" read - raw button read");
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(" 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)");
@@ -377,7 +328,7 @@ void setup() {
Serial.println("================================="); Serial.println("=================================");
Serial.println(" Loopy MIDI Controller v0.1"); Serial.println(" Loopy MIDI Controller v0.1");
Serial.println(" Phase 1: USB MIDI + Expression Pedal"); Serial.println(" Phase 1: USB MIDI");
Serial.println(" Board: ESP32-S3-WROOM-1"); Serial.println(" Board: ESP32-S3-WROOM-1");
Serial.println("================================="); Serial.println("=================================");
@@ -395,9 +346,6 @@ void setup() {
Serial.println("[INIT] Initializing USB MIDI..."); Serial.println("[INIT] Initializing USB MIDI...");
midi_transport.begin(); midi_transport.begin();
Serial.println("[INIT] Initializing Expression Pedal...");
exp_pedal.begin();
Serial.println("[INIT] Registering MIDI callbacks..."); Serial.println("[INIT] Registering MIDI callbacks...");
controller.begin(); controller.begin();
@@ -429,5 +377,5 @@ void loop() {
handle_serial_command(cmd); handle_serial_command(cmd);
} }
delay(1); delay(10);
} }
+21 -68
View File
@@ -2,11 +2,6 @@
#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) {
@@ -18,11 +13,11 @@ UsbMidiTransport::~UsbMidiTransport() {
bool UsbMidiTransport::begin() { bool UsbMidiTransport::begin() {
Serial.println("[MIDI] Setting up USB MIDI device..."); Serial.println("[MIDI] Setting up USB MIDI device...");
// Use Launchpad X VID/PID (0x1235/0x0103) so Loopy Pro recognizes us // Novation Launchpad X identifiers so Loopy Pro recognizes us
// but with our own name TinyUSBDevice.setID(0x1235, 0x0103);
TinyUSBDevice.setManufacturerDescriptor("JOC"); TinyUSBDevice.setManufacturerDescriptor("Novation");
TinyUSBDevice.setProductDescriptor("JOC Midi"); TinyUSBDevice.setProductDescriptor("Launchpad X");
TinyUSBDevice.setSerialDescriptor("JOCMIDI001"); TinyUSBDevice.setSerialDescriptor("LPX00001");
TinyUSBDevice.begin(0); TinyUSBDevice.begin(0);
if (!usb_midi.begin()) { if (!usb_midi.begin()) {
@@ -53,68 +48,25 @@ void UsbMidiTransport::update() {
TinyUSBDevice.mounted() ? "YES" : "NO"); TinyUSBDevice.mounted() ? "YES" : "NO");
} }
if (usb_midi.available()) { while (usb_midi.available()) {
uint8_t packet[4]; uint8_t packet[4];
if (usb_midi.readPacket(packet)) { if (usb_midi.readPacket(packet)) {
uint8_t cin = packet[0] & 0x0F; MidiEvent event;
if (cin == 0x0F) { parse_midi_packet(packet, 4, event);
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);
// Auto-detect time signature from SPP delta (sent at bar boundaries) const char* type_str = "UNK";
if (prev_spp > 0 && last_spp_position > prev_spp) { switch (event.type) {
uint16_t delta = last_spp_position - prev_spp; case MidiEvent::NOTE_ON: type_str = "NOTE_ON"; break;
if (delta >= 8 && delta <= 64 && delta % 4 == 0) { case MidiEvent::NOTE_OFF: type_str = "NOTE_OFF"; break;
uint8_t detected = delta / 4; case MidiEvent::CONTROL_CHANGE: type_str = "CC"; break;
if (detected >= 2 && detected <= 16 && detected != beats_per_bar) { case MidiEvent::PROGRAM_CHANGE: type_str = "PC"; break;
beats_per_bar = detected; case MidiEvent::PITCH_BEND: type_str = "PB"; break;
Serial.printf("[CLK] Auto-detected %d/4 time from SPP delta=%d\n", detected, delta); default: break;
} }
} 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);
const char* type_str = "UNK"; if (receive_callback) {
switch (event.type) { receive_callback(event);
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);
}
} }
} }
} }
@@ -142,6 +94,7 @@ void UsbMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) {
if (!initialized) return; if (!initialized) return;
uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value}; uint8_t packet[4] = {0x0B, (uint8_t)(0xB0 | (channel - 1)), cc, value};
usb_midi.writePacket(packet); usb_midi.writePacket(packet);
Serial.printf("[MIDI OUT] Ch:%d CC:%d:%d\n", channel, cc, value);
} }
bool UsbMidiTransport::is_connected() { bool UsbMidiTransport::is_connected() {
+1 -1
View File
@@ -11,7 +11,7 @@ DefaultSwitchStub::DefaultSwitchStub() : initialized(false) {
switch_states[i].current_state = false; switch_states[i].current_state = false;
switch_states[i].previous_state = false; switch_states[i].previous_state = false;
switch_states[i].last_change_time = 0; switch_states[i].last_change_time = 0;
switch_states[i].debounce_time = 5; switch_states[i].debounce_time = 50;
} }
} }