Accents and ties with saving
This commit is contained in:
parent
8759d7f46a
commit
5a773d31a4
@ -3,6 +3,7 @@
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <EEPROM.h>
|
||||
|
||||
// --- HARDWARE CONFIGURATION ---
|
||||
#define SCREEN_WIDTH 128
|
||||
@ -30,6 +31,8 @@
|
||||
|
||||
struct Step {
|
||||
int8_t note; // MIDI Note (0-127), -1 for OFF
|
||||
bool accent;
|
||||
bool tie;
|
||||
};
|
||||
|
||||
Step sequence[NUM_STEPS];
|
||||
@ -50,9 +53,9 @@ UIState currentState = UI_TRACKER;
|
||||
|
||||
const char* mainMenu[] = { "Tracker", "Randomize", "Setup" };
|
||||
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 char* setupMenu[] = { "Back", "Channel" };
|
||||
const char* setupMenu[] = { "Back", "Channel", "Save", "Load" };
|
||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
||||
|
||||
int menuSelection = 0;
|
||||
@ -61,7 +64,9 @@ int playbackStep = 0;
|
||||
int midiChannel = 1;
|
||||
int scaleNotes[12];
|
||||
int numScaleNotes = 0;
|
||||
int melodySeed = 0;
|
||||
int queuedTheme = -1;
|
||||
const uint32_t EEPROM_MAGIC = 0x42424244;
|
||||
|
||||
bool isEditing = false;
|
||||
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() {
|
||||
Serial.begin(115200);
|
||||
// Use random ADC noise for seed
|
||||
@ -143,31 +197,25 @@ void setup() {
|
||||
pixels.clear();
|
||||
pixels.show();
|
||||
|
||||
// 4. Init Sequence
|
||||
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
|
||||
// 4. Setup MIDI Serial
|
||||
Serial1.setTX(PIN_MIDI_TX);
|
||||
Serial1.begin(31250);
|
||||
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."));
|
||||
}
|
||||
|
||||
@ -195,11 +243,14 @@ void generateRandomScale() {
|
||||
|
||||
void generateTheme(int themeType) {
|
||||
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();
|
||||
|
||||
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
|
||||
isPlaying = true;
|
||||
@ -293,18 +344,27 @@ void handleInput() {
|
||||
break;
|
||||
case UI_MENU_RANDOMIZE:
|
||||
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
|
||||
if (menuSelection == 1) generateRandomScale();
|
||||
if (menuSelection >= 2) {
|
||||
if (menuSelection == 1) generateRandomScale(); // Scale
|
||||
if (menuSelection == 2) melodySeed = random(10000); // Melody
|
||||
if (menuSelection >= 3) { // Themes
|
||||
if (isPlaying) {
|
||||
queuedTheme = menuSelection - 1;
|
||||
queuedTheme = menuSelection - 2;
|
||||
} else {
|
||||
generateTheme(menuSelection - 1);
|
||||
generateTheme(menuSelection - 2);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@ -342,9 +402,16 @@ void handlePlayback() {
|
||||
if (millis() - lastStepTime > interval) {
|
||||
lastStepTime = millis();
|
||||
|
||||
// Note Off for previous step
|
||||
if (sequence[playbackStep].note != -1) {
|
||||
sendMidi(0x80, sequence[playbackStep].note, 0);
|
||||
// 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++;
|
||||
@ -358,7 +425,13 @@ void handlePlayback() {
|
||||
|
||||
// Note On for new step
|
||||
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
|
||||
@ -374,8 +447,16 @@ 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 = 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) {
|
||||
display.fillRect(0, y, 128, 8, 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
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= 2 && queuedTheme == (i - 1)) {
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= 3 && queuedTheme == (i - 2)) {
|
||||
display.print(F(" [NEXT]"));
|
||||
}
|
||||
|
||||
y += 9;
|
||||
|
||||
// Special case for scale display
|
||||
if (currentState == UI_MENU_RANDOMIZE && i == 1) { // After "Gen Scale"
|
||||
display.setTextColor(SSD1306_WHITE); // Ensure it's not highlighted
|
||||
display.setCursor(2, y);
|
||||
if (numScaleNotes > 0) {
|
||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||
for (int j = 0; j < numScaleNotes; j++) {
|
||||
display.print(noteNames[scaleNotes[j]]);
|
||||
if (j < numScaleNotes - 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 {
|
||||
display.print(F("[No scale generated]"));
|
||||
} else if (i == 2) { // Melody
|
||||
display.print(F(": "));
|
||||
display.print(melodySeed);
|
||||
}
|
||||
y += 9;
|
||||
}
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,33 +614,45 @@ void updateLeds() {
|
||||
pixels.clear(); // Clear buffer
|
||||
|
||||
for (int s = 0; s < NUM_STEPS; s++) {
|
||||
uint32_t color = 0; // Default off
|
||||
int stepNavIndex = navigationSelection - 1;
|
||||
int blockX = (s % 4) * 2;
|
||||
int blockY = (s / 4) * 2;
|
||||
|
||||
// --- Top Row: Attributes ---
|
||||
uint32_t colorTL = 0; // Octave
|
||||
uint32_t colorTR = 0; // Accent/Tie
|
||||
|
||||
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) {
|
||||
color = pixels.Color(40, 40, 40); // White for navigation
|
||||
// --- Bottom Row: Note / Cursor ---
|
||||
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) {
|
||||
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) {
|
||||
color = pixels.Color(50, 0, 0); // Red for editing (highest precedence)
|
||||
}
|
||||
|
||||
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.setPixelColor(getPixelIndex(blockX, blockY), colorTL);
|
||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), colorTR);
|
||||
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), colorBL);
|
||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), colorBR);
|
||||
}
|
||||
|
||||
pixels.show();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user