Compare commits
No commits in common. "c6e2248090bea5d3c4e69dd6bdd41c623a2796d0" and "1e475eeaa54803b83f814880a697a5aff425005b" have entirely different histories.
c6e2248090
...
1e475eeaa5
102
ArpStrategy.h
102
ArpStrategy.h
@ -10,102 +10,24 @@ public:
|
||||
randomSeed(seed);
|
||||
if (numScaleNotes == 0) return;
|
||||
|
||||
// 1. Select Subset
|
||||
int subsetSize = random(2, numScaleNotes + 1);
|
||||
if (subsetSize > 12) subsetSize = 12;
|
||||
int subset[12];
|
||||
int currentNoteIndex = 0;
|
||||
int octave = 4;
|
||||
|
||||
// Create indices to shuffle
|
||||
int indices[12];
|
||||
for(int i=0; i<numScaleNotes; i++) indices[i] = i;
|
||||
|
||||
// Shuffle indices
|
||||
for(int i=0; i<numScaleNotes; i++) {
|
||||
int r = random(numScaleNotes);
|
||||
int temp = indices[i];
|
||||
indices[i] = indices[r];
|
||||
indices[r] = temp;
|
||||
}
|
||||
|
||||
// Pick subset
|
||||
for(int i=0; i<subsetSize; i++) {
|
||||
subset[i] = scaleNotes[indices[i]];
|
||||
}
|
||||
sortArray(subset, subsetSize);
|
||||
|
||||
// 2. Pick Arp Length & Pattern
|
||||
int arpLength = random(2, 17); // 2 to 16
|
||||
Step arpPattern[16];
|
||||
|
||||
int mode = random(3); // 0: Up, 1: Down, 2: Up/Down
|
||||
int numOctaves = 3;
|
||||
int baseOctave = 3;
|
||||
int totalNotes = subsetSize * numOctaves;
|
||||
|
||||
int currentIndex = 0;
|
||||
int direction = 1;
|
||||
|
||||
if (mode == 1) { // Down
|
||||
currentIndex = totalNotes - 1;
|
||||
direction = -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < arpLength; i++) {
|
||||
// Chance of rest (15%)
|
||||
if (random(100) < 15) {
|
||||
arpPattern[i].note = -1;
|
||||
arpPattern[i].accent = false;
|
||||
arpPattern[i].tie = false;
|
||||
} else {
|
||||
int octaveOffset = currentIndex / subsetSize;
|
||||
int noteIndex = currentIndex % subsetSize;
|
||||
int octave = baseOctave + octaveOffset;
|
||||
|
||||
arpPattern[i].note = 12 * octave + subset[noteIndex];
|
||||
arpPattern[i].accent = (i % 4 == 0); // Accent on beat
|
||||
arpPattern[i].tie = false;
|
||||
}
|
||||
|
||||
if (mode == 0) { // Up
|
||||
currentIndex++;
|
||||
if (currentIndex >= totalNotes) currentIndex = 0;
|
||||
} else if (mode == 1) { // Down
|
||||
currentIndex--;
|
||||
if (currentIndex < 0) currentIndex = totalNotes - 1;
|
||||
} else { // Up/Down
|
||||
currentIndex += direction;
|
||||
if (currentIndex >= totalNotes) {
|
||||
currentIndex = max(0, totalNotes - 2);
|
||||
direction = -1;
|
||||
} else if (currentIndex < 0) {
|
||||
currentIndex = min(totalNotes - 1, 1);
|
||||
direction = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fill Sequence
|
||||
for (int i = 0; i < numSteps; i++) {
|
||||
sequence[i] = arpPattern[i % arpLength];
|
||||
sequence[i].note = 12 * octave + scaleNotes[currentNoteIndex];
|
||||
sequence[i].accent = (i % 4 == 0); // Accent on beat
|
||||
sequence[i].tie = false;
|
||||
|
||||
currentNoteIndex++;
|
||||
if (currentNoteIndex >= numScaleNotes) {
|
||||
currentNoteIndex = 0;
|
||||
octave++;
|
||||
if (octave > 5) octave = 3;
|
||||
}
|
||||
}
|
||||
randomSeed(micros());
|
||||
}
|
||||
|
||||
void generateScale(int* scaleNotes, int& numScaleNotes) override {
|
||||
numScaleNotes = random(3, 13); // 3 to 12 notes
|
||||
for (int i = 0; i < 12; i++) {
|
||||
scaleNotes[i] = i; // Fill with all notes
|
||||
}
|
||||
// Shuffle
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int j = random(12);
|
||||
int temp = scaleNotes[i];
|
||||
scaleNotes[i] = scaleNotes[j];
|
||||
scaleNotes[j] = temp;
|
||||
}
|
||||
sortArray(scaleNotes, numScaleNotes);
|
||||
}
|
||||
|
||||
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
|
||||
// Swap two notes
|
||||
int s1 = random(numSteps);
|
||||
|
||||
@ -19,21 +19,6 @@ public:
|
||||
randomSeed(micros());
|
||||
}
|
||||
|
||||
void generateScale(int* scaleNotes, int& numScaleNotes) override {
|
||||
numScaleNotes = random(3, 13); // 3 to 12 notes
|
||||
for (int i = 0; i < 12; i++) {
|
||||
scaleNotes[i] = i; // Fill with all notes
|
||||
}
|
||||
// Shuffle
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int j = random(12);
|
||||
int temp = scaleNotes[i];
|
||||
scaleNotes[i] = scaleNotes[j];
|
||||
scaleNotes[j] = temp;
|
||||
}
|
||||
sortArray(scaleNotes, numScaleNotes);
|
||||
}
|
||||
|
||||
void mutate(Step* sequence, int numSteps, int* scaleNotes, int numScaleNotes) override {
|
||||
// Mutate 1 or 2 steps
|
||||
int count = random(1, 3);
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
class MelodyStrategy {
|
||||
public:
|
||||
virtual void generate(Step* sequence, 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 const char* getName() = 0;
|
||||
virtual ~MelodyStrategy() {}
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
#include "MidiDriver.h"
|
||||
|
||||
// MIDI UART Pins (GP0/GP1)
|
||||
#define PIN_MIDI_TX 0
|
||||
|
||||
MidiDriver midi;
|
||||
|
||||
MidiDriver::MidiDriver() {
|
||||
}
|
||||
|
||||
void MidiDriver::begin() {
|
||||
mutex_init(&_mutex);
|
||||
Serial1.setTX(PIN_MIDI_TX);
|
||||
Serial1.begin(31250);
|
||||
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
||||
}
|
||||
|
||||
void MidiDriver::lock() {
|
||||
mutex_enter_blocking(&_mutex);
|
||||
}
|
||||
|
||||
void MidiDriver::unlock() {
|
||||
mutex_exit(&_mutex);
|
||||
}
|
||||
|
||||
void MidiDriver::sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel) {
|
||||
uint8_t status = 0x90 | (channel - 1);
|
||||
Serial1.write(status);
|
||||
Serial1.write(note);
|
||||
Serial1.write(velocity);
|
||||
}
|
||||
|
||||
void MidiDriver::sendNoteOff(uint8_t note, uint8_t channel) {
|
||||
uint8_t status = 0x80 | (channel - 1);
|
||||
Serial1.write(status);
|
||||
Serial1.write(note);
|
||||
Serial1.write((uint8_t)0);
|
||||
}
|
||||
|
||||
void MidiDriver::sendRealtime(uint8_t status) {
|
||||
Serial1.write(status);
|
||||
}
|
||||
|
||||
void MidiDriver::panic(uint8_t channel) {
|
||||
uint8_t status = 0xB0 | (channel - 1);
|
||||
Serial1.write(status);
|
||||
Serial1.write(123); // All Notes Off
|
||||
Serial1.write((uint8_t)0);
|
||||
}
|
||||
26
MidiDriver.h
26
MidiDriver.h
@ -1,26 +0,0 @@
|
||||
#ifndef MIDI_DRIVER_H
|
||||
#define MIDI_DRIVER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <pico/mutex.h>
|
||||
|
||||
class MidiDriver {
|
||||
public:
|
||||
MidiDriver();
|
||||
void begin();
|
||||
|
||||
void sendNoteOn(uint8_t note, uint8_t velocity, uint8_t channel);
|
||||
void sendNoteOff(uint8_t note, uint8_t channel);
|
||||
void sendRealtime(uint8_t status);
|
||||
void panic(uint8_t channel);
|
||||
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
private:
|
||||
mutex_t _mutex;
|
||||
};
|
||||
|
||||
extern MidiDriver midi;
|
||||
|
||||
#endif
|
||||
@ -1,12 +1,14 @@
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <EEPROM.h>
|
||||
#include <pico/mutex.h>
|
||||
#include "TrackerTypes.h"
|
||||
#include "MelodyStrategy.h"
|
||||
#include "LuckyStrategy.h"
|
||||
#include "ArpStrategy.h"
|
||||
#include "MidiDriver.h"
|
||||
#include "UIManager.h"
|
||||
|
||||
// --- HARDWARE CONFIGURATION ---
|
||||
#define SCREEN_WIDTH 128
|
||||
@ -22,6 +24,13 @@
|
||||
#define ENC_DT 13
|
||||
#define ENC_SW 14
|
||||
|
||||
// MIDI UART Pins (GP0/GP1) -- OUT only so far
|
||||
#define PIN_MIDI_TX 0
|
||||
|
||||
// NeoPixel Pin (any GPIO is fine, I've chosen 16)
|
||||
#define PIN_NEOPIXEL 16
|
||||
#define NUM_PIXELS 64 // For 8x8 WS2812B matrix
|
||||
|
||||
// --- TRACKER DATA ---
|
||||
#define NUM_STEPS 16
|
||||
|
||||
@ -30,14 +39,30 @@ Step nextSequence[NUM_STEPS];
|
||||
volatile bool sequenceChangeScheduled = false;
|
||||
volatile bool needsPanic = false;
|
||||
|
||||
mutex_t midiMutex;
|
||||
|
||||
// --- STATE ---
|
||||
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
|
||||
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
enum UIState {
|
||||
UI_TRACKER,
|
||||
UI_MENU_MAIN,
|
||||
UI_MENU_RANDOMIZE,
|
||||
UI_MENU_SETUP,
|
||||
UI_SETUP_CHANNEL_EDIT,
|
||||
UI_EDIT_TEMPO,
|
||||
UI_EDIT_FLAVOUR
|
||||
};
|
||||
|
||||
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 char* randomizeMenu[] = { "Back", "Scale", "Melody", "Flavour", "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* setupMenu[] = { "Back", "Channel", "Save", "Load" };
|
||||
const int setupMenuCount = sizeof(setupMenu) / sizeof(char*);
|
||||
|
||||
int menuSelection = 0;
|
||||
@ -100,6 +125,29 @@ void readEncoder() {
|
||||
}
|
||||
}
|
||||
|
||||
void sortArray(int arr[], int size) {
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
for (int j = 0; j < size - i - 1; j++) {
|
||||
if (arr[j] > arr[j + 1]) {
|
||||
int temp = arr[j];
|
||||
arr[j] = arr[j + 1];
|
||||
arr[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(const char* msg) {
|
||||
display.clearDisplay();
|
||||
display.setCursor(10, 25);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setTextSize(2);
|
||||
display.print(msg);
|
||||
display.display();
|
||||
delay(500);
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
void saveSequence(bool quiet = false) {
|
||||
int addr = 0;
|
||||
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
|
||||
@ -113,13 +161,13 @@ void saveSequence(bool quiet = false) {
|
||||
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
||||
}
|
||||
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
for (int i=0; i<NUM_STEPS; i++) {
|
||||
EEPROM.put(addr, sequence[i]); addr += sizeof(Step);
|
||||
}
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
EEPROM.commit();
|
||||
if (!quiet) ui.showMessage("SAVED!");
|
||||
if (!quiet) showMessage("SAVED!");
|
||||
}
|
||||
|
||||
bool loadSequence() {
|
||||
@ -141,25 +189,17 @@ bool loadSequence() {
|
||||
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
||||
}
|
||||
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
for (int i=0; i<NUM_STEPS; i++) {
|
||||
EEPROM.get(addr, sequence[i]); addr += sizeof(Step);
|
||||
}
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void factoryReset() {
|
||||
ui.showMessage("RESETTING...");
|
||||
uint32_t magic = 0;
|
||||
EEPROM.put(0, magic);
|
||||
EEPROM.commit();
|
||||
delay(500);
|
||||
rp2040.reboot();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
mutex_init(&midiMutex);
|
||||
|
||||
// Use random ADC noise for seed
|
||||
delay(5000);
|
||||
@ -173,8 +213,26 @@ void setup() {
|
||||
attachInterrupt(digitalPinToInterrupt(ENC_CLK), readEncoder, CHANGE);
|
||||
attachInterrupt(digitalPinToInterrupt(ENC_DT), readEncoder, CHANGE);
|
||||
|
||||
ui.begin();
|
||||
midi.begin();
|
||||
// 2. Setup Display
|
||||
// Note: Using default I2C pins (SDA=GP4, SCL=GP5) which works on both cores.
|
||||
Wire.begin();
|
||||
|
||||
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
|
||||
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
||||
Serial.println(F("SSD1306 allocation failed"));
|
||||
for(;;); // Don't proceed, loop forever
|
||||
}
|
||||
|
||||
// 3. Setup NeoPixel Matrix
|
||||
pixels.begin();
|
||||
pixels.setBrightness(40); // Set brightness to a medium-low value (0-255)
|
||||
pixels.clear();
|
||||
pixels.show();
|
||||
|
||||
// 4. Setup MIDI Serial
|
||||
Serial1.setTX(PIN_MIDI_TX);
|
||||
Serial1.begin(31250);
|
||||
Serial.println(F("MIDI Serial initialized on GP0/GP1"));
|
||||
|
||||
// 5. Init Sequence
|
||||
randomSeed(micros());
|
||||
@ -187,11 +245,38 @@ void setup() {
|
||||
}
|
||||
isPlaying = false; // Don't start playing on boot
|
||||
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
|
||||
Serial.println(F("Started."));
|
||||
}
|
||||
|
||||
void sendMidi(uint8_t status, uint8_t note, uint8_t velocity) {
|
||||
uint8_t channelStatus = status | (shMidiChannel - 1);
|
||||
Serial1.write(channelStatus);
|
||||
Serial1.write(note);
|
||||
Serial1.write(velocity);
|
||||
}
|
||||
|
||||
void sendMidiRealtime(uint8_t status) {
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
Serial1.write(status);
|
||||
mutex_exit(&midiMutex);
|
||||
}
|
||||
|
||||
void generateRandomScale() {
|
||||
strategies[currentStrategyIndex]->generateScale(scaleNotes, numScaleNotes);
|
||||
numScaleNotes = random(3, 13); // 3 to 12 notes
|
||||
for (int i = 0; i < 12; i++) {
|
||||
scaleNotes[i] = i; // Fill with all notes
|
||||
}
|
||||
// Shuffle
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int j = random(12);
|
||||
int temp = scaleNotes[i];
|
||||
scaleNotes[i] = scaleNotes[j];
|
||||
scaleNotes[j] = temp;
|
||||
}
|
||||
sortArray(scaleNotes, numScaleNotes);
|
||||
}
|
||||
|
||||
void generateSequenceData(int themeType, Step* target) {
|
||||
@ -202,14 +287,14 @@ void generateSequenceData(int themeType, Step* target) {
|
||||
void generateTheme(int themeType) {
|
||||
currentThemeIndex = themeType;
|
||||
needsPanic = true;
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
generateSequenceData(themeType, sequence);
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
|
||||
clockCount = 0;
|
||||
lastClockTime = micros();
|
||||
playbackStep = 0;
|
||||
midi.sendRealtime(0xFA); // MIDI Start
|
||||
sendMidiRealtime(0xFA); // MIDI Start
|
||||
isPlaying = true;
|
||||
}
|
||||
|
||||
@ -234,9 +319,9 @@ void handleInput() {
|
||||
int newNote = sequence[stepIndex].note + delta;
|
||||
if (newNote < -1) newNote = -1;
|
||||
if (newNote > 127) newNote = 127;
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
sequence[stepIndex].note = newNote;
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
} else {
|
||||
// Move Cursor
|
||||
navigationSelection += (delta > 0 ? 1 : -1);
|
||||
@ -319,28 +404,28 @@ void handleInput() {
|
||||
case UI_MENU_RANDOMIZE:
|
||||
if (menuSelection == 0) { currentState = UI_MENU_MAIN; menuSelection = 1; }
|
||||
if (menuSelection == 1) {
|
||||
generateRandomScale(); // Scale
|
||||
if (isPlaying) {
|
||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
generateSequenceData(theme, nextSequence);
|
||||
sequenceChangeScheduled = true;
|
||||
mutex_exit(&midiMutex);
|
||||
}
|
||||
saveSequence(true);
|
||||
}
|
||||
if (menuSelection == 2) {
|
||||
melodySeed = random(10000); // Melody
|
||||
if (isPlaying) {
|
||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
generateSequenceData(theme, nextSequence);
|
||||
sequenceChangeScheduled = true;
|
||||
midi.unlock();
|
||||
}
|
||||
saveSequence(true);
|
||||
}
|
||||
if (menuSelection == 2) currentState = UI_EDIT_FLAVOUR; // Flavour
|
||||
if (menuSelection == 3) { // Scale
|
||||
generateRandomScale();
|
||||
if (isPlaying) {
|
||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
||||
midi.lock();
|
||||
generateSequenceData(theme, nextSequence);
|
||||
sequenceChangeScheduled = true;
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
}
|
||||
saveSequence(true);
|
||||
}
|
||||
if (menuSelection == 3) currentState = UI_EDIT_FLAVOUR;
|
||||
if (menuSelection == 4) currentState = UI_EDIT_TEMPO;
|
||||
if (menuSelection == 5) mutationEnabled = !mutationEnabled;
|
||||
if (menuSelection == 6) {
|
||||
@ -353,10 +438,10 @@ void handleInput() {
|
||||
const int selectedTheme = menuSelection - THEME_1_INDEX + 1;
|
||||
if (isPlaying) {
|
||||
queuedTheme = selectedTheme;
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
generateSequenceData(queuedTheme, nextSequence);
|
||||
sequenceChangeScheduled = true;
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
} else {
|
||||
generateTheme(selectedTheme);
|
||||
}
|
||||
@ -365,7 +450,14 @@ void handleInput() {
|
||||
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 == 2) {
|
||||
saveSequence();
|
||||
currentState = UI_MENU_MAIN;
|
||||
}
|
||||
if (menuSelection == 3) {
|
||||
if (loadSequence()) showMessage("LOADED!");
|
||||
currentState = UI_MENU_MAIN;
|
||||
}
|
||||
break;
|
||||
case UI_SETUP_CHANNEL_EDIT:
|
||||
currentState = UI_MENU_SETUP;
|
||||
@ -377,13 +469,6 @@ void handleInput() {
|
||||
break;
|
||||
case UI_EDIT_FLAVOUR:
|
||||
currentState = UI_MENU_RANDOMIZE;
|
||||
if (isPlaying) {
|
||||
queuedTheme = currentThemeIndex;
|
||||
midi.lock();
|
||||
generateSequenceData(currentThemeIndex, nextSequence);
|
||||
sequenceChangeScheduled = true;
|
||||
midi.unlock();
|
||||
}
|
||||
saveSequence(true);
|
||||
break;
|
||||
}
|
||||
@ -402,11 +487,11 @@ void handleInput() {
|
||||
playbackStep = 0;
|
||||
clockCount = 0;
|
||||
lastClockTime = micros();
|
||||
midi.sendRealtime(0xFA); // MIDI Start
|
||||
sendMidiRealtime(0xFA); // MIDI Start
|
||||
} else {
|
||||
// Send All Notes Off on stop (CC 123)
|
||||
needsPanic = true;
|
||||
midi.sendRealtime(0xFC); // MIDI Stop
|
||||
sendMidiRealtime(0xFC); // MIDI Stop
|
||||
queuedTheme = -1;
|
||||
}
|
||||
}
|
||||
@ -424,13 +509,13 @@ void handlePlayback() {
|
||||
if (currentMicros - lastClockTime >= clockInterval) {
|
||||
lastClockTime += clockInterval;
|
||||
|
||||
midi.sendRealtime(0xF8); // MIDI Clock
|
||||
sendMidiRealtime(0xF8); // MIDI Clock
|
||||
|
||||
clockCount++;
|
||||
if (clockCount < 6) return; // 24 ppqn / 4 = 6 pulses per 16th note
|
||||
clockCount = 0;
|
||||
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
|
||||
// Determine if we are tying to the next note
|
||||
int nextStep = playbackStep + 1;
|
||||
@ -441,7 +526,7 @@ void handlePlayback() {
|
||||
|
||||
// Note Off for previous step (if NOT tied)
|
||||
if (!isTied && prevNote != -1) {
|
||||
midi.sendNoteOff(prevNote, shMidiChannel);
|
||||
sendMidi(0x80, prevNote, 0);
|
||||
}
|
||||
|
||||
playbackStep++;
|
||||
@ -464,7 +549,7 @@ void handlePlayback() {
|
||||
sequenceChangeScheduled = true;
|
||||
}
|
||||
|
||||
midi.panic(shMidiChannel); // Panic / All Notes Off
|
||||
sendMidi(0xB0, 123, 0); // Panic / All Notes Off
|
||||
|
||||
if (sequenceChangeScheduled) {
|
||||
memcpy(sequence, nextSequence, sizeof(sequence));
|
||||
@ -491,35 +576,336 @@ void handlePlayback() {
|
||||
// Note On for new step
|
||||
if (sequence[playbackStep].note != -1) {
|
||||
uint8_t velocity = sequence[playbackStep].accent ? 127 : 100;
|
||||
midi.sendNoteOn(sequence[playbackStep].note, velocity, shMidiChannel);
|
||||
sendMidi(0x90, sequence[playbackStep].note, velocity);
|
||||
}
|
||||
|
||||
// Note Off for previous step (if tied - delayed Note Off)
|
||||
if (isTied && prevNote != -1) {
|
||||
midi.sendNoteOff(prevNote, shMidiChannel);
|
||||
sendMidi(0x80, prevNote, 0);
|
||||
}
|
||||
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
}
|
||||
}
|
||||
|
||||
void drawMenu(const char* title, const char* items[], int count, int selection) {
|
||||
display.println(title);
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
// Simple scrolling: keep selection visible
|
||||
int start = 0;
|
||||
if (selection >= 5) {
|
||||
start = selection - 4;
|
||||
}
|
||||
|
||||
int y = 10;
|
||||
for (int i = start; i < count; i++) {
|
||||
if (y > 55) break; // Stop if we run out of screen space
|
||||
|
||||
if (i == selection) {
|
||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
} else {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
display.print(items[i]);
|
||||
|
||||
// Special case for channel display
|
||||
if (currentState == UI_MENU_SETUP && i == 1) {
|
||||
display.print(F(": "));
|
||||
display.print(midiChannel);
|
||||
}
|
||||
|
||||
// Special case for queued theme
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= THEME_1_INDEX && queuedTheme == (i - THEME_1_INDEX + 1)) {
|
||||
display.print(F(" [NEXT]"));
|
||||
}
|
||||
|
||||
// Special case for active theme
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= THEME_1_INDEX && currentThemeIndex == (i - THEME_1_INDEX + 1)) {
|
||||
display.print(F(" *"));
|
||||
}
|
||||
|
||||
// Special cases for Randomize Menu values
|
||||
if (currentState == UI_MENU_RANDOMIZE) {
|
||||
if (i == 1) { // Scale
|
||||
display.print(F(": "));
|
||||
if (numScaleNotes > 0) {
|
||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||
for (int j = 0; j < min(numScaleNotes, 6); j++) {
|
||||
display.print(noteNames[scaleNotes[j]]);
|
||||
if (j < min(numScaleNotes, 6) - 1) display.print(F(" "));
|
||||
}
|
||||
}
|
||||
} else if (i == 2) { // Melody
|
||||
display.print(F(": "));
|
||||
display.print(melodySeed);
|
||||
} else if (i == 3) { // Flavour
|
||||
display.print(F(": "));
|
||||
display.print(strategies[currentStrategyIndex]->getName());
|
||||
} else if (i == 4) { // Tempo
|
||||
display.print(F(": "));
|
||||
display.print(tempo);
|
||||
} else if (i == 5) { // Mutation
|
||||
display.print(F(": "));
|
||||
display.print(mutationEnabled ? F("ON") : F("OFF"));
|
||||
} else if (i == 6) { // Song Mode
|
||||
display.print(F(": "));
|
||||
display.print(songModeEnabled ? F("ON") : F("OFF"));
|
||||
}
|
||||
}
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
||||
void drawTracker() {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
// Header
|
||||
display.print(F("SEQ "));
|
||||
if (navigationSelection > 0 && isEditing) {
|
||||
display.print(F("[EDIT]"));
|
||||
} else {
|
||||
display.print(F("[NAV] "));
|
||||
}
|
||||
display.print(F(" CH:"));
|
||||
display.print(midiChannel);
|
||||
|
||||
display.println();
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
// Steps
|
||||
int y = 10;
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int itemIndex = i + scrollOffset;
|
||||
if (itemIndex > NUM_STEPS) break;
|
||||
|
||||
// Draw Cursor
|
||||
if (itemIndex == navigationSelection) {
|
||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Invert text
|
||||
} else {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
|
||||
if (itemIndex == 0) {
|
||||
display.print(F(">> MENU"));
|
||||
} else {
|
||||
int stepIndex = itemIndex - 1;
|
||||
bool isPlayback = isPlaying && (stepIndex == playbackStep);
|
||||
|
||||
if (isPlayback) {
|
||||
if (itemIndex == navigationSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
|
||||
else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
}
|
||||
|
||||
// Step Number
|
||||
if (stepIndex < 10) display.print(F("0"));
|
||||
display.print(stepIndex);
|
||||
|
||||
if (isPlayback) {
|
||||
if (itemIndex == navigationSelection) display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
else display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
|
||||
display.print(F(" | "));
|
||||
|
||||
// Note Value
|
||||
int n = sequence[stepIndex].note;
|
||||
if (n == -1) {
|
||||
display.print(F("---"));
|
||||
} else {
|
||||
// Basic Note to String conversion
|
||||
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); // Octave
|
||||
}
|
||||
}
|
||||
|
||||
y += 9;
|
||||
}
|
||||
mutex_exit(&midiMutex);
|
||||
}
|
||||
|
||||
void drawUI() {
|
||||
midi.lock();
|
||||
ui.draw(currentState, menuSelection, navigationSelection, isEditing, midiChannel, tempo, strategies[currentStrategyIndex], queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, sequence, scrollOffset, playbackStep, isPlaying, mainMenu, mainMenuCount, randomizeMenu, randomizeMenuCount, setupMenu, setupMenuCount, THEME_1_INDEX);
|
||||
midi.unlock();
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
switch(currentState) {
|
||||
case UI_TRACKER:
|
||||
drawTracker();
|
||||
break;
|
||||
case UI_MENU_MAIN:
|
||||
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection);
|
||||
break;
|
||||
case UI_MENU_RANDOMIZE:
|
||||
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection);
|
||||
break;
|
||||
case UI_MENU_SETUP:
|
||||
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection);
|
||||
break;
|
||||
case UI_SETUP_CHANNEL_EDIT:
|
||||
display.println(F("SET MIDI CHANNEL"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(F("CH: "));
|
||||
if (midiChannel < 10) display.print(F(" "));
|
||||
display.print(midiChannel);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 50);
|
||||
display.println(F(" (Press to confirm)"));
|
||||
break;
|
||||
case UI_EDIT_TEMPO:
|
||||
display.println(F("SET TEMPO"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(F("BPM: "));
|
||||
display.print(tempo);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 50);
|
||||
display.println(F(" (Press to confirm)"));
|
||||
break;
|
||||
case UI_EDIT_FLAVOUR:
|
||||
display.println(F("SET FLAVOUR"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(strategies[currentStrategyIndex]->getName());
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 50);
|
||||
display.println(F(" (Press to confirm)"));
|
||||
break;
|
||||
}
|
||||
|
||||
display.display();
|
||||
}
|
||||
|
||||
// Helper to convert X,Y to pixel index for an 8x8 matrix.
|
||||
// Assumes row-major wiring (NOT serpentine).
|
||||
// If your matrix is wired differently, you'll need to change this function.
|
||||
int getPixelIndex(int x, int y) {
|
||||
return y * 8 + x;
|
||||
}
|
||||
|
||||
uint32_t getNoteColor(int note, bool dim) {
|
||||
if (note == -1) return 0;
|
||||
// Map note to hue, avoiding Green (approx 21845) which is used for playback cursor.
|
||||
// We start from Cyan (~30000) and go up to Orange (~8000 wrapped).
|
||||
// Range: 30000 to 65536+8000 = 73536. Width = 43536.
|
||||
// Step per semitone: 43536 / 12 = 3628.
|
||||
// This ensures notes are distinct colors but never pure Green.
|
||||
uint16_t hue = 30000 + (note % 12) * 3628;
|
||||
return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
|
||||
}
|
||||
|
||||
void updateLeds() {
|
||||
midi.lock();
|
||||
ui.updateLeds(sequence, navigationSelection, playbackStep, isPlaying, currentState, isEditing, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled);
|
||||
midi.unlock();
|
||||
pixels.clear(); // Clear buffer
|
||||
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
for (int s = 0; s < NUM_STEPS; s++) {
|
||||
int x = s % 8;
|
||||
int yBase = (s / 8) * 4;
|
||||
|
||||
uint32_t color = 0;
|
||||
uint32_t dimColor = 0;
|
||||
if (sequence[s].note != -1) {
|
||||
color = getNoteColor(sequence[s].note, sequence[s].tie);
|
||||
dimColor = getNoteColor(sequence[s].note, true);
|
||||
}
|
||||
|
||||
uint32_t colorP0 = 0;
|
||||
uint32_t colorP1 = 0;
|
||||
uint32_t colorP2 = 0;
|
||||
uint32_t colorP3 = 0;
|
||||
|
||||
if (sequence[s].note != -1) {
|
||||
int octave = sequence[s].note / 12;
|
||||
|
||||
if (octave > 4) {
|
||||
// Octave Up -> Top Row (P0)
|
||||
colorP0 = color;
|
||||
if (sequence[s].accent) colorP1 = dimColor;
|
||||
} else if (octave < 4) {
|
||||
// Octave Down -> Bottom Row (P2)
|
||||
colorP2 = color;
|
||||
if (sequence[s].accent) colorP1 = dimColor;
|
||||
} else {
|
||||
// Normal -> Middle Row (P1)
|
||||
colorP1 = color;
|
||||
if (sequence[s].accent) {
|
||||
colorP0 = dimColor;
|
||||
colorP2 = dimColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int stepNavIndex = navigationSelection - 1;
|
||||
uint32_t cursorColor = pixels.Color(0 + (s%2) * 20, 0 + (s%2) * 20, 50 + (s%2) * 20); // Blue for paused
|
||||
if (isPlaying) {
|
||||
cursorColor = pixels.Color(0, 50, 0); // Green for playback
|
||||
if (songModeEnabled) {
|
||||
// Song Mode: Show repeats on bottom row
|
||||
// Right aligned.
|
||||
// If repeats = 1 (last one), blink yellow.
|
||||
if (s >= NUM_STEPS/2) { // second half = bottom row
|
||||
int col = x;
|
||||
int repeatsToDraw = min(songRepeatsRemaining, 8);
|
||||
if (col >= (8 - repeatsToDraw)) {
|
||||
if (songRepeatsRemaining == 1 && col == 7 && (millis() / 250) % 2) {
|
||||
cursorColor = pixels.Color(0, 250, 0); // Max green, change is about to happen
|
||||
} else {
|
||||
cursorColor = pixels.Color(80, 100, 0); // Yellow-green, remaining repeat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (currentState == UI_TRACKER) {
|
||||
cursorColor = isEditing ? pixels.Color(50, 0, 0) : pixels.Color(40, 40, 40);
|
||||
}
|
||||
|
||||
if (cursorColor != 0) {
|
||||
bool isCursorHere = (isPlaying && s == playbackStep) ||
|
||||
(!isPlaying && currentState == UI_TRACKER && s == stepNavIndex);
|
||||
if (isCursorHere) {
|
||||
colorP3 = cursorColor;
|
||||
} else {
|
||||
// Lightly colored background for cursor row
|
||||
uint8_t r = (uint8_t)(cursorColor >> 16);
|
||||
uint8_t g = (uint8_t)(cursorColor >> 8);
|
||||
uint8_t b = (uint8_t)cursorColor;
|
||||
colorP3 = pixels.Color(r/5, g/5, b/5);
|
||||
}
|
||||
}
|
||||
|
||||
pixels.setPixelColor(getPixelIndex(x, yBase), colorP0);
|
||||
pixels.setPixelColor(getPixelIndex(x, yBase + 1), colorP1);
|
||||
pixels.setPixelColor(getPixelIndex(x, yBase + 2), colorP2);
|
||||
pixels.setPixelColor(getPixelIndex(x, yBase + 3), colorP3);
|
||||
}
|
||||
|
||||
if (sequenceChangeScheduled && (millis() / 125) % 2) {
|
||||
pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
|
||||
}
|
||||
mutex_exit(&midiMutex);
|
||||
|
||||
pixels.show();
|
||||
}
|
||||
|
||||
void loop1() {
|
||||
if (needsPanic) {
|
||||
midi.lock();
|
||||
midi.panic(shMidiChannel);
|
||||
midi.unlock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
sendMidi(0xB0, 123, 0);
|
||||
mutex_exit(&midiMutex);
|
||||
needsPanic = false;
|
||||
}
|
||||
handlePlayback();
|
||||
@ -531,12 +917,12 @@ void loop() {
|
||||
int nextTheme = random(1, 8); // Themes 1-7
|
||||
int repeats = random(1, 9); // 1-8 repeats
|
||||
|
||||
midi.lock();
|
||||
mutex_enter_blocking(&midiMutex);
|
||||
generateSequenceData(nextTheme, nextSequence);
|
||||
queuedTheme = nextTheme;
|
||||
nextSongRepeats = repeats;
|
||||
sequenceChangeScheduled = true;
|
||||
midi.unlock();
|
||||
mutex_exit(&midiMutex);
|
||||
|
||||
songModeNeedsNext = false;
|
||||
}
|
||||
|
||||
@ -9,26 +9,4 @@ struct Step {
|
||||
bool tie;
|
||||
};
|
||||
|
||||
enum UIState {
|
||||
UI_TRACKER,
|
||||
UI_MENU_MAIN,
|
||||
UI_MENU_RANDOMIZE,
|
||||
UI_MENU_SETUP,
|
||||
UI_SETUP_CHANNEL_EDIT,
|
||||
UI_EDIT_TEMPO,
|
||||
UI_EDIT_FLAVOUR
|
||||
};
|
||||
|
||||
inline void sortArray(int arr[], int size) {
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
for (int j = 0; j < size - i - 1; j++) {
|
||||
if (arr[j] > arr[j + 1]) {
|
||||
int temp = arr[j];
|
||||
arr[j] = arr[j + 1];
|
||||
arr[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
275
UIManager.cpp
275
UIManager.cpp
@ -1,275 +0,0 @@
|
||||
#include "UIManager.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;
|
||||
|
||||
UIManager::UIManager()
|
||||
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET),
|
||||
pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800)
|
||||
{
|
||||
}
|
||||
|
||||
void UIManager::begin() {
|
||||
// Setup Display
|
||||
Wire.begin();
|
||||
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
||||
Serial.println(F("SSD1306 allocation failed"));
|
||||
for(;;);
|
||||
}
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
|
||||
// Setup NeoPixel Matrix
|
||||
pixels.begin();
|
||||
pixels.setBrightness(40);
|
||||
pixels.clear();
|
||||
pixels.show();
|
||||
}
|
||||
|
||||
void UIManager::showMessage(const char* msg) {
|
||||
display.clearDisplay();
|
||||
display.setCursor(10, 25);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setTextSize(2);
|
||||
display.print(msg);
|
||||
display.display();
|
||||
delay(500);
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
void UIManager::draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
|
||||
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 char* mainMenu[], int mainMenuCount,
|
||||
const char* randomizeMenu[], int randomizeMenuCount,
|
||||
const char* setupMenu[], int setupMenuCount,
|
||||
int theme1Index) {
|
||||
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
switch(currentState) {
|
||||
case UI_TRACKER:
|
||||
drawTracker(navSelection, isEditing, midiChannel, sequence, scrollOffset, playbackStep, isPlaying);
|
||||
break;
|
||||
case UI_MENU_MAIN:
|
||||
drawMenu("MAIN MENU", mainMenu, mainMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
|
||||
break;
|
||||
case UI_MENU_RANDOMIZE:
|
||||
drawMenu("RANDOMIZE", randomizeMenu, randomizeMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
|
||||
break;
|
||||
case UI_MENU_SETUP:
|
||||
drawMenu("SETUP", setupMenu, setupMenuCount, menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, mutationEnabled, songModeEnabled, theme1Index);
|
||||
break;
|
||||
case UI_SETUP_CHANNEL_EDIT:
|
||||
display.println(F("SET MIDI CHANNEL"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(F("CH: "));
|
||||
if (midiChannel < 10) display.print(F(" "));
|
||||
display.print(midiChannel);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 50);
|
||||
display.println(F(" (Press to confirm)"));
|
||||
break;
|
||||
case UI_EDIT_TEMPO:
|
||||
display.println(F("SET TEMPO"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(F("BPM: "));
|
||||
display.print(tempo);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 50);
|
||||
display.println(F(" (Press to confirm)"));
|
||||
break;
|
||||
case UI_EDIT_FLAVOUR:
|
||||
display.println(F("SET FLAVOUR"));
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
display.setCursor(20, 25);
|
||||
display.setTextSize(2);
|
||||
display.print(currentStrategy->getName());
|
||||
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) {
|
||||
display.println(title);
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
int start = 0;
|
||||
if (selection >= 5) start = selection - 4;
|
||||
|
||||
int y = 10;
|
||||
for (int i = start; i < count; i++) {
|
||||
if (y > 55) break;
|
||||
|
||||
if (i == selection) {
|
||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
} else {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
display.print(items[i]);
|
||||
|
||||
if (currentState == UI_MENU_SETUP && i == 1) {
|
||||
display.print(F(": ")); display.print(midiChannel);
|
||||
}
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && queuedTheme == (i - theme1Index + 1)) {
|
||||
display.print(F(" [NEXT]"));
|
||||
}
|
||||
if (currentState == UI_MENU_RANDOMIZE && i >= theme1Index && currentThemeIndex == (i - theme1Index + 1)) {
|
||||
display.print(F(" *"));
|
||||
}
|
||||
if (currentState == UI_MENU_RANDOMIZE) {
|
||||
if (i == 1) { // Melody
|
||||
display.print(F(": ")); display.print(melodySeed);
|
||||
} else if (i == 2) { // Flavour
|
||||
display.print(F(": ")); display.print(flavourName);
|
||||
} else if (i == 3) { // Scale
|
||||
display.print(F(": "));
|
||||
if (numScaleNotes > 0) {
|
||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||
for (int j = 0; j < min(numScaleNotes, 6); j++) {
|
||||
display.print(noteNames[scaleNotes[j]]);
|
||||
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")); }
|
||||
}
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
||||
void UIManager::drawTracker(int navSelection, bool isEditing, int midiChannel,
|
||||
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying) {
|
||||
display.print(F("SEQ "));
|
||||
if (navSelection > 0 && isEditing) display.print(F("[EDIT]"));
|
||||
else display.print(F("[NAV] "));
|
||||
display.print(F(" CH:")); display.print(midiChannel);
|
||||
display.println();
|
||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||
|
||||
int y = 10;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int itemIndex = i + scrollOffset;
|
||||
if (itemIndex > NUM_STEPS) break;
|
||||
|
||||
if (itemIndex == navSelection) {
|
||||
display.fillRect(0, y, 128, 8, SSD1306_WHITE);
|
||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
} else {
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
|
||||
if (itemIndex == 0) {
|
||||
display.print(F(">> MENU"));
|
||||
} else {
|
||||
int stepIndex = itemIndex - 1;
|
||||
bool isPlayback = isPlaying && (stepIndex == playbackStep);
|
||||
if (isPlayback) {
|
||||
if (itemIndex == navSelection) display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
|
||||
else display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
||||
}
|
||||
if (stepIndex < 10) display.print(F("0"));
|
||||
display.print(stepIndex);
|
||||
if (isPlayback) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t UIManager::getNoteColor(int note, bool dim) {
|
||||
if (note == -1) return 0;
|
||||
uint16_t hue = 30000 + (note % 12) * 3628;
|
||||
return Adafruit_NeoPixel::ColorHSV(hue, 255, dim ? 10 : 50);
|
||||
}
|
||||
|
||||
int UIManager::getPixelIndex(int x, int y) {
|
||||
return y * 8 + x;
|
||||
}
|
||||
|
||||
void UIManager::updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
|
||||
UIState currentState, bool isEditing, bool songModeEnabled,
|
||||
int songRepeatsRemaining, bool sequenceChangeScheduled) {
|
||||
pixels.clear();
|
||||
for (int s = 0; s < NUM_STEPS; s++) {
|
||||
int x = s % 8;
|
||||
int yBase = (s / 8) * 4;
|
||||
uint32_t color = 0, dimColor = 0;
|
||||
if (sequence[s].note != -1) {
|
||||
color = getNoteColor(sequence[s].note, sequence[s].tie);
|
||||
dimColor = getNoteColor(sequence[s].note, true);
|
||||
}
|
||||
uint32_t c[4] = {0};
|
||||
if (sequence[s].note != -1) {
|
||||
int octave = sequence[s].note / 12;
|
||||
if (octave > 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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
51
UIManager.h
51
UIManager.h
@ -1,51 +0,0 @@
|
||||
#ifndef UI_MANAGER_H
|
||||
#define UI_MANAGER_H
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include "TrackerTypes.h"
|
||||
#include "MelodyStrategy.h"
|
||||
|
||||
class UIManager {
|
||||
public:
|
||||
UIManager();
|
||||
void begin();
|
||||
|
||||
void showMessage(const char* msg);
|
||||
|
||||
void draw(UIState currentState, int menuSelection, int navSelection, bool isEditing,
|
||||
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 char* mainMenu[], int mainMenuCount,
|
||||
const char* randomizeMenu[], int randomizeMenuCount,
|
||||
const char* setupMenu[], int setupMenuCount,
|
||||
int theme1Index);
|
||||
|
||||
void updateLeds(const Step* sequence, int navSelection, int playbackStep, bool isPlaying,
|
||||
UIState currentState, bool isEditing, bool songModeEnabled,
|
||||
int songRepeatsRemaining, bool sequenceChangeScheduled);
|
||||
|
||||
private:
|
||||
Adafruit_SSD1306 display;
|
||||
Adafruit_NeoPixel pixels;
|
||||
|
||||
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);
|
||||
|
||||
void drawTracker(int navSelection, bool isEditing, int midiChannel,
|
||||
const Step* sequence, int scrollOffset, int playbackStep, bool isPlaying);
|
||||
|
||||
uint32_t getNoteColor(int note, bool dim);
|
||||
int getPixelIndex(int x, int y);
|
||||
};
|
||||
|
||||
extern UIManager ui;
|
||||
|
||||
#endif
|
||||
Loading…
Reference in New Issue
Block a user