Compare commits
No commits in common. "1e475eeaa54803b83f814880a697a5aff425005b" and "4c6a4bfbc18c95aa36f49cf9a7590f29f7c1ac74" have entirely different histories.
1e475eeaa5
...
4c6a4bfbc1
@ -1,47 +0,0 @@
|
|||||||
#ifndef ARP_STRATEGY_H
|
|
||||||
#define ARP_STRATEGY_H
|
|
||||||
|
|
||||||
#include "MelodyStrategy.h"
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
class ArpStrategy : public MelodyStrategy {
|
|
||||||
public:
|
|
||||||
void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
|
|
||||||
randomSeed(seed);
|
|
||||||
if (numScaleNotes == 0) return;
|
|
||||||
|
|
||||||
int currentNoteIndex = 0;
|
|
||||||
int octave = 4;
|
|
||||||
|
|
||||||
for (int i = 0; i < numSteps; i++) {
|
|
||||||
sequence[i].note = 12 * octave + scaleNotes[currentNoteIndex];
|
|
||||||
sequence[i].accent = (i % 4 == 0); // Accent on beat
|
|
||||||
sequence[i].tie = false;
|
|
||||||
|
|
||||||
currentNoteIndex++;
|
|
||||||
if (currentNoteIndex >= numScaleNotes) {
|
|
||||||
currentNoteIndex = 0;
|
|
||||||
octave++;
|
|
||||||
if (octave > 5) octave = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
randomSeed(micros());
|
|
||||||
}
|
|
||||||
|
|
||||||
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
|
|
||||||
// Swap two notes
|
|
||||||
int s1 = random(numSteps);
|
|
||||||
int s2 = random(numSteps);
|
|
||||||
if (sequence[s1].note != -1 && sequence[s2].note != -1) {
|
|
||||||
int8_t temp = sequence[s1].note;
|
|
||||||
sequence[s1].note = sequence[s2].note;
|
|
||||||
sequence[s2].note = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* getName() override {
|
|
||||||
return "Arp";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
#ifndef LUCKY_STRATEGY_H
|
|
||||||
#define LUCKY_STRATEGY_H
|
|
||||||
|
|
||||||
#include "MelodyStrategy.h"
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
class LuckyStrategy : public MelodyStrategy {
|
|
||||||
public:
|
|
||||||
void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override {
|
|
||||||
randomSeed(seed);
|
|
||||||
if (numScaleNotes == 0) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < numSteps; i++) {
|
|
||||||
int octave = random(3) + 3; // 3, 4, 5 (Base is 4)
|
|
||||||
sequence[i].note = (random(100) < 50) ? (12 * octave + scaleNotes[random(numScaleNotes)]) : -1;
|
|
||||||
sequence[i].accent = (random(100) < 30);
|
|
||||||
sequence[i].tie = (random(100) < 20);
|
|
||||||
}
|
|
||||||
randomSeed(micros());
|
|
||||||
}
|
|
||||||
|
|
||||||
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
|
|
||||||
// Mutate 1 or 2 steps
|
|
||||||
int count = random(1, 3);
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
int s = random(numSteps);
|
|
||||||
if (sequence[s].note != -1) {
|
|
||||||
int r = random(100);
|
|
||||||
if (r < 30) sequence[s].accent = !sequence[s].accent;
|
|
||||||
else if (r < 60) sequence[s].tie = !sequence[s].tie;
|
|
||||||
else if (r < 80) sequence[s].note += 12; // Up octave
|
|
||||||
else sequence[s].note -= 12; // Down octave
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* getName() override {
|
|
||||||
return "Lucky";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
#ifndef MELODY_STRATEGY_H
|
|
||||||
#define MELODY_STRATEGY_H
|
|
||||||
|
|
||||||
#include "TrackerTypes.h"
|
|
||||||
|
|
||||||
class MelodyStrategy {
|
|
||||||
public:
|
|
||||||
virtual void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0;
|
|
||||||
virtual void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) = 0;
|
|
||||||
virtual const char* getName() = 0;
|
|
||||||
virtual ~MelodyStrategy() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@ -3,12 +3,6 @@
|
|||||||
#include <Adafruit_GFX.h>
|
#include <Adafruit_GFX.h>
|
||||||
#include <Adafruit_SSD1306.h>
|
#include <Adafruit_SSD1306.h>
|
||||||
#include <Adafruit_NeoPixel.h>
|
#include <Adafruit_NeoPixel.h>
|
||||||
#include <EEPROM.h>
|
|
||||||
#include <pico/mutex.h>
|
|
||||||
#include "TrackerTypes.h"
|
|
||||||
#include "MelodyStrategy.h"
|
|
||||||
#include "LuckyStrategy.h"
|
|
||||||
#include "ArpStrategy.h"
|
|
||||||
|
|
||||||
// --- HARDWARE CONFIGURATION ---
|
// --- HARDWARE CONFIGURATION ---
|
||||||
#define SCREEN_WIDTH 128
|
#define SCREEN_WIDTH 128
|
||||||
@ -34,64 +28,22 @@
|
|||||||
// --- TRACKER DATA ---
|
// --- TRACKER DATA ---
|
||||||
#define NUM_STEPS 16
|
#define NUM_STEPS 16
|
||||||
|
|
||||||
Step sequence[NUM_STEPS];
|
struct Step {
|
||||||
Step nextSequence[NUM_STEPS];
|
int8_t note; // MIDI Note (0-127), -1 for OFF
|
||||||
volatile bool sequenceChangeScheduled = false;
|
};
|
||||||
volatile bool needsPanic = false;
|
|
||||||
|
|
||||||
mutex_t midiMutex;
|
Step sequence[NUM_STEPS];
|
||||||
|
|
||||||
// --- STATE ---
|
// --- STATE ---
|
||||||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||||
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
||||||
|
|
||||||
enum UIState {
|
int currentStep = 0;
|
||||||
UI_TRACKER,
|
bool isEditing = false;
|
||||||
UI_MENU_MAIN,
|
int scrollOffset = 0;
|
||||||
UI_MENU_RANDOMIZE,
|
bool isPlaying = false;
|
||||||
UI_MENU_SETUP,
|
unsigned long lastStepTime = 0;
|
||||||
UI_SETUP_CHANNEL_EDIT,
|
int tempo = 120; // BPM
|
||||||
UI_EDIT_TEMPO,
|
|
||||||
UI_EDIT_FLAVOUR
|
|
||||||
};
|
|
||||||
|
|
||||||
UIState currentState = UI_MENU_MAIN;
|
|
||||||
|
|
||||||
const char* mainMenu[] = { "Tracker", "Randomize", "Setup" };
|
|
||||||
const int mainMenuCount = sizeof(mainMenu) / sizeof(char*);
|
|
||||||
const char* randomizeMenu[] = { "Back", "Scale", "Melody", "Flavour", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
|
|
||||||
const int THEME_1_INDEX = 7;
|
|
||||||
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
|
|
||||||
const char* setupMenu[] = { "Back", "Channel", "Save", "Load" };
|
|
||||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
|
||||||
|
|
||||||
int menuSelection = 0;
|
|
||||||
volatile int navigationSelection = 1;
|
|
||||||
volatile int playbackStep = 0;
|
|
||||||
int midiChannel = 1;
|
|
||||||
volatile int shMidiChannel = midiChannel;
|
|
||||||
int scaleNotes[12];
|
|
||||||
int numScaleNotes = 0;
|
|
||||||
int melodySeed = 0;
|
|
||||||
volatile int queuedTheme = -1;
|
|
||||||
volatile int currentThemeIndex = 1;
|
|
||||||
const uint32_t EEPROM_MAGIC = 0x42424246;
|
|
||||||
|
|
||||||
MelodyStrategy* strategies[] = { new LuckyStrategy(), new ArpStrategy() };
|
|
||||||
const int numStrategies = 2;
|
|
||||||
int currentStrategyIndex = 0;
|
|
||||||
|
|
||||||
volatile bool mutationEnabled = false;
|
|
||||||
volatile bool songModeEnabled = false;
|
|
||||||
volatile int songRepeatsRemaining = 0;
|
|
||||||
volatile int nextSongRepeats = 0;
|
|
||||||
volatile bool songModeNeedsNext = false;
|
|
||||||
volatile bool isEditing = false;
|
|
||||||
volatile int scrollOffset = 0;
|
|
||||||
volatile bool isPlaying = false;
|
|
||||||
volatile int tempo = 120; // BPM
|
|
||||||
volatile unsigned long lastClockTime = 0;
|
|
||||||
volatile int clockCount = 0;
|
|
||||||
|
|
||||||
|
|
||||||
// Encoder State
|
// Encoder State
|
||||||
@ -125,83 +77,8 @@ void readEncoder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sortArray(int arr[], int size) {
|
|
||||||
for (int i = 0; i < size - 1; i++) {
|
|
||||||
for (int j = 0; j < size - i - 1; j++) {
|
|
||||||
if (arr[j] > arr[j + 1]) {
|
|
||||||
int temp = arr[j];
|
|
||||||
arr[j] = arr[j + 1];
|
|
||||||
arr[j + 1] = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showMessage(const char* msg) {
|
|
||||||
display.clearDisplay();
|
|
||||||
display.setCursor(10, 25);
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
display.setTextSize(2);
|
|
||||||
display.print(msg);
|
|
||||||
display.display();
|
|
||||||
delay(500);
|
|
||||||
display.setTextSize(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveSequence(bool quiet = false) {
|
|
||||||
int addr = 0;
|
|
||||||
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
|
|
||||||
EEPROM.put(addr, midiChannel); addr += sizeof(midiChannel);
|
|
||||||
EEPROM.put(addr, melodySeed); addr += sizeof(melodySeed);
|
|
||||||
EEPROM.put(addr, currentStrategyIndex); addr += sizeof(currentStrategyIndex);
|
|
||||||
EEPROM.put(addr, (int)tempo); addr += sizeof(int);
|
|
||||||
|
|
||||||
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
for (int i=0; i<12; i++) {
|
|
||||||
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
for (int i=0; i<NUM_STEPS; i++) {
|
|
||||||
EEPROM.put(addr, sequence[i]); addr += sizeof(Step);
|
|
||||||
}
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
EEPROM.commit();
|
|
||||||
if (!quiet) showMessage("SAVED!");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool loadSequence() {
|
|
||||||
int addr = 0;
|
|
||||||
uint32_t magic;
|
|
||||||
EEPROM.get(addr, magic); addr += sizeof(magic);
|
|
||||||
if (magic != EEPROM_MAGIC) return false;
|
|
||||||
|
|
||||||
EEPROM.get(addr, midiChannel); addr += sizeof(midiChannel);
|
|
||||||
shMidiChannel = midiChannel;
|
|
||||||
EEPROM.get(addr, melodySeed); addr += sizeof(melodySeed);
|
|
||||||
EEPROM.get(addr, currentStrategyIndex); addr += sizeof(currentStrategyIndex);
|
|
||||||
int t;
|
|
||||||
EEPROM.get(addr, t); addr += sizeof(int);
|
|
||||||
tempo = t;
|
|
||||||
|
|
||||||
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
for (int i=0; i<12; i++) {
|
|
||||||
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
for (int i=0; i<NUM_STEPS; i++) {
|
|
||||||
EEPROM.get(addr, sequence[i]); addr += sizeof(Step);
|
|
||||||
}
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
mutex_init(&midiMutex);
|
|
||||||
|
|
||||||
// Use random ADC noise for seed
|
|
||||||
delay(5000);
|
delay(5000);
|
||||||
Serial.println(F("Starting."));
|
Serial.println(F("Starting."));
|
||||||
|
|
||||||
@ -229,79 +106,36 @@ void setup() {
|
|||||||
pixels.clear();
|
pixels.clear();
|
||||||
pixels.show();
|
pixels.show();
|
||||||
|
|
||||||
// 4. Setup MIDI Serial
|
// 4. Init Sequence
|
||||||
Serial1.setTX(PIN_MIDI_TX);
|
for(int i=0; i<NUM_STEPS; i++) {
|
||||||
Serial1.begin(31250);
|
sequence[i].note = -1; // Default to empty
|
||||||
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
|
||||||
|
|
||||||
// 5. Init Sequence
|
|
||||||
randomSeed(micros());
|
|
||||||
generateRandomScale();
|
|
||||||
melodySeed = random(10000);
|
|
||||||
|
|
||||||
EEPROM.begin(512);
|
|
||||||
if (!loadSequence()) {
|
|
||||||
generateTheme(1);
|
|
||||||
}
|
}
|
||||||
isPlaying = false; // Don't start playing on boot
|
// Add a simple C-Major scale for testing
|
||||||
|
sequence[0].note = 60; // C4
|
||||||
|
sequence[2].note = 62; // D4
|
||||||
|
sequence[4].note = 64; // E4
|
||||||
|
sequence[6].note = 65; // F4
|
||||||
|
sequence[8].note = 65; // F4
|
||||||
|
sequence[9].note = 62; // D4
|
||||||
|
sequence[12].note = 64; // E4
|
||||||
|
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.display();
|
display.display();
|
||||||
|
|
||||||
|
// 5. Setup MIDI Serial
|
||||||
|
Serial1.setTX(PIN_MIDI_TX);
|
||||||
|
Serial1.begin(31250);
|
||||||
|
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
||||||
|
|
||||||
Serial.println(F("Started."));
|
Serial.println(F("Started."));
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
|
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
|
||||||
uint8_t channelStatus = status | (shMidiChannel - 1);
|
Serial1.write(status);
|
||||||
Serial1.write(channelStatus);
|
|
||||||
Serial1.write(note);
|
Serial1.write(note);
|
||||||
Serial1.write(velocity);
|
Serial1.write(velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMidiRealtime(uint8_t status) {
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
Serial1.write(status);
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateRandomScale() {
|
|
||||||
numScaleNotes = random(3, 13); // 3 to 12 notes
|
|
||||||
for (int i = 0; i < 12; i++) {
|
|
||||||
scaleNotes[i] = i; // Fill with all notes
|
|
||||||
}
|
|
||||||
// Shuffle
|
|
||||||
for (int i = 0; i < 12; i++) {
|
|
||||||
int j = random(12);
|
|
||||||
int temp = scaleNotes[i];
|
|
||||||
scaleNotes[i] = scaleNotes[j];
|
|
||||||
scaleNotes[j] = temp;
|
|
||||||
}
|
|
||||||
sortArray(scaleNotes, numScaleNotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateSequenceData(int themeType, Step* target) {
|
|
||||||
randomSeed(melodySeed + themeType * 12345); // Deterministic seed for this theme
|
|
||||||
strategies[currentStrategyIndex]->generate(target, NUM_STEPS, scaleNotes, numScaleNotes, melodySeed + themeType * 12345);
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateTheme(int themeType) {
|
|
||||||
currentThemeIndex = themeType;
|
|
||||||
needsPanic = true;
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
generateSequenceData(themeType, sequence);
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
|
|
||||||
clockCount = 0;
|
|
||||||
lastClockTime = micros();
|
|
||||||
playbackStep = 0;
|
|
||||||
sendMidiRealtime(0xFA); // MIDI Start
|
|
||||||
isPlaying = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void mutateSequence(Step* target) {
|
|
||||||
strategies[currentStrategyIndex]->mutate(target, NUM_STEPS, scaleNotes, numScaleNotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleInput() {
|
void handleInput() {
|
||||||
// Handle Encoder Rotation
|
// Handle Encoder Rotation
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
@ -311,59 +145,22 @@ void handleInput() {
|
|||||||
interrupts();
|
interrupts();
|
||||||
|
|
||||||
if (delta != 0) {
|
if (delta != 0) {
|
||||||
switch(currentState) {
|
if (isEditing) {
|
||||||
case UI_TRACKER:
|
// Change Note
|
||||||
if (isEditing && navigationSelection > 0) {
|
int newNote = sequence[currentStep].note + delta;
|
||||||
// Change Note
|
if (newNote < -1) newNote = -1;
|
||||||
int stepIndex = navigationSelection - 1;
|
if (newNote > 127) newNote = 127;
|
||||||
int newNote = sequence[stepIndex].note + delta;
|
sequence[currentStep].note = newNote;
|
||||||
if (newNote < -1) newNote = -1;
|
Serial.print(F("Note changed: ")); Serial.println(newNote);
|
||||||
if (newNote > 127) newNote = 127;
|
} else {
|
||||||
mutex_enter_blocking(&midiMutex);
|
// Move Cursor
|
||||||
sequence[stepIndex].note = newNote;
|
currentStep += (delta > 0 ? 1 : -1);
|
||||||
mutex_exit(&midiMutex);
|
if (currentStep < 0) currentStep = NUM_STEPS - 1;
|
||||||
} else {
|
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||||
// Move Cursor
|
|
||||||
navigationSelection += (delta > 0 ? 1 : -1);
|
|
||||||
if (navigationSelection < 0) navigationSelection = NUM_STEPS;
|
|
||||||
if (navigationSelection > NUM_STEPS) navigationSelection = 0;
|
|
||||||
|
|
||||||
// Adjust Scroll to keep cursor in view
|
// Adjust Scroll to keep cursor in view
|
||||||
if (navigationSelection < scrollOffset) scrollOffset = navigationSelection;
|
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||||
if (navigationSelection >= scrollOffset + 6) scrollOffset = navigationSelection - 5;
|
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UI_MENU_MAIN:
|
|
||||||
menuSelection += (delta > 0 ? 1 : -1);
|
|
||||||
if (menuSelection < 0) menuSelection = mainMenuCount - 1;
|
|
||||||
if (menuSelection >= mainMenuCount) menuSelection = 0;
|
|
||||||
break;
|
|
||||||
case UI_MENU_RANDOMIZE:
|
|
||||||
menuSelection += (delta > 0 ? 1 : -1);
|
|
||||||
if (menuSelection < 0) menuSelection = randomizeMenuCount - 1;
|
|
||||||
if (menuSelection >= randomizeMenuCount) menuSelection = 0;
|
|
||||||
break;
|
|
||||||
case UI_MENU_SETUP:
|
|
||||||
menuSelection += (delta > 0 ? 1 : -1);
|
|
||||||
if (menuSelection < 0) menuSelection = setupMenuCount - 1;
|
|
||||||
if (menuSelection >= setupMenuCount) menuSelection = 0;
|
|
||||||
break;
|
|
||||||
case UI_SETUP_CHANNEL_EDIT:
|
|
||||||
midiChannel += (delta > 0 ? 1 : -1);
|
|
||||||
if (midiChannel < 1) midiChannel = 16;
|
|
||||||
if (midiChannel > 16) midiChannel = 1;
|
|
||||||
shMidiChannel = midiChannel;
|
|
||||||
break;
|
|
||||||
case UI_EDIT_TEMPO:
|
|
||||||
tempo += delta;
|
|
||||||
if (tempo < 40) tempo = 40;
|
|
||||||
if (tempo > 240) tempo = 240;
|
|
||||||
break;
|
|
||||||
case UI_EDIT_FLAVOUR:
|
|
||||||
currentStrategyIndex += (delta > 0 ? 1 : -1);
|
|
||||||
if (currentStrategyIndex < 0) currentStrategyIndex = numStrategies - 1;
|
|
||||||
if (currentStrategyIndex >= numStrategies) currentStrategyIndex = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,114 +183,21 @@ void handleInput() {
|
|||||||
if (reading == HIGH && buttonActive) {
|
if (reading == HIGH && buttonActive) {
|
||||||
// Button Released
|
// Button Released
|
||||||
buttonActive = false;
|
buttonActive = false;
|
||||||
if (!buttonConsumed) { // Short press action
|
if (!buttonConsumed) {
|
||||||
switch(currentState) {
|
isEditing = !isEditing;
|
||||||
case UI_TRACKER:
|
Serial.print(F("Mode toggled: ")); Serial.println(isEditing ? F("EDIT") : F("NAV"));
|
||||||
if (navigationSelection == 0) { // Menu item selected
|
|
||||||
currentState = UI_MENU_MAIN;
|
|
||||||
menuSelection = 0;
|
|
||||||
} else { // A step is selected
|
|
||||||
isEditing = !isEditing;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UI_MENU_MAIN:
|
|
||||||
if (menuSelection == 0) currentState = UI_TRACKER;
|
|
||||||
if (menuSelection == 1) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; }
|
|
||||||
if (menuSelection == 2) { currentState = UI_MENU_SETUP; menuSelection = 0; }
|
|
||||||
break;
|
|
||||||
case UI_MENU_RANDOMIZE:
|
|
||||||
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
|
|
||||||
if (menuSelection == 1) {
|
|
||||||
generateRandomScale(); // Scale
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
}
|
|
||||||
saveSequence(true);
|
|
||||||
}
|
|
||||||
if (menuSelection == 2) {
|
|
||||||
melodySeed = random(10000); // Melody
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
}
|
|
||||||
saveSequence(true);
|
|
||||||
}
|
|
||||||
if (menuSelection == 3) currentState = UI_EDIT_FLAVOUR;
|
|
||||||
if (menuSelection == 4) currentState = UI_EDIT_TEMPO;
|
|
||||||
if (menuSelection == 5) mutationEnabled = !mutationEnabled;
|
|
||||||
if (menuSelection == 6) {
|
|
||||||
songModeEnabled = !songModeEnabled;
|
|
||||||
if (songModeEnabled) {
|
|
||||||
songModeNeedsNext = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (menuSelection >= THEME_1_INDEX) { // Themes
|
|
||||||
const int selectedTheme = menuSelection - THEME_1_INDEX + 1;
|
|
||||||
if (isPlaying) {
|
|
||||||
queuedTheme = selectedTheme;
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
generateSequenceData(queuedTheme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
} else {
|
|
||||||
generateTheme(selectedTheme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UI_MENU_SETUP:
|
|
||||||
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; }
|
|
||||||
if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT;
|
|
||||||
if (menuSelection == 2) {
|
|
||||||
saveSequence();
|
|
||||||
currentState = UI_MENU_MAIN;
|
|
||||||
}
|
|
||||||
if (menuSelection == 3) {
|
|
||||||
if (loadSequence()) showMessage("LOADED!");
|
|
||||||
currentState = UI_MENU_MAIN;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UI_SETUP_CHANNEL_EDIT:
|
|
||||||
currentState = UI_MENU_SETUP;
|
|
||||||
saveSequence(true);
|
|
||||||
break;
|
|
||||||
case UI_EDIT_TEMPO:
|
|
||||||
currentState = UI_MENU_RANDOMIZE;
|
|
||||||
saveSequence(true);
|
|
||||||
break;
|
|
||||||
case UI_EDIT_FLAVOUR:
|
|
||||||
currentState = UI_MENU_RANDOMIZE;
|
|
||||||
saveSequence(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Long Press (Start/Stop Playback)
|
// Check for Long Press (Start/Stop Playback)
|
||||||
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
|
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
|
||||||
// Long press only works from tracker view
|
isPlaying = !isPlaying;
|
||||||
if (currentState == UI_TRACKER) {
|
buttonConsumed = true; // Prevent short press action
|
||||||
isPlaying = !isPlaying;
|
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
||||||
buttonConsumed = true; // Prevent short press action
|
if (!isPlaying) {
|
||||||
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
// Send All Notes Off on stop (CC 123)
|
||||||
if (isPlaying) {
|
sendMidi(0xB0, 123, 0);
|
||||||
playbackStep = 0;
|
|
||||||
clockCount = 0;
|
|
||||||
lastClockTime = micros();
|
|
||||||
sendMidiRealtime(0xFA); // MIDI Start
|
|
||||||
} else {
|
|
||||||
// Send All Notes Off on stop (CC 123)
|
|
||||||
needsPanic = true;
|
|
||||||
sendMidiRealtime(0xFC); // MIDI Stop
|
|
||||||
queuedTheme = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,288 +207,72 @@ void handleInput() {
|
|||||||
void handlePlayback() {
|
void handlePlayback() {
|
||||||
if (!isPlaying) return;
|
if (!isPlaying) return;
|
||||||
|
|
||||||
unsigned long currentMicros = micros();
|
unsigned long interval = 15000 / tempo; // 16th notes (60000 / tempo / 4)
|
||||||
unsigned long clockInterval = 2500000 / tempo; // 60s * 1000000us / (tempo * 24ppqn)
|
if (millis() - lastStepTime > interval) {
|
||||||
|
lastStepTime = millis();
|
||||||
|
|
||||||
if (currentMicros - lastClockTime >= clockInterval) {
|
// Note Off for current step (before advancing)
|
||||||
lastClockTime += clockInterval;
|
if (sequence[currentStep].note != -1) {
|
||||||
|
sendMidi(0x80, sequence[currentStep].note, 0);
|
||||||
sendMidiRealtime(0xF8); // MIDI Clock
|
|
||||||
|
|
||||||
clockCount++;
|
|
||||||
if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note
|
|
||||||
clockCount = 0;
|
|
||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
|
|
||||||
// Determine if we are tying to the next note
|
|
||||||
int nextStep = playbackStep + 1;
|
|
||||||
if (nextStep >= NUM_STEPS) nextStep = 0;
|
|
||||||
|
|
||||||
bool isTied = sequence[playbackStep].tie && (sequence[nextStep].note != -1);
|
|
||||||
int prevNote = sequence[playbackStep].note;
|
|
||||||
|
|
||||||
// Note Off for previous step (if NOT tied)
|
|
||||||
if (!isTied && prevNote != -1) {
|
|
||||||
sendMidi(0x80, prevNote, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackStep++;
|
currentStep++;
|
||||||
if (playbackStep >= NUM_STEPS) {
|
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||||
playbackStep = 0;
|
|
||||||
|
|
||||||
// Theme change
|
|
||||||
if (sequenceChangeScheduled && queuedTheme != -1) {
|
|
||||||
currentThemeIndex = queuedTheme;
|
|
||||||
queuedTheme = -1;
|
|
||||||
// nextSequence is already generated
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutation
|
|
||||||
if (mutationEnabled) {
|
|
||||||
if (!sequenceChangeScheduled) {
|
|
||||||
memcpy(nextSequence, sequence, sizeof(sequence));
|
|
||||||
}
|
|
||||||
mutateSequence(nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
|
||||||
|
|
||||||
if (sequenceChangeScheduled) {
|
|
||||||
memcpy(sequence, nextSequence, sizeof(sequence));
|
|
||||||
sequenceChangeScheduled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Song Mode? Advance repeats
|
|
||||||
if (songModeEnabled) {
|
|
||||||
// we just used one repeat
|
|
||||||
if (songRepeatsRemaining <= 1) {
|
|
||||||
// let's start another round
|
|
||||||
songRepeatsRemaining = nextSongRepeats;
|
|
||||||
} else {
|
|
||||||
// next repeat
|
|
||||||
songRepeatsRemaining--;
|
|
||||||
}
|
|
||||||
// Trigger next song segment generation if we are on the last repeat
|
|
||||||
if (songRepeatsRemaining <= 1 && !sequenceChangeScheduled && !songModeNeedsNext) {
|
|
||||||
songModeNeedsNext = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note On for new step
|
// Note On for new step
|
||||||
if (sequence[playbackStep].note != -1) {
|
if (sequence[currentStep].note != -1) {
|
||||||
uint8_t velocity = sequence[playbackStep].accent ? 127 : 100;
|
sendMidi(0x90, sequence[currentStep].note, 100);
|
||||||
sendMidi(0x90, sequence[playbackStep].note, velocity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note Off for previous step (if tied - delayed Note Off)
|
// Auto-scroll logic is handled in drawUI based on currentStep
|
||||||
if (isTied && prevNote != -1) {
|
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||||
sendMidi(0x80, prevNote, 0);
|
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||||
}
|
|
||||||
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawMenu(const char* title, const char* items[], int count, int selection) {
|
|
||||||
display.println(title);
|
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
||||||
|
|
||||||
// Simple scrolling: keep selection visible
|
|
||||||
int start = 0;
|
|
||||||
if (selection >= 5) {
|
|
||||||
start = selection - 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
int y = 10;
|
|
||||||
for (int i = start; i < count; i++) {
|
|
||||||
if (y > 55) break; // Stop if we run out of screen space
|
|
||||||
|
|
||||||
if (i == selection) {
|
|
||||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
|
||||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
||||||
} else {
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
display.setCursor(2, y);
|
|
||||||
display.print(items[i]);
|
|
||||||
|
|
||||||
// Special case for channel display
|
|
||||||
if (currentState == UI_MENU_SETUP && i == 1) {
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(midiChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case for queued theme
|
|
||||||
if (currentState == UI_MENU_RANDOMIZE && i >= THEME_1_INDEX && queuedTheme == (i - THEME_1_INDEX + 1)) {
|
|
||||||
display.print(F(" [NEXT]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case for active theme
|
|
||||||
if (currentState == UI_MENU_RANDOMIZE && i >= THEME_1_INDEX && currentThemeIndex == (i - THEME_1_INDEX + 1)) {
|
|
||||||
display.print(F(" *"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special cases for Randomize Menu values
|
|
||||||
if (currentState == UI_MENU_RANDOMIZE) {
|
|
||||||
if (i == 1) { // Scale
|
|
||||||
display.print(F(": "));
|
|
||||||
if (numScaleNotes > 0) {
|
|
||||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
||||||
for (int j = 0; j < min(numScaleNotes, 6); j++) {
|
|
||||||
display.print(noteNames[scaleNotes[j]]);
|
|
||||||
if (j < min(numScaleNotes, 6) - 1) display.print(F(" "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (i == 2) { // Melody
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(melodySeed);
|
|
||||||
} else if (i == 3) { // Flavour
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(strategies[currentStrategyIndex]->getName());
|
|
||||||
} else if (i == 4) { // Tempo
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(tempo);
|
|
||||||
} else if (i == 5) { // Mutation
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(mutationEnabled ? F("ON") : F("OFF"));
|
|
||||||
} else if (i == 6) { // Song Mode
|
|
||||||
display.print(F(": "));
|
|
||||||
display.print(songModeEnabled ? F("ON") : F("OFF"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y += 9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTracker() {
|
|
||||||
display.clearDisplay();
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
display.setCursor(0, 0);
|
|
||||||
|
|
||||||
// Header
|
|
||||||
display.print(F("SEQ "));
|
|
||||||
if (navigationSelection > 0 && isEditing) {
|
|
||||||
display.print(F("[EDIT]"));
|
|
||||||
} else {
|
|
||||||
display.print(F("[NAV] "));
|
|
||||||
}
|
|
||||||
display.print(F(" CH:"));
|
|
||||||
display.print(midiChannel);
|
|
||||||
|
|
||||||
display.println();
|
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
||||||
|
|
||||||
// Steps
|
|
||||||
int y = 10;
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
for (int i = 0; i < 6; i++) {
|
|
||||||
int itemIndex = i + scrollOffset;
|
|
||||||
if (itemIndex > NUM_STEPS) break;
|
|
||||||
|
|
||||||
// Draw Cursor
|
|
||||||
if (itemIndex == navigationSelection) {
|
|
||||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
|
||||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text
|
|
||||||
} else {
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
display.setCursor(2, y);
|
|
||||||
|
|
||||||
if (itemIndex == 0) {
|
|
||||||
display.print(F(">> MENU"));
|
|
||||||
} else {
|
|
||||||
int stepIndex = itemIndex - 1;
|
|
||||||
bool isPlayback = isPlaying && (stepIndex == playbackStep);
|
|
||||||
|
|
||||||
if (isPlayback) {
|
|
||||||
if (itemIndex == navigationSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
|
|
||||||
else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step Number
|
|
||||||
if (stepIndex < 10) display.print(F("0"));
|
|
||||||
display.print(stepIndex);
|
|
||||||
|
|
||||||
if (isPlayback) {
|
|
||||||
if (itemIndex == navigationSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
||||||
else display.setTextColor(SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
display.print(F(" | "));
|
|
||||||
|
|
||||||
// Note Value
|
|
||||||
int n = sequence[stepIndex].note;
|
|
||||||
if (n == -1) {
|
|
||||||
display.print(F("---"));
|
|
||||||
} else {
|
|
||||||
// Basic Note to String conversion
|
|
||||||
const char* noteNames[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"};
|
|
||||||
display.print(noteNames[n % 12]);
|
|
||||||
display.print(n / 12 - 1); // Octave
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
y += 9;
|
|
||||||
}
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawUI() {
|
void drawUI() {
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.setTextColor(SSD1306_WHITE);
|
display.setTextColor(SSD1306_WHITE);
|
||||||
display.setCursor(0, 0);
|
display.setCursor(0, 0);
|
||||||
|
|
||||||
switch(currentState) {
|
// Header
|
||||||
case UI_TRACKER:
|
display.print(F("TRACKER "));
|
||||||
drawTracker();
|
display.print(isEditing ? F("[EDIT]") : F("[NAV]"));
|
||||||
break;
|
display.println();
|
||||||
case UI_MENU_MAIN:
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||||
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection);
|
|
||||||
break;
|
// Steps
|
||||||
case UI_MENU_RANDOMIZE:
|
int y = 10;
|
||||||
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection);
|
for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) {
|
||||||
break;
|
|
||||||
case UI_MENU_SETUP:
|
// Draw Cursor
|
||||||
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection);
|
if (i == currentStep) {
|
||||||
break;
|
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||||
case UI_SETUP_CHANNEL_EDIT:
|
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text
|
||||||
display.println(F("SET MIDI CHANNEL"));
|
} else {
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
display.setTextColor(SSD1306_WHITE);
|
||||||
display.setCursor(20, 25);
|
}
|
||||||
display.setTextSize(2);
|
|
||||||
display.print(F("CH: "));
|
display.setCursor(2, y);
|
||||||
if (midiChannel < 10) display.print(F(" "));
|
|
||||||
display.print(midiChannel);
|
// Step Number
|
||||||
display.setTextSize(1);
|
if (i < 10) display.print(F("0"));
|
||||||
display.setCursor(0, 50);
|
display.print(i);
|
||||||
display.println(F(" (Press to confirm)"));
|
display.print(F(" | "));
|
||||||
break;
|
|
||||||
case UI_EDIT_TEMPO:
|
// Note Value
|
||||||
display.println(F("SET TEMPO"));
|
int n = sequence[i].note;
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
if (n == -1) {
|
||||||
display.setCursor(20, 25);
|
display.print(F("---"));
|
||||||
display.setTextSize(2);
|
} else {
|
||||||
display.print(F("BPM: "));
|
// Basic Note to String conversion
|
||||||
display.print(tempo);
|
const char* noteNames[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"};
|
||||||
display.setTextSize(1);
|
display.print(noteNames[n % 12]);
|
||||||
display.setCursor(0, 50);
|
display.print(n / 12 - 1); // Octave
|
||||||
display.println(F(" (Press to confirm)"));
|
}
|
||||||
break;
|
|
||||||
case UI_EDIT_FLAVOUR:
|
y += 9;
|
||||||
display.println(F("SET FLAVOUR"));
|
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
||||||
display.setCursor(20, 25);
|
|
||||||
display.setTextSize(2);
|
|
||||||
display.print(strategies[currentStrategyIndex]->getName());
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.setCursor(0, 50);
|
|
||||||
display.println(F(" (Press to confirm)"));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display.display();
|
display.display();
|
||||||
@ -797,137 +285,42 @@ int getPixelIndex(int x, int y) {
|
|||||||
return y * 8 + x;
|
return y * 8 + x;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t getNoteColor(int note, bool dim) {
|
|
||||||
if (note == -1) return 0;
|
|
||||||
// Map note to hue, avoiding Green (approx 21845) which is used for playback cursor.
|
|
||||||
// We start from Cyan (~30000) and go up to Orange (~8000 wrapped).
|
|
||||||
// Range: 30000 to 65536+8000 = 73536. Width = 43536.
|
|
||||||
// Step per semitone: 43536 / 12 = 3628.
|
|
||||||
// This ensures notes are distinct colors but never pure Green.
|
|
||||||
uint16_t hue = 30000 + (note % 12) * 3628;
|
|
||||||
return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateLeds() {
|
void updateLeds() {
|
||||||
pixels.clear(); // Clear buffer
|
pixels.clear(); // Clear buffer
|
||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
for (int s = 0; s < NUM_STEPS; s++) {
|
for (int s = 0; s < NUM_STEPS; s++) {
|
||||||
int x = s % 8;
|
int blockX = (s % 4) * 2;
|
||||||
int yBase = (s / 8) * 4;
|
int blockY = (s / 4) * 2;
|
||||||
|
|
||||||
uint32_t color = 0;
|
uint32_t color;
|
||||||
uint32_t dimColor = 0;
|
|
||||||
if (sequence[s].note != -1) {
|
|
||||||
color = getNoteColor(sequence[s].note, sequence[s].tie);
|
|
||||||
dimColor = getNoteColor(sequence[s].note, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t colorP0 = 0;
|
if (s == currentStep) {
|
||||||
uint32_t colorP1 = 0;
|
if (isEditing) {
|
||||||
uint32_t colorP2 = 0;
|
color = pixels.Color(50, 0, 0); // Dim Red for editing
|
||||||
uint32_t colorP3 = 0;
|
|
||||||
|
|
||||||
if (sequence[s].note != -1) {
|
|
||||||
int octave = sequence[s].note / 12;
|
|
||||||
|
|
||||||
if (octave > 4) {
|
|
||||||
// Octave Up -> Top Row (P0)
|
|
||||||
colorP0 = color;
|
|
||||||
if (sequence[s].accent) colorP1 = dimColor;
|
|
||||||
} else if (octave < 4) {
|
|
||||||
// Octave Down -> Bottom Row (P2)
|
|
||||||
colorP2 = color;
|
|
||||||
if (sequence[s].accent) colorP1 = dimColor;
|
|
||||||
} else {
|
} else {
|
||||||
// Normal -> Middle Row (P1)
|
color = pixels.Color(40, 40, 40); // Dim White for current step
|
||||||
colorP1 = color;
|
|
||||||
if (sequence[s].accent) {
|
|
||||||
colorP0 = dimColor;
|
|
||||||
colorP2 = dimColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if (sequence[s].note != -1) {
|
||||||
int stepNavIndex = navigationSelection - 1;
|
color = pixels.Color(0, 0, 50); // Dim Blue for step with note
|
||||||
uint32_t cursorColor = pixels.Color(0 + (s%2) * 20, 0 + (s%2) * 20, 50 + (s%2) * 20); // Blue for paused
|
|
||||||
if (isPlaying) {
|
|
||||||
cursorColor = pixels.Color(0, 50, 0); // Green for playback
|
|
||||||
if (songModeEnabled) {
|
|
||||||
// Song Mode: Show repeats on bottom row
|
|
||||||
// Right aligned.
|
|
||||||
// If repeats = 1 (last one), blink yellow.
|
|
||||||
if (s >= NUM_STEPS/2) { // second half = bottom row
|
|
||||||
int col = x;
|
|
||||||
int repeatsToDraw = min(songRepeatsRemaining, 8);
|
|
||||||
if (col >= (8 - repeatsToDraw)) {
|
|
||||||
if (songRepeatsRemaining == 1 && col == 7 && (millis() / 250) % 2) {
|
|
||||||
cursorColor = pixels.Color(0, 250, 0); // Max green, change is about to happen
|
|
||||||
} else {
|
|
||||||
cursorColor = pixels.Color(80, 100, 0); // Yellow-green, remaining repeat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (currentState == UI_TRACKER) {
|
|
||||||
cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorColor != 0) {
|
|
||||||
bool isCursorHere = (isPlaying && s == playbackStep) ||
|
|
||||||
(!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
|
|
||||||
if (isCursorHere) {
|
|
||||||
colorP3 = cursorColor;
|
|
||||||
} else {
|
} else {
|
||||||
// Lightly colored background for cursor row
|
color = 0; // Off
|
||||||
uint8_t r = (uint8_t)(cursorColor >> 16);
|
|
||||||
uint8_t g = (uint8_t)(cursorColor >> 8);
|
|
||||||
uint8_t b = (uint8_t)cursorColor;
|
|
||||||
colorP3 = pixels.Color(r/5, g/5, b/5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pixels.setPixelColor(getPixelIndex(x, yBase), colorP0);
|
// Set the 4 pixels for the 2x2 block
|
||||||
pixels.setPixelColor(getPixelIndex(x, yBase + 1), colorP1);
|
pixels.setPixelColor(getPixelIndex(blockX, blockY), color);
|
||||||
pixels.setPixelColor(getPixelIndex(x, yBase + 2), colorP2);
|
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), color);
|
||||||
pixels.setPixelColor(getPixelIndex(x, yBase + 3), colorP3);
|
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), color);
|
||||||
|
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sequenceChangeScheduled && (millis() / 125) % 2) {
|
|
||||||
pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
|
|
||||||
}
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
|
|
||||||
pixels.show();
|
pixels.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop1() {
|
|
||||||
if (needsPanic) {
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
sendMidi(0xB0, 123, 0);
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
needsPanic = false;
|
|
||||||
}
|
|
||||||
handlePlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Handle Song Mode Generation in UI Thread
|
|
||||||
if (songModeNeedsNext) {
|
|
||||||
int nextTheme = random(1, 8); // Themes 1-7
|
|
||||||
int repeats = random(1, 9); // 1-8 repeats
|
|
||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
|
||||||
generateSequenceData(nextTheme, nextSequence);
|
|
||||||
queuedTheme = nextTheme;
|
|
||||||
nextSongRepeats = repeats;
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
mutex_exit(&midiMutex);
|
|
||||||
|
|
||||||
songModeNeedsNext = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput();
|
handleInput();
|
||||||
|
handlePlayback();
|
||||||
drawUI();
|
drawUI();
|
||||||
updateLeds();
|
updateLeds();
|
||||||
delay(10); // Small delay to prevent screen tearing/excessive refresh
|
delay(10); // Small delay to prevent screen tearing/excessive refresh
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
#ifndef TRACKER_TYPES_H
|
|
||||||
#define TRACKER_TYPES_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
struct Step {
|
|
||||||
int8_t note; // MIDI Note (0-127), -1 for OFF
|
|
||||||
bool accent;
|
|
||||||
bool tie;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Loading…
Reference in New Issue
Block a user