Compare commits

...

3 Commits

Author SHA1 Message Date
Dejvino
c6e2248090 Factory Reset 2026-02-17 00:52:36 +01:00
Dejvino
225d04a53c Better arp 2026-02-17 00:49:17 +01:00
Dejvino
6734572a67 Refactor into files 2026-02-17 00:26:53 +01:00
9 changed files with 598 additions and 467 deletions

View File

@ -10,24 +10,102 @@ public:
randomSeed(seed); randomSeed(seed);
if (numScaleNotes == 0) return; if (numScaleNotes == 0) return;
int currentNoteIndex = 0; // 1. Select Subset
int octave = 4; int subsetSize = random(2, numScaleNotes + 1);
if (subsetSize > 12) subsetSize = 12;
int subset[12];
for (int i = 0; i < numSteps; i++) { // Create indices to shuffle
sequence[i].note = 12 * octave + scaleNotes[currentNoteIndex]; int indices[12];
sequence[i].accent = (i % 4 == 0); // Accent on beat for(int i=0; i<numScaleNotes; i++) indices[i] = i;
sequence[i].tie = false;
currentNoteIndex++; // Shuffle indices
if (currentNoteIndex >= numScaleNotes) { for(int i=0; i<numScaleNotes; i++) {
currentNoteIndex = 0; int r = random(numScaleNotes);
octave++; int temp = indices[i];
if (octave > 5) octave = 3; indices[i] = indices[r];
indices[r] = temp;
} }
// Pick subset
for(int i=0; i<subsetSize; i++) {
subset[i] = scaleNotes[indices[i]];
}
sortArray(subset, subsetSize);
// 2. Pick Arp Length & Pattern
int arpLength = random(2, 17); // 2 to 16
Step arpPattern[16];
int mode = random(3); // 0: Up, 1: Down, 2: Up/Down
int numOctaves = 3;
int baseOctave = 3;
int totalNotes = subsetSize * numOctaves;
int currentIndex = 0;
int direction = 1;
if (mode == 1) { // Down
currentIndex = totalNotes - 1;
direction = -1;
}
for (int i = 0; i < arpLength; i++) {
// Chance of rest (15%)
if (random(100) < 15) {
arpPattern[i].note = -1;
arpPattern[i].accent = false;
arpPattern[i].tie = false;
} else {
int octaveOffset = currentIndex / subsetSize;
int noteIndex = currentIndex % subsetSize;
int octave = baseOctave + octaveOffset;
arpPattern[i].note = 12 * octave + subset[noteIndex];
arpPattern[i].accent = (i % 4 == 0); // Accent on beat
arpPattern[i].tie = false;
}
if (mode == 0) { // Up
currentIndex++;
if (currentIndex >= totalNotes) currentIndex = 0;
} else if (mode == 1) { // Down
currentIndex--;
if (currentIndex < 0) currentIndex = totalNotes - 1;
} else { // Up/Down
currentIndex += direction;
if (currentIndex >= totalNotes) {
currentIndex = max(0, totalNotes - 2);
direction = -1;
} else if (currentIndex < 0) {
currentIndex = min(totalNotes - 1, 1);
direction = 1;
}
}
}
// 3. Fill Sequence
for (int i = 0; i < numSteps; i++) {
sequence[i] = arpPattern[i % arpLength];
} }
randomSeed(micros()); randomSeed(micros());
} }
void generateScale(int* scaleNotes, int& numScaleNotes) override {
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 mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override { void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Swap two notes // Swap two notes
int s1 = random(numSteps); int s1 = random(numSteps);

View File

@ -19,6 +19,21 @@ public:
randomSeed(micros()); randomSeed(micros());
} }
void generateScale(int* scaleNotes, int& numScaleNotes) override {
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 mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override { void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
// Mutate 1 or 2 steps // Mutate 1 or 2 steps
int count = random(1, 3); int count = random(1, 3);

View File

@ -6,6 +6,7 @@
class MelodyStrategy { class MelodyStrategy {
public: public:
virtual void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0; virtual void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0;
virtual void generateScale(int* scaleNotes, int& numScaleNotes) = 0;
virtual void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) = 0; virtual void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) = 0;
virtual const char* getName() = 0; virtual const char* getName() = 0;
virtual ~MelodyStrategy() {} virtual ~MelodyStrategy() {}

49
MidiDriver.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "MidiDriver.h"
// MIDI UART Pins (GP0/GP1)
#define PIN_MIDI_TX 0
MidiDriver midi;
MidiDriver::MidiDriver() {
}
void MidiDriver::begin() {
mutex_init(&_mutex);
Serial1.setTX(PIN_MIDI_TX);
Serial1.begin(31250);
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
}
void MidiDriver::lock() {
mutex_enter_blocking(&_mutex);
}
void MidiDriver::unlock() {
mutex_exit(&_mutex);
}
void MidiDriver::sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel) {
uint8_t status = 0x90 | (channel - 1);
Serial1.write(status);
Serial1.write(note);
Serial1.write(velocity);
}
void MidiDriver::sendNoteOff(uint8_t note, uint8_t channel) {
uint8_t status = 0x80 | (channel - 1);
Serial1.write(status);
Serial1.write(note);
Serial1.write((uint8_t)0);
}
void MidiDriver::sendRealtime(uint8_t status) {
Serial1.write(status);
}
void MidiDriver::panic(uint8_t channel) {
uint8_t status = 0xB0 | (channel - 1);
Serial1.write(status);
Serial1.write(123); // All Notes Off
Serial1.write((uint8_t)0);
}

26
MidiDriver.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef MIDI_DRIVER_H
#define MIDI_DRIVER_H
#include <Arduino.h>
#include <pico/mutex.h>
class MidiDriver {
public:
MidiDriver();
void begin();
void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel);
void sendNoteOff(uint8_t note, uint8_t channel);
void sendRealtime(uint8_t status);
void panic(uint8_t channel);
void lock();
void unlock();
private:
mutex_t _mutex;
};
extern MidiDriver midi;
#endif

