Randomized melody
This commit is contained in:
parent
4c6a4bfbc1
commit
8759d7f46a
@ -38,7 +38,31 @@ Step sequence[NUM_STEPS];
|
||||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
int currentStep = 0;
|
||||
enum UIState {
|
||||
UI_TRACKER,
|
||||
UI_MENU_MAIN,
|
||||
UI_MENU_RANDOMIZE,
|
||||
UI_MENU_SETUP,
|
||||
UI_SETUP_CHANNEL_EDIT
|
||||
};
|
||||
|
||||
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 int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*);
|
||||
const char* setupMenu[] = { "Back", "Channel" };
|
||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
||||
|
||||
int menuSelection = 0;
|
||||
int navigationSelection = 1;
|
||||
int playbackStep = 0;
|
||||
int midiChannel = 1;
|
||||
int scaleNotes[12];
|
||||
int numScaleNotes = 0;
|
||||
int queuedTheme = -1;
|
||||
|
||||
bool isEditing = false;
|
||||
int scrollOffset = 0;
|
||||
bool isPlaying = false;
|
||||
@ -77,8 +101,21 @@ 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 setup() {
|
||||
Serial.begin(115200);
|
||||
// Use random ADC noise for seed
|
||||
delay(5000);
|
||||
Serial.println(F("Starting."));
|
||||
|
||||
@ -119,6 +156,10 @@ void setup() {
|
||||
sequence[9].note = 62; // D4
|
||||
sequence[12].note = 64; // E4
|
||||
|
||||
// Init randomizer
|
||||
randomSeed(micros());
|
||||
generateRandomScale();
|
||||
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
|
||||
@ -131,11 +172,39 @@ void setup() {
|
||||
}
|
||||
|
||||
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
|
||||
Serial1.write(status);
|
||||
uint8_t channelStatus = status | (midiChannel - 1);
|
||||
Serial1.write(channelStatus);
|
||||
Serial1.write(note);
|
||||
Serial1.write(velocity);
|
||||
}
|
||||
|
||||
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 generateTheme(int themeType) {
|
||||
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
||||
randomSeed(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;
|
||||
}
|
||||
randomSeed(micros()); // Restore randomness
|
||||
isPlaying = true;
|
||||
}
|
||||
|
||||
void handleInput() {
|
||||
// Handle Encoder Rotation
|
||||
int delta = 0;
|
||||
@ -145,22 +214,46 @@ void handleInput() {
|
||||
interrupts();
|
||||
|
||||
if (delta != 0) {
|
||||
if (isEditing) {
|
||||
switch(currentState) {
|
||||
case UI_TRACKER:
|
||||
if (isEditing && navigationSelection > 0) {
|
||||
// Change Note
|
||||
int newNote = sequence[currentStep].note + delta;
|
||||
int stepIndex = navigationSelection - 1;
|
||||
int newNote = sequence[stepIndex].note + delta;
|
||||
if (newNote < -1) newNote = -1;
|
||||
if (newNote > 127) newNote = 127;
|
||||
sequence[currentStep].note = newNote;
|
||||
Serial.print(F("Note changed: ")); Serial.println(newNote);
|
||||
sequence[stepIndex].note = newNote;
|
||||
} else {
|
||||
// Move Cursor
|
||||
currentStep += (delta > 0 ? 1 : -1);
|
||||
if (currentStep < 0) currentStep = NUM_STEPS - 1;
|
||||
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||
navigationSelection += (delta > 0 ? 1 : -1);
|
||||
if (navigationSelection < 0) navigationSelection = NUM_STEPS;
|
||||
if (navigationSelection > NUM_STEPS) navigationSelection = 0;
|
||||
|
||||
// Adjust Scroll to keep cursor in view
|
||||
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||
if (navigationSelection < scrollOffset) scrollOffset = navigationSelection;
|
||||
if (navigationSelection >= scrollOffset + 6) scrollOffset = navigationSelection - 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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,21 +276,59 @@ void handleInput() {
|
||||
if (reading == HIGH && buttonActive) {
|
||||
// Button Released
|
||||
buttonActive = false;
|
||||
if (!buttonConsumed) {
|
||||
if (!buttonConsumed) { // Short press action
|
||||
switch(currentState) {
|
||||
case UI_TRACKER:
|
||||
if (navigationSelection == 0) { // Menu item selected
|
||||
currentState = UI_MENU_MAIN;
|
||||
menuSelection = 0;
|
||||
} else { // A step is selected
|
||||
isEditing = !isEditing;
|
||||
Serial.print(F("Mode toggled: ")); Serial.println(isEditing ? F("EDIT") : F("NAV"));
|
||||
}
|
||||
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();
|
||||
if (menuSelection >= 2) {
|
||||
if (isPlaying) {
|
||||
queuedTheme = menuSelection - 1;
|
||||
} else {
|
||||
generateTheme(menuSelection - 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UI_MENU_SETUP:
|
||||
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; }
|
||||
if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT;
|
||||
break;
|
||||
case UI_SETUP_CHANNEL_EDIT:
|
||||
currentState = UI_MENU_SETUP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Long Press (Start/Stop Playback)
|
||||
if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) {
|
||||
// Long press only works from tracker view
|
||||
if (currentState == UI_TRACKER) {
|
||||
isPlaying = !isPlaying;
|
||||
buttonConsumed = true; // Prevent short press action
|
||||
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
||||
if (!isPlaying) {
|
||||
if (isPlaying) {
|
||||
playbackStep = navigationSelection > 0 ? navigationSelection - 1 : 0;
|
||||
lastStepTime = millis(); // Reset timer to start immediately
|
||||
} else {
|
||||
// Send All Notes Off on stop (CC 123)
|
||||
sendMidi(0xB0, 123, 0);
|
||||
queuedTheme = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,22 +342,136 @@ void handlePlayback() {
|
||||
if (millis() - lastStepTime > interval) {
|
||||
lastStepTime = millis();
|
||||
|
||||
// Note Off for current step (before advancing)
|
||||
if (sequence[currentStep].note != -1) {
|
||||
sendMidi(0x80, sequence[currentStep].note, 0);
|
||||
// Note Off for previous step
|
||||
if (sequence[playbackStep].note != -1) {
|
||||
sendMidi(0x80, sequence[playbackStep].note, 0);
|
||||
}
|
||||
|
||||
currentStep++;
|
||||
if (currentStep >= NUM_STEPS) currentStep = 0;
|
||||
playbackStep++;
|
||||
if (playbackStep >= NUM_STEPS) {
|
||||
playbackStep = 0;
|
||||
if (queuedTheme != -1) {
|
||||
generateTheme(queuedTheme);
|
||||
queuedTheme = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Note On for new step
|
||||
if (sequence[currentStep].note != -1) {
|
||||
sendMidi(0x90, sequence[currentStep].note, 100);
|
||||
if (sequence[playbackStep].note != -1) {
|
||||
sendMidi(0x90, sequence[playbackStep].note, 100);
|
||||
}
|
||||
|
||||
// Auto-scroll logic is handled in drawUI based on currentStep
|
||||
if (currentStep < scrollOffset) scrollOffset = currentStep;
|
||||
if (currentStep >= scrollOffset + 6) scrollOffset = currentStep - 5;
|
||||
// Auto-scroll navigation cursor if not editing
|
||||
if (!isEditing) {
|
||||
navigationSelection = playbackStep + 1; // +1 because 0 is menu
|
||||
if (navigationSelection < scrollOffset) scrollOffset = navigationSelection;
|
||||
if (navigationSelection >= scrollOffset + 6) scrollOffset = navigationSelection - 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawMenu(const char* title, const char* items[], int count, int selection) {
|
||||
display.println(title);
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
int y = 10;
|
||||
for (int i = 0; i < count; i++) {
|
||||
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 >= 2 && queuedTheme == (i - 1)) {
|
||||
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(" "));
|
||||
}
|
||||
} else {
|
||||
display.print(F("[No scale generated]"));
|
||||
}
|
||||
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;
|
||||
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;
|
||||
// Step Number
|
||||
if (stepIndex < 10) display.print(F("0"));
|
||||
display.print(stepIndex);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,43 +481,31 @@ void drawUI() {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
// Header
|
||||
display.print(F("TRACKER "));
|
||||
display.print(isEditing ? F("[EDIT]") : F("[NAV]"));
|
||||
display.println();
|
||||
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);
|
||||
|
||||
// Steps
|
||||
int y = 10;
|
||||
for (int i = scrollOffset; i < min(scrollOffset + 6, NUM_STEPS); i++) {
|
||||
|
||||
// Draw Cursor
|
||||
if (i == currentStep) {
|
||||
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);
|
||||
|
||||
// Step Number
|
||||
if (i < 10) display.print(F("0"));
|
||||
display.print(i);
|
||||
display.print(F(" | "));
|
||||
|
||||
// Note Value
|
||||
int n = sequence[i].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;
|
||||
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;
|
||||
}
|
||||
|
||||
display.display();
|
||||
@ -285,35 +518,49 @@ int getPixelIndex(int x, int y) {
|
||||
return y * 8 + x;
|
||||
}
|
||||
|
||||
uint32_t getNoteColor(int note) {
|
||||
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, 50);
|
||||
}
|
||||
|
||||
void updateLeds() {
|
||||
pixels.clear(); // Clear buffer
|
||||
|
||||
for (int s = 0; s < NUM_STEPS; s++) {
|
||||
uint32_t color = 0; // Default off
|
||||
int stepNavIndex = navigationSelection - 1;
|
||||
|
||||
if (sequence[s].note != -1) {
|
||||
color = getNoteColor(sequence[s].note);
|
||||
}
|
||||
|
||||
if (currentState == UI_TRACKER && s == stepNavIndex && !isEditing) {
|
||||
color = pixels.Color(40, 40, 40); // White for navigation
|
||||
}
|
||||
|
||||
if (isPlaying && s == playbackStep) {
|
||||
color = pixels.Color(0, 50, 0); // Green for playback (overwrites nav cursor)
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
uint32_t color;
|
||||
|
||||
if (s == currentStep) {
|
||||
if (isEditing) {
|
||||
color = pixels.Color(50, 0, 0); // Dim Red for editing
|
||||
} else {
|
||||
color = pixels.Color(40, 40, 40); // Dim White for current step
|
||||
}
|
||||
} else {
|
||||
if (sequence[s].note != -1) {
|
||||
color = pixels.Color(0, 0, 50); // Dim Blue for step with note
|
||||
} else {
|
||||
color = 0; // Off
|
||||
}
|
||||
}
|
||||
|
||||
// Set the 4 pixels for the 2x2 block
|
||||
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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user