Sequence mutation
This commit is contained in:
parent
5f7f241e82
commit
46d0ca5250
@ -60,7 +60,7 @@ 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", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" };
|
const char* randomizeMenu[] = { "Back", "Scale", "Melody", "Mutation", "Song Mode", "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", "Tempo", "Save", "Load" };
|
const char* setupMenu[] = { "Back", "Channel", "Tempo", "Save", "Load" };
|
||||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
||||||
@ -73,8 +73,14 @@ int scaleNotes[12];
|
|||||||
int numScaleNotes = 0;
|
int numScaleNotes = 0;
|
||||||
int melodySeed = 0;
|
int melodySeed = 0;
|
||||||
volatile int queuedTheme = -1;
|
volatile int queuedTheme = -1;
|
||||||
|
volatile int currentThemeIndex = 1;
|
||||||
const uint32_t EEPROM_MAGIC = 0x42424244;
|
const uint32_t EEPROM_MAGIC = 0x42424244;
|
||||||
|
|
||||||
|
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 bool isEditing = false;
|
||||||
volatile int scrollOffset = 0;
|
volatile int scrollOffset = 0;
|
||||||
volatile bool isPlaying = false;
|
volatile bool isPlaying = false;
|
||||||
@ -275,13 +281,34 @@ void generateSequenceData(int themeType, Step* target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void generateTheme(int themeType) {
|
void generateTheme(int themeType) {
|
||||||
|
currentThemeIndex = themeType;
|
||||||
needsPanic = true;
|
needsPanic = true;
|
||||||
mutex_enter_blocking(&midiMutex);
|
mutex_enter_blocking(&midiMutex);
|
||||||
generateSequenceData(themeType, sequence);
|
generateSequenceData(themeType, sequence);
|
||||||
mutex_exit(&midiMutex);
|
mutex_exit(&midiMutex);
|
||||||
|
|
||||||
|
clockCount = 0;
|
||||||
|
lastClockTime = micros();
|
||||||
|
playbackStep = 0;
|
||||||
|
sendMidiRealtime(0xFA); // MIDI Start
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void mutateSequence(Step* target) {
|
||||||
|
// Mutate 1 or 2 steps
|
||||||
|
int count = random(1, 3);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int s = random(NUM_STEPS);
|
||||||
|
if (target[s].note != -1) {
|
||||||
|
int r = random(100);
|
||||||
|
if (r < 30) target[s].accent = !target[s].accent;
|
||||||
|
else if (r < 60) target[s].tie = !target[s].tie;
|
||||||
|
else if (r < 80) target[s].note += 12; // Up octave
|
||||||
|
else target[s].note -= 12; // Down octave
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleInput() {
|
void handleInput() {
|
||||||
// Handle Encoder Rotation
|
// Handle Encoder Rotation
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
@ -377,13 +404,47 @@ 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(); // Scale
|
if (menuSelection == 1) {
|
||||||
if (menuSelection == 2) melodySeed = random(10000); // Melody
|
generateRandomScale(); // Scale
|
||||||
if (menuSelection >= 3) { // Themes
|
if (isPlaying) {
|
||||||
|
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
||||||
|
mutex_enter_blocking(&midiMutex);
|
||||||
|
generateSequenceData(theme, nextSequence);
|
||||||
|
nextSequenceReady = true;
|
||||||
|
mutex_exit(&midiMutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuSelection == 2) {
|
||||||
|
melodySeed = random(10000); // Melody
|
||||||
|
if (isPlaying) {
|
||||||
|
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
||||||
|
mutex_enter_blocking(&midiMutex);
|
||||||
|
generateSequenceData(theme, nextSequence);
|
||||||
|
nextSequenceReady = true;
|
||||||
|
mutex_exit(&midiMutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuSelection == 3) mutationEnabled = !mutationEnabled;
|
||||||
|
if (menuSelection == 4) {
|
||||||
|
songModeEnabled = !songModeEnabled;
|
||||||
|
if (songModeEnabled) {
|
||||||
|
// Start Song Mode immediately
|
||||||
|
if (isPlaying) {
|
||||||
|
songModeNeedsNext = true;
|
||||||
|
} else {
|
||||||
|
// If stopped, just generate a start
|
||||||
|
songModeNeedsNext = true;
|
||||||
|
// We rely on the loop() to pick it up, or we can force it here if not playing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuSelection >= 5) { // Themes
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
queuedTheme = menuSelection - 2;
|
queuedTheme = menuSelection - 2;
|
||||||
|
mutex_enter_blocking(&midiMutex);
|
||||||
generateSequenceData(queuedTheme, nextSequence);
|
generateSequenceData(queuedTheme, nextSequence);
|
||||||
nextSequenceReady = true;
|
nextSequenceReady = true;
|
||||||
|
mutex_exit(&midiMutex);
|
||||||
} else {
|
} else {
|
||||||
generateTheme(menuSelection - 2);
|
generateTheme(menuSelection - 2);
|
||||||
}
|
}
|
||||||
@ -421,7 +482,7 @@ void handleInput() {
|
|||||||
buttonConsumed = true; // Prevent short press action
|
buttonConsumed = true; // Prevent short press action
|
||||||
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF"));
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
playbackStep = navigationSelection > 0 ? navigationSelection - 1 : 0;
|
playbackStep = 0;
|
||||||
clockCount = 0;
|
clockCount = 0;
|
||||||
lastClockTime = micros();
|
lastClockTime = micros();
|
||||||
sendMidiRealtime(0xFA); // MIDI Start
|
sendMidiRealtime(0xFA); // MIDI Start
|
||||||
@ -469,12 +530,31 @@ void handlePlayback() {
|
|||||||
playbackStep++;
|
playbackStep++;
|
||||||
if (playbackStep >= NUM_STEPS) {
|
if (playbackStep >= NUM_STEPS) {
|
||||||
playbackStep = 0;
|
playbackStep = 0;
|
||||||
|
|
||||||
|
// Mutation
|
||||||
|
if (mutationEnabled && !nextSequenceReady) {
|
||||||
|
memcpy(nextSequence, sequence, sizeof(sequence));
|
||||||
|
mutateSequence(nextSequence);
|
||||||
|
nextSequenceReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Song Mode Logic
|
||||||
|
if (songModeEnabled && songRepeatsRemaining > 0) {
|
||||||
|
songRepeatsRemaining--;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextSequenceReady) {
|
if (nextSequenceReady) {
|
||||||
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
||||||
memcpy(sequence, nextSequence, sizeof(sequence));
|
memcpy(sequence, nextSequence, sizeof(sequence));
|
||||||
nextSequenceReady = false;
|
nextSequenceReady = false;
|
||||||
|
if (queuedTheme != -1) {
|
||||||
|
currentThemeIndex = queuedTheme;
|
||||||
queuedTheme = -1;
|
queuedTheme = -1;
|
||||||
}
|
}
|
||||||
|
if (songModeEnabled) {
|
||||||
|
songRepeatsRemaining = nextSongRepeats;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note On for new step
|
// Note On for new step
|
||||||
@ -488,6 +568,11 @@ void handlePlayback() {
|
|||||||
sendMidi(0x80, prevNote, 0);
|
sendMidi(0x80, prevNote, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger next song segment generation if we are on the last repeat
|
||||||
|
if (songModeEnabled && songRepeatsRemaining == 1 && !nextSequenceReady && !songModeNeedsNext) {
|
||||||
|
songModeNeedsNext = true;
|
||||||
|
}
|
||||||
|
|
||||||
mutex_exit(&midiMutex);
|
mutex_exit(&midiMutex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -527,10 +612,15 @@ 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 >= 3 && queuedTheme == (i - 2)) {
|
if (currentState == UI_MENU_RANDOMIZE && i >= 5 && queuedTheme == (i - 4)) {
|
||||||
display.print(F(" [NEXT]"));
|
display.print(F(" [NEXT]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case for active theme
|
||||||
|
if (currentState == UI_MENU_RANDOMIZE && i >= 5 && currentThemeIndex == (i - 4)) {
|
||||||
|
display.print(F(" *"));
|
||||||
|
}
|
||||||
|
|
||||||
// Special cases for Randomize Menu values
|
// Special cases for Randomize Menu values
|
||||||
if (currentState == UI_MENU_RANDOMIZE) {
|
if (currentState == UI_MENU_RANDOMIZE) {
|
||||||
if (i == 1) { // Scale
|
if (i == 1) { // Scale
|
||||||
@ -545,6 +635,12 @@ void drawMenu(const char* title, const char* items[], int count, int selection)
|
|||||||
} else if (i == 2) { // Melody
|
} else if (i == 2) { // Melody
|
||||||
display.print(F(": "));
|
display.print(F(": "));
|
||||||
display.print(melodySeed);
|
display.print(melodySeed);
|
||||||
|
} else if (i == 3) { // Mutation
|
||||||
|
display.print(F(": "));
|
||||||
|
display.print(mutationEnabled ? F("ON") : F("OFF"));
|
||||||
|
} else if (i == 4) { // Song Mode
|
||||||
|
display.print(F(": "));
|
||||||
|
display.print(songModeEnabled ? F("ON") : F("OFF"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
y += 9;
|
y += 9;
|
||||||
@ -679,7 +775,7 @@ int getPixelIndex(int x, int y) {
|
|||||||
return y * 8 + x;
|
return y * 8 + x;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t getNoteColor(int note) {
|
uint32_t getNoteColor(int note, bool dim) {
|
||||||
if (note == -1) return 0;
|
if (note == -1) return 0;
|
||||||
// Map note to hue, avoiding Green (approx 21845) which is used for playback cursor.
|
// 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).
|
// We start from Cyan (~30000) and go up to Orange (~8000 wrapped).
|
||||||
@ -687,7 +783,7 @@ uint32_t getNoteColor(int note) {
|
|||||||
// Step per semitone: 43536 / 12 = 3628.
|
// Step per semitone: 43536 / 12 = 3628.
|
||||||
// This ensures notes are distinct colors but never pure Green.
|
// This ensures notes are distinct colors but never pure Green.
|
||||||
uint16_t hue = 30000 + (note % 12) * 3628;
|
uint16_t hue = 30000 + (note % 12) * 3628;
|
||||||
return Adafruit_NeoPixel::ColorHSV(hue, 255, 50);
|
return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateLeds() {
|
void updateLeds() {
|
||||||
@ -695,45 +791,88 @@ void updateLeds() {
|
|||||||
|
|
||||||
mutex_enter_blocking(&midiMutex);
|
mutex_enter_blocking(&midiMutex);
|
||||||
for (int s = 0; s < NUM_STEPS; s++) {
|
for (int s = 0; s < NUM_STEPS; s++) {
|
||||||
int blockX = (s % 4) * 2;
|
int x = s % 8;
|
||||||
int blockY = (s / 4) * 2;
|
int yBase = (s / 8) * 4;
|
||||||
|
|
||||||
// --- Top Row: Attributes ---
|
uint32_t color = 0;
|
||||||
uint32_t colorTL = 0; // Octave
|
uint32_t dimColor = 0;
|
||||||
uint32_t colorTR = 0; // Accent/Tie
|
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) {
|
if (sequence[s].note != -1) {
|
||||||
int octave = sequence[s].note / 12;
|
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)
|
if (octave > 4) {
|
||||||
else if (sequence[s].accent) colorTR = pixels.Color(50, 50, 50); // Accent (White)
|
// Octave Up -> Top Row (P0)
|
||||||
else if (sequence[s].tie) colorTR = pixels.Color(50, 50, 0); // Tie (Yellow)
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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;
|
int stepNavIndex = navigationSelection - 1;
|
||||||
if (isPlaying && s == playbackStep) {
|
uint32_t cursorColor = pixels.Color(0 + (s%2) * 20, 0 + (s%2) * 20, 50 + (s%2) * 20); // Blue for paused
|
||||||
colorBL = colorBR = pixels.Color(0, 50, 0); // Green for playback
|
if (isPlaying) {
|
||||||
} else if (currentState == UI_TRACKER && s == stepNavIndex) {
|
cursorColor = pixels.Color(0, 50, 0); // Green for playback
|
||||||
colorBL = colorBR = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pixels.setPixelColor(getPixelIndex(blockX, blockY), colorTL);
|
if (cursorColor != 0) {
|
||||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY), colorTR);
|
bool isCursorHere = (isPlaying && s == playbackStep) ||
|
||||||
pixels.setPixelColor(getPixelIndex(blockX, blockY + 1), colorBL);
|
(!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
|
||||||
pixels.setPixelColor(getPixelIndex(blockX + 1, blockY + 1), colorBR);
|
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 (nextSequenceReady && (millis() / 125) % 2) {
|
||||||
|
pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
|
||||||
}
|
}
|
||||||
mutex_exit(&midiMutex);
|
mutex_exit(&midiMutex);
|
||||||
|
|
||||||
@ -742,13 +881,30 @@ void updateLeds() {
|
|||||||
|
|
||||||
void loop1() {
|
void loop1() {
|
||||||
if (needsPanic) {
|
if (needsPanic) {
|
||||||
|
mutex_enter_blocking(&midiMutex);
|
||||||
sendMidi(0xB0, 123, 0);
|
sendMidi(0xB0, 123, 0);
|
||||||
|
mutex_exit(&midiMutex);
|
||||||
needsPanic = false;
|
needsPanic = false;
|
||||||
}
|
}
|
||||||
handlePlayback();
|
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;
|
||||||
|
nextSequenceReady = true;
|
||||||
|
mutex_exit(&midiMutex);
|
||||||
|
|
||||||
|
songModeNeedsNext = false;
|
||||||
|
}
|
||||||
|
|
||||||
handleInput();
|
handleInput();
|
||||||
drawUI();
|
drawUI();
|
||||||
updateLeds();
|
updateLeds();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user