Compare commits

..

6 Commits

Author SHA1 Message Date
Dejvino
e6a4711861 Protected mode 2026-02-22 00:33:13 +01:00
Dejvino
217d8dbacf Presets patches 2026-02-21 23:32:27 +01:00
Dejvino
4a3f2b8eb2 Scale editor 2026-02-21 23:19:51 +01:00
Dejvino
36d3fd2e80 Single power source info 2026-02-21 22:38:15 +01:00
Dejvino
8ef4371711 MIDI input 2026-02-21 21:12:30 +01:00
Dejvino
94ade20143 Pinout reformat 2026-02-21 20:24:47 +01:00
10 changed files with 425 additions and 37 deletions

View File

@ -2,19 +2,83 @@
// MIDI UART Pins (GP0/GP1) // MIDI UART Pins (GP0/GP1)
#define PIN_MIDI_TX 0 #define PIN_MIDI_TX 0
#define PIN_MIDI_RX 1
MidiDriver midi; MidiDriver midi;
MidiDriver::MidiDriver() { MidiDriver::MidiDriver() : lastInputNote(-1), lastInputVelocity(0), _runningStatus(0), _byteIndex(0), _data1(0), _data2(0) {
} }
void MidiDriver::begin() { void MidiDriver::begin() {
mutex_init(&_mutex); mutex_init(&_mutex);
Serial1.setTX(PIN_MIDI_TX); Serial1.setTX(PIN_MIDI_TX);
Serial1.setRX(PIN_MIDI_RX);
Serial1.begin(31250); Serial1.begin(31250);
Serial.println(F("MIDI Serial initialized on GP0/GP1")); Serial.println(F("MIDI Serial initialized on GP0/GP1"));
} }
void MidiDriver::update() {
while (Serial1.available()) {
uint8_t b = Serial1.read();
Serial1.write(b); // Soft THRU: Merge input with output
// Realtime messages don't affect running status
if (b >= 0xF8) continue;
if (b >= 0x80) {
_runningStatus = b;
_byteIndex = 0;
} else if (_runningStatus) {
if (_byteIndex == 0) {
_data1 = b;
_byteIndex++;
// Handle 2-byte messages (Program Change 0xC0, Channel Pressure 0xD0)
uint8_t type = _runningStatus & 0xF0;
if (type == 0xC0 || type == 0xD0) {
_byteIndex = 0; // Message complete
}
} else if (_byteIndex == 1) {
_data2 = b;
_byteIndex = 0; // Message complete
uint8_t channel = (_runningStatus & 0x0F) + 1;
uint8_t type = _runningStatus & 0xF0;
if (type == 0x90) {
if (_data2 > 0) {
// Serial.print(F("Note On CH"));
// Serial.print(channel);
// Serial.print(F(": "));
// Serial.print(_data1);
// Serial.print(F(" Vel: "));
// Serial.println(_data2);
lastInputNote = _data1;
lastInputVelocity = _data2;
} else {
// Serial.print(F("Note Off CH"));
// Serial.print(channel);
// Serial.print(F(": "));
// Serial.println(_data1);
if (lastInputNote == _data1) {
lastInputNote = -1; // Note On vel 0 is Note Off
lastInputVelocity = 0;
}
}
} else if (type == 0x80) {
// Serial.print(F("Note Off CH"));
// Serial.print(channel);
// Serial.print(F(": "));
// Serial.println(_data1);
if (lastInputNote == _data1) {
lastInputNote = -1;
lastInputVelocity = 0;
}
}
}
}
}
}
void MidiDriver::lock() { void MidiDriver::lock() {
mutex_enter_blocking(&_mutex); mutex_enter_blocking(&_mutex);
} }

View File

@ -8,6 +8,10 @@ class MidiDriver {
public: public:
MidiDriver(); MidiDriver();
void begin(); void begin();
void update();
volatile int lastInputNote;
volatile int lastInputVelocity;
void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel); void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel);
void sendNoteOff(uint8_t note, uint8_t channel); void sendNoteOff(uint8_t note, uint8_t channel);
@ -19,6 +23,10 @@ public:
private: private:
mutex_t _mutex; mutex_t _mutex;
uint8_t _runningStatus;
uint8_t _byteIndex;
uint8_t _data1;
uint8_t _data2;
}; };
extern MidiDriver midi; extern MidiDriver midi;

