diff --git a/ArpStrategy.h b/ArpStrategy.h index 2c253a2..e56fcd2 100644 --- a/ArpStrategy.h +++ b/ArpStrategy.h @@ -6,7 +6,7 @@ class ArpStrategy : public MelodyStrategy { public: - void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { + void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { randomSeed(seed); if (numScaleNotes == 0) return; @@ -86,7 +86,7 @@ public: // 3. Fill Sequence for (int i = 0; i < numSteps; i++) { - sequence[i] = arpPattern[i % arpLength]; + sequence[track][i] = arpPattern[i % arpLength]; } randomSeed(micros()); } @@ -106,14 +106,14 @@ public: sortArray(scaleNotes, numScaleNotes); } - void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override { + void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override { // Swap two notes int s1 = random(numSteps); int s2 = random(numSteps); - if (sequence[s1].note != -1 && sequence[s2].note != -1) { - int8_t temp = sequence[s1].note; - sequence[s1].note = sequence[s2].note; - sequence[s2].note = temp; + if (sequence[track][s1].note != -1 && sequence[track][s2].note != -1) { + int8_t temp = sequence[track][s1].note; + sequence[track][s1].note = sequence[track][s2].note; + sequence[track][s2].note = temp; } } diff --git a/EuclideanStrategy.h b/EuclideanStrategy.h index 844d8c8..3dd07e0 100644 --- a/EuclideanStrategy.h +++ b/EuclideanStrategy.h @@ -6,7 +6,7 @@ class EuclideanStrategy : public MelodyStrategy { public: - void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { + void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { randomSeed(seed); if (numScaleNotes == 0) return; @@ -34,13 +34,13 @@ public: if (pattern[i]) { int octave = random(3) + 3; // 3, 4, 5 - sequence[stepIndex].note = 12 * octave + scaleNotes[random(numScaleNotes)]; - sequence[stepIndex].accent = (random(100) < 30); - sequence[stepIndex].tie = (random(100) < 10); + sequence[track][stepIndex].note = 12 * octave + scaleNotes[random(numScaleNotes)]; + sequence[track][stepIndex].accent = (random(100) < 30); + sequence[track][stepIndex].tie = (random(100) < 10); } else { - sequence[stepIndex].note = -1; - sequence[stepIndex].accent = false; - sequence[stepIndex].tie = false; + sequence[track][stepIndex].note = -1; + sequence[track][stepIndex].accent = false; + sequence[track][stepIndex].tie = false; } } randomSeed(micros()); @@ -61,20 +61,20 @@ public: sortArray(scaleNotes, numScaleNotes); } - void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override { + void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override { // Rotate sequence if (random(2) == 0) { - Step last = sequence[numSteps - 1]; + Step last = sequence[track][numSteps - 1]; for (int i = numSteps - 1; i > 0; i--) { - sequence[i] = sequence[i - 1]; + sequence[track][i] = sequence[track][i - 1]; } - sequence[0] = last; + sequence[track][0] = last; } else { // Randomize a note int s = random(numSteps); - if (sequence[s].note != -1) { + if (sequence[track][s].note != -1) { int octave = random(3) + 3; - sequence[s].note = 12 * octave + scaleNotes[random(numScaleNotes)]; + sequence[track][s].note = 12 * octave + scaleNotes[random(numScaleNotes)]; } } } diff --git a/LuckyStrategy.h b/LuckyStrategy.h index 7815fee..2ea0dc1 100644 --- a/LuckyStrategy.h +++ b/LuckyStrategy.h @@ -6,15 +6,15 @@ class LuckyStrategy : public MelodyStrategy { public: - void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { + void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) override { randomSeed(seed); if (numScaleNotes == 0) return; for (int i = 0; i < numSteps; i++) { int octave = random(3) + 3; // 3, 4, 5 (Base is 4) - sequence[i].note = (random(100) < 50) ? (12 * octave + scaleNotes[random(numScaleNotes)]) : -1; - sequence[i].accent = (random(100) < 30); - sequence[i].tie = (random(100) < 20); + sequence[track][i].note = (random(100) < 50) ? (12 * octave + scaleNotes[random(numScaleNotes)]) : -1; + sequence[track][i].accent = (random(100) < 30); + sequence[track][i].tie = (random(100) < 20); } randomSeed(micros()); } @@ -34,17 +34,17 @@ public: sortArray(scaleNotes, numScaleNotes); } - void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override { + void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) override { // Mutate 1 or 2 steps int count = random(1, 3); for (int i = 0; i < count; i++) { int s = random(numSteps); - if (sequence[s].note != -1) { + if (sequence[track][s].note != -1) { int r = random(100); - if (r < 30) sequence[s].accent = !sequence[s].accent; - else if (r < 60) sequence[s].tie = !sequence[s].tie; - else if (r < 80) sequence[s].note += 12; // Up octave - else sequence[s].note -= 12; // Down octave + if (r < 30) sequence[track][s].accent = !sequence[track][s].accent; + else if (r < 60) sequence[track][s].tie = !sequence[track][s].tie; + else if (r < 80) sequence[track][s].note += 12; // Up octave + else sequence[track][s].note -= 12; // Down octave } } } diff --git a/MelodyStrategy.h b/MelodyStrategy.h index c00af2f..4d9c040 100644 --- a/MelodyStrategy.h +++ b/MelodyStrategy.h @@ -1,13 +1,14 @@ #ifndef MELODY_STRATEGY_H #define MELODY_STRATEGY_H +#include "config.h" #include "TrackerTypes.h" class MelodyStrategy { public: - virtual void generate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0; + virtual void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed) = 0; virtual void generateScale(int* scaleNotes, int& numScaleNotes) = 0; - virtual void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) = 0; + virtual void mutate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes) = 0; virtual const char* getName() = 0; virtual ~MelodyStrategy() {} }; diff --git a/RP2040_Tracker.ino b/RP2040_Tracker.ino index 4747914..25acbaa 100644 --- a/RP2040_Tracker.ino +++ b/RP2040_Tracker.ino @@ -8,26 +8,10 @@ #include "EuclideanStrategy.h" #include "MidiDriver.h" #include "UIManager.h" +#include "config.h" -// --- HARDWARE CONFIGURATION --- -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) -#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 - -// Pin Definitions for Raspberry Pi Pico (RP2040) -#define PIN_SDA 4 -#define PIN_SCL 5 - -#define ENC_CLK 12 -#define ENC_DT 13 -#define ENC_SW 14 - -// --- TRACKER DATA --- -#define NUM_STEPS 16 - -Step sequence[NUM_STEPS]; -Step nextSequence[NUM_STEPS]; +Step sequence[NUM_TRACKS][NUM_STEPS]; +Step nextSequence[NUM_TRACKS][NUM_STEPS]; volatile bool sequenceChangeScheduled = false; volatile bool needsPanic = false; @@ -35,34 +19,43 @@ UIState currentState = UI_MENU_MAIN; const char* mainMenu[] = { "Tracker", "Randomize", "Setup" }; const int mainMenuCount = sizeof(mainMenu) / sizeof(char*); -const char* randomizeMenu[] = { "Back", "Melody", "Flavour", "Scale", "Tempo", "Mutation", "Song Mode", "Theme 1", "Theme 2", "Theme 3", "Theme 4", "Theme 5", "Theme 6", "Theme 7" }; -const int THEME_1_INDEX = 7; -const int randomizeMenuCount = sizeof(randomizeMenu) / sizeof(char*); -const char* setupMenu[] = { "Back", "Channel", "Factory Reset" }; + +const char* randomizeMenuMono[] = { "Back", "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[] = { "Back", "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 int navigationSelection = 1; +volatile int currentTrack = 0; +volatile bool trackMute[NUM_TRACKS]; +int randomizeTrack = 0; volatile int playbackStep = 0; -int midiChannel = 1; -volatile int shMidiChannel = midiChannel; +volatile int midiChannels[NUM_TRACKS]; int scaleNotes[12]; int numScaleNotes = 0; -int melodySeed = 0; +int melodySeeds[NUM_TRACKS]; volatile int queuedTheme = -1; volatile int currentThemeIndex = 1; -const uint32_t EEPROM_MAGIC = 0x42424247; +const uint32_t EEPROM_MAGIC = 0x4242424A; MelodyStrategy* strategies[] = { new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy() }; const int numStrategies = 3; -int currentStrategyIndex = 0; +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 isEditing = false; +volatile EditMode editMode = NAV_STEP; volatile int scrollOffset = 0; volatile bool isPlaying = false; volatile int tempo = 120; // BPM @@ -70,6 +63,11 @@ 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; @@ -104,10 +102,16 @@ void readEncoder() { void saveSequence(bool quiet = false) { int addr = 0; EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC); - EEPROM.put(addr, midiChannel); addr += sizeof(midiChannel); - EEPROM.put(addr, melodySeed); addr += sizeof(melodySeed); - EEPROM.put(addr, currentStrategyIndex); addr += sizeof(currentStrategyIndex); + int channels[NUM_TRACKS]; + for(int i=0; igenerate(target, track, NUM_STEPS, scaleNotes, numScaleNotes, melodySeeds[track] + themeType * 12345); } void generateRandomScale() { - strategies[currentStrategyIndex]->generateScale(scaleNotes, numScaleNotes); + // All tracks share the same scale for now + strategies[currentStrategyIndices[0]]->generateScale(scaleNotes, numScaleNotes); } -void generateSequenceData(int themeType, Step* target) { - randomSeed(melodySeed + themeType * 12345); // Deterministic seed for this theme - strategies[currentStrategyIndex]->generate(target, NUM_STEPS, scaleNotes, numScaleNotes, melodySeed + themeType * 12345); +void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) { + for(int i=0; imutate(target, NUM_STEPS, scaleNotes, numScaleNotes); +void mutateSequence(Step (*target)[NUM_STEPS]) { + if (playMode == MODE_POLY) { + for(int i=0; imutate(target, i, NUM_STEPS, scaleNotes, numScaleNotes); + } else { + strategies[currentStrategyIndices[0]]->mutate(target, 0, NUM_STEPS, scaleNotes, numScaleNotes); + } } void handleInput() { @@ -229,14 +256,20 @@ void handleInput() { if (delta != 0) { switch(currentState) { case UI_TRACKER: - if (isEditing && navigationSelection > 0) { + if (editMode == EDIT_NOTE && navigationSelection > 0) { // Change Note int stepIndex = navigationSelection - 1; - int newNote = sequence[stepIndex].note + delta; + int newNote = sequence[currentTrack][stepIndex].note + delta; if (newNote < -1) newNote = -1; if (newNote > 127) newNote = 127; midi.lock(); - sequence[stepIndex].note = newNote; + sequence[currentTrack][stepIndex].note = newNote; + midi.unlock(); + } else if (editMode == NAV_TRACK) { + midi.lock(); + currentTrack += (delta > 0 ? 1 : -1); + if(currentTrack < 0) currentTrack = NUM_TRACKS - 1; + if(currentTrack >= NUM_TRACKS) currentTrack = 0; midi.unlock(); } else { // Move Cursor @@ -255,9 +288,12 @@ void handleInput() { 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; + { + 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); @@ -265,10 +301,12 @@ void handleInput() { 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; - shMidiChannel = midiChannel; + { + int trackToEdit = (playMode == MODE_POLY) ? currentTrack : 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; @@ -276,11 +314,25 @@ void handleInput() { if (tempo > 240) tempo = 240; break; case UI_EDIT_FLAVOUR: - currentStrategyIndex += (delta > 0 ? 1 : -1); - if (currentStrategyIndex < 0) currentStrategyIndex = numStrategies - 1; - if (currentStrategyIndex >= numStrategies) currentStrategyIndex = 0; + { + 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; + // Reset edit mode when switching + editMode = NAV_STEP; 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 @@ -309,49 +361,72 @@ void handleInput() { currentState = UI_MENU_MAIN; menuSelection = 0; } else { // A step is selected - isEditing = !isEditing; + editMode = (EditMode)((editMode + 1) % 3); + // In mono mode, skip track navigation + if(playMode == MODE_MONO && editMode == NAV_TRACK) { + editMode = EDIT_NOTE; + } + if(editMode == NAV_STEP) { // Cycled back to start + // No action needed + } } 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; } + if (menuSelection == 0) { currentState = UI_TRACKER; break; } + if (menuSelection == 1) { currentState = UI_MENU_RANDOMIZE; menuSelection = 0; break; } + if (menuSelection == 2) { currentState = UI_MENU_SETUP; menuSelection = 0; break; } break; case UI_MENU_RANDOMIZE: - if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; } - if (menuSelection == 1) { - melodySeed = random(10000); // Melody + { + 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_MAIN; menuSelection = 1; 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; - midi.lock(); - generateSequenceData(theme, nextSequence); + if (!sequenceChangeScheduled) { + memcpy(nextSequence, sequence, sizeof(sequence)); + } + generateTrackData(track, theme, nextSequence); sequenceChangeScheduled = true; - midi.unlock(); } + midi.unlock(); saveSequence(true); + break; } - if (menuSelection == 2) currentState = UI_EDIT_FLAVOUR; // Flavour - if (menuSelection == 3) { // Scale + 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) currentState = UI_EDIT_TEMPO; - if (menuSelection == 5) mutationEnabled = !mutationEnabled; - if (menuSelection == 6) { + 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 (menuSelection >= theme_1_index) { // Themes + const int selectedTheme = menuSelection - theme_1_index + 1; if (isPlaying) { queuedTheme = selectedTheme; midi.lock(); @@ -361,12 +436,15 @@ void handleInput() { } else { generateTheme(selectedTheme); } + break; + } } break; case UI_MENU_SETUP: - if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; } - if (menuSelection == 1) currentState = UI_SETUP_CHANNEL_EDIT; - if (menuSelection == 2) factoryReset(); + if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 2; 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; @@ -379,14 +457,26 @@ void handleInput() { case UI_EDIT_FLAVOUR: currentState = UI_MENU_RANDOMIZE; if (isPlaying) { - queuedTheme = currentThemeIndex; + int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex; + int track = playMode == MODE_POLY ? randomizeTrack : 0; midi.lock(); - generateSequenceData(currentThemeIndex, nextSequence); + 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; } } } @@ -421,6 +511,7 @@ void handlePlayback() { 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; @@ -433,16 +524,21 @@ void handlePlayback() { midi.lock(); - // Determine if we are tying to the next note - int nextStep = playbackStep + 1; - if (nextStep >= NUM_STEPS) nextStep = 0; - bool isTied = sequence[playbackStep].tie && (sequence[nextStep].note != -1); - int prevNote = sequence[playbackStep].note; - // Note Off for previous step (if NOT tied) - if (!isTied && prevNote != -1) { - midi.sendNoteOff(prevNote, shMidiChannel); + 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++; @@ -465,7 +561,7 @@ void handlePlayback() { sequenceChangeScheduled = true; } - midi.panic(shMidiChannel); // Panic / All Notes Off + for (int i=0; i 1000)) { + Serial.println("Core 0 Freeze detected"); + rp2040.reboot(); + } + if (needsPanic) { midi.lock(); - midi.panic(shMidiChannel); + 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 diff --git a/TrackerTypes.h b/TrackerTypes.h index 9e6f858..68618a0 100644 --- a/TrackerTypes.h +++ b/TrackerTypes.h @@ -2,6 +2,7 @@ #define TRACKER_TYPES_H #include +#include "config.h" struct Step { int8_t note; // MIDI Note (0-127), -1 for OFF @@ -9,6 +10,17 @@ struct Step { bool tie; }; +enum PlayMode { + MODE_MONO, + MODE_POLY +}; + +enum EditMode { + NAV_STEP, + NAV_TRACK, + EDIT_NOTE +}; + enum UIState { UI_TRACKER, UI_MENU_MAIN, @@ -16,7 +28,9 @@ enum UIState { UI_MENU_SETUP, UI_SETUP_CHANNEL_EDIT, UI_EDIT_TEMPO, - UI_EDIT_FLAVOUR + UI_EDIT_FLAVOUR, + UI_SETUP_PLAYMODE_EDIT, + UI_RANDOMIZE_TRACK_EDIT }; inline void sortArray(int arr[], int size) { diff --git a/UIManager.cpp b/UIManager.cpp index 65a92e0..b5384de 100644 --- a/UIManager.cpp +++ b/UIManager.cpp @@ -1,14 +1,13 @@ #include "UIManager.h" +#include "config.h" // --- HARDWARE CONFIGURATION --- #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C - #define PIN_NEOPIXEL 16 #define NUM_PIXELS 64 -#define NUM_STEPS 16 UIManager ui; @@ -46,16 +45,16 @@ void UIManager::showMessage(const char* msg) { display.setTextSize(1); } -void UIManager::draw(UIState currentState, int menuSelection, int navSelection, bool isEditing, +void UIManager::draw(UIState currentState, int menuSelection, int navSelection, EditMode editMode, int midiChannel, int tempo, MelodyStrategy* currentStrategy, int queuedTheme, int currentThemeIndex, int numScaleNotes, const int* scaleNotes, int melodySeed, bool mutationEnabled, bool songModeEnabled, - const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying, + const Step sequence[][NUM_STEPS], int scrollOffset, int playbackStep, bool isPlaying, const char* mainMenu[], int mainMenuCount, const char* randomizeMenu[], int randomizeMenuCount, - const char* setupMenu[], int setupMenuCount, - int theme1Index) { + const char* setupMenu[], int setupMenuCount, int theme1Index, + PlayMode playMode, int currentTrack, int randomizeTrack, const bool* trackMute) { display.clearDisplay(); display.setTextSize(1); @@ -64,16 +63,16 @@ void UIManager::draw(UIState currentState, int menuSelection, int navSelection, switch(currentState) { case UI_TRACKER: - drawTracker(navSelection, isEditing, midiChannel, sequence, scrollOffset, playbackStep, isPlaying); + drawTracker(navSelection, editMode, midiChannel, sequence, scrollOffset, playbackStep, isPlaying, playMode, currentTrack); break; case UI_MENU_MAIN: - drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute); break; case UI_MENU_RANDOMIZE: - drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute); break; case UI_MENU_SETUP: - drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index); + drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index, playMode, randomizeTrack, trackMute); break; case UI_SETUP_CHANNEL_EDIT: display.println(F("SET MIDI CHANNEL")); @@ -108,15 +107,36 @@ void UIManager::draw(UIState currentState, int menuSelection, int navSelection, display.setCursor(0, 50); display.println(F(" (Press to confirm)")); break; + case UI_SETUP_PLAYMODE_EDIT: + display.println(F("SET PLAY MODE")); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + display.setCursor(20, 25); + display.setTextSize(2); + display.print(playMode == MODE_MONO ? "Mono" : "Poly"); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F(" (Press to confirm)")); + break; + case UI_RANDOMIZE_TRACK_EDIT: + display.println(F("SET TRACK")); + display.drawLine(0, 8, 128, 8, SSD1306_WHITE); + display.setCursor(20, 25); + display.setTextSize(2); + display.print(F("TRK: ")); + display.print(randomizeTrack + 1); + display.setTextSize(1); + display.setCursor(0, 50); + display.println(F(" (Press to confirm)")); + break; } display.display(); } void UIManager::drawMenu(const char* title, const char* items[], int count, int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName, - int queuedTheme, int currentThemeIndex, - int numScaleNotes, const int* scaleNotes, int melodySeed, - bool mutationEnabled, bool songModeEnabled, int theme1Index) { + int queuedTheme, int currentThemeIndex, int numScaleNotes, + const int* scaleNotes, int melodySeed, bool mutationEnabled, + bool songModeEnabled, int theme1Index, PlayMode playMode, int randomizeTrack, const bool* trackMute) { display.println(title); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); @@ -137,8 +157,17 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int display.print(items[i]); if (currentState == UI_MENU_SETUP && i == 1) { + display.print(F(": ")); display.print(playMode == MODE_MONO ? "Mono" : "Poly"); + } + if (currentState == UI_MENU_SETUP && i == 2) { display.print(F(": ")); display.print(midiChannel); } + if (currentState == UI_MENU_RANDOMIZE && playMode == MODE_POLY && i == 1) { + display.print(F(": ")); display.print(randomizeTrack + 1); + } + if (currentState == UI_MENU_RANDOMIZE && playMode == MODE_POLY && i == 2) { + display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO")); + } if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && queuedTheme == (i - theme1Index + 1)) { display.print(F(" [NEXT]")); } @@ -146,11 +175,12 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int display.print(F(" *")); } if (currentState == UI_MENU_RANDOMIZE) { - if (i == 1) { // Melody + int track_offset = (playMode == MODE_POLY) ? 2 : 0; + if (i == 1 + track_offset) { // Melody display.print(F(": ")); display.print(melodySeed); - } else if (i == 2) { // Flavour + } else if (i == 2 + track_offset) { // Flavour display.print(F(": ")); display.print(flavourName); - } else if (i == 3) { // Scale + } else if (i == 3 + track_offset) { // Scale display.print(F(": ")); if (numScaleNotes > 0) { const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; @@ -159,19 +189,29 @@ void UIManager::drawMenu(const char* title, const char* items[], int count, int if (j < min(numScaleNotes, 6) - 1) display.print(F(" ")); } } - } else if (i == 4) { display.print(F(": ")); display.print(tempo); } - else if (i == 5) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); } - else if (i == 6) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); } + } else if (i == 4 + track_offset) { display.print(F(": ")); display.print(tempo); } + else if (i == 5 + track_offset) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); } + else if (i == 6 + track_offset) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); } } y += 9; } } -void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel, - const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying) { +void UIManager::drawTracker(int navSelection, EditMode editMode, int midiChannel, + const Step sequence[][NUM_STEPS], int scrollOffset, int playbackStep, bool isPlaying, + PlayMode playMode, int currentTrack) { display.print(F("SEQ ")); - if (navSelection > 0 && isEditing) display.print(F("[EDIT]")); - else display.print(F("[NAV] ")); + if (playMode == MODE_POLY) { + switch(editMode) { + case NAV_STEP: display.print(F("[STP]")); break; + case NAV_TRACK: display.print(F("[TRK]")); break; + case EDIT_NOTE: display.print(F("[EDT]")); break; + } + } else { + if (navSelection > 0 && editMode == EDIT_NOTE) display.print(F("[EDT]")); + else display.print(F("[NAV]")); + } + display.print(F(" CH:")); display.print(midiChannel); display.println(); display.drawLine(0, 8, 128, 8, SSD1306_WHITE); @@ -181,7 +221,7 @@ void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel, int itemIndex = i + scrollOffset; if (itemIndex > NUM_STEPS) break; - if (itemIndex == navSelection) { + if (itemIndex == navSelection && editMode != NAV_TRACK) { display.fillRect(0, y, 128, 8, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); } else { @@ -194,23 +234,38 @@ void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel, } else { int stepIndex = itemIndex - 1; bool isPlayback = isPlaying && (stepIndex == playbackStep); - if (isPlayback) { - if (itemIndex == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); + if (isPlayback && editMode != NAV_TRACK) { + if (itemIndex == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); // Cursor on playback row else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); } if (stepIndex < 10) display.print(F("0")); display.print(stepIndex); - if (isPlayback) { + if (isPlayback && editMode != NAV_TRACK) { if (itemIndex == navSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); else display.setTextColor(SSD1306_WHITE); } display.print(F(" | ")); - int n = sequence[stepIndex].note; - if (n == -1) display.print(F("---")); - else { - 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); + + int tracksToShow = (playMode == MODE_POLY) ? NUM_TRACKS : 1; + for(int t=0; t 4) { c[0] = color; if (sequence[s].accent) c[1] = dimColor; } - else if (octave < 4) { c[2] = color; if (sequence[s].accent) c[1] = dimColor; } - else { c[1] = color; if (sequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } } - } - int stepNavIndex = navSelection - 1; - // Apply cursor color logic from original code more strictly - uint32_t cursorColor = 0; - if (isPlaying) { - cursorColor = pixels.Color(0, 50, 0); - if (songModeEnabled && s >= 8) { - int repeats = min(songRepeatsRemaining, 8); - if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40); - } - } else if (currentState == UI_TRACKER) { - cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40); - } - - bool isCursorHere = (isPlaying && s == playbackStep) || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex); - if (cursorColor != 0) { - if (isCursorHere) c[3] = cursorColor; - else { - uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor; - c[3] = pixels.Color(r/5, g/5, b/5); + + const uint32_t COLOR_PLAYHEAD = pixels.Color(0, 255, 0); + const uint32_t COLOR_PLAYHEAD_DIM = pixels.Color(0, 32, 0); + const uint32_t COLOR_MUTED_PLAYHEAD = pixels.Color(0, 0, 255); + const uint32_t COLOR_CURSOR = pixels.Color(255, 255, 255); + const uint32_t COLOR_CURSOR_DIM = pixels.Color(32, 0, 0); + + if(playMode == MODE_POLY) { + for(int t=0; t 4) { c[0] = color; if (sequence[0][s].accent) c[1] = dimColor; } + else if (octave < 4) { c[2] = color; if (sequence[0][s].accent) c[1] = dimColor; } + else { c[1] = color; if (sequence[0][s].accent) { c[0] = dimColor; c[2] = dimColor; } } + } + int stepNavIndex = navSelection - 1; + uint32_t cursorColor = 0; + if (isPlaying) { + cursorColor = pixels.Color(0, 50, 0); + if (songModeEnabled && s >= 8) { + int repeats = min(songRepeatsRemaining, 8); + if (x >= (8 - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40); + } + } else if (currentState == UI_TRACKER) { + cursorColor = (editMode == EDIT_NOTE) ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40); + } + + bool isCursorHere = (isPlaying && s == playbackStep) || (!isPlaying && currentState == UI_TRACKER && s == stepNavIndex); + if (cursorColor != 0) { + if (isCursorHere) c[3] = cursorColor; + else { + uint8_t r = (uint8_t)(cursorColor >> 16), g = (uint8_t)(cursorColor >> 8), b = (uint8_t)cursorColor; + c[3] = pixels.Color(r/5, g/5, b/5); + } + } + for(int i=0; i<4; i++) pixels.setPixelColor(getPixelIndex(x, yBase + i), c[i]); + } } if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0)); pixels.show(); diff --git a/UIManager.h b/UIManager.h index a6e9b20..50cafdd 100644 --- a/UIManager.h +++ b/UIManager.h @@ -14,33 +14,36 @@ public: void showMessage(const char* msg); - void draw(UIState currentState, int menuSelection, int navSelection, bool isEditing, + void draw(UIState currentState, int menuSelection, int navSelection, EditMode editMode, int midiChannel, int tempo, MelodyStrategy* currentStrategy, int queuedTheme, int currentThemeIndex, int numScaleNotes, const int* scaleNotes, int melodySeed, bool mutationEnabled, bool songModeEnabled, - const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying, + const Step sequence[][NUM_STEPS], int scrollOffset, int playbackStep, bool isPlaying, const char* mainMenu[], int mainMenuCount, const char* randomizeMenu[], int randomizeMenuCount, - const char* setupMenu[], int setupMenuCount, - int theme1Index); + const char* setupMenu[], int setupMenuCount, int theme1Index, + PlayMode playMode, int currentTrack, int randomizeTrack, const bool* trackMute); - void updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying, - UIState currentState, bool isEditing, bool songModeEnabled, - int songRepeatsRemaining, bool sequenceChangeScheduled); + void updateLeds(const Step sequence[][NUM_STEPS], int navSelection, int playbackStep, bool isPlaying, + UIState currentState, EditMode editMode, bool songModeEnabled, + int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode, int currentTrack, + int numScaleNotes, const int* scaleNotes, const bool* trackMute); private: Adafruit_SSD1306 display; Adafruit_NeoPixel pixels; + uint32_t leds_buffer[8][8]; // For piano roll void drawMenu(const char* title, const char* items[], int count, int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName, int queuedTheme, int currentThemeIndex, int numScaleNotes, const int* scaleNotes, int melodySeed, - bool mutationEnabled, bool songModeEnabled, int theme1Index); + bool mutationEnabled, bool songModeEnabled, int theme1Index, PlayMode playMode, int randomizeTrack, const bool* trackMute); - void drawTracker(int navSelection, bool isEditing, int midiChannel, - const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying); + void drawTracker(int navSelection, EditMode editMode, int midiChannel, + const Step sequence[][NUM_STEPS], int scrollOffset, int playbackStep, bool isPlaying, + PlayMode playMode, int currentTrack); uint32_t getNoteColor(int note, bool dim); int getPixelIndex(int x, int y); diff --git a/config.h b/config.h new file mode 100644 index 0000000..e7fc66f --- /dev/null +++ b/config.h @@ -0,0 +1,22 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// --- HARDWARE CONFIGURATION --- +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 + +// Pin Definitions for Raspberry Pi Pico (RP2040) +#define PIN_SDA 4 +#define PIN_SCL 5 + +#define ENC_CLK 12 +#define ENC_DT 13 +#define ENC_SW 14 + +// --- TRACKER DATA --- +#define NUM_STEPS 16 +#define NUM_TRACKS 4 + +#endif \ No newline at end of file