#include #include #include #include "TrackerTypes.h" #include "MelodyStrategy.h" #include "LuckyStrategy.h" #include "ArpStrategy.h" #include "EuclideanStrategy.h" #include "MarkovStrategy.h" #include "CellularAutomataStrategy.h" #include "LSystemStrategy.h" #include "MidiDriver.h" #include "UIManager.h" #include "config.h" #include "UIThread.h" #include "SharedState.h" static Step local_sequence[NUM_TRACKS][NUM_STEPS]; static void handleInput(); static void drawUI(); static void updateLeds(); static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]); static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]); void saveSequence(bool quiet) { midi.lock(); int addr = 0; EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC); int channels[NUM_TRACKS]; for(int i=0; igenerate(target, track, NUM_STEPS, scaleNotes, numScaleNotes, melodySeeds[track] + themeType * 12345); } void generateRandomScale() { // All tracks share the same scale for now strategies[currentStrategyIndices[0]]->generateScale(scaleNotes, numScaleNotes); } static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) { for(int i=0; imutate(target, i, NUM_STEPS, scaleNotes, numScaleNotes); } static void handleInput() { // Handle Encoder Rotation int delta = 0; noInterrupts(); delta = encoderDelta; encoderDelta = 0; interrupts(); if (delta != 0) { switch(currentState) { case UI_MENU_MAIN: { int next = menuSelection; int count = 0; do { next += (delta > 0 ? 1 : -1); if (next < 0) next = menuItemsCount - 1; if (next >= menuItemsCount) next = 0; count++; } while (!isItemVisible(next) && count < menuItemsCount); menuSelection = next; } break; case UI_SETUP_CHANNEL_EDIT: { midiChannels[randomizeTrack] += (delta > 0 ? 1 : -1); if (midiChannels[randomizeTrack] < 1) midiChannels[randomizeTrack] = 16; if (midiChannels[randomizeTrack] > 16) midiChannels[randomizeTrack] = 1; } break; case UI_EDIT_TEMPO: tempo += delta; if (tempo < 40) tempo = 40; if (tempo > 240) tempo = 240; break; case UI_EDIT_FLAVOUR: { currentStrategyIndices[randomizeTrack] += (delta > 0 ? 1 : -1); if (currentStrategyIndices[randomizeTrack] < 0) currentStrategyIndices[randomizeTrack] = numStrategies - 1; if (currentStrategyIndices[randomizeTrack] >= numStrategies) currentStrategyIndices[randomizeTrack] = 0; } break; } if (currentState == UI_RANDOMIZE_TRACK_EDIT) { randomizeTrack += (delta > 0 ? 1 : -1); if (randomizeTrack < 0) randomizeTrack = NUM_TRACKS - 1; if (randomizeTrack >= NUM_TRACKS) randomizeTrack = 0; } } // Handle Button int reading = digitalRead(ENC_SW); if (reading != lastButtonState) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > 50) { if (reading == LOW && !buttonActive) { // Button Pressed buttonActive = true; buttonPressTime = millis(); buttonConsumed = false; Serial.println(F("Button Down")); } if (reading == HIGH && buttonActive) { // Button Released buttonActive = false; if (!buttonConsumed) { // Short press action switch(currentState) { case UI_MENU_MAIN: if (menuItems[menuSelection].isGroup) { menuItems[menuSelection].expanded = !menuItems[menuSelection].expanded; break; } switch(menuItems[menuSelection].id) { case MENU_ID_MELODY: midi.lock(); melodySeeds[randomizeTrack] = random(10000); if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; if (!sequenceChangeScheduled) { memcpy(nextSequence, sequence, sizeof(sequence)); } generateTrackData(randomizeTrack, theme, nextSequence); sequenceChangeScheduled = true; } midi.unlock(); saveSequence(true); break; case MENU_ID_SCALE: generateRandomScale(); if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; midi.lock(); // Regenerate all tracks with new scale generateSequenceData(theme, nextSequence); sequenceChangeScheduled = true; midi.unlock(); } saveSequence(true); break; case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break; case MENU_ID_SONG_MODE: songModeEnabled = !songModeEnabled; if (songModeEnabled) { songModeNeedsNext = true; } break; case MENU_ID_TRACK_SELECT: currentState = UI_RANDOMIZE_TRACK_EDIT; break; case MENU_ID_MUTE: trackMute[randomizeTrack] = !trackMute[randomizeTrack]; break; case MENU_ID_FLAVOUR: currentState = UI_EDIT_FLAVOUR; break; case MENU_ID_MUTATION: mutationEnabled = !mutationEnabled; break; case MENU_ID_CHANNEL: currentState = UI_SETUP_CHANNEL_EDIT; break; case MENU_ID_RESET: factoryReset(); break; default: if (menuItems[menuSelection].id >= MENU_ID_THEME_1 && menuItems[menuSelection].id <= MENU_ID_THEME_7) { const int selectedTheme = menuItems[menuSelection].id - MENU_ID_THEME_1 + 1; if (isPlaying) { queuedTheme = selectedTheme; midi.lock(); generateSequenceData(queuedTheme, nextSequence); sequenceChangeScheduled = true; midi.unlock(); } else { generateTheme(selectedTheme); } break; } break; } break; case UI_SETUP_CHANNEL_EDIT: currentState = UI_MENU_MAIN; saveSequence(true); break; case UI_EDIT_TEMPO: currentState = UI_MENU_MAIN; saveSequence(true); break; case UI_EDIT_FLAVOUR: currentState = UI_MENU_MAIN; if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; midi.lock(); if (!sequenceChangeScheduled) { memcpy(nextSequence, sequence, sizeof(sequence)); } generateTrackData(randomizeTrack, theme, nextSequence); sequenceChangeScheduled = true; midi.unlock(); } saveSequence(true); break; case UI_RANDOMIZE_TRACK_EDIT: currentState = UI_MENU_MAIN; saveSequence(true); break; } } } } // Check for Long Press (Start/Stop Playback) if (buttonActive && !buttonConsumed && (millis() - buttonPressTime > 600)) { isPlaying = !isPlaying; buttonConsumed = true; // Prevent short press action Serial.print(F("Playback: ")); Serial.println(isPlaying ? F("ON") : F("OFF")); if (isPlaying) { playbackStep = 0; clockCount = 0; lastClockTime = micros(); } else { queuedTheme = -1; } } lastButtonState = reading; } static void drawUI() { // Make local copies of shared data inside a critical section // to avoid holding the lock during slow display operations. UIState local_currentState; int local_menuSelection, local_randomizeTrack, local_tempo, local_currentThemeIndex, local_queuedTheme, local_numScaleNotes; int local_melodySeed; bool local_mutationEnabled, local_songModeEnabled, local_isPlaying; bool local_trackMute[NUM_TRACKS]; int local_midiChannel; MelodyStrategy* local_strategy; int local_playbackStep; int local_scaleNotes[12]; midi.lock(); local_randomizeTrack = randomizeTrack; local_currentState = currentState; local_menuSelection = menuSelection; local_midiChannel = midiChannels[local_randomizeTrack]; local_tempo = tempo; local_strategy = strategies[currentStrategyIndices[local_randomizeTrack]]; local_queuedTheme = queuedTheme; local_currentThemeIndex = currentThemeIndex; local_numScaleNotes = numScaleNotes; memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes)); local_melodySeed = melodySeeds[local_randomizeTrack]; local_mutationEnabled = mutationEnabled; local_songModeEnabled = songModeEnabled; memcpy(local_sequence, sequence, sizeof(local_sequence)); local_playbackStep = playbackStep; local_isPlaying = isPlaying; memcpy(local_trackMute, (const void*)trackMute, sizeof(local_trackMute)); midi.unlock(); ui.draw(local_currentState, local_menuSelection, local_midiChannel, local_tempo, local_strategy, local_queuedTheme, local_currentThemeIndex, local_numScaleNotes, local_scaleNotes, local_melodySeed, local_mutationEnabled, local_songModeEnabled, (const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying, local_randomizeTrack, (const bool*)local_trackMute); } static void updateLeds() { // Make local copies of shared data inside a critical section // to avoid holding the lock during slow LED update operations. int local_playbackStep; bool local_isPlaying; UIState local_currentState; int local_menuSelection; bool local_songModeEnabled; int local_songRepeatsRemaining; bool local_sequenceChangeScheduled; PlayMode local_playMode; int local_numScaleNotes; int local_scaleNotes[12]; bool local_trackMute[NUM_TRACKS]; int local_randomizeTrack; midi.lock(); memcpy(local_sequence, sequence, sizeof(local_sequence)); local_playbackStep = playbackStep; local_isPlaying = isPlaying; local_currentState = currentState; local_menuSelection = menuSelection; local_songModeEnabled = songModeEnabled; local_songRepeatsRemaining = songRepeatsRemaining; local_sequenceChangeScheduled = sequenceChangeScheduled; local_playMode = playMode; local_numScaleNotes = numScaleNotes; local_randomizeTrack = randomizeTrack; memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes)); memcpy(local_trackMute, (const void*)trackMute, sizeof(local_trackMute)); midi.unlock(); PlayMode ledDisplayMode = MODE_POLY; // Default to POLY (MAIN section view) if (local_currentState == UI_MENU_MAIN) { MenuItemID id = menuItems[local_menuSelection].id; // Check if we are in the Track group (IDs between TRACK_SELECT and THEME_7) if (id >= MENU_ID_TRACK_SELECT && id <= MENU_ID_THEME_7) { // It's a TRACK section item (Track, Mute, Flavour, Mutation, Themes) ledDisplayMode = MODE_MONO; } } else if (local_currentState == UI_EDIT_FLAVOUR || local_currentState == UI_RANDOMIZE_TRACK_EDIT) { // These are entered from TRACK section items ledDisplayMode = MODE_MONO; } ui.updateLeds((const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying, local_currentState, local_songModeEnabled, local_songRepeatsRemaining, local_sequenceChangeScheduled, ledDisplayMode, local_randomizeTrack, local_numScaleNotes, local_scaleNotes, (const bool*)local_trackMute); } void loopUI() { // 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 generateSequenceData(nextTheme, nextSequence); queuedTheme = nextTheme; nextSongRepeats = repeats; sequenceChangeScheduled = true; songModeNeedsNext = false; } handleInput(); drawUI(); updateLeds(); delay(10); // Small delay to prevent screen tearing/excessive refresh }