View File

@ -1,14 +1,12 @@
#include <SPI.h> #include <SPI.h>
#include <Wire.h> #include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h> #include <EEPROM.h>
#include <pico/mutex.h>
#include "TrackerTypes.h" #include "TrackerTypes.h"
#include "MelodyStrategy.h" #include "MelodyStrategy.h"
#include "LuckyStrategy.h" #include "LuckyStrategy.h"
#include "ArpStrategy.h" #include "ArpStrategy.h"
#include "MidiDriver.h"
#include "UIManager.h"
// --- HARDWARE CONFIGURATION --- // --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128 #define SCREEN_WIDTH 128
@ -24,13 +22,6 @@
#define ENC_DT 13 #define ENC_DT 13
#define ENC_SW 14 #define ENC_SW 14
// MIDI UART Pins (GP0/GP1) -- OUT only so far
#define PIN_MIDI_TX 0
// NeoPixel Pin (any GPIO is fine, I've chosen 16)
#define PIN_NEOPIXEL 16
#define NUM_PIXELS 64 // For 8x8 WS2812B matrix
// --- TRACKER DATA --- // --- TRACKER DATA ---
#define NUM_STEPS 16 #define NUM_STEPS 16
@ -39,30 +30,14 @@ Step nextSequence[NUM_STEPS];
volatile bool sequenceChangeScheduled = false; volatile bool sequenceChangeScheduled = false;
volatile bool needsPanic = false; volatile bool needsPanic = false;
mutex_t midiMutex;
// --- STATE ---
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
enum UIState {
UI_TRACKER,
UI_MENU_MAIN,
UI_MENU_RANDOMIZE,
UI_MENU_SETUP,
UI_SETUP_CHANNEL_EDIT,
UI_EDIT_TEMPO,
UI_EDIT_FLAVOUR
};
UIState currentState = UI_MENU_MAIN; UIState currentState = UI_MENU_MAIN;
const char* mainMenu[] = { "Tracker", "Randomize", "Setup" }; const char* mainMenu[] = { "Tracker", "Randomize", "Setup" };
const int mainMenuCount = sizeof(mainMenu) / sizeof(char*); 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 char* randomizeMenu[] = { "Back", "Melody", "Flavour", "Scale", "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 THEME_1_INDEX = 7;
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*); const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
const char* setupMenu[] = { "Back", "Channel", "Save", "Load" }; const char* setupMenu[] = { "Back", "Channel", "Factory Reset" };
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*); const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
int menuSelection = 0; int menuSelection = 0;
@ -125,29 +100,6 @@ 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) { void saveSequence(bool quiet = false) {
int addr = 0; int addr = 0;
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC); EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
@ -161,13 +113,13 @@ void saveSequence(bool quiet = false) {
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int); EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
} }
mutex_enter_blocking(&midiMutex); midi.lock();
for (int i=0; i<NUM_STEPS; i++) { for (int i=0; i<NUM_STEPS; i++) {
EEPROM.put(addr, sequence[i]); addr += sizeof(Step); EEPROM.put(addr, sequence[i]); addr += sizeof(Step);
} }
mutex_exit(&midiMutex); midi.unlock();
EEPROM.commit(); EEPROM.commit();
if (!quiet) showMessage("SAVED!"); if (!quiet) ui.showMessage("SAVED!");
} }
bool loadSequence() { bool loadSequence() {
@ -189,17 +141,25 @@ bool loadSequence() {
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int); EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
} }
mutex_enter_blocking(&midiMutex); midi.lock();
for (int i=0; i<NUM_STEPS; i++) { for (int i=0; i<NUM_STEPS; i++) {
EEPROM.get(addr, sequence[i]); addr += sizeof(Step); EEPROM.get(addr, sequence[i]); addr += sizeof(Step);
} }
mutex_exit(&midiMutex); midi.unlock();
return true; return true;
} }
void factoryReset() {
ui.showMessage("RESETTING...");
uint32_t magic = 0;
EEPROM.put(0, magic);
EEPROM.commit();
delay(500);
rp2040.reboot();
}
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
mutex_init(&midiMutex);
// Use random ADC noise for seed // Use random ADC noise for seed
delay(5000); delay(5000);
@ -213,26 +173,8 @@ void setup() {
attachInterrupt(digitalPinToInterrupt(ENC_CLK), readEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_CLK), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE);
// 2. Setup Display ui.begin();
// Note: Using default I2C pins (SDA=GP4, SCL=GP5) which works on both cores. midi.begin();
Wire.begin();
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// 3. Setup NeoPixel Matrix
pixels.begin();
pixels.setBrightness(40); // Set brightness to a medium-low value (0-255)
pixels.clear();
pixels.show();
// 4. Setup MIDI Serial
Serial1.setTX(PIN_MIDI_TX);
Serial1.begin(31250);
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
// 5. Init Sequence // 5. Init Sequence
randomSeed(micros()); randomSeed(micros());
@ -245,38 +187,11 @@ void setup() {
} }
isPlaying = false; // Don't start playing on boot isPlaying = false; // Don't start playing on boot
display.clearDisplay();
display.display();
Serial.println(F("Started.")); Serial.println(F("Started."));
} }
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
uint8_t channelStatus = status | (shMidiChannel - 1);
Serial1.write(channelStatus);
Serial1.write(note);
Serial1.write(velocity);
}
void sendMidiRealtime(uint8_t status) {
mutex_enter_blocking(&midiMutex);
Serial1.write(status);
mutex_exit(&midiMutex);
}
void generateRandomScale() { void generateRandomScale() {
numScaleNotes = random(3, 13); // 3 to 12 notes strategies[currentStrategyIndex]->generateScale(scaleNotes, numScaleNotes);
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) { void generateSequenceData(int themeType, Step* target) {
@ -287,14 +202,14 @@ void generateSequenceData(int themeType, Step* target) {
void generateTheme(int themeType) { void generateTheme(int themeType) {
currentThemeIndex = themeType; currentThemeIndex = themeType;
needsPanic = true; needsPanic = true;
mutex_enter_blocking(&midiMutex); midi.lock();
generateSequenceData(themeType, sequence); generateSequenceData(themeType, sequence);
mutex_exit(&midiMutex); midi.unlock();
clockCount = 0; clockCount = 0;
lastClockTime = micros(); lastClockTime = micros();
playbackStep = 0; playbackStep = 0;
sendMidiRealtime(0xFA); // MIDI Start midi.sendRealtime(0xFA); // MIDI Start
isPlaying = true; isPlaying = true;
} }
@ -319,9 +234,9 @@ void handleInput() {
int newNote = sequence[stepIndex].note + delta; int newNote = sequence[stepIndex].note + delta;
if (newNote < -1) newNote = -1; if (newNote < -1) newNote = -1;
if (newNote > 127) newNote = 127; if (newNote > 127) newNote = 127;
mutex_enter_blocking(&midiMutex); midi.lock();
sequence[stepIndex].note = newNote; sequence[stepIndex].note = newNote;
mutex_exit(&midiMutex); midi.unlock();
} else { } else {
// Move Cursor // Move Cursor
navigationSelection += (delta > 0 ? 1 : -1); navigationSelection += (delta > 0 ? 1 : -1);
@ -404,28 +319,28 @@ void handleInput() {
case UI_MENU_RANDOMIZE: case UI_MENU_RANDOMIZE:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; } if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
if (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 melodySeed = random(10000); // Melody
if (isPlaying) { if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
mutex_enter_blocking(&midiMutex); midi.lock();
generateSequenceData(theme, nextSequence); generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true; sequenceChangeScheduled = true;
mutex_exit(&midiMutex); midi.unlock();
}
saveSequence(true);
}
if (menuSelection == 2) currentState = UI_EDIT_FLAVOUR; // Flavour
if (menuSelection == 3) { // Scale
generateRandomScale();
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
generateSequenceData(theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
} }
saveSequence(true); saveSequence(true);
} }
if (menuSelection == 3) currentState = UI_EDIT_FLAVOUR;
if (menuSelection == 4) currentState = UI_EDIT_TEMPO; if (menuSelection == 4) currentState = UI_EDIT_TEMPO;
if (menuSelection == 5) mutationEnabled = !mutationEnabled; if (menuSelection == 5) mutationEnabled = !mutationEnabled;
if (menuSelection == 6) { if (menuSelection == 6) {
@ -438,10 +353,10 @@ void handleInput() {
const int selectedTheme = menuSelection - THEME_1_INDEX + 1; const int selectedTheme = menuSelection - THEME_1_INDEX + 1;
if (isPlaying) { if (isPlaying) {
queuedTheme = selectedTheme; queuedTheme = selectedTheme;
mutex_enter_blocking(&midiMutex); midi.lock();
generateSequenceData(queuedTheme, nextSequence); generateSequenceData(queuedTheme, nextSequence);
sequenceChangeScheduled = true; sequenceChangeScheduled = true;
mutex_exit(&midiMutex); midi.unlock();
} else { } else {
generateTheme(selectedTheme); generateTheme(selectedTheme);
} }
@ -450,14 +365,7 @@ void handleInput() {
case UI_MENU_SETUP: case UI_MENU_SETUP:
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; } if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; }
if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT; if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT;
if (menuSelection == 2) { if (menuSelection == 2) factoryReset();
saveSequence();
currentState = UI_MENU_MAIN;
}
if (menuSelection == 3) {
if (loadSequence()) showMessage("LOADED!");
currentState = UI_MENU_MAIN;
}
break; break;
case UI_SETUP_CHANNEL_EDIT: case UI_SETUP_CHANNEL_EDIT:
currentState = UI_MENU_SETUP; currentState = UI_MENU_SETUP;
@ -469,6 +377,13 @@ void handleInput() {
break; break;
case UI_EDIT_FLAVOUR: case UI_EDIT_FLAVOUR:
currentState = UI_MENU_RANDOMIZE; currentState = UI_MENU_RANDOMIZE;
if (isPlaying) {
queuedTheme = currentThemeIndex;
midi.lock();
generateSequenceData(currentThemeIndex, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true); saveSequence(true);
break; break;
} }
@ -487,11 +402,11 @@ void handleInput() {
playbackStep = 0; playbackStep = 0;
clockCount = 0; clockCount = 0;
lastClockTime = micros(); lastClockTime = micros();
sendMidiRealtime(0xFA); // MIDI Start midi.sendRealtime(0xFA); // MIDI Start
} else { } else {
// Send All Notes Off on stop (CC 123) // Send All Notes Off on stop (CC 123)
needsPanic = true; needsPanic = true;
sendMidiRealtime(0xFC); // MIDI Stop midi.sendRealtime(0xFC); // MIDI Stop
queuedTheme = -1; queuedTheme = -1;
} }
} }
@ -509,13 +424,13 @@ void handlePlayback() {
if (currentMicros - lastClockTime >= clockInterval) { if (currentMicros - lastClockTime >= clockInterval) {
lastClockTime += clockInterval; lastClockTime += clockInterval;
sendMidiRealtime(0xF8); // MIDI Clock midi.sendRealtime(0xF8); // MIDI Clock
clockCount++; clockCount++;
if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note
clockCount = 0; clockCount = 0;
mutex_enter_blocking(&midiMutex); midi.lock();
// Determine if we are tying to the next note // Determine if we are tying to the next note
int nextStep = playbackStep + 1; int nextStep = playbackStep + 1;
@ -526,7 +441,7 @@ void handlePlayback() {
// Note Off for previous step (if NOT tied) // Note Off for previous step (if NOT tied)
if (!isTied && prevNote != -1) { if (!isTied && prevNote != -1) {
sendMidi(0x80, prevNote, 0); midi.sendNoteOff(prevNote, shMidiChannel);
} }
playbackStep++; playbackStep++;
@ -549,7 +464,7 @@ void handlePlayback() {
sequenceChangeScheduled = true; sequenceChangeScheduled = true;
} }
sendMidi(0xB0, 123, 0); // Panic / All Notes Off midi.panic(shMidiChannel); // Panic / All Notes Off
if (sequenceChangeScheduled) { if (sequenceChangeScheduled) {
memcpy(sequence, nextSequence, sizeof(sequence)); memcpy(sequence, nextSequence, sizeof(sequence));
@ -576,336 +491,35 @@ void handlePlayback() {
// Note On for new step // Note On for new step
if (sequence[playbackStep].note != -1) { if (sequence[playbackStep].note != -1) {
uint8_t velocity = sequence[playbackStep].accent ? 127 : 100; uint8_t velocity = sequence[playbackStep].accent ? 127 : 100;
sendMidi(0x90, sequence[playbackStep].note, velocity); midi.sendNoteOn(sequence[playbackStep].note, velocity, shMidiChannel);
} }
// Note Off for previous step (if tied - delayed Note Off) // Note Off for previous step (if tied - delayed Note Off)
if (isTied && prevNote != -1) { if (isTied && prevNote != -1) {
sendMidi(0x80, prevNote, 0); midi.sendNoteOff(prevNote, shMidiChannel);
} }
mutex_exit(&midiMutex); midi.unlock();
} }
} }
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(); midi.lock();
display.setTextSize(1); ui.draw(currentState, menuSelection, navigationSelection, isEditing, midiChannel, tempo, strategies[currentStrategyIndex], queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, sequence, scrollOffset, playbackStep, isPlaying, mainMenu, mainMenuCount, randomizeMenu, randomizeMenuCount, setupMenu, setupMenuCount, THEME_1_INDEX);
display.setTextColor(SSD1306_WHITE); midi.unlock();
display.setCursor(0, 0);
switch(currentState) {
case UI_TRACKER:
drawTracker();
break;
case UI_MENU_MAIN:
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection);
break;
case UI_MENU_RANDOMIZE:
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection);
break;
case UI_MENU_SETUP:
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection);
break;
case UI_SETUP_CHANNEL_EDIT:
display.println(F("SET MIDI CHANNEL"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(F("CH: "));
if (midiChannel < 10) display.print(F(" "));
display.print(midiChannel);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_EDIT_TEMPO:
display.println(F("SET TEMPO"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(F("BPM: "));
display.print(tempo);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_EDIT_FLAVOUR:
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();
}
// Helper to convert X,Y to pixel index for an 8x8 matrix.
// Assumes row-major wiring (NOT serpentine).
// If your matrix is wired differently, you'll need to change this function.
int getPixelIndex(int x, int y) {
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 midi.lock();
ui.updateLeds(sequence, navigationSelection, playbackStep, isPlaying, currentState, isEditing, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled);
mutex_enter_blocking(&midiMutex); midi.unlock();
for (int s = 0; s < NUM_STEPS; s++) {
int x = s % 8;
int yBase = (s / 8) * 4;
uint32_t color = 0;
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;
uint32_t colorP1 = 0;
uint32_t colorP2 = 0;
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 {
// Normal -> Middle Row (P1)
colorP1 = color;
if (sequence[s].accent) {
colorP0 = dimColor;
colorP2 = dimColor;
}
}
}
int stepNavIndex = navigationSelection - 1;
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 {
// Lightly colored background for cursor row
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);
pixels.setPixelColor(getPixelIndex(x, yBase + 1), colorP1);
pixels.setPixelColor(getPixelIndex(x, yBase + 2), colorP2);
pixels.setPixelColor(getPixelIndex(x, yBase + 3), colorP3);
}
if (sequenceChangeScheduled && (millis() / 125) % 2) {
pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
}
mutex_exit(&midiMutex);
pixels.show();
} }
void loop1() { void loop1() {
if (needsPanic) { if (needsPanic) {
mutex_enter_blocking(&midiMutex); midi.lock();
sendMidi(0xB0, 123, 0); midi.panic(shMidiChannel);
mutex_exit(&midiMutex); midi.unlock();
needsPanic = false; needsPanic = false;
} }
handlePlayback(); handlePlayback();
@ -917,12 +531,12 @@ void loop() {
int nextTheme = random(1, 8); // Themes 1-7 int nextTheme = random(1, 8); // Themes 1-7
int repeats = random(1, 9); // 1-8 repeats int repeats = random(1, 9); // 1-8 repeats
mutex_enter_blocking(&midiMutex); midi.lock();
generateSequenceData(nextTheme, nextSequence); generateSequenceData(nextTheme, nextSequence);
queuedTheme = nextTheme; queuedTheme = nextTheme;
nextSongRepeats = repeats; nextSongRepeats = repeats;
sequenceChangeScheduled = true; sequenceChangeScheduled = true;
mutex_exit(&midiMutex); midi.unlock();
songModeNeedsNext = false; songModeNeedsNext = false;
} }

View File

@ -9,4 +9,26 @@ struct Step {
bool tie; bool tie;
}; };
enum UIState {
UI_TRACKER,
UI_MENU_MAIN,
UI_MENU_RANDOMIZE,
UI_MENU_SETUP,
UI_SETUP_CHANNEL_EDIT,
UI_EDIT_TEMPO,
UI_EDIT_FLAVOUR
};
inline 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;
}
}
}
}
#endif #endif

