PicoWaveTracker/UIThread.cpp
2026-03-07 21:31:17 +01:00

541 lines
20 KiB
C++

#include <SPI.h>
#include <Wire.h>
#include "TrackerTypes.h"
#include "MidiDriver.h"
#include "UIManager.h"
#include "config.h"
#include "UIThread.h"
#include "SharedState.h"
#include "SequenceGenerator.h"
#include "Persistence.h"
static Step local_sequence[NUM_TRACKS][NUM_STEPS];
static void handleInput();
static void drawUI();
static void updateLeds();
void saveSequence(bool quiet) {
Persistence::saveSequence(quiet);
}
bool loadSequence() {
return Persistence::loadSequence();
}
void factoryReset() {
Persistence::factoryReset();
}
void generateRandomScale() {
SequenceGenerator::generateRandomScale();
}
void generateTheme(int themeType) {
SequenceGenerator::pickRandomScaleType(themeType);
SequenceGenerator::generateSequenceData(themeType, local_sequence);
Serial.println(F("Generating theme."));
midi.lock();
memcpy(sequence, local_sequence, sizeof(local_sequence));
needsPanic = true;
midi.unlock();
Serial.println(F("Theme ready."));
currentThemeIndex = themeType;
clockCount = 0;
lastClockTime = micros();
playbackStep = 0;
isPlaying = true;
}
void mutateSequence(Step (*target)[NUM_STEPS]) {
SequenceGenerator::mutateSequence(target);
}
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_STEPS:
numSteps[randomizeTrack] += delta;
if (numSteps[randomizeTrack] < 1) numSteps[randomizeTrack] = 1;
if (numSteps[randomizeTrack] > NUM_STEPS) numSteps[randomizeTrack] = NUM_STEPS;
break;
case UI_EDIT_ROOT:
globalRoot += delta;
if (globalRoot < 0) globalRoot = 11;
if (globalRoot > 11) globalRoot = 0;
currentRoot = globalRoot; // Reset current to global
SequenceGenerator::updateScale();
if (isPlaying) {
sequenceChangeScheduled = true;
SequenceGenerator::generateSequenceData((queuedTheme != -1) ? queuedTheme : currentThemeIndex, nextSequence);
}
break;
case UI_EDIT_PROG_ORDER:
if (delta > 0) progressionOrder = PROG_ORDER_RANDOM;
else progressionOrder = PROG_ORDER_SEQUENCE;
break;
case UI_EDIT_PROG_TEMPLATE:
templateSelectionIndex += delta;
if (templateSelectionIndex < 0) templateSelectionIndex = numProgressions - 1;
if (templateSelectionIndex >= numProgressions) templateSelectionIndex = 0;
break;
case UI_EDIT_PROG_LENGTH:
currentProgression.length += delta;
if (currentProgression.length < 1) currentProgression.length = 1;
if (currentProgression.length > MAX_PROG_STEPS) currentProgression.length = MAX_PROG_STEPS;
progressionStep = 0; // Reset step
break;
case UI_EDIT_PROG_STEP_ROOT:
{
int stepIdx = stepEditIndex / 3;
currentProgression.steps[stepIdx].rootOffset += delta;
if (currentProgression.steps[stepIdx].rootOffset < -12) currentProgression.steps[stepIdx].rootOffset = -12;
if (currentProgression.steps[stepIdx].rootOffset > 12) currentProgression.steps[stepIdx].rootOffset = 12;
}
break;
case UI_EDIT_PROG_STEP_TYPE:
{
int stepIdx = stepEditIndex / 3;
int type = currentProgression.steps[stepIdx].scaleType;
type += delta;
if (type < 0) type = 9;
if (type > 9) type = 0;
currentProgression.steps[stepIdx].scaleType = type;
}
break;
case UI_EDIT_PROG_STEP_REPS:
{
int stepIdx = stepEditIndex / 3;
int val = currentProgression.steps[stepIdx].repeats + delta;
if (val < 1) val = 1;
if (val > 16) val = 16;
currentProgression.steps[stepIdx].repeats = val;
}
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;
case UI_EDIT_INTENSITY:
{
int current = trackIntensity[randomizeTrack];
current += delta;
if (current < 1) current = 1;
if (current > 10) current = 10;
trackIntensity[randomizeTrack] = current;
}
break;
case UI_EDIT_CHANCE:
{
int current = trackChance[randomizeTrack];
current += delta;
if (current < 0) current = 0;
if (current > 100) current = 100;
trackChance[randomizeTrack] = current;
}
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_PLAYBACK:
isPlaying = !isPlaying;
if (isPlaying) {
playbackStep = 0;
clockCount = 0;
lastClockTime = micros();
} else {
queuedTheme = -1;
}
break;
case MENU_ID_MELODY:
midi.lock();
melodySeeds[randomizeTrack] = random(10000);
randomSeed(melodySeeds[randomizeTrack]);
for (int i = 0; i < NUM_TRACKS; i++) {
numSteps[i] = random(1, NUM_STEPS + 1);
}
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
sequenceChangeScheduled = true;
}
midi.unlock();
saveSequence(true);
break;
case MENU_ID_ROOT: currentState = UI_EDIT_ROOT; break;
case MENU_ID_PROG_ORDER: currentState = UI_EDIT_PROG_ORDER; break;
case MENU_ID_PROG_APPLY_TEMPLATE: currentState = UI_EDIT_PROG_TEMPLATE; templateSelectionIndex = 0; break;
case MENU_ID_PROG_LENGTH: currentState = UI_EDIT_PROG_LENGTH; break;
case MENU_ID_PROG_STEP_START ... (MENU_ID_PROG_STEP_END - 1):
stepEditIndex = menuItems[menuSelection].id - MENU_ID_PROG_STEP_START;
if (stepEditIndex % 3 == 0) currentState = UI_EDIT_PROG_STEP_ROOT;
else if (stepEditIndex % 3 == 1) currentState = UI_EDIT_PROG_STEP_TYPE;
else currentState = UI_EDIT_PROG_STEP_REPS;
break;
case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break;
case MENU_ID_STEPS: currentState = UI_EDIT_STEPS; 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_INTENSITY: currentState = UI_EDIT_INTENSITY; break;
case MENU_ID_CHANCE: currentState = UI_EDIT_CHANCE; break;
case MENU_ID_MUTATION: mutationEnabled = !mutationEnabled; break;
case MENU_ID_CHANNEL: currentState = UI_SETUP_CHANNEL_EDIT; break;
case MENU_ID_PROTECTED_MODE: protectedMode = !protectedMode; 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;
SequenceGenerator::pickRandomScaleType(queuedTheme);
midi.lock();
SequenceGenerator::generateSequenceData(queuedTheme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
} else {
generateTheme(selectedTheme);
}
break;
}
if (menuItems[menuSelection].id >= MENU_ID_PATCH_ACTIONS_START) {
int offset = menuItems[menuSelection].id - MENU_ID_PATCH_ACTIONS_START;
int bank = offset / 8;
int sub = offset % 8;
bool isSave = sub >= 4;
int slot = sub % 4;
if (isSave) Persistence::savePatch(bank, slot);
else Persistence::loadPatch(bank, slot, local_sequence);
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_STEPS:
currentState = UI_MENU_MAIN;
saveSequence(true);
break;
case UI_EDIT_ROOT:
currentState = UI_MENU_MAIN;
saveSequence(true);
break;
case UI_EDIT_PROG_ORDER:
currentState = UI_MENU_MAIN;
saveSequence(true);
break;
case UI_EDIT_PROG_TEMPLATE:
// Apply template
if (templateSelectionIndex > 0) {
currentProgression = progressions[templateSelectionIndex];
progressionStep = 0;
} else {
// Off/Clear
currentProgression.length = 0;
}
currentState = UI_MENU_MAIN;
saveSequence(true);
break;
case UI_EDIT_PROG_LENGTH:
case UI_EDIT_PROG_STEP_ROOT:
case UI_EDIT_PROG_STEP_TYPE:
case UI_EDIT_PROG_STEP_REPS:
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));
}
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break;
case UI_EDIT_INTENSITY:
currentState = UI_MENU_MAIN;
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
sequenceChangeScheduled = true;
midi.unlock();
}
saveSequence(true);
break;
case UI_EDIT_CHANCE:
currentState = UI_MENU_MAIN;
if (isPlaying) {
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
midi.lock();
if (!sequenceChangeScheduled) {
memcpy(nextSequence, sequence, sizeof(sequence));
}
SequenceGenerator::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, local_currentTrackNumSteps;
bool local_mutationEnabled, local_songModeEnabled, local_isPlaying;
bool local_trackMute[NUM_TRACKS];
int local_midiChannel;
int local_trackIntensities[NUM_TRACKS];
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_currentTrackNumSteps = numSteps[local_randomizeTrack];
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));
memcpy(local_trackIntensities, (const void*)trackIntensity, sizeof(local_trackIntensities));
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_currentTrackNumSteps,
local_mutationEnabled, local_songModeEnabled, (const Step (*)[NUM_STEPS])local_sequence, local_playbackStep, local_isPlaying, local_randomizeTrack, (const bool*)local_trackMute, (const int*)local_trackIntensities);
}
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], local_numSteps[NUM_TRACKS];
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;
for (int i = 0; i < NUM_TRACKS; i++) local_numSteps[i] = numSteps[i];
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_STEPS || 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_numSteps, 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
if (currentProgression.length > 0) {
// Progression Mode
const ChordProgression& prog = currentProgression;
currentRoot = (globalRoot + prog.steps[progressionStep].rootOffset) % 12;
if (currentRoot < 0) currentRoot += 12;
currentScaleType = prog.steps[progressionStep].scaleType;
SequenceGenerator::updateScale();
queuedProgressionStep = progressionStep;
repeats = prog.steps[progressionStep].repeats;
if (progressionOrder == PROG_ORDER_SEQUENCE)
progressionStep = (progressionStep + 1) % prog.length;
else
progressionStep = random(prog.length);
} else {
// Random Mode
SequenceGenerator::pickRandomScaleType(nextTheme);
}
SequenceGenerator::generateSequenceData(nextTheme, nextSequence);
queuedTheme = nextTheme;
nextSongRepeats = repeats;
sequenceChangeScheduled = true;
songModeNeedsNext = false;
}
handleInput();
drawUI();
updateLeds();
delay(10); // Small delay to prevent screen tearing/excessive refresh
}