Accents and ties with saving
This commit is contained in:
parent
8759d7f46a
commit
5a773d31a4
@ -3,6 +3,7 @@
|
|||||||
#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>
|
||||||
|
|
||||||
// --- HARDWARE CONFIGURATION ---
|
// --- HARDWARE CONFIGURATION ---
|
||||||
#define SCREEN_WIDTH 128
|
#define SCREEN_WIDTH 128
|
||||||
@ -30,6 +31,8 @@
|
|||||||
|
|
||||||
struct Step {
|
struct Step {
|
||||||
int8_t note; // MIDI Note (0-127), -1 for OFF
|
int8_t note; // MIDI Note (0-127), -1 for OFF
|
||||||
|
bool accent;
|
||||||
|
bool tie;
|
||||||
};
|
};
|
||||||
|
|
||||||
Step sequence[NUM_STEPS];
|
Step sequence[NUM_STEPS];
|
||||||
@ -50,9 +53,9 @@ UIState currentState = UI_TRACKER;
|
|||||||
|
|
||||||
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", "Gen Scale", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
|
const char* randomizeMenu[] = { "Back", "Scale", "Melody", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
|
||||||
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
|
const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
|
||||||
const char* setupMenu[] = { "Back", "Channel" };
|
const char* setupMenu[] = { "Back", "Channel", "Save", "Load" };
|
||||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
||||||
|
|
||||||
int menuSelection = 0;
|
int menuSelection = 0;
|
||||||
@ -61,7 +64,9 @@ int playbackStep = 0;
|
|||||||
int midiChannel = 1;
|
int midiChannel = 1;
|
||||||
int scaleNotes[12];
|
int scaleNotes[12];
|
||||||
int numScaleNotes = 0;
|
int numScaleNotes = 0;
|
||||||
|
int melodySeed = 0;
|
||||||
int queuedTheme = -1;
|
int queuedTheme = -1;
|
||||||
|
const uint32_t EEPROM_MAGIC = 0x42424244;
|
||||||
|
|
||||||
bool isEditing = false;
|
bool isEditing = false;
|
||||||
int scrollOffset = 0;
|
int scrollOffset = 0;
|
||||||
@ -113,6 +118,55 @@ void sortArray(int arr[], int size) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
for (int i=0; i<12; i++) {
|
||||||
|
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i=0; i<NUM_STEPS; i++) {
|
||||||
|
EEPROM.put(addr, sequence[i]); addr += sizeof(Step);
|
||||||
|
}
|
||||||
|
EEPROM.commit();
|
||||||
|
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);
|
||||||
|
EEPROM.get(addr, melodySeed); addr += sizeof(melodySeed);
|
||||||
|
|
||||||
|
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
for (int i=0; i<12; i++) {
|
||||||
|
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i=0; i<NUM_STEPS; i++) {
|
||||||
|
EEPROM.get(addr, sequence[i]); addr += sizeof(Step);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
// Use random ADC noise for seed
|
// Use random ADC noise for seed
|
||||||
@ -143,31 +197,25 @@ void setup() {
|
|||||||
pixels.clear();
|
pixels.clear();
|
||||||
pixels.show();
|
pixels.show();
|
||||||
|
|
||||||
// 4. Init Sequence
|
// 4. Setup MIDI Serial
|
||||||
for(int i=0; i<NUM_STEPS; i++) {
|
|
||||||
sequence[i].note = -1; // Default to empty
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Init randomizer
|
|
||||||
randomSeed(micros());
|
|
||||||
generateRandomScale();
|
|
||||||
|
|
||||||
display.clearDisplay();
|
|
||||||
display.display();
|
|
||||||
|
|
||||||
// 5. Setup MIDI Serial
|
|
||||||
Serial1.setTX(PIN_MIDI_TX);
|
Serial1.setTX(PIN_MIDI_TX);
|
||||||
Serial1.begin(31250);
|
Serial1.begin(31250);
|
||||||
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
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
|
||||||
|
|
||||||
|
display.clearDisplay();
|
||||||
|
display.display();
|
||||||
|
|
||||||
Serial.println(F("Started."));
|
Serial.println(F("Started."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,11 +243,14 @@ void generateRandomScale() {
|
|||||||
|
|
||||||
void generateTheme(int themeType) {
|
void generateTheme(int themeType) {
|
||||||
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
||||||
randomSeed(themeType * 12345); // Deterministic seed for this theme
|
randomSeed(melodySeed + themeType * 12345); // Deterministic seed for this theme
|
||||||
if (numScaleNotes == 0) generateRandomScale();
|
if (numScaleNotes == 0) generateRandomScale();
|
||||||
|
|
||||||
for (int i = 0; i < NUM_STEPS; i++) {
|
for (int i = 0; i < NUM_STEPS; i++) {
|
||||||
sequence[i].note = (random(100) < 50) ? (12 * 4 + scaleNotes[random(numScaleNotes)]) : -1;
|
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()); // Restore randomness
|
randomSeed(micros()); // Restore randomness
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
@ -293,18 +344,27 @@ void handleInput() {
|
|||||||
break;
|
break;
|
||||||
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) generateRandomScale();
|
if (menuSelection == 1) generateRandomScale(); // Scale
|
||||||
if (menuSelection >= 2) {
|
if (menuSelection == 2) melodySeed = random(10000); // Melody
|
||||||
|
if (menuSelection >= 3) { // Themes
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
queuedTheme = menuSelection - 1;
|
queuedTheme = menuSelection - 2;
|
||||||
} else {
|
} else {
|
||||||
generateTheme(menuSelection - 1);
|
generateTheme(menuSelection - 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
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) {
|
||||||
|
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;
|
||||||
@ -342,9 +402,16 @@ void handlePlayback() {
|
|||||||
if (millis() - lastStepTime > interval) {
|
if (millis() - lastStepTime > interval) {
|
||||||
lastStepTime = millis();
|
lastStepTime = millis();
|
||||||
|
|
||||||
// Note Off for previous step
|
// Determine if we are tying to the next note
|
||||||
if (sequence[playbackStep].note != -1) {
|
int nextStep = playbackStep + 1;
|
||||||
sendMidi(0x80, sequence[playbackStep].note, 0);
|
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++;
|
playbackStep++;
|
||||||
@ -358,7 +425,13 @@ void handlePlayback() {
|
|||||||
|
|
||||||
// Note On for new step
|
// Note On for new step
|
||||||
if (sequence[playbackStep].note != -1) {
|
if (sequence[playbackStep].note != -1) {
|
||||||
sendMidi(0x90, sequence[playbackStep].note, 100);
|
uint8_t velocity = sequence[playbackStep].accent ? 127 : 100;
|
||||||
|
sendMidi(0x90, sequence[playbackStep].note, velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note Off for previous step (if tied - delayed Note Off)
|
||||||
|
if (isTied && prevNote != -1) {
|
||||||
|
sendMidi(0x80, prevNote, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scroll navigation cursor if not editing
|
// Auto-scroll navigation cursor if not editing
|
||||||
@ -374,8 +447,16 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
|
|||||||
display.println(title);
|
display.println(title);
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
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;
|
int y = 10;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = start; i < count; i++) {
|
||||||
|
if (y > 55) break; // Stop if we run out of screen space
|
||||||
|
|
||||||
if (i == selection) {
|
if (i == selection) {
|
||||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||||
@ -392,27 +473,27 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Special case for queued theme
|
// Special case for queued theme
|
||||||
if (currentState == UI_MENU_RANDOMIZE && i >= 2 && queuedTheme == (i - 1)) {
|
if (currentState == UI_MENU_RANDOMIZE && i >= 3 && queuedTheme == (i - 2)) {
|
||||||
display.print(F(" [NEXT]"));
|
display.print(F(" [NEXT]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
y += 9;
|
// Special cases for Randomize Menu values
|
||||||
|
if (currentState == UI_MENU_RANDOMIZE) {
|
||||||
// Special case for scale display
|
if (i == 1) { // Scale
|
||||||
if (currentState == UI_MENU_RANDOMIZE && i == 1) { // After "Gen Scale"
|
display.print(F(": "));
|
||||||
display.setTextColor(SSD1306_WHITE); // Ensure it's not highlighted
|
if (numScaleNotes > 0) {
|
||||||
display.setCursor(2, y);
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||||
if (numScaleNotes > 0) {
|
for (int j = 0; j < min(numScaleNotes, 6); j++) {
|
||||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
display.print(noteNames[scaleNotes[j]]);
|
||||||
for (int j = 0; j < numScaleNotes; j++) {
|
if (j < min(numScaleNotes, 6) - 1) display.print(F(" "));
|
||||||
display.print(noteNames[scaleNotes[j]]);
|
}
|
||||||
if (j < numScaleNotes - 1) display.print(F(" "));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else if (i == 2) { // Melody
|
||||||
display.print(F("[No scale generated]"));
|
display.print(F(": "));
|
||||||
|
display.print(melodySeed);
|
||||||
}
|
}
|
||||||
y += 9;
|
|
||||||
}
|
}
|
||||||
|
y += 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,33 +614,45 @@ void updateLeds() {
|
|||||||
pixels.clear(); // Clear buffer
|
pixels.clear(); // Clear buffer
|
||||||
|
|
||||||
for (int s = 0; s < NUM_STEPS; s++) {
|
for (int s = 0; s < NUM_STEPS; s++) {
|
||||||
uint32_t color = 0; // Default off
|
int blockX = (s % 4) * 2;
|
||||||
int stepNavIndex = navigationSelection - 1;
|
int blockY = (s / 4) * 2;
|
||||||
|
|
||||||
|
// --- Top Row: Attributes ---
|
||||||
|
uint32_t colorTL = 0; // Octave
|
||||||
|
uint32_t colorTR = 0; // Accent/Tie
|
||||||
|
|
||||||
if (sequence[s].note != -1) {
|
if (sequence[s].note != -1) {
|
||||||
color = getNoteColor(sequence[s].note);
|
int octave = sequence[s].note / 12;
|
||||||
|
// Base octave 4 (MIDI 48-59).
|
||||||
|
if (octave > 4) colorTL = pixels.Color(0, 50, 0); // Up (Green)
|
||||||
|
else if (octave < 4) colorTL = pixels.Color(50, 0, 0); // Down (Red)
|
||||||
|
|
||||||
|
if (sequence[s].accent && sequence[s].tie) colorTR = pixels.Color(50, 0, 50); // Both (Purple)
|
||||||
|
else if (sequence[s].accent) colorTR = pixels.Color(50, 50, 50); // Accent (White)
|
||||||
|
else if (sequence[s].tie) colorTR = pixels.Color(50, 50, 0); // Tie (Yellow)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentState == UI_TRACKER && s == stepNavIndex && !isEditing) {
|
// --- Bottom Row: Note / Cursor ---
|
||||||
color = pixels.Color(40, 40, 40); // White for navigation
|
uint32_t colorBL = 0;
|
||||||
|
uint32_t colorBR = 0;
|
||||||
|
|
||||||
|
if (sequence[s].note != -1) {
|
||||||
|
uint32_t noteColor = getNoteColor(sequence[s].note);
|
||||||
|
colorBL = noteColor;
|
||||||
|
colorBR = noteColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int stepNavIndex = navigationSelection - 1;
|
||||||
if (isPlaying && s == playbackStep) {
|
if (isPlaying && s == playbackStep) {
|
||||||
color = pixels.Color(0, 50, 0); // Green for playback (overwrites nav cursor)
|
colorBL = colorBR = pixels.Color(0, 50, 0); // Green for playback
|
||||||
|
} else if (currentState == UI_TRACKER && s == stepNavIndex) {
|
||||||
|
colorBL = colorBR = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentState == UI_TRACKER && s == stepNavIndex && isEditing) {
|
pixels.setPixelColor(getPixelIndex(blockX, blockY), colorTL);
|
||||||
color = pixels.Color(50, 0, 0); // Red for editing (highest precedence)
|
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), colorTR);
|
||||||
}
|
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), colorBL);
|
||||||
|
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), colorBR);
|
||||||
if (color != 0) {
|
|
||||||
int blockX = (s % 4) * 2;
|
|
||||||
int blockY = (s / 4) * 2;
|
|
||||||
pixels.setPixelColor(getPixelIndex(blockX, blockY), color);
|
|
||||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), color);
|
|
||||||
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), color);
|
|
||||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pixels.show();
|
pixels.show();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user