275
UIManager.cpp Normal file
View File

@ -0,0 +1,275 @@
#include "UIManager.h"
// --- HARDWARE CONFIGURATION ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define PIN_NEOPIXEL 16
#define NUM_PIXELS 64
#define NUM_STEPS 16
UIManager ui;
UIManager::UIManager()
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET),
pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800)
{
}
void UIManager::begin() {
// Setup Display
Wire.begin();
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.display();
// Setup NeoPixel Matrix
pixels.begin();
pixels.setBrightness(40);
pixels.clear();
pixels.show();
}
void UIManager::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 UIManager::draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
int midiChannel, int tempo, MelodyStrategy* currentStrategy,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying,
const char* mainMenu[], int mainMenuCount,
const char* randomizeMenu[], int randomizeMenuCount,
const char* setupMenu[], int setupMenuCount,
int theme1Index) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
switch(currentState) {
case UI_TRACKER:
drawTracker(navSelection, isEditing, midiChannel, sequence, scrollOffset, playbackStep, isPlaying);
break;
case UI_MENU_MAIN:
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
break;
case UI_MENU_RANDOMIZE:
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
break;
case UI_MENU_SETUP:
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
break;
case UI_SETUP_CHANNEL_EDIT:
display.println(F("SET MIDI CHANNEL"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(F("CH: "));
if (midiChannel < 10) display.print(F(" "));
display.print(midiChannel);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_EDIT_TEMPO:
display.println(F("SET TEMPO"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(F("BPM: "));
display.print(tempo);
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
case UI_EDIT_FLAVOUR:
display.println(F("SET FLAVOUR"));
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
display.setCursor(20, 25);
display.setTextSize(2);
display.print(currentStrategy->getName());
display.setTextSize(1);
display.setCursor(0, 50);
display.println(F(" (Press to confirm)"));
break;
}
display.display();
}
void UIManager::drawMenu(const char* title, const char* items[], int count, int selection,
UIState currentState, int midiChannel, int tempo, const char* flavourName,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled, int theme1Index) {
display.println(title);
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
int start = 0;
if (selection >= 5) start = selection - 4;
int y = 10;
for (int i = start; i < count; i++) {
if (y > 55) break;
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]);
if (currentState == UI_MENU_SETUP && i == 1) {
display.print(F(": ")); display.print(midiChannel);
}
if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && queuedTheme == (i - theme1Index + 1)) {
display.print(F(" [NEXT]"));
}
if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && currentThemeIndex == (i - theme1Index + 1)) {
display.print(F(" *"));
}
if (currentState == UI_MENU_RANDOMIZE) {
if (i == 1) { // Melody
display.print(F(": ")); display.print(melodySeed);
} else if (i == 2) { // Flavour
display.print(F(": ")); display.print(flavourName);
} else if (i == 3) { // 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 == 4) { display.print(F(": ")); display.print(tempo); }
else if (i == 5) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); }
else if (i == 6) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
}
y += 9;
}
}
void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying) {
display.print(F("SEQ "));
if (navSelection > 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);
int y = 10;
for (int i = 0; i < 6; i++) {
int itemIndex = i + scrollOffset;
if (itemIndex > NUM_STEPS) break;
if (itemIndex == navSelection) {
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
} 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 == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
}
if (stepIndex < 10) display.print(F("0"));
display.print(stepIndex);
if (isPlayback) {
if (itemIndex == navSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
else display.setTextColor(SSD1306_WHITE);
}
display.print(F(" | "));
int n = sequence[stepIndex].note;
if (n == -1) display.print(F("---"));
else {
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);
}
}
y += 9;
}
}
uint32_t UIManager::getNoteColor(int note, bool dim) {
if (note == -1) return 0;
uint16_t hue = 30000 + (note % 12) * 3628;
return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
}
int UIManager::getPixelIndex(int x, int y) {
return y * 8 + x;
}
void UIManager::updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
UIState currentState, bool isEditing, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled) {
pixels.clear();
for (int s = 0; s < NUM_STEPS; s++) {
int x = s % 8;
int yBase = (s / 8) * 4;
uint32_t color = 0, dimColor = 0;
if (sequence[s].note != -1) {
color = getNoteColor(sequence[s].note, sequence[s].tie);
dimColor = getNoteColor(sequence[s].note, true);
}
uint32_t c[4] = {0};
if (sequence[s].note != -1) {
int octave = sequence[s].note / 12;
if (octave > 4) { c[0] = color; if (sequence[s].accent) c[1] = dimColor; }
else if (octave < 4) { c[2] = color; if (sequence[s].accent) c[1] = dimColor; }
else { c[1] = color; if (sequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } }
}
int stepNavIndex = navSelection - 1;
// Apply cursor color logic from original code more strictly
uint32_t cursorColor = 0;
if (isPlaying) {
cursorColor = pixels.Color(0, 50, 0);
if (songModeEnabled && s >= 8) {
int repeats = min(songRepeatsRemaining, 8);
if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40);
}
} else if (currentState == UI_TRACKER) {
cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
}
bool isCursorHere = (isPlaying && s == playbackStep) || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
if (cursorColor != 0) {
if (isCursorHere) c[3] = cursorColor;
else {
uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor;
c[3] = pixels.Color(r/5, g/5, b/5);
}
}
for(int i=0; i<4; i++) pixels.setPixelColor(getPixelIndex(x, yBase + i), c[i]);
}
if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
pixels.show();
}

