diff --git a/PlaybackThread.cpp b/PlaybackThread.cpp new file mode 100644 index 0000000..a2ac77d --- /dev/null +++ b/PlaybackThread.cpp @@ -0,0 +1,142 @@ +#include "MidiDriver.h" +#include "TrackerTypes.h" +#include "UIThread.h" +#include "config.h" +#include "SharedState.h" + +bool wasPlaying = false; + +static void handlePlayback() { + bool nowPlaying = isPlaying; + int tracksToPlay = (playMode == MODE_POLY) ? NUM_TRACKS : 1; + if (!wasPlaying && !nowPlaying) { + midi.sendRealtime(0xFA); // MIDI Start + } else if (wasPlaying && !nowPlaying) { + midi.sendRealtime(0xFC); // MIDI Stop + for (int i=0; i= 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(); + Step local_sequence[NUM_TRACKS][NUM_STEPS]; + memcpy(local_sequence, sequence, sizeof(local_sequence)); + Step local_nextSequence[NUM_TRACKS][NUM_STEPS]; + memcpy(local_nextSequence, nextSequence, sizeof(local_nextSequence)); + midi.unlock(); + + for(int t=0; t= NUM_STEPS) nextStep = 0; + + // Determine if we are tying to the next note + bool isTied = local_sequence[t][playbackStep].tie && (local_sequence[t][nextStep].note != -1); + int prevNote = local_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(local_nextSequence, local_sequence, sizeof(sequence)); + } + mutateSequence(local_nextSequence); + midi.lock(); + memcpy(nextSequence, local_nextSequence, sizeof(sequence)); + sequenceChangeScheduled = true; + midi.unlock(); + } + + for (int i=0; i 1000)) { + Serial.println("Core 0 Freeze detected"); + rp2040.reboot(); + } + + if (needsPanic) { + if (playMode == MODE_POLY) { + for (int i=0; i #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" - -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(), - new MarkovStrategy(), new CellularAutomataStrategy(), new LSystemStrategy()}; -const int numStrategies = 6; -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; +#include "UIThread.h" +#include "PlaybackThread.h" +#include "SharedState.h" // 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() { @@ -100,72 +32,6 @@ void readEncoder() { } } -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 + loopUI(); } \ No newline at end of file diff --git a/SharedState.cpp b/SharedState.cpp new file mode 100644 index 0000000..cd78327 --- /dev/null +++ b/SharedState.cpp @@ -0,0 +1,74 @@ +#include "SharedState.h" +#include "LuckyStrategy.h" +#include "ArpStrategy.h" +#include "EuclideanStrategy.h" +#include "MarkovStrategy.h" +#include "CellularAutomataStrategy.h" +#include "LSystemStrategy.h" + +// Global state variables +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; + +// Menus +const char* mainMenu[] = { "Randomize", "Setup" }; +extern 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" }; +extern const int randomizeMenuMonoCount = sizeof(randomizeMenuMono) / sizeof(char*); +extern 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" }; +extern const int randomizeMenuPolyCount = sizeof(randomizeMenuPoly) / sizeof(char*); +extern const int THEME_1_INDEX_POLY = 9; + +const char* setupMenu[] = { "Back", "Play Mode", "Channel", "Factory Reset" }; +extern 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; +extern const uint32_t EEPROM_MAGIC = 0x4242424B; + +MelodyStrategy* strategies[] = { + new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy(), + new MarkovStrategy(), new CellularAutomataStrategy(), new LSystemStrategy()}; +extern const int numStrategies = 6; +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; + +// Button State +bool lastButtonState = HIGH; +unsigned long lastDebounceTime = 0; +bool buttonActive = false; +bool buttonConsumed = false; +unsigned long buttonPressTime = 0; diff --git a/SharedState.h b/SharedState.h new file mode 100644 index 0000000..6c515cd --- /dev/null +++ b/SharedState.h @@ -0,0 +1,68 @@ +#ifndef SHARED_STATE_H +#define SHARED_STATE_H + +#include "TrackerTypes.h" +#include "MelodyStrategy.h" +#include "config.h" + +// Global state variables defined in main .ino +extern Step sequence[NUM_TRACKS][NUM_STEPS]; +extern Step nextSequence[NUM_TRACKS][NUM_STEPS]; +extern volatile bool sequenceChangeScheduled; +extern volatile bool needsPanic; + +extern UIState currentState; + +// Menus +extern const char* mainMenu[]; +extern const int mainMenuCount; +extern const char* randomizeMenuMono[]; +extern const int randomizeMenuMonoCount; +extern const int THEME_1_INDEX_MONO; +extern const char* randomizeMenuPoly[]; +extern const int randomizeMenuPolyCount; +extern const int THEME_1_INDEX_POLY; +extern const char* setupMenu[]; +extern const int setupMenuCount; + +extern int menuSelection; +extern volatile bool trackMute[NUM_TRACKS]; +extern int randomizeTrack; +extern volatile int playbackStep; +extern volatile int midiChannels[NUM_TRACKS]; +extern int scaleNotes[12]; +extern int numScaleNotes; +extern int melodySeeds[NUM_TRACKS]; +extern volatile int queuedTheme; +extern volatile int currentThemeIndex; +extern const uint32_t EEPROM_MAGIC; + +extern MelodyStrategy* strategies[]; +extern const int numStrategies; +extern int currentStrategyIndices[NUM_TRACKS]; + +extern volatile PlayMode playMode; +extern volatile bool mutationEnabled; +extern volatile bool songModeEnabled; +extern volatile int songRepeatsRemaining; +extern volatile int nextSongRepeats; +extern volatile bool songModeNeedsNext; +extern volatile bool isPlaying; +extern volatile int tempo; +extern volatile unsigned long lastClockTime; +extern volatile int clockCount; + +// Watchdog & Loop timing +extern volatile unsigned long lastLoop0Time; +extern volatile unsigned long lastLoop1Time; +extern volatile bool watchdogActive; + +// Input state +extern volatile int encoderDelta; +extern bool lastButtonState; +extern unsigned long lastDebounceTime; +extern bool buttonActive; +extern bool buttonConsumed; +extern unsigned long buttonPressTime; + +#endif diff --git a/UIThread.cpp b/UIThread.cpp new file mode 100644 index 0000000..89a7667 --- /dev/null +++ b/UIThread.cpp @@ -0,0 +1,460 @@ +#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 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); + } else { + strategies[currentStrategyIndices[0]]->mutate(target, 0, 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: + 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(); + } else { + queuedTheme = -1; + } + } + + lastButtonState = reading; +} + +static void drawUI() { + const char **randMenu; + int randMenuCount; + int themeIndex; + + // Make local copies of shared data inside a critical section + // to avoid holding the lock during slow display operations. + UIState local_currentState; + PlayMode local_playMode; + 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; + Step local_sequence[NUM_TRACKS][NUM_STEPS]; + int local_playbackStep; + int local_scaleNotes[12]; + + midi.lock(); + local_playMode = playMode; + local_randomizeTrack = randomizeTrack; + int ui_track = (local_playMode == MODE_POLY) ? local_randomizeTrack : 0; + + if (local_playMode == MODE_POLY) { + randMenu = randomizeMenuPoly; + randMenuCount = randomizeMenuPolyCount; + themeIndex = THEME_1_INDEX_POLY; + } else { + randMenu = randomizeMenuMono; + randMenuCount = randomizeMenuMonoCount; + themeIndex = THEME_1_INDEX_MONO; + } + + local_currentState = currentState; + local_menuSelection = menuSelection; + local_midiChannel = midiChannels[ui_track]; + local_tempo = tempo; + local_strategy = strategies[currentStrategyIndices[ui_track]]; + local_queuedTheme = queuedTheme; + local_currentThemeIndex = currentThemeIndex; + local_numScaleNotes = numScaleNotes; + memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes)); + local_melodySeed = melodySeeds[ui_track]; + 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, + mainMenu, mainMenuCount, randMenu, randMenuCount, setupMenu, setupMenuCount, + themeIndex, local_playMode, 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. + Step local_sequence[NUM_TRACKS][NUM_STEPS]; + int local_playbackStep; + bool local_isPlaying; + UIState local_currentState; + bool local_songModeEnabled; + int local_songRepeatsRemaining; + bool local_sequenceChangeScheduled; + PlayMode local_playMode; + int local_numScaleNotes; + int local_scaleNotes[12]; + bool local_trackMute[NUM_TRACKS]; + + midi.lock(); + memcpy(local_sequence, sequence, sizeof(local_sequence)); + local_playbackStep = playbackStep; + local_isPlaying = isPlaying; + local_currentState = currentState; + local_songModeEnabled = songModeEnabled; + local_songRepeatsRemaining = songRepeatsRemaining; + local_sequenceChangeScheduled = sequenceChangeScheduled; + local_playMode = playMode; + local_numScaleNotes = numScaleNotes; + memcpy(local_scaleNotes, scaleNotes, sizeof(local_scaleNotes)); + memcpy(local_trackMute, (const void*)trackMute, sizeof(local_trackMute)); + midi.unlock(); + + ui.updateLeds((const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying, + local_currentState, local_songModeEnabled, local_songRepeatsRemaining, + local_sequenceChangeScheduled, local_playMode, local_numScaleNotes, + local_scaleNotes, (const bool*)local_trackMute); +} + +void loopUI() { + unsigned long now = millis(); + lastLoop0Time = now; + if (watchdogActive && (now - lastLoop1Time > 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 + + generateSequenceData(nextTheme, nextSequence); + queuedTheme = nextTheme; + nextSongRepeats = repeats; + sequenceChangeScheduled = true; + songModeNeedsNext = false; + } + + handleInput(); + drawUI(); + updateLeds(); + delay(10); // Small delay to prevent screen tearing/excessive refresh +} \ No newline at end of file diff --git a/UIThread.h b/UIThread.h new file mode 100644 index 0000000..99873ed --- /dev/null +++ b/UIThread.h @@ -0,0 +1,14 @@ +#ifndef UI_THREAD_H +#define UI_THREAD_H + +#include "TrackerTypes.h" + +void loopUI(); +void mutateSequence(Step (*target)[NUM_STEPS]); +void generateRandomScale(); +void generateTheme(int themeType); +bool loadSequence(); +void factoryReset(); +void saveSequence(bool quiet = false); + +#endif \ No newline at end of file