#include #include #include #include "TrackerTypes.h" #include "MelodyStrategy.h" #include "LuckyStrategy.h" #include "ArpStrategy.h" #include "EuclideanStrategy.h" #include "MidiDriver.h" #include "UIManager.h" #include "config.h" Step sequence[NUM_TRACKS][NUM_STEPS]; Step nextSequence[NUM_TRACKS][NUM_STEPS]; volatile bool sequenceChangeScheduled = false; volatile bool needsPanic = false; UIState currentState = UI_MENU_RANDOMIZE; // Let's start in the Play menu const char* mainMenu[] = { "Randomize", "Setup" }; const int mainMenuCount = sizeof(mainMenu) / sizeof(char*); const char* randomizeMenuMono[] = { "Setup", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" }; const int randomizeMenuMonoCount = sizeof(randomizeMenuMono) / sizeof(char*); const int THEME_1_INDEX_MONO = 7; const char* randomizeMenuPoly[] = { "Setup", "Track", "Mute", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" }; const int randomizeMenuPolyCount = sizeof(randomizeMenuPoly) / sizeof(char*); const int THEME_1_INDEX_POLY = 9; const char* setupMenu[] = { "Back", "Play Mode", "Channel", "Factory Reset" }; const int setupMenuCount = sizeof(setupMenu) / sizeof(char*); int menuSelection = 0; volatile bool trackMute[NUM_TRACKS]; int randomizeTrack = 0; volatile int playbackStep = 0; volatile int midiChannels[NUM_TRACKS]; int scaleNotes[12]; int numScaleNotes = 0; int melodySeeds[NUM_TRACKS]; volatile int queuedTheme = -1; volatile int currentThemeIndex = 1; const uint32_t EEPROM_MAGIC = 0x4242424B; MelodyStrategy* strategies[] = { new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy() }; const int numStrategies = 3; int currentStrategyIndices[NUM_TRACKS]; volatile PlayMode playMode = MODE_MONO; volatile bool mutationEnabled = false; volatile bool songModeEnabled = false; volatile int songRepeatsRemaining = 0; volatile int nextSongRepeats = 0; volatile bool songModeNeedsNext = false; volatile bool isPlaying = false; volatile int tempo = 120; // BPM volatile unsigned long lastClockTime = 0; volatile int clockCount = 0; // Watchdog volatile unsigned long lastLoop0Time = 0; volatile unsigned long lastLoop1Time = 0; volatile bool watchdogActive = false; // Encoder State volatile int encoderDelta = 0; static uint8_t prevNextCode = 0; static uint16_t store = 0; // Button State bool lastButtonState = HIGH; unsigned long lastDebounceTime = 0; bool buttonActive = false; bool buttonConsumed = false; unsigned long buttonPressTime = 0; // --- ENCODER INTERRUPT --- // Robust Rotary Encoder reading void readEncoder() { static int8_t rot_enc_table[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0}; prevNextCode <<= 2; if (digitalRead(ENC_DT)) prevNextCode |= 0x02; if (digitalRead(ENC_CLK)) prevNextCode |= 0x01; prevNextCode &= 0x0f; // If valid state if (rot_enc_table[prevNextCode]) { store <<= 4; store |= prevNextCode; if ((store & 0xff) == 0x2b) encoderDelta--; if ((store & 0xff) == 0x17) encoderDelta++; } } void saveSequence(bool quiet = false) { 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); } void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) { for(int i=0; imutate(target, i, NUM_STEPS, scaleNotes, numScaleNotes); } else { strategies[currentStrategyIndices[0]]->mutate(target, 0, NUM_STEPS, scaleNotes, numScaleNotes); } } void handleInput() { // Handle Encoder Rotation int delta = 0; noInterrupts(); delta = encoderDelta; encoderDelta = 0; interrupts(); if (delta != 0) { switch(currentState) { 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); int count = (playMode == MODE_POLY) ? randomizeMenuPolyCount : randomizeMenuMonoCount; if (menuSelection < 0) menuSelection = count - 1; if (menuSelection >= count) 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: { int trackToEdit = (playMode == MODE_POLY) ? randomizeTrack : 0; midiChannels[trackToEdit] += (delta > 0 ? 1 : -1); if (midiChannels[trackToEdit] < 1) midiChannels[trackToEdit] = 16; if (midiChannels[trackToEdit] > 16) midiChannels[trackToEdit] = 1; } break; case UI_EDIT_TEMPO: tempo += delta; if (tempo < 40) tempo = 40; if (tempo > 240) tempo = 240; break; case UI_EDIT_FLAVOUR: { int trackToEdit = playMode == MODE_POLY ? randomizeTrack : 0; currentStrategyIndices[trackToEdit] += (delta > 0 ? 1 : -1); if (currentStrategyIndices[trackToEdit] < 0) currentStrategyIndices[trackToEdit] = numStrategies - 1; if (currentStrategyIndices[trackToEdit] >= numStrategies) currentStrategyIndices[trackToEdit] = 0; } break; case UI_SETUP_PLAYMODE_EDIT: playMode = (playMode == MODE_MONO) ? MODE_POLY : MODE_MONO; 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 (menuSelection == 0) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; break; } if (menuSelection == 1) { currentState = UI_MENU_SETUP; menuSelection = 0; break; } break; case UI_MENU_RANDOMIZE: { int track_offset = (playMode == MODE_POLY) ? 2 : 0; int theme_1_index = (playMode == MODE_POLY) ? THEME_1_INDEX_POLY : THEME_1_INDEX_MONO; if (menuSelection == 0) { currentState = UI_MENU_SETUP; menuSelection = 0; break; } if (playMode == MODE_POLY) { if (menuSelection == 1) { currentState = UI_RANDOMIZE_TRACK_EDIT; break; } if (menuSelection == 2) { trackMute[randomizeTrack] = !trackMute[randomizeTrack]; break; } } if (menuSelection == 1 + track_offset) { // Melody int track = playMode == MODE_POLY ? randomizeTrack : 0; midi.lock(); melodySeeds[track] = random(10000); if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; if (!sequenceChangeScheduled) { memcpy(nextSequence, sequence, sizeof(sequence)); } generateTrackData(track, theme, nextSequence); sequenceChangeScheduled = true; } midi.unlock(); saveSequence(true); break; } if (menuSelection == 2 + track_offset) { currentState = UI_EDIT_FLAVOUR; break; } // Flavour if (menuSelection == 3 + track_offset) { // 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; } if (menuSelection == 4 + track_offset) { currentState = UI_EDIT_TEMPO; break; } if (menuSelection == 5 + track_offset) { mutationEnabled = !mutationEnabled; break; } if (menuSelection == 6 + track_offset) { songModeEnabled = !songModeEnabled; if (songModeEnabled) { songModeNeedsNext = true; } break; } if (menuSelection >= theme_1_index) { // Themes const int selectedTheme = menuSelection - theme_1_index + 1; if (isPlaying) { queuedTheme = selectedTheme; midi.lock(); generateSequenceData(queuedTheme, nextSequence); sequenceChangeScheduled = true; midi.unlock(); } else { generateTheme(selectedTheme); } break; } } break; case UI_MENU_SETUP: if (menuSelection == 0) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; break; } if (menuSelection == 1) { currentState = UI_SETUP_PLAYMODE_EDIT; break; } if (menuSelection == 2) { currentState = UI_SETUP_CHANNEL_EDIT; break; } if (menuSelection == 3) { factoryReset(); break; } break; case UI_SETUP_CHANNEL_EDIT: currentState = UI_MENU_SETUP; saveSequence(true); break; case UI_EDIT_TEMPO: currentState = UI_MENU_RANDOMIZE; saveSequence(true); break; case UI_EDIT_FLAVOUR: currentState = UI_MENU_RANDOMIZE; if (isPlaying) { int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; int track = playMode == MODE_POLY ? randomizeTrack : 0; midi.lock(); if (!sequenceChangeScheduled) { memcpy(nextSequence, sequence, sizeof(sequence)); } generateTrackData(track, theme, nextSequence); sequenceChangeScheduled = true; midi.unlock(); } saveSequence(true); break; case UI_SETUP_PLAYMODE_EDIT: currentState = UI_MENU_SETUP; saveSequence(true); break; case UI_RANDOMIZE_TRACK_EDIT: currentState = UI_MENU_RANDOMIZE; 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(); midi.sendRealtime(0xFA); // MIDI Start } else { // Send All Notes Off on stop (CC 123) needsPanic = true; midi.sendRealtime(0xFC); // MIDI Stop queuedTheme = -1; } } lastButtonState = reading; } void handlePlayback() { if (!isPlaying) return; unsigned long currentMicros = micros(); unsigned long clockInterval = 2500000 / tempo; // 60s * 1000000us / (tempo * 24ppqn) int tracksToPlay = (playMode == MODE_POLY) ? NUM_TRACKS : 1; if (currentMicros - lastClockTime >= clockInterval) { lastClockTime += clockInterval; midi.sendRealtime(0xF8); // MIDI Clock clockCount++; if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note clockCount = 0; midi.lock(); for(int t=0; t= NUM_STEPS) nextStep = 0; // Determine if we are tying to the next note bool isTied = sequence[t][playbackStep].tie && (sequence[t][nextStep].note != -1); int prevNote = sequence[t][playbackStep].note; // Note Off for previous step (if NOT tied) if (!isTied && prevNote != -1) { midi.sendNoteOff(prevNote, trackChannel); } } playbackStep++; if (playbackStep >= NUM_STEPS) { playbackStep = 0; // Theme change if (sequenceChangeScheduled && queuedTheme != -1) { currentThemeIndex = queuedTheme; queuedTheme = -1; // nextSequence is already generated } // Mutation if (mutationEnabled) { if (!sequenceChangeScheduled) { memcpy(nextSequence, sequence, sizeof(sequence)); } mutateSequence(nextSequence); sequenceChangeScheduled = true; } for (int i=0; i 1000)) { Serial.println("Core 0 Freeze detected"); rp2040.reboot(); } if (needsPanic) { midi.lock(); if (playMode == MODE_POLY) { for (int i=0; i 1000)) { Serial.println("Core 1 Freeze detected"); rp2040.reboot(); } // 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 midi.lock(); generateSequenceData(nextTheme, nextSequence); queuedTheme = nextTheme; nextSongRepeats = repeats; sequenceChangeScheduled = true; midi.unlock(); songModeNeedsNext = false; } handleInput(); drawUI(); updateLeds(); delay(10); // Small delay to prevent screen tearing/excessive refresh }