51
UIManager.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef UI_MANAGER_H
#define UI_MANAGER_H
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include "TrackerTypes.h"
#include "MelodyStrategy.h"
class UIManager {
public:
UIManager();
void begin();
void showMessage(const char* msg);
void draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
int midiChannel, int tempo, MelodyStrategy* currentStrategy,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying,
const char* mainMenu[], int mainMenuCount,
const char* randomizeMenu[], int randomizeMenuCount,
const char* setupMenu[], int setupMenuCount,
int theme1Index);
void updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
UIState currentState, bool isEditing, bool songModeEnabled,
int songRepeatsRemaining, bool sequenceChangeScheduled);
private:
Adafruit_SSD1306 display;
Adafruit_NeoPixel pixels;
void drawMenu(const char* title, const char* items[], int count, int selection,
UIState currentState, int midiChannel, int tempo, const char* flavourName,
int queuedTheme, int currentThemeIndex,
int numScaleNotes, const int* scaleNotes, int melodySeed,
bool mutationEnabled, bool songModeEnabled, int theme1Index);
void drawTracker(int navSelection, bool isEditing, int midiChannel,
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying);
uint32_t getNoteColor(int note, bool dim);
int getPixelIndex(int x, int y);
};
extern UIManager ui;
#endif