View File

@ -129,6 +129,7 @@ static void handlePlayback() {
} }
void loopPlayback() { void loopPlayback() {
midi.update();
if (needsPanic) { if (needsPanic) {
for (int i=0; i<NUM_TRACKS; i++) { for (int i=0; i<NUM_TRACKS; i++) {
midi.panic(midiChannels[i]); midi.panic(midiChannels[i]);

View File

@ -22,35 +22,52 @@ The project is best powered in two parts:
1. **Raspberry Pi Pico W**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter. 1. **Raspberry Pi Pico W**: Power the Pico via its Micro-USB port from a computer or a USB wall adapter.
2. **NeoPixel 8x8 Matrix**: This component is power-hungry and **requires a separate, external 5V power supply**. A power supply capable of delivering at least 2A is recommended. 2. **NeoPixel 8x8 Matrix**: This component is power-hungry and **requires a separate, external 5V power supply**. A power supply capable of delivering at least 2A is recommended.
### Single 5V Supply with Diode
You can power the Pico from the same external 5V source used for the NeoPixels by connecting a diode (e.g., 1N4001 silicon or 1N5817 Schottky).
* **Anode (+)**: Connect to External 5V Supply (+).
* **Cathode (-/Striped)**: Connect to **VSYS (Pin 39)** on the Pico.
This drops the voltage (to ~4.3V with silicon, ~4.7V with Schottky) which is safe for the Pico, and prevents power back-feeding into your computer's USB port if both are connected.
> **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. VBUS is connected directly to the USB port, which is typically limited to 500mA. The matrix can draw over 2A, which could overload and damage your host computer's USB port. > **WARNING**: Do NOT power the NeoPixel matrix from the Pico's 3.3V or VBUS pins. VBUS is connected directly to the USB port, which is typically limited to 500mA. The matrix can draw over 2A, which could overload and damage your host computer's USB port.
### Component Wiring ### Component Wiring
Make sure to establish a **common ground** by connecting the ground from your external 5V power supply to one of the GND pins on the Pico. Make sure to establish a **common ground** by connecting the ground from your external 5V power supply to one of the GND pins on the Pico.
+----------------------+------------------------+-----------------------------+
| Component | Pico Pin | Description | | Component | Pico Pin | Description |
| :--- | :--- | :--- | +----------------------+------------------------+-----------------------------+
| **SSD1306 OLED** | | | | SSD1306 OLED | | |
| VCC | 3V3 (OUT) (Pin 36) | 3.3V Power | | VCC | 3V3 (OUT) (Pin 36) | 3.3V Power |
| GND | GND (Pin 38) | Ground | | GND | GND (Pin 38) | Ground |
| SDA | GP4 (Pin 6) | I2C Data | | SDA | GP4 (Pin 6) | I2C Data |
| SCL | GP5 (Pin 7) | I2C Clock | | SCL | GP5 (Pin 7) | I2C Clock |
| **Rotary Encoder** | | | +----------------------+------------------------+-----------------------------+
| Rotary Encoder | | |
| + (VCC) | 3V3 (OUT) (Pin 36) | 3.3V Power | | + (VCC) | 3V3 (OUT) (Pin 36) | 3.3V Power |
| GND | GND (Pin 33) | Ground | | GND | GND (Pin 33) | Ground |
| CLK | GP12 (Pin 16) | Encoder Clock | | CLK | GP12 (Pin 16) | Encoder Clock |
| DT | GP13 (Pin 17) | Encoder Data | | DT | GP13 (Pin 17) | Encoder Data |
| SW | GP14 (Pin 19) | Encoder Switch | | SW | GP14 (Pin 19) | Encoder Switch |
| **WS2812B 8x8 Matrix**| | | +----------------------+------------------------+-----------------------------+
| WS2812B 8x8 Matrix | | |
| DIN (Data In) | GP2 (Pin 4) | NeoPixel Data | | DIN (Data In) | GP2 (Pin 4) | NeoPixel Data |
| 5V / VCC | External 5V Supply `+` | **External 5V Power** | | 5V / VCC | External 5V Supply + | External 5V Power |
| GND | External 5V Supply `-` | **External Power Ground** | | GND | External 5V Supply - | External Power Ground |
| | GND (Pin 18) | **Common Ground with Pico** | | (Matrix GND) | GND (Pin 18) | Common Ground with Pico |
| **MIDI DIN (Serial)** | | | +----------------------+------------------------+-----------------------------+
| TX (MIDI OUT) | GP0 (Pin 1) | To DIN Pin 5 (via 220Ω) | | MIDI OUT (Serial) | | |
| TX (MIDI OUT) | GP0 (Pin 1) | To DIN Pin 5 (via 220 ohm) |
| MIDI IN (Serial) | | (optional!) |
| RX (MIDI IN) | GP1 (Pin 2) | To DIN Pin 3 (optoisolator) |
+----------------------+------------------------+-----------------------------+
**MIDI Hardware Note**: **MIDI Hardware Note**:
* **MIDI OUT**: Connect **GP0** to DIN Pin 5 via a 220Ω resistor. Connect DIN Pin 4 to 3.3V (3V3) via a 220Ω resistor. Connect DIN Pin 2 to GND. * **MIDI OUT**: Connect **GP0** to DIN Pin 5 via a 220Ω resistor. Connect DIN Pin 4 to 3.3V (3V3) via a 220Ω resistor. Connect DIN Pin 2 to GND.
* **MIDI IN**: Optional! Uses a 6N137 optocoupler based on [this guide](https://www.kieranreck.co.uk/MIDI-6N137-vs-6N138-vs-6N139/). Connect the optocoupler output to **GP1**.
Once everything is wired up, you can upload the code and your tracker should be ready to go! Once everything is wired up, you can upload the code and your tracker should be ready to go!

View File

@ -59,7 +59,7 @@ void setup() {
randomSeed(micros()); randomSeed(micros());
Serial.println(F("Loading sequence.")); Serial.println(F("Loading sequence."));
EEPROM.begin(512); EEPROM.begin(4096);
if (!loadSequence()) { if (!loadSequence()) {
Serial.println(F("Starting fresh instead.")); Serial.println(F("Starting fresh instead."));
numSteps = NUM_STEPS; numSteps = NUM_STEPS;

View File

@ -13,6 +13,7 @@ volatile bool sequenceChangeScheduled = false;
volatile bool needsPanic = false; volatile bool needsPanic = false;
UIState currentState = UI_MENU_MAIN; UIState currentState = UI_MENU_MAIN;
bool protectedMode = false;
// Menus // Menus
MenuItem menuItems[] = { MenuItem menuItems[] = {
@ -37,11 +38,57 @@ MenuItem menuItems[] = {
{ "Theme 7", MENU_ID_THEME_7, false, false, 1 }, { "Theme 7", MENU_ID_THEME_7, false, false, 1 },
{ "Setup", MENU_ID_GROUP_SETUP, true, false, 0 }, { "Setup", MENU_ID_GROUP_SETUP, true, false, 0 },
{ "Channel", MENU_ID_CHANNEL, false, false, 1 }, { "Channel", MENU_ID_CHANNEL, false, false, 1 },
{ "Reset", MENU_ID_RESET, false, false, 1 } { "Protected", MENU_ID_PROTECTED_MODE, false, false, 1 },
{ "Reset", MENU_ID_RESET, false, false, 1 },
{ "Patches", MENU_ID_GROUP_PATCHES, true, false, 0 },
{ "Bank 1", MENU_ID_BANK_1, true, false, 1 },
{ "Load 1.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 0), false, false, 2 },
{ "Load 1.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 1), false, false, 2 },
{ "Load 1.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 2), false, false, 2 },
{ "Load 1.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 3), false, false, 2 },
{ "Save 1.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 4), false, false, 2 },
{ "Save 1.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 5), false, false, 2 },
{ "Save 1.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 6), false, false, 2 },
{ "Save 1.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 7), false, false, 2 },
{ "Bank 2", MENU_ID_BANK_2, true, false, 1 },
{ "Load 2.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 8), false, false, 2 },
{ "Load 2.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 9), false, false, 2 },
{ "Load 2.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 10), false, false, 2 },
{ "Load 2.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 11), false, false, 2 },
{ "Save 2.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 12), false, false, 2 },
{ "Save 2.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 13), false, false, 2 },
{ "Save 2.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 14), false, false, 2 },
{ "Save 2.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 15), false, false, 2 },
{ "Bank 3", MENU_ID_BANK_3, true, false, 1 },
{ "Load 3.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 16), false, false, 2 },
{ "Load 3.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 17), false, false, 2 },
{ "Load 3.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 18), false, false, 2 },
{ "Load 3.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 19), false, false, 2 },
{ "Save 3.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 20), false, false, 2 },
{ "Save 3.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 21), false, false, 2 },
{ "Save 3.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 22), false, false, 2 },
{ "Save 3.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 23), false, false, 2 },
{ "Bank 4", MENU_ID_BANK_4, true, false, 1 },
{ "Load 4.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 24), false, false, 2 },
{ "Load 4.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 25), false, false, 2 },
{ "Load 4.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 26), false, false, 2 },
{ "Load 4.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 27), false, false, 2 },
{ "Save 4.1", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 28), false, false, 2 },
{ "Save 4.2", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 29), false, false, 2 },
{ "Save 4.3", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 30), false, false, 2 },
{ "Save 4.4", (MenuItemID)(MENU_ID_PATCH_ACTIONS_START + 31), false, false, 2 }
}; };
extern const int menuItemsCount = sizeof(menuItems) / sizeof(MenuItem); extern const int menuItemsCount = sizeof(menuItems) / sizeof(MenuItem);
bool isItemVisible(int index) { bool isItemVisible(int index) {
if (protectedMode) {
MenuItemID id = menuItems[index].id;
if (id >= MENU_ID_PATCH_ACTIONS_START) {
int offset = id - MENU_ID_PATCH_ACTIONS_START;
if ((offset % 8) >= 4) return false; // Save options are the last 4 in the block of 8
}
}
if (menuItems[index].indentLevel == 0) return true; if (menuItems[index].indentLevel == 0) return true;
for (int i = index - 1; i >= 0; i--) { for (int i = index - 1; i >= 0; i--) {
if (menuItems[i].indentLevel < menuItems[index].indentLevel) { if (menuItems[i].indentLevel < menuItems[index].indentLevel) {

View File

@ -38,7 +38,15 @@ enum MenuItemID {
MENU_ID_GROUP_SETUP, MENU_ID_GROUP_SETUP,
MENU_ID_CHANNEL, MENU_ID_CHANNEL,
MENU_ID_RESET MENU_ID_PROTECTED_MODE,
MENU_ID_RESET,
MENU_ID_GROUP_PATCHES,
MENU_ID_BANK_1,
MENU_ID_BANK_2,
MENU_ID_BANK_3,
MENU_ID_BANK_4,
MENU_ID_PATCH_ACTIONS_START
}; };
struct MenuItem { struct MenuItem {
@ -77,6 +85,7 @@ extern volatile int songRepeatsRemaining;
extern volatile int nextSongRepeats; extern volatile int nextSongRepeats;
extern volatile bool songModeNeedsNext; extern volatile bool songModeNeedsNext;
extern volatile bool isPlaying; extern volatile bool isPlaying;
extern bool protectedMode;
extern volatile int tempo; extern volatile int tempo;
extern volatile unsigned long lastClockTime; extern volatile unsigned long lastClockTime;
extern volatile int clockCount; extern volatile int clockCount;

View File

@ -22,7 +22,10 @@ enum UIState {
UI_EDIT_STEPS, UI_EDIT_STEPS,
UI_EDIT_FLAVOUR, UI_EDIT_FLAVOUR,
UI_SETUP_PLAYMODE_EDIT, UI_SETUP_PLAYMODE_EDIT,
UI_RANDOMIZE_TRACK_EDIT UI_RANDOMIZE_TRACK_EDIT,
UI_SCALE_EDIT,
UI_SCALE_NOTE_EDIT,
UI_SCALE_TRANSPOSE
}; };
inline void sortArray(int arr[], int size) { inline void sortArray(int arr[], int size) {

View File

@ -119,6 +119,85 @@ void UIManager::draw(UIState currentState, int menuSelection,
display.setCursor(0, 50); display.setCursor(0, 50);
display.println(F(" (Press to confirm)")); display.println(F(" (Press to confirm)"));
break; break;
case UI_SCALE_EDIT:
case UI_SCALE_NOTE_EDIT:
case UI_SCALE_TRANSPOSE:
display.println(F("EDIT SCALE"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
int totalItems = numScaleNotes + 5; // Back + Randomize + Notes + Add + Remove + Transpose
int startIdx = 0;
if (menuSelection >= 4) startIdx = menuSelection - 3;
int y = 12;
for (int i = startIdx; i < totalItems; i++) {
if (y > 54) break;
if (i == menuSelection) {
display.fillRect(0, y, 75, 9, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(2, y + 1);
if (i == 0) {
display.print(F("Back"));
} else if (i == 1) {
display.print(F("Randomize"));
} else if (i <= numScaleNotes + 1) {
int noteIdx = i - 2;
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
display.print(noteNames[scaleNotes[noteIdx]]);
if (currentState == UI_SCALE_NOTE_EDIT && i == menuSelection) {
display.print(F(" <"));
}
} else if (i == numScaleNotes + 2) {
display.print(F("Transpose"));
if (currentState == UI_SCALE_TRANSPOSE) {
display.print(F(" < >"));
}
} else if (i == numScaleNotes + 3) {
display.print(F("Add Note"));
} else if (i == numScaleNotes + 4) {
display.print(F("Remove Note"));
}
y += 9;
}
// Piano Roll Preview
int px = 82;
int py = 20;
int wk_w = 5;
int wk_h = 20;
int bk_w = 4;
int bk_h = 12;
// White keys: C, D, E, F, G, A, B
int whiteNotes[] = {0, 2, 4, 5, 7, 9, 11};
for (int k = 0; k < 7; k++) {
bool active = false;
for (int j = 0; j < numScaleNotes; j++) {
if (scaleNotes[j] == whiteNotes[k]) { active = true; break; }
}
if (active) display.fillRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
else display.drawRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
}
// Black keys: C#, D#, F#, G#, A#
int blackNotes[] = {1, 3, 6, 8, 10};
int blackOffsets[] = {3, 9, 21, 27, 33};
for (int k = 0; k < 5; k++) {
bool active = false;
for (int j = 0; j < numScaleNotes; j++) {
if (scaleNotes[j] == blackNotes[k]) { active = true; break; }
}
int bx = px + blackOffsets[k];
display.fillRect(bx - 1, py - 1, bk_w + 2, bk_h + 2, SSD1306_BLACK);
if (active) display.fillRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
else display.drawRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
}
break;
} }
display.display(); display.display();
} }
@ -191,6 +270,7 @@ void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, i
else if (id == MENU_ID_MUTE) { display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO")); } else if (id == MENU_ID_MUTE) { display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO")); }
else if (id == MENU_ID_FLAVOUR) { display.print(F(": ")); display.print(flavourName); } else if (id == MENU_ID_FLAVOUR) { display.print(F(": ")); display.print(flavourName); }
else if (id == MENU_ID_MUTATION) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); } else if (id == MENU_ID_MUTATION) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); }
else if (id == MENU_ID_PROTECTED_MODE) { display.print(F(": ")); display.print(protectedMode ? F("ON") : F("OFF")); }
if (id >= MENU_ID_THEME_1 && id <= MENU_ID_THEME_7) { if (id >= MENU_ID_THEME_1 && id <= MENU_ID_THEME_7) {
int themeIdx = id - MENU_ID_THEME_1 + 1; int themeIdx = id - MENU_ID_THEME_1 + 1;

View File

@ -18,10 +18,14 @@
static Step local_sequence[NUM_TRACKS][NUM_STEPS]; static Step local_sequence[NUM_TRACKS][NUM_STEPS];
static void handleInput(); static void handleInput();
static int scaleEditSelection = 0;
static int scaleEditNoteIndex = 0;
static void drawUI(); static void drawUI();
static void updateLeds(); static void updateLeds();
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]); static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]);
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]); static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]);
static void savePatch(int bank, int slot);
static void loadPatch(int bank, int slot);
void saveSequence(bool quiet) { void saveSequence(bool quiet) {
midi.lock(); midi.lock();
@ -84,6 +88,57 @@ bool loadSequence() {
return true; return true;
} }
static void savePatch(int bank, int slot) {
int patchIndex = bank * 4 + slot;
int addr = 512 + patchIndex * 128; // Start after main save, 128 bytes per patch
midi.lock();
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i = 0; i < 12; i++) {
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
}
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
EEPROM.put(addr, (int)numSteps); addr += sizeof(int);
bool mutes[NUM_TRACKS];
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
EEPROM.put(addr, mutes); addr += sizeof(mutes);
midi.unlock();
EEPROM.commit();
ui.showMessage("SAVED!");
}
static void loadPatch(int bank, int slot) {
int patchIndex = bank * 4 + slot;
int addr = 512 + patchIndex * 128;
midi.lock();
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
for (int i = 0; i < 12; i++) {
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
}
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
int t;
EEPROM.get(addr, t); addr += sizeof(int);
numSteps = t;
if (numSteps <= 0 || numSteps >= NUM_STEPS) numSteps = NUM_STEPS;
bool mutes[NUM_TRACKS];
EEPROM.get(addr, mutes); addr += sizeof(mutes);
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
if (isPlaying) {
generateSequenceData(currentThemeIndex, nextSequence);
sequenceChangeScheduled = true;
} else {
generateSequenceData(currentThemeIndex, local_sequence);
memcpy(sequence, local_sequence, sizeof(local_sequence));
}
midi.unlock();
ui.showMessage("LOADED!");
}
void factoryReset() { void factoryReset() {
ui.showMessage("RESETTING..."); ui.showMessage("RESETTING...");
uint32_t magic = 0; uint32_t magic = 0;
@ -179,6 +234,38 @@ static void handleInput() {
if (currentStrategyIndices[randomizeTrack] >= numStrategies) currentStrategyIndices[randomizeTrack] = 0; if (currentStrategyIndices[randomizeTrack] >= numStrategies) currentStrategyIndices[randomizeTrack] = 0;
} }
break; break;
case UI_SCALE_EDIT:
scaleEditSelection += (delta > 0 ? 1 : -1);
if (scaleEditSelection < 0) scaleEditSelection = numScaleNotes + 4;
if (scaleEditSelection > numScaleNotes + 4) scaleEditSelection = 0;
break;
case UI_SCALE_NOTE_EDIT:
scaleNotes[scaleEditNoteIndex] += (delta > 0 ? 1 : -1);
if (scaleNotes[scaleEditNoteIndex] < 0) scaleNotes[scaleEditNoteIndex] = 11;
if (scaleNotes[scaleEditNoteIndex] > 11) scaleNotes[scaleEditNoteIndex] = 0;
midi.lock();
midi.sendNoteOn(60 + scaleNotes[scaleEditNoteIndex], 100, midiChannels[randomizeTrack]);
midi.unlock();
delay(50);
midi.lock();
midi.sendNoteOff(60 + scaleNotes[scaleEditNoteIndex], midiChannels[randomizeTrack]);
midi.unlock();
break;
case UI_SCALE_TRANSPOSE:
if (delta != 0) {
int shift = delta % 12;
if (shift < 0) shift += 12;
for(int i=0; i<numScaleNotes; i++) scaleNotes[i] = (scaleNotes[i] + shift) % 12;
sortArray(scaleNotes, numScaleNotes);
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
}
break;
} }
if (currentState == UI_RANDOMIZE_TRACK_EDIT) { if (currentState == UI_RANDOMIZE_TRACK_EDIT) {
randomizeTrack += (delta > 0 ? 1 : -1); randomizeTrack += (delta > 0 ? 1 : -1);
@ -242,16 +329,8 @@ static void handleInput() {
break; break;
case MENU_ID_SCALE: case MENU_ID_SCALE:
generateRandomScale(); currentState = UI_SCALE_EDIT;
if (isPlaying) { scaleEditSelection = 0;
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
// Regenerate all tracks with new scale
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break; break;
case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break; case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break;
@ -270,6 +349,7 @@ static void handleInput() {
case MENU_ID_MUTATION: mutationEnabled = !mutationEnabled; break; case MENU_ID_MUTATION: mutationEnabled = !mutationEnabled; break;
case MENU_ID_CHANNEL: currentState = UI_SETUP_CHANNEL_EDIT; break; case MENU_ID_CHANNEL: currentState = UI_SETUP_CHANNEL_EDIT; break;
case MENU_ID_PROTECTED_MODE: protectedMode = !protectedMode; break;
case MENU_ID_RESET: factoryReset(); break; case MENU_ID_RESET: factoryReset(); break;
default: default:
@ -286,6 +366,16 @@ static void handleInput() {
} }
break; break;
} }
if (menuItems[menuSelection].id >= MENU_ID_PATCH_ACTIONS_START) {
int offset = menuItems[menuSelection].id - MENU_ID_PATCH_ACTIONS_START;
int bank = offset / 8;
int sub = offset % 8;
bool isSave = sub >= 4;
int slot = sub % 4;
if (isSave) savePatch(bank, slot);
else loadPatch(bank, slot);
break;
}
break; break;
} }
break; break;
@ -319,6 +409,71 @@ static void handleInput() {
currentState = UI_MENU_MAIN; currentState = UI_MENU_MAIN;
saveSequence(true); saveSequence(true);
break; break;
case UI_SCALE_EDIT:
if (scaleEditSelection == 0) {
currentState = UI_MENU_MAIN;
saveSequence(true);
} else if (scaleEditSelection == 1) {
generateRandomScale();
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
} else if (scaleEditSelection <= numScaleNotes + 1) {
scaleEditNoteIndex = scaleEditSelection - 2;
currentState = UI_SCALE_NOTE_EDIT;
} else if (scaleEditSelection == numScaleNotes + 2) {
currentState = UI_SCALE_TRANSPOSE;
} else if (scaleEditSelection == numScaleNotes + 3) {
if (numScaleNotes < 12) {
int next = (numScaleNotes > 0) ? (scaleNotes[numScaleNotes-1] + 1) % 12 : 0;
scaleNotes[numScaleNotes] = next;
numScaleNotes++;
scaleEditSelection--; // Move cursor to the new note
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
}
} else {
if (numScaleNotes > 1) {
numScaleNotes--;
scaleEditSelection--;
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
}
}
break;
case UI_SCALE_NOTE_EDIT:
sortArray(scaleNotes, numScaleNotes);
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
currentState = UI_SCALE_EDIT;
saveSequence(true);
break;
case UI_SCALE_TRANSPOSE:
currentState = UI_SCALE_EDIT;
saveSequence(true);
break;
} }
} }
} }
@ -358,7 +513,11 @@ static void drawUI() {
local_randomizeTrack = randomizeTrack; local_randomizeTrack = randomizeTrack;
local_currentState = currentState; local_currentState = currentState;
if (local_currentState == UI_SCALE_EDIT || local_currentState == UI_SCALE_NOTE_EDIT || local_currentState == UI_SCALE_TRANSPOSE) {
local_menuSelection = scaleEditSelection;
} else {
local_menuSelection = menuSelection; local_menuSelection = menuSelection;
}
local_midiChannel = midiChannels[local_randomizeTrack]; local_midiChannel = midiChannels[local_randomizeTrack];
local_tempo = tempo; local_tempo = tempo;
local_numSteps = numSteps; local_numSteps = numSteps;
@ -424,7 +583,7 @@ static void updateLeds() {
// It's a TRACK section item (Track, Mute, Flavour, Mutation, Themes) // It's a TRACK section item (Track, Mute, Flavour, Mutation, Themes)
ledDisplayMode = MODE_MONO; ledDisplayMode = MODE_MONO;
} }
} else if (local_currentState == UI_EDIT_FLAVOUR || local_currentState == UI_RANDOMIZE_TRACK_EDIT) { } else if (local_currentState == UI_EDIT_FLAVOUR || local_currentState == UI_RANDOMIZE_TRACK_EDIT || local_currentState == UI_SCALE_EDIT || local_currentState == UI_SCALE_NOTE_EDIT || local_currentState == UI_SCALE_TRANSPOSE) {
// These are entered from TRACK section items // These are entered from TRACK section items
ledDisplayMode = MODE_MONO; ledDisplayMode = MODE_MONO;
} }