Compare commits
5 Commits
84ef40a555
...
bbe6c4ff2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbe6c4ff2f | ||
|
|
7da78f5fb9 | ||
|
|
f979c2c5d7 | ||
|
|
71583bdb4e | ||
|
|
b6150cbacd |
91
CallAndResponseStrategy.h
Normal file
91
CallAndResponseStrategy.h
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#include "MelodyStrategy.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef CALL_AND_RESPONSE_STRATEGY_H
|
||||||
|
#define CALL_AND_RESPONSE_STRATEGY_H
|
||||||
|
|
||||||
|
class CallAndResponseStrategy : public MelodyStrategy {
|
||||||
|
public:
|
||||||
|
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed, int intensity) override {
|
||||||
|
randomSeed(seed);
|
||||||
|
if (numScaleNotes == 0) return;
|
||||||
|
|
||||||
|
int halfSteps = numSteps / 2;
|
||||||
|
if (halfSteps < 1) halfSteps = 1;
|
||||||
|
|
||||||
|
// Generate Call (First Half)
|
||||||
|
for (int i = 0; i < halfSteps; i++) {
|
||||||
|
// Simple random generation for the call, weighted by intensity
|
||||||
|
if (random(100) < (intensity * 8 + 20)) {
|
||||||
|
int octave = 3 + random(3);
|
||||||
|
sequence[track][i].note = 12 * octave + scaleNotes[random(numScaleNotes)];
|
||||||
|
sequence[track][i].accent = (random(100) < 30);
|
||||||
|
sequence[track][i].tie = (random(100) < 10);
|
||||||
|
} else {
|
||||||
|
sequence[track][i].note = -1;
|
||||||
|
sequence[track][i].accent = false;
|
||||||
|
sequence[track][i].tie = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Response (Second Half)
|
||||||
|
for (int i = halfSteps; i < numSteps; i++) {
|
||||||
|
int srcIndex = i - halfSteps;
|
||||||
|
Step srcStep = sequence[track][srcIndex];
|
||||||
|
|
||||||
|
// Default: Copy
|
||||||
|
sequence[track][i] = srcStep;
|
||||||
|
|
||||||
|
// Variation based on intensity
|
||||||
|
if (srcStep.note != -1) {
|
||||||
|
int r = random(100);
|
||||||
|
if (r < intensity * 5) {
|
||||||
|
// Transpose / Shift
|
||||||
|
int shift = random(-2, 3);
|
||||||
|
int octave = srcStep.note / 12;
|
||||||
|
int noteVal = srcStep.note % 12;
|
||||||
|
|
||||||
|
// Find index in scale
|
||||||
|
int idx = 0;
|
||||||
|
for(int k=0; k<numScaleNotes; k++) if(scaleNotes[k] == noteVal) idx = k;
|
||||||
|
|
||||||
|
idx = (idx + shift + numScaleNotes) % numScaleNotes;
|
||||||
|
sequence[track][i].note = 12 * octave + scaleNotes[idx];
|
||||||
|
} else if (r < intensity * 8) {
|
||||||
|
// New random note (Variation)
|
||||||
|
int octave = 3 + random(3);
|
||||||
|
sequence[track][i].note = 12 * octave + scaleNotes[random(numScaleNotes)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
randomSeed(micros());
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateScale(int* scaleNotes, int& numScaleNotes) override {
|
||||||
|
numScaleNotes = random(5, 8);
|
||||||
|
for (int i = 0; i < 12; i++) scaleNotes[i] = i;
|
||||||
|
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)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int intensity) override {
|
||||||
|
// Swap call and response halves
|
||||||
|
int half = numSteps / 2;
|
||||||
|
for(int i=0; i<half; i++) {
|
||||||
|
Step temp = sequence[track][i];
|
||||||
|
sequence[track][i] = sequence[track][i+half];
|
||||||
|
sequence[track][i+half] = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* getName() override {
|
||||||
|
return "CallResp";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
102
IsorhythmStrategy.h
Normal file
102
IsorhythmStrategy.h
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#ifndef ISORHYTHM_STRATEGY_H
|
||||||
|
#define ISORHYTHM_STRATEGY_H
|
||||||
|
|
||||||
|
#include "MelodyStrategy.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
class IsorhythmStrategy : public MelodyStrategy {
|
||||||
|
public:
|
||||||
|
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed, int intensity) override {
|
||||||
|
randomSeed(seed);
|
||||||
|
if (numScaleNotes == 0) return;
|
||||||
|
|
||||||
|
// 1. Create the Color (pitch pattern)
|
||||||
|
// Intensity affects color length. Higher intensity = longer, more complex color.
|
||||||
|
int colorLength = 2 + random(intensity + 2); // 2 to intensity+3 notes
|
||||||
|
if (colorLength > numScaleNotes) colorLength = numScaleNotes;
|
||||||
|
if (colorLength > 8) colorLength = 8; // Keep it reasonable
|
||||||
|
|
||||||
|
int color[8];
|
||||||
|
// Pick unique notes from the scale for the color
|
||||||
|
int scaleIndices[12];
|
||||||
|
for(int i=0; i<numScaleNotes; i++) scaleIndices[i] = i;
|
||||||
|
for(int i=0; i<numScaleNotes; i++) { // shuffle
|
||||||
|
int r = random(numScaleNotes);
|
||||||
|
int temp = scaleIndices[i];
|
||||||
|
scaleIndices[i] = scaleIndices[r];
|
||||||
|
scaleIndices[r] = temp;
|
||||||
|
}
|
||||||
|
for(int i=0; i<colorLength; i++) {
|
||||||
|
color[i] = scaleIndices[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create the Talea (rhythmic pattern)
|
||||||
|
// Intensity affects talea length and density.
|
||||||
|
int taleaLength = 4 + random(numSteps / 2); // 4 to 12 steps for a 16-step sequence
|
||||||
|
if (taleaLength > numSteps) taleaLength = numSteps;
|
||||||
|
|
||||||
|
bool talea[NUM_STEPS];
|
||||||
|
int pulses = 2 + random(taleaLength - 2); // At least 2 pulses
|
||||||
|
|
||||||
|
// Euclidean distribution for a nice rhythm
|
||||||
|
int bucket = 0;
|
||||||
|
for(int i=0; i<taleaLength; i++) {
|
||||||
|
bucket += pulses;
|
||||||
|
if (bucket >= taleaLength) {
|
||||||
|
bucket -= taleaLength;
|
||||||
|
talea[i] = true;
|
||||||
|
} else {
|
||||||
|
talea[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Apply Color and Talea to the sequence
|
||||||
|
int colorIdx = 0;
|
||||||
|
for (int i = 0; i < numSteps; i++) {
|
||||||
|
int taleaIdx = i % taleaLength;
|
||||||
|
if (talea[taleaIdx]) {
|
||||||
|
int octave = 3 + random(3);
|
||||||
|
int noteScaleIndex = color[colorIdx % colorLength];
|
||||||
|
sequence[track][i].note = 12 * octave + scaleNotes[noteScaleIndex];
|
||||||
|
sequence[track][i].accent = (i % 4 == 0); // Accent on downbeats
|
||||||
|
sequence[track][i].tie = false;
|
||||||
|
colorIdx++;
|
||||||
|
} else {
|
||||||
|
sequence[track][i].note = -1;
|
||||||
|
sequence[track][i].accent = false;
|
||||||
|
sequence[track][i].tie = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
randomSeed(micros());
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateScale(int* scaleNotes, int& numScaleNotes) override {
|
||||||
|
numScaleNotes = random(5, 8);
|
||||||
|
for (int i = 0; i < 12; i++) scaleNotes[i] = i;
|
||||||
|
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)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int intensity) override {
|
||||||
|
// Mutation: rotate the talea (rhythmic displacement)
|
||||||
|
int rotation = random(1, numSteps);
|
||||||
|
Step temp[NUM_STEPS];
|
||||||
|
for(int i=0; i<numSteps; i++) {
|
||||||
|
temp[i] = sequence[track][(i + rotation) % numSteps];
|
||||||
|
}
|
||||||
|
for(int i=0; i<numSteps; i++) {
|
||||||
|
sequence[track][i] = temp[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* getName() override {
|
||||||
|
return "Isorhythm";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
168
LedMatrix.cpp
Normal file
168
LedMatrix.cpp
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#include "LedMatrix.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// --- HARDWARE CONFIGURATION ---
|
||||||
|
#define PIN_NEOPIXEL 6
|
||||||
|
#define NEOPIXELS_X 16
|
||||||
|
#define NEOPIXELS_Y 8
|
||||||
|
#define NUM_PIXELS 64*2 // 8x8 LEDs in 2 panels
|
||||||
|
|
||||||
|
LedMatrix::LedMatrix()
|
||||||
|
: pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedMatrix::begin() {
|
||||||
|
pixels.setPin(PIN_NEOPIXEL);
|
||||||
|
pixels.begin();
|
||||||
|
pixels.setBrightness(40);
|
||||||
|
pixels.clear();
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedMatrix::setBrightness(uint8_t b) {
|
||||||
|
pixels.setBrightness(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedMatrix::clear() {
|
||||||
|
pixels.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedMatrix::show() {
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t LedMatrix::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 LedMatrix::getPixelIndex(int x, int y) {
|
||||||
|
return NEOPIXELS_Y * x + (NEOPIXELS_Y - 1 - y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LedMatrix::update(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
||||||
|
UIState currentState, bool songModeEnabled,
|
||||||
|
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
|
||||||
|
int selectedTrack, const int* numSteps, const bool* trackMute) {
|
||||||
|
pixels.clear();
|
||||||
|
|
||||||
|
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, 100, 100);
|
||||||
|
const uint32_t COLOR_CURSOR_DIM = pixels.Color(32, 0, 0);
|
||||||
|
|
||||||
|
if(playMode == MODE_POLY) {
|
||||||
|
for(int t=0; t<NUM_TRACKS; t++) {
|
||||||
|
int currentTrackSteps = numSteps[t];
|
||||||
|
for(int s=0; s<NUM_STEPS; s++) {
|
||||||
|
int col = s; // Each step is a column
|
||||||
|
|
||||||
|
// --- First row of track pair: Notes ---
|
||||||
|
int note_row = t * 2;
|
||||||
|
uint32_t note_color = 0;
|
||||||
|
if (s < currentTrackSteps) {
|
||||||
|
int note = sequence[t][s].note;
|
||||||
|
if (note != -1) {
|
||||||
|
note_color = getNoteColor(note, !sequence[t][s].accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels.setPixelColor(getPixelIndex(col, note_row), note_color);
|
||||||
|
|
||||||
|
// --- Second row of track pair: Steps & Playhead ---
|
||||||
|
int step_row = t * 2 + 1;
|
||||||
|
uint32_t step_color = 0; // Off by default for steps > currentTrackSteps
|
||||||
|
if (s < currentTrackSteps) {
|
||||||
|
step_color = COLOR_PLAYHEAD_DIM; // It's a valid step
|
||||||
|
if (isPlaying && (s == (playbackStep % currentTrackSteps))) {
|
||||||
|
if (trackMute[t]) {
|
||||||
|
step_color = COLOR_MUTED_PLAYHEAD;
|
||||||
|
} else {
|
||||||
|
step_color = COLOR_PLAYHEAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels.setPixelColor(getPixelIndex(col, step_row), step_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// --- Mono Mode (original) ---
|
||||||
|
const Step* trackSequence = sequence[selectedTrack];
|
||||||
|
for (int s = 0; s < NUM_STEPS; s++) {
|
||||||
|
if (s >= numSteps[selectedTrack]) continue;
|
||||||
|
|
||||||
|
int x = s % NUM_STEPS;
|
||||||
|
int yBase = (s / NUM_STEPS) * 2;
|
||||||
|
uint32_t color = 0, dimColor = 0;
|
||||||
|
bool isCursorHere = (isPlaying && s == (playbackStep % numSteps[selectedTrack]));
|
||||||
|
if (trackSequence[s].note != -1) {
|
||||||
|
color = getNoteColor(trackSequence[s].note, trackSequence[s].tie);
|
||||||
|
dimColor = getNoteColor(trackSequence[s].note, true);
|
||||||
|
}
|
||||||
|
uint32_t c[4] = {0};
|
||||||
|
if (trackSequence[s].note != -1) {
|
||||||
|
int octave = trackSequence[s].note / 12;
|
||||||
|
if (octave > 4) { c[0] = color; if (trackSequence[s].accent) c[1] = dimColor; }
|
||||||
|
else if (octave < 4) { c[2] = color; if (trackSequence[s].accent) c[1] = dimColor; }
|
||||||
|
else { c[1] = color; if (trackSequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } }
|
||||||
|
}
|
||||||
|
uint32_t cursorColor = pixels.Color(0, 0, 50);
|
||||||
|
if (isPlaying) {
|
||||||
|
cursorColor = pixels.Color(0, 50, 0);
|
||||||
|
if (songModeEnabled && s >= NUM_STEPS) {
|
||||||
|
int repeats = min(songRepeatsRemaining, NUM_STEPS);
|
||||||
|
if (x >= (NUM_STEPS - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorColor != 0) {
|
||||||
|
if (isCursorHere) {
|
||||||
|
for(int i=0; i<4; i++) {
|
||||||
|
if (c[i] == 0) c[i] = 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Overview of all tracks on bottom 4 rows ---
|
||||||
|
for(int t=0; t<NUM_TRACKS; t++) {
|
||||||
|
int row = 4 + t;
|
||||||
|
int currentTrackSteps = numSteps[t];
|
||||||
|
for(int s=0; s<NUM_STEPS; s++) {
|
||||||
|
if (s >= currentTrackSteps) continue;
|
||||||
|
|
||||||
|
uint32_t pixelColor = 0;
|
||||||
|
|
||||||
|
// Background for active track
|
||||||
|
if (t == selectedTrack) {
|
||||||
|
pixelColor = COLOR_CURSOR_DIM;
|
||||||
|
}
|
||||||
|
else if (sequence[t][s].note != -1) // Note
|
||||||
|
{
|
||||||
|
pixelColor = getNoteColor(sequence[t][s].note, !sequence[t][s].accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playhead
|
||||||
|
if (isPlaying && (s == (playbackStep % currentTrackSteps))) {
|
||||||
|
if (t == selectedTrack) {
|
||||||
|
pixelColor = COLOR_CURSOR;
|
||||||
|
} else if (trackMute[t]) {
|
||||||
|
pixelColor = COLOR_MUTED_PLAYHEAD;
|
||||||
|
} else {
|
||||||
|
pixelColor = COLOR_PLAYHEAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels.setPixelColor(getPixelIndex(s, row), pixelColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
28
LedMatrix.h
Normal file
28
LedMatrix.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef LED_MATRIX_H
|
||||||
|
#define LED_MATRIX_H
|
||||||
|
|
||||||
|
#include <Adafruit_NeoPixel.h>
|
||||||
|
#include "TrackerTypes.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class LedMatrix {
|
||||||
|
public:
|
||||||
|
LedMatrix();
|
||||||
|
void begin();
|
||||||
|
void update(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
||||||
|
UIState currentState, bool songModeEnabled,
|
||||||
|
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
|
||||||
|
int selectedTrack, const int* numSteps, const bool* trackMute);
|
||||||
|
|
||||||
|
void setBrightness(uint8_t b);
|
||||||
|
void clear();
|
||||||
|
void show();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Adafruit_NeoPixel pixels;
|
||||||
|
|
||||||
|
uint32_t getNoteColor(int note, bool dim);
|
||||||
|
int getPixelIndex(int x, int y);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
195
Persistence.cpp
Normal file
195
Persistence.cpp
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
#include "Persistence.h"
|
||||||
|
#include "SharedState.h"
|
||||||
|
#include "MidiDriver.h"
|
||||||
|
#include "UIManager.h"
|
||||||
|
#include "SequenceGenerator.h"
|
||||||
|
#include <EEPROM.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void Persistence::saveSequence(bool quiet) {
|
||||||
|
midi.lock();
|
||||||
|
int addr = 0;
|
||||||
|
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
|
||||||
|
int channels[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) channels[i] = midiChannels[i];
|
||||||
|
EEPROM.put(addr, channels); addr += sizeof(channels);
|
||||||
|
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
|
||||||
|
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
||||||
|
bool mutes[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
|
||||||
|
EEPROM.put(addr, mutes); addr += sizeof(mutes);
|
||||||
|
EEPROM.put(addr, (int)tempo); addr += sizeof(int);
|
||||||
|
int intensities[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) intensities[i] = (int)trackIntensity[i];
|
||||||
|
EEPROM.put(addr, intensities); addr += sizeof(intensities);
|
||||||
|
int steps[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) steps[i] = numSteps[i];
|
||||||
|
EEPROM.put(addr, steps); addr += sizeof(steps);
|
||||||
|
|
||||||
|
EEPROM.put(addr, currentRoot); addr += sizeof(currentRoot);
|
||||||
|
EEPROM.put(addr, currentScaleType); addr += sizeof(currentScaleType);
|
||||||
|
EEPROM.put(addr, enabledScaleTypes); addr += sizeof(enabledScaleTypes);
|
||||||
|
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
for (int i = 0; i<12; i++) {
|
||||||
|
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
}
|
||||||
|
|
||||||
|
EEPROM.put(addr, sequence); addr += sizeof(sequence);
|
||||||
|
midi.unlock();
|
||||||
|
EEPROM.commit();
|
||||||
|
if (!quiet) ui.showMessage("SAVED!");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Persistence::loadSequence() {
|
||||||
|
midi.lock();
|
||||||
|
int addr = 0;
|
||||||
|
uint32_t magic;
|
||||||
|
EEPROM.get(addr, magic); addr += sizeof(magic);
|
||||||
|
if (magic != EEPROM_MAGIC) {
|
||||||
|
midi.unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int channels[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, channels); addr += sizeof(channels);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
midiChannels[i] = channels[i];
|
||||||
|
if (midiChannels[i] < 1) midiChannels[i] = 1;
|
||||||
|
if (midiChannels[i] > 16) midiChannels[i] = 16;
|
||||||
|
}
|
||||||
|
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
|
||||||
|
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
if (currentStrategyIndices[i] < 0 || currentStrategyIndices[i] >= numStrategies) currentStrategyIndices[i] = 0;
|
||||||
|
}
|
||||||
|
bool mutes[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, mutes); addr += sizeof(mutes);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
|
||||||
|
int t;
|
||||||
|
EEPROM.get(addr, t); addr += sizeof(int);
|
||||||
|
tempo = t;
|
||||||
|
if (tempo < 40) tempo = 40;
|
||||||
|
if (tempo > 240) tempo = 240;
|
||||||
|
int intensities[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, intensities); addr += sizeof(intensities);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
trackIntensity[i] = intensities[i];
|
||||||
|
if (trackIntensity[i] < 1) trackIntensity[i] = 1;
|
||||||
|
if (trackIntensity[i] > 10) trackIntensity[i] = 10;
|
||||||
|
}
|
||||||
|
int steps[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, steps); addr += sizeof(steps);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
numSteps[i] = steps[i];
|
||||||
|
if (numSteps[i] <= 0 || numSteps[i] > NUM_STEPS) {
|
||||||
|
numSteps[i] = NUM_STEPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EEPROM.get(addr, currentRoot); addr += sizeof(currentRoot);
|
||||||
|
EEPROM.get(addr, currentScaleType); addr += sizeof(currentScaleType);
|
||||||
|
EEPROM.get(addr, enabledScaleTypes); addr += sizeof(enabledScaleTypes);
|
||||||
|
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
if (numScaleNotes < 0 || numScaleNotes > 12) numScaleNotes = 0;
|
||||||
|
for (int i = 0; i<12; i++) {
|
||||||
|
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
if (scaleNotes[i] < 0) scaleNotes[i] = 0;
|
||||||
|
if (scaleNotes[i] > 11) scaleNotes[i] = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
EEPROM.get(addr, sequence); addr += sizeof(sequence);
|
||||||
|
midi.unlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Persistence::savePatch(int bank, int slot) {
|
||||||
|
int patchIndex = bank * 4 + slot;
|
||||||
|
int addr = 512 + patchIndex * 256; // Start after main save, 256 bytes per patch
|
||||||
|
|
||||||
|
midi.lock();
|
||||||
|
EEPROM.put(addr, currentRoot); addr += sizeof(currentRoot);
|
||||||
|
EEPROM.put(addr, currentScaleType); addr += sizeof(currentScaleType);
|
||||||
|
EEPROM.put(addr, enabledScaleTypes); addr += sizeof(enabledScaleTypes);
|
||||||
|
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
}
|
||||||
|
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
||||||
|
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
|
||||||
|
int steps[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) steps[i] = numSteps[i];
|
||||||
|
EEPROM.put(addr, steps); addr += sizeof(steps);
|
||||||
|
bool mutes[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
|
||||||
|
EEPROM.put(addr, mutes); addr += sizeof(mutes);
|
||||||
|
int intensities[NUM_TRACKS];
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) intensities[i] = trackIntensity[i];
|
||||||
|
EEPROM.put(addr, intensities); addr += sizeof(intensities);
|
||||||
|
midi.unlock();
|
||||||
|
EEPROM.commit();
|
||||||
|
ui.showMessage("SAVED!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Persistence::loadPatch(int bank, int slot, Step (*scratchBuffer)[NUM_STEPS]) {
|
||||||
|
int patchIndex = bank * 4 + slot;
|
||||||
|
int addr = 512 + patchIndex * 256;
|
||||||
|
|
||||||
|
midi.lock();
|
||||||
|
EEPROM.get(addr, currentRoot); addr += sizeof(currentRoot);
|
||||||
|
EEPROM.get(addr, currentScaleType); addr += sizeof(currentScaleType);
|
||||||
|
EEPROM.get(addr, enabledScaleTypes); addr += sizeof(enabledScaleTypes);
|
||||||
|
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
||||||
|
if (numScaleNotes < 0 || numScaleNotes > 12) numScaleNotes = 0;
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
||||||
|
if (scaleNotes[i] < 0) scaleNotes[i] = 0;
|
||||||
|
if (scaleNotes[i] > 11) scaleNotes[i] = 11;
|
||||||
|
}
|
||||||
|
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
if (currentStrategyIndices[i] < 0 || currentStrategyIndices[i] >= numStrategies) currentStrategyIndices[i] = 0;
|
||||||
|
}
|
||||||
|
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
|
||||||
|
int steps[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, steps); addr += sizeof(steps);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
numSteps[i] = steps[i];
|
||||||
|
if (numSteps[i] <= 0 || numSteps[i] > NUM_STEPS) {
|
||||||
|
numSteps[i] = NUM_STEPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mutes[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, mutes); addr += sizeof(mutes);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
|
||||||
|
|
||||||
|
int intensities[NUM_TRACKS];
|
||||||
|
EEPROM.get(addr, intensities); addr += sizeof(intensities);
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
trackIntensity[i] = intensities[i];
|
||||||
|
if (trackIntensity[i] < 1) trackIntensity[i] = 1;
|
||||||
|
if (trackIntensity[i] > 10) trackIntensity[i] = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
SequenceGenerator::generateSequenceData(currentThemeIndex, nextSequence);
|
||||||
|
sequenceChangeScheduled = true;
|
||||||
|
} else {
|
||||||
|
SequenceGenerator::generateSequenceData(currentThemeIndex, scratchBuffer);
|
||||||
|
memcpy(sequence, scratchBuffer, sizeof(sequence));
|
||||||
|
}
|
||||||
|
midi.unlock();
|
||||||
|
ui.showMessage("LOADED!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Persistence::factoryReset() {
|
||||||
|
ui.showMessage("RESETTING...");
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
trackIntensity[i] = 10;
|
||||||
|
}
|
||||||
|
uint32_t magic = 0;
|
||||||
|
EEPROM.put(0, magic);
|
||||||
|
EEPROM.commit();
|
||||||
|
delay(500);
|
||||||
|
rp2040.reboot();
|
||||||
|
}
|
||||||
16
Persistence.h
Normal file
16
Persistence.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#ifndef PERSISTENCE_H
|
||||||
|
#define PERSISTENCE_H
|
||||||
|
|
||||||
|
#include "TrackerTypes.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class Persistence {
|
||||||
|
public:
|
||||||
|
static void saveSequence(bool quiet);
|
||||||
|
static bool loadSequence();
|
||||||
|
static void savePatch(int bank, int slot);
|
||||||
|
static void loadPatch(int bank, int slot, Step (*scratchBuffer)[NUM_STEPS]);
|
||||||
|
static void factoryReset();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
94
SequenceGenerator.cpp
Normal file
94
SequenceGenerator.cpp
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#include "SequenceGenerator.h"
|
||||||
|
#include "SharedState.h"
|
||||||
|
#include "MidiDriver.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void SequenceGenerator::generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]) {
|
||||||
|
randomSeed(melodySeeds[track] + themeType * 12345);
|
||||||
|
strategies[currentStrategyIndices[track]]->generate(target, track, numSteps[track], scaleNotes, numScaleNotes, melodySeeds[track] + themeType * 12345, trackIntensity[track]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequenceGenerator::generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) {
|
||||||
|
Serial.println(F("Generating sequence."));
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
SequenceGenerator::generateTrackData(i, themeType, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequenceGenerator::mutateSequence(Step (*target)[NUM_STEPS]) {
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) {
|
||||||
|
if (random(100) < (trackIntensity[i] * 10)) {
|
||||||
|
strategies[currentStrategyIndices[i]]->mutate(target, i, numSteps[i], scaleNotes, numScaleNotes, trackIntensity[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequenceGenerator::generateRandomScale() {
|
||||||
|
Serial.println(F("Generating new scale."));
|
||||||
|
SequenceGenerator::updateScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequenceGenerator::updateScale() {
|
||||||
|
// 0: Chromatic, 1: Major, 2: Minor, 3: Harm Min, 4: Pent Maj, 5: Pent Min, 6: Chord Maj, 7: Chord Min, 8: Chord Dim, 9: Chord 7
|
||||||
|
int intervals[12];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
switch(currentScaleType) {
|
||||||
|
case 0: // Chromatic
|
||||||
|
for(int i=0; i<12; i++) intervals[count++] = i;
|
||||||
|
break;
|
||||||
|
case 1: // Major
|
||||||
|
intervals[0]=0; intervals[1]=2; intervals[2]=4; intervals[3]=5; intervals[4]=7; intervals[5]=9; intervals[6]=11; count=7;
|
||||||
|
break;
|
||||||
|
case 2: // Minor
|
||||||
|
intervals[0]=0; intervals[1]=2; intervals[2]=3; intervals[3]=5; intervals[4]=7; intervals[5]=8; intervals[6]=10; count=7;
|
||||||
|
break;
|
||||||
|
case 3: // Harmonic Minor
|
||||||
|
intervals[0]=0; intervals[1]=2; intervals[2]=3; intervals[3]=5; intervals[4]=7; intervals[5]=8; intervals[6]=11; count=7;
|
||||||
|
break;
|
||||||
|
case 4: // Pentatonic Major
|
||||||
|
intervals[0]=0; intervals[1]=2; intervals[2]=4; intervals[3]=7; intervals[4]=9; count=5;
|
||||||
|
break;
|
||||||
|
case 5: // Pentatonic Minor
|
||||||
|
intervals[0]=0; intervals[1]=3; intervals[2]=5; intervals[3]=7; intervals[4]=10; count=5;
|
||||||
|
break;
|
||||||
|
case 6: // Chord Major
|
||||||
|
intervals[0]=0; intervals[1]=4; intervals[2]=7; count=3;
|
||||||
|
break;
|
||||||
|
case 7: // Chord Minor
|
||||||
|
intervals[0]=0; intervals[1]=3; intervals[2]=7; count=3;
|
||||||
|
break;
|
||||||
|
case 8: // Chord Dim
|
||||||
|
intervals[0]=0; intervals[1]=3; intervals[2]=6; count=3;
|
||||||
|
break;
|
||||||
|
case 9: // Chord 7
|
||||||
|
intervals[0]=0; intervals[1]=4; intervals[2]=7; intervals[3]=10; count=4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
midi.lock();
|
||||||
|
numScaleNotes = count;
|
||||||
|
for(int i=0; i<count; i++) {
|
||||||
|
scaleNotes[i] = (currentRoot + intervals[i]) % 12;
|
||||||
|
}
|
||||||
|
sortArray(scaleNotes, numScaleNotes);
|
||||||
|
midi.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SequenceGenerator::pickRandomScaleType(int themeType) {
|
||||||
|
unsigned long seed = themeType * 9999;
|
||||||
|
for(int i=0; i<NUM_TRACKS; i++) seed += melodySeeds[i];
|
||||||
|
randomSeed(seed);
|
||||||
|
|
||||||
|
int candidates[10];
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (enabledScaleTypes & (1 << i)) {
|
||||||
|
candidates[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
currentScaleType = candidates[random(count)];
|
||||||
|
SequenceGenerator::updateScale();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
SequenceGenerator.h
Normal file
17
SequenceGenerator.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#ifndef SEQUENCE_GENERATOR_H
|
||||||
|
#define SEQUENCE_GENERATOR_H
|
||||||
|
|
||||||
|
#include "TrackerTypes.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class SequenceGenerator {
|
||||||
|
public:
|
||||||
|
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]);
|
||||||
|
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]);
|
||||||
|
static void mutateSequence(Step (*target)[NUM_STEPS]);
|
||||||
|
static void generateRandomScale();
|
||||||
|
static void updateScale();
|
||||||
|
static void pickRandomScaleType(int themeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -6,6 +6,9 @@
|
|||||||
#include "CellularAutomataStrategy.h"
|
#include "CellularAutomataStrategy.h"
|
||||||
#include "LSystemStrategy.h"
|
#include "LSystemStrategy.h"
|
||||||
#include "DroneStrategy.h"
|
#include "DroneStrategy.h"
|
||||||
|
#include "CallAndResponseStrategy.h"
|
||||||
|
#include "WaveStrategy.h"
|
||||||
|
#include "IsorhythmStrategy.h"
|
||||||
|
|
||||||
// Global state variables
|
// Global state variables
|
||||||
Step sequence[NUM_TRACKS][NUM_STEPS];
|
Step sequence[NUM_TRACKS][NUM_STEPS];
|
||||||
@ -22,7 +25,18 @@ MenuItem menuItems[] = {
|
|||||||
{ "Main", MENU_ID_GROUP_MAIN, true, true, 0 },
|
{ "Main", MENU_ID_GROUP_MAIN, true, true, 0 },
|
||||||
{ "Playback", MENU_ID_PLAYBACK, false, false, 1 },
|
{ "Playback", MENU_ID_PLAYBACK, false, false, 1 },
|
||||||
{ "Melody", MENU_ID_MELODY, false, false, 1 },
|
{ "Melody", MENU_ID_MELODY, false, false, 1 },
|
||||||
{ "Scale", MENU_ID_SCALE, false, false, 1 },
|
{ "Root", MENU_ID_ROOT, false, false, 1 },
|
||||||
|
{ "Scale Type", MENU_ID_SCALE_TYPE, true, false, 1 },
|
||||||
|
{ "Chromatic", MENU_ID_SCALE_TYPE_CHROMATIC, false, false, 2 },
|
||||||
|
{ "Major", MENU_ID_SCALE_TYPE_MAJOR, false, false, 2 },
|
||||||
|
{ "Minor", MENU_ID_SCALE_TYPE_MINOR, false, false, 2 },
|
||||||
|
{ "Harm. Min", MENU_ID_SCALE_TYPE_HARM_MIN, false, false, 2 },
|
||||||
|
{ "Pent. Maj", MENU_ID_SCALE_TYPE_PENT_MAJ, false, false, 2 },
|
||||||
|
{ "Pent. Min", MENU_ID_SCALE_TYPE_PENT_MIN, false, false, 2 },
|
||||||
|
{ "Chord Maj", MENU_ID_SCALE_TYPE_CHORD_MAJ, false, false, 2 },
|
||||||
|
{ "Chord Min", MENU_ID_SCALE_TYPE_CHORD_MIN, false, false, 2 },
|
||||||
|
{ "Chord Dim", MENU_ID_SCALE_TYPE_CHORD_DIM, false, false, 2 },
|
||||||
|
{ "Chord 7", MENU_ID_SCALE_TYPE_CHORD_7, false, false, 2 },
|
||||||
{ "Tempo", MENU_ID_TEMPO, false, false, 1 },
|
{ "Tempo", MENU_ID_TEMPO, false, false, 1 },
|
||||||
{ "Song Mode", MENU_ID_SONG_MODE, false, false, 1 },
|
{ "Song Mode", MENU_ID_SONG_MODE, false, false, 1 },
|
||||||
{ "Track", MENU_ID_GROUP_TRACK, true, true, 0 },
|
{ "Track", MENU_ID_GROUP_TRACK, true, true, 0 },
|
||||||
@ -112,12 +126,17 @@ int numScaleNotes = 0;
|
|||||||
int melodySeeds[NUM_TRACKS];
|
int melodySeeds[NUM_TRACKS];
|
||||||
volatile int queuedTheme = -1;
|
volatile int queuedTheme = -1;
|
||||||
volatile int currentThemeIndex = 1;
|
volatile int currentThemeIndex = 1;
|
||||||
extern const uint32_t EEPROM_MAGIC = 0x4242424E;
|
int currentRoot = 0; // C
|
||||||
|
int currentScaleType = 1; // Major
|
||||||
|
int enabledScaleTypes = 2; // Major (1<<1)
|
||||||
|
extern const uint32_t EEPROM_MAGIC = 0x42424250;
|
||||||
|
|
||||||
MelodyStrategy* strategies[] = {
|
MelodyStrategy* strategies[] = {
|
||||||
new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy(),
|
new LuckyStrategy(), new ArpStrategy(), new EuclideanStrategy(),
|
||||||
new MarkovStrategy(), new CellularAutomataStrategy(), new LSystemStrategy(), new DroneStrategy()};
|
new MarkovStrategy(), new CellularAutomataStrategy(), new LSystemStrategy(), new DroneStrategy(),
|
||||||
extern const int numStrategies = 7;
|
new CallAndResponseStrategy(), new IsorhythmStrategy(), new WaveStrategy()
|
||||||
|
};
|
||||||
|
extern const int numStrategies = 10;
|
||||||
int currentStrategyIndices[NUM_TRACKS];
|
int currentStrategyIndices[NUM_TRACKS];
|
||||||
|
|
||||||
volatile PlayMode playMode = MODE_POLY;
|
volatile PlayMode playMode = MODE_POLY;
|
||||||
|
|||||||
@ -18,7 +18,18 @@ enum MenuItemID {
|
|||||||
MENU_ID_GROUP_MAIN,
|
MENU_ID_GROUP_MAIN,
|
||||||
MENU_ID_PLAYBACK,
|
MENU_ID_PLAYBACK,
|
||||||
MENU_ID_MELODY,
|
MENU_ID_MELODY,
|
||||||
MENU_ID_SCALE,
|
MENU_ID_ROOT,
|
||||||
|
MENU_ID_SCALE_TYPE,
|
||||||
|
MENU_ID_SCALE_TYPE_CHROMATIC,
|
||||||
|
MENU_ID_SCALE_TYPE_MAJOR,
|
||||||
|
MENU_ID_SCALE_TYPE_MINOR,
|
||||||
|
MENU_ID_SCALE_TYPE_HARM_MIN,
|
||||||
|
MENU_ID_SCALE_TYPE_PENT_MAJ,
|
||||||
|
MENU_ID_SCALE_TYPE_PENT_MIN,
|
||||||
|
MENU_ID_SCALE_TYPE_CHORD_MAJ,
|
||||||
|
MENU_ID_SCALE_TYPE_CHORD_MIN,
|
||||||
|
MENU_ID_SCALE_TYPE_CHORD_DIM,
|
||||||
|
MENU_ID_SCALE_TYPE_CHORD_7,
|
||||||
MENU_ID_TEMPO,
|
MENU_ID_TEMPO,
|
||||||
MENU_ID_SONG_MODE,
|
MENU_ID_SONG_MODE,
|
||||||
|
|
||||||
@ -73,6 +84,10 @@ extern int numScaleNotes;
|
|||||||
extern int melodySeeds[NUM_TRACKS];
|
extern int melodySeeds[NUM_TRACKS];
|
||||||
extern volatile int queuedTheme;
|
extern volatile int queuedTheme;
|
||||||
extern volatile int currentThemeIndex;
|
extern volatile int currentThemeIndex;
|
||||||
|
extern int currentRoot;
|
||||||
|
extern volatile int trackIntensity[NUM_TRACKS];
|
||||||
|
extern int currentScaleType;
|
||||||
|
extern int enabledScaleTypes;
|
||||||
extern const uint32_t EEPROM_MAGIC;
|
extern const uint32_t EEPROM_MAGIC;
|
||||||
|
|
||||||
extern MelodyStrategy* strategies[];
|
extern MelodyStrategy* strategies[];
|
||||||
|
|||||||
@ -24,9 +24,7 @@ enum UIState {
|
|||||||
UI_EDIT_INTENSITY,
|
UI_EDIT_INTENSITY,
|
||||||
UI_SETUP_PLAYMODE_EDIT,
|
UI_SETUP_PLAYMODE_EDIT,
|
||||||
UI_RANDOMIZE_TRACK_EDIT,
|
UI_RANDOMIZE_TRACK_EDIT,
|
||||||
UI_SCALE_EDIT,
|
UI_EDIT_ROOT
|
||||||
UI_SCALE_NOTE_EDIT,
|
|
||||||
UI_SCALE_TRANSPOSE
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline void sortArray(int arr[], int size) {
|
inline void sortArray(int arr[], int size) {
|
||||||
|
|||||||
323
UIManager.cpp
323
UIManager.cpp
@ -7,16 +7,11 @@
|
|||||||
#define SCREEN_HEIGHT 64
|
#define SCREEN_HEIGHT 64
|
||||||
#define OLED_RESET -1
|
#define OLED_RESET -1
|
||||||
#define SCREEN_ADDRESS 0x3C
|
#define SCREEN_ADDRESS 0x3C
|
||||||
#define PIN_NEOPIXEL 6
|
|
||||||
#define NEOPIXELS_X 16
|
|
||||||
#define NEOPIXELS_Y 8
|
|
||||||
#define NUM_PIXELS 64*2 // 8x8 LEDs in 2 panels
|
|
||||||
|
|
||||||
UIManager ui;
|
UIManager ui;
|
||||||
|
|
||||||
UIManager::UIManager()
|
UIManager::UIManager()
|
||||||
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET),
|
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET)
|
||||||
pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +26,7 @@ void UIManager::begin() {
|
|||||||
display.display();
|
display.display();
|
||||||
|
|
||||||
// Setup NeoPixel Matrix
|
// Setup NeoPixel Matrix
|
||||||
pixels.setPin(PIN_NEOPIXEL);
|
ledMatrix.begin();
|
||||||
pixels.begin();
|
|
||||||
pixels.setBrightness(40);
|
|
||||||
pixels.clear();
|
|
||||||
pixels.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIManager::showMessage(const char* msg) {
|
void UIManager::showMessage(const char* msg) {
|
||||||
@ -67,141 +58,28 @@ void UIManager::draw(UIState currentState, int menuSelection,
|
|||||||
drawMenu(menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, currentTrackNumSteps, mutationEnabled, songModeEnabled, isPlaying, randomizeTrack, trackMute, trackIntensities);
|
drawMenu(menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, currentTrackNumSteps, mutationEnabled, songModeEnabled, isPlaying, randomizeTrack, trackMute, trackIntensities);
|
||||||
break;
|
break;
|
||||||
case UI_SETUP_CHANNEL_EDIT:
|
case UI_SETUP_CHANNEL_EDIT:
|
||||||
display.println(F("SET MIDI CHANNEL"));
|
drawEditScreen("SET MIDI CHANNEL", "CH: ", midiChannel);
|
||||||
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;
|
break;
|
||||||
case UI_EDIT_TEMPO:
|
case UI_EDIT_TEMPO:
|
||||||
display.println(F("SET TEMPO"));
|
drawEditScreen("SET TEMPO", "BPM: ", 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;
|
break;
|
||||||
case UI_EDIT_STEPS:
|
case UI_EDIT_STEPS:
|
||||||
display.println(F("SET STEPS"));
|
drawEditScreen("SET STEPS", "LEN: ", currentTrackNumSteps);
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
||||||
display.setCursor(20, 25);
|
|
||||||
display.setTextSize(2);
|
|
||||||
display.print(F("LEN: "));
|
|
||||||
display.print(currentTrackNumSteps);
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.setCursor(0, 50);
|
|
||||||
display.println(F(" (Press to confirm)"));
|
|
||||||
break;
|
break;
|
||||||
case UI_EDIT_FLAVOUR:
|
case UI_EDIT_FLAVOUR:
|
||||||
display.println(F("SET FLAVOUR"));
|
drawEditScreen("SET FLAVOUR", "", currentStrategy->getName());
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
break;
|
||||||
display.setCursor(20, 25);
|
case UI_EDIT_ROOT:
|
||||||
display.setTextSize(2);
|
{
|
||||||
display.print(currentStrategy->getName());
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||||
display.setTextSize(1);
|
drawEditScreen("SET ROOT NOTE", "", noteNames[currentRoot % 12]);
|
||||||
display.setCursor(0, 50);
|
}
|
||||||
display.println(F(" (Press to confirm)"));
|
|
||||||
break;
|
break;
|
||||||
case UI_EDIT_INTENSITY:
|
case UI_EDIT_INTENSITY:
|
||||||
drawNumberEditor("SET INTENSITY", trackIntensities[randomizeTrack], 1, 10);
|
drawNumberEditor("SET INTENSITY", trackIntensities[randomizeTrack], 1, 10);
|
||||||
break;
|
break;
|
||||||
case UI_RANDOMIZE_TRACK_EDIT:
|
case UI_RANDOMIZE_TRACK_EDIT:
|
||||||
display.println(F("SET TRACK"));
|
drawEditScreen("SET TRACK", "TRK: ", randomizeTrack + 1);
|
||||||
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;
|
|
||||||
case UI_SCALE_EDIT:
|
|
||||||
case UI_SCALE_NOTE_EDIT:
|
|
||||||
case UI_SCALE_TRANSPOSE:
|
|
||||||
display.println(F("EDIT SCALE"));
|
|
||||||
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
||||||
|
|
||||||
int totalItems = numScaleNotes + 5; // Back + Randomize + Notes + Add + Remove + Transpose
|
|
||||||
int startIdx = 0;
|
|
||||||
if (menuSelection >= 4) startIdx = menuSelection - 3;
|
|
||||||
|
|
||||||
int y = 12;
|
|
||||||
for (int i = startIdx; i < totalItems; i++) {
|
|
||||||
if (y > 54) break;
|
|
||||||
|
|
||||||
if (i == menuSelection) {
|
|
||||||
display.fillRect(0, y, 75, 9, SSD1306_WHITE);
|
|
||||||
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
||||||
} else {
|
|
||||||
display.setTextColor(SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
display.setCursor(2, y + 1);
|
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
display.print(F("Back"));
|
|
||||||
} else if (i == 1) {
|
|
||||||
display.print(F("Randomize"));
|
|
||||||
} else if (i <= numScaleNotes + 1) {
|
|
||||||
int noteIdx = i - 2;
|
|
||||||
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
||||||
display.print(noteNames[scaleNotes[noteIdx]]);
|
|
||||||
if (currentState == UI_SCALE_NOTE_EDIT && i == menuSelection) {
|
|
||||||
display.print(F(" <"));
|
|
||||||
}
|
|
||||||
} else if (i == numScaleNotes + 2) {
|
|
||||||
display.print(F("Transpose"));
|
|
||||||
if (currentState == UI_SCALE_TRANSPOSE) {
|
|
||||||
display.print(F(" < >"));
|
|
||||||
}
|
|
||||||
} else if (i == numScaleNotes + 3) {
|
|
||||||
display.print(F("Add Note"));
|
|
||||||
} else if (i == numScaleNotes + 4) {
|
|
||||||
display.print(F("Remove Note"));
|
|
||||||
}
|
|
||||||
y += 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Piano Roll Preview
|
|
||||||
int px = 82;
|
|
||||||
int py = 20;
|
|
||||||
int wk_w = 5;
|
|
||||||
int wk_h = 20;
|
|
||||||
int bk_w = 4;
|
|
||||||
int bk_h = 12;
|
|
||||||
|
|
||||||
// White keys: C, D, E, F, G, A, B
|
|
||||||
int whiteNotes[] = {0, 2, 4, 5, 7, 9, 11};
|
|
||||||
for (int k = 0; k < 7; k++) {
|
|
||||||
bool active = false;
|
|
||||||
for (int j = 0; j < numScaleNotes; j++) {
|
|
||||||
if (scaleNotes[j] == whiteNotes[k]) { active = true; break; }
|
|
||||||
}
|
|
||||||
if (active) display.fillRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
|
|
||||||
else display.drawRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Black keys: C#, D#, F#, G#, A#
|
|
||||||
int blackNotes[] = {1, 3, 6, 8, 10};
|
|
||||||
int blackOffsets[] = {3, 9, 21, 27, 33};
|
|
||||||
for (int k = 0; k < 5; k++) {
|
|
||||||
bool active = false;
|
|
||||||
for (int j = 0; j < numScaleNotes; j++) {
|
|
||||||
if (scaleNotes[j] == blackNotes[k]) { active = true; break; }
|
|
||||||
}
|
|
||||||
int bx = px + blackOffsets[k];
|
|
||||||
display.fillRect(bx - 1, py - 1, bk_w + 2, bk_h + 2, SSD1306_BLACK);
|
|
||||||
if (active) display.fillRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
|
|
||||||
else display.drawRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
display.display();
|
display.display();
|
||||||
@ -232,6 +110,24 @@ void UIManager::drawNumberEditor(const char* title, int value, int minVal, int m
|
|||||||
display.println(F(" (Press to confirm)"));
|
display.println(F(" (Press to confirm)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UIManager::drawEditScreen(const char* title, const char* label, const char* valueStr) {
|
||||||
|
display.println(title);
|
||||||
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
||||||
|
display.setCursor(20, 25);
|
||||||
|
display.setTextSize(2);
|
||||||
|
if (label && *label) display.print(label);
|
||||||
|
display.print(valueStr);
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.setCursor(0, 50);
|
||||||
|
display.println(F(" (Press to confirm)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIManager::drawEditScreen(const char* title, const char* label, int value) {
|
||||||
|
char buf[16];
|
||||||
|
itoa(value, buf, 10);
|
||||||
|
drawEditScreen(title, label, buf);
|
||||||
|
}
|
||||||
|
|
||||||
void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName,
|
void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName,
|
||||||
int queuedTheme, int currentThemeIndex, int numScaleNotes,
|
int queuedTheme, int currentThemeIndex, int numScaleNotes,
|
||||||
const int* scaleNotes, int melodySeed, int currentTrackNumSteps, bool mutationEnabled,
|
const int* scaleNotes, int melodySeed, int currentTrackNumSteps, bool mutationEnabled,
|
||||||
@ -274,6 +170,18 @@ void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, i
|
|||||||
|
|
||||||
display.print(menuItems[i].label);
|
display.print(menuItems[i].label);
|
||||||
|
|
||||||
|
// Render checkbox for scale types
|
||||||
|
if (menuItems[i].id >= MENU_ID_SCALE_TYPE_CHROMATIC && menuItems[i].id <= MENU_ID_SCALE_TYPE_CHORD_7) {
|
||||||
|
int typeIndex = menuItems[i].id - MENU_ID_SCALE_TYPE_CHROMATIC;
|
||||||
|
bool enabled = (enabledScaleTypes & (1 << typeIndex));
|
||||||
|
int boxX = x - 10;
|
||||||
|
display.drawRect(boxX, y + 1, 7, 7, (i == selection) ? SSD1306_BLACK : SSD1306_WHITE);
|
||||||
|
if (enabled) display.fillRect(boxX + 2, y + 3, 3, 3, (i == selection) ? SSD1306_BLACK : SSD1306_WHITE);
|
||||||
|
if (typeIndex == currentScaleType) {
|
||||||
|
display.print(F(" *"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MenuItemID id = menuItems[i].id;
|
MenuItemID id = menuItems[i].id;
|
||||||
|
|
||||||
if (id == MENU_ID_CHANNEL) {
|
if (id == MENU_ID_CHANNEL) {
|
||||||
@ -284,16 +192,11 @@ void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, i
|
|||||||
if (id == MENU_ID_PLAYBACK) { display.print(F(": ")); display.print(isPlaying ? F("ON") : F("OFF")); }
|
if (id == MENU_ID_PLAYBACK) { display.print(F(": ")); display.print(isPlaying ? F("ON") : F("OFF")); }
|
||||||
else if (id == MENU_ID_MELODY) {
|
else if (id == MENU_ID_MELODY) {
|
||||||
display.print(F(": ")); display.print(melodySeed);
|
display.print(F(": ")); display.print(melodySeed);
|
||||||
} else if (id == MENU_ID_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 (id == MENU_ID_TEMPO) { display.print(F(": ")); display.print(tempo); }
|
} else if (id == MENU_ID_TEMPO) { display.print(F(": ")); display.print(tempo); }
|
||||||
|
else if (id == MENU_ID_ROOT) {
|
||||||
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||||
|
display.print(F(": ")); display.print(noteNames[currentRoot % 12]);
|
||||||
|
}
|
||||||
else if (id == MENU_ID_STEPS) { display.print(F(": ")); display.print(currentTrackNumSteps); }
|
else if (id == MENU_ID_STEPS) { display.print(F(": ")); display.print(currentTrackNumSteps); }
|
||||||
else if (id == MENU_ID_SONG_MODE) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
|
else if (id == MENU_ID_SONG_MODE) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
|
||||||
else if (id == MENU_ID_TRACK_SELECT) { display.print(F(": ")); display.print(randomizeTrack + 1); }
|
else if (id == MENU_ID_TRACK_SELECT) { display.print(F(": ")); display.print(randomizeTrack + 1); }
|
||||||
@ -327,139 +230,9 @@ void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
// [i] Here you can adjust the mapping from "logical" pixel coordinates
|
|
||||||
// to your physical NeoPixel layout. It depends on how you connected it.
|
|
||||||
return NEOPIXELS_Y * x + (NEOPIXELS_Y - 1 - y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UIManager::updateLeds(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
void UIManager::updateLeds(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
||||||
UIState currentState, bool songModeEnabled,
|
UIState currentState, bool songModeEnabled,
|
||||||
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
|
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
|
||||||
int selectedTrack, const int* numSteps, int numScaleNotes, const int* scaleNotes, const bool* trackMute) {
|
int selectedTrack, const int* numSteps, int numScaleNotes, const int* scaleNotes, const bool* trackMute) {
|
||||||
pixels.clear();
|
ledMatrix.update(sequence, playbackStep, isPlaying, currentState, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled, playMode, selectedTrack, numSteps, trackMute);
|
||||||
|
|
||||||
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, 100, 100);
|
|
||||||
const uint32_t COLOR_CURSOR_DIM = pixels.Color(32, 0, 0);
|
|
||||||
|
|
||||||
if(playMode == MODE_POLY) {
|
|
||||||
for(int t=0; t<NUM_TRACKS; t++) {
|
|
||||||
int currentTrackSteps = numSteps[t];
|
|
||||||
for(int s=0; s<NUM_STEPS; s++) {
|
|
||||||
int col = s; // Each step is a column
|
|
||||||
|
|
||||||
// --- First row of track pair: Notes ---
|
|
||||||
int note_row = t * 2;
|
|
||||||
uint32_t note_color = 0;
|
|
||||||
if (s < currentTrackSteps) {
|
|
||||||
int note = sequence[t][s].note;
|
|
||||||
if (note != -1) {
|
|
||||||
note_color = getNoteColor(note, !sequence[t][s].accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pixels.setPixelColor(getPixelIndex(col, note_row), note_color);
|
|
||||||
|
|
||||||
// --- Second row of track pair: Steps & Playhead ---
|
|
||||||
int step_row = t * 2 + 1;
|
|
||||||
uint32_t step_color = 0; // Off by default for steps > currentTrackSteps
|
|
||||||
if (s < currentTrackSteps) {
|
|
||||||
step_color = COLOR_PLAYHEAD_DIM; // It's a valid step
|
|
||||||
if (isPlaying && (s == (playbackStep % currentTrackSteps))) {
|
|
||||||
if (trackMute[t]) {
|
|
||||||
step_color = COLOR_MUTED_PLAYHEAD;
|
|
||||||
} else {
|
|
||||||
step_color = COLOR_PLAYHEAD;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pixels.setPixelColor(getPixelIndex(col, step_row), step_color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// --- Mono Mode (original) ---
|
|
||||||
const Step* trackSequence = sequence[selectedTrack];
|
|
||||||
for (int s = 0; s < NUM_STEPS; s++) {
|
|
||||||
if (s >= numSteps[selectedTrack]) continue;
|
|
||||||
|
|
||||||
int x = s % NUM_STEPS;
|
|
||||||
int yBase = (s / NUM_STEPS) * 2;
|
|
||||||
uint32_t color = 0, dimColor = 0;
|
|
||||||
bool isCursorHere = (isPlaying && s == (playbackStep % numSteps[selectedTrack]));
|
|
||||||
if (trackSequence[s].note != -1) {
|
|
||||||
color = getNoteColor(trackSequence[s].note, trackSequence[s].tie);
|
|
||||||
dimColor = getNoteColor(trackSequence[s].note, true);
|
|
||||||
}
|
|
||||||
uint32_t c[4] = {0};
|
|
||||||
if (trackSequence[s].note != -1) {
|
|
||||||
int octave = trackSequence[s].note / 12;
|
|
||||||
if (octave > 4) { c[0] = color; if (trackSequence[s].accent) c[1] = dimColor; }
|
|
||||||
else if (octave < 4) { c[2] = color; if (trackSequence[s].accent) c[1] = dimColor; }
|
|
||||||
else { c[1] = color; if (trackSequence[s].accent) { c[0] = dimColor; c[2] = dimColor; } }
|
|
||||||
}
|
|
||||||
uint32_t cursorColor = pixels.Color(0, 0, 50);
|
|
||||||
if (isPlaying) {
|
|
||||||
cursorColor = pixels.Color(0, 50, 0);
|
|
||||||
if (songModeEnabled && s >= NUM_STEPS) {
|
|
||||||
int repeats = min(songRepeatsRemaining, NUM_STEPS);
|
|
||||||
if (x >= (NUM_STEPS - repeats)) cursorColor = (songRepeatsRemaining == 1 && x == 7 && (millis()/250)%2) ? pixels.Color(255, 200, 0) : pixels.Color(100, 220, 40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorColor != 0) {
|
|
||||||
if (isCursorHere) {
|
|
||||||
for(int i=0; i<4; i++) {
|
|
||||||
if (c[i] == 0) c[i] = 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Overview of all tracks on bottom 4 rows ---
|
|
||||||
for(int t=0; t<NUM_TRACKS; t++) {
|
|
||||||
int row = 4 + t;
|
|
||||||
int currentTrackSteps = numSteps[t];
|
|
||||||
for(int s=0; s<NUM_STEPS; s++) {
|
|
||||||
if (s >= currentTrackSteps) continue;
|
|
||||||
|
|
||||||
uint32_t pixelColor = 0;
|
|
||||||
|
|
||||||
// Background for active track
|
|
||||||
if (t == selectedTrack) {
|
|
||||||
pixelColor = COLOR_CURSOR_DIM;
|
|
||||||
}
|
|
||||||
else if (sequence[t][s].note != -1) // Note
|
|
||||||
{
|
|
||||||
pixelColor = getNoteColor(sequence[t][s].note, !sequence[t][s].accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playhead
|
|
||||||
if (isPlaying && (s == (playbackStep % currentTrackSteps))) {
|
|
||||||
if (t == selectedTrack) {
|
|
||||||
pixelColor = COLOR_CURSOR;
|
|
||||||
} else if (trackMute[t]) {
|
|
||||||
pixelColor = COLOR_MUTED_PLAYHEAD;
|
|
||||||
} else {
|
|
||||||
pixelColor = COLOR_PLAYHEAD;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pixels.setPixelColor(getPixelIndex(s, row), pixelColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sequenceChangeScheduled && (millis() / 125) % 2) pixels.setPixelColor(NUM_PIXELS - 1, pixels.Color(127, 50, 0));
|
|
||||||
pixels.show();
|
|
||||||
}
|
}
|
||||||
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
#include <Adafruit_GFX.h>
|
#include <Adafruit_GFX.h>
|
||||||
#include <Adafruit_SSD1306.h>
|
#include <Adafruit_SSD1306.h>
|
||||||
#include <Adafruit_NeoPixel.h>
|
|
||||||
#include "TrackerTypes.h"
|
#include "TrackerTypes.h"
|
||||||
#include "MelodyStrategy.h"
|
#include "MelodyStrategy.h"
|
||||||
|
#include "LedMatrix.h"
|
||||||
|
|
||||||
class UIManager {
|
class UIManager {
|
||||||
public:
|
public:
|
||||||
@ -29,7 +29,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Adafruit_SSD1306 display;
|
Adafruit_SSD1306 display;
|
||||||
Adafruit_NeoPixel pixels;
|
LedMatrix ledMatrix;
|
||||||
|
|
||||||
void drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName,
|
void drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName,
|
||||||
int queuedTheme, int currentThemeIndex,
|
int queuedTheme, int currentThemeIndex,
|
||||||
@ -37,9 +37,8 @@ private:
|
|||||||
bool mutationEnabled, bool songModeEnabled, bool isPlaying, int randomizeTrack, const bool* trackMute, const int* trackIntensities);
|
bool mutationEnabled, bool songModeEnabled, bool isPlaying, int randomizeTrack, const bool* trackMute, const int* trackIntensities);
|
||||||
|
|
||||||
void drawNumberEditor(const char* title, int value, int minVal, int maxVal);
|
void drawNumberEditor(const char* title, int value, int minVal, int maxVal);
|
||||||
|
void drawEditScreen(const char* title, const char* label, const char* valueStr);
|
||||||
uint32_t getNoteColor(int note, bool dim);
|
void drawEditScreen(const char* title, const char* label, int value);
|
||||||
int getPixelIndex(int x, int y);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern UIManager ui;
|
extern UIManager ui;
|
||||||
|
|||||||
361
UIThread.cpp
361
UIThread.cpp
@ -1,231 +1,39 @@
|
|||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <EEPROM.h>
|
|
||||||
#include "TrackerTypes.h"
|
#include "TrackerTypes.h"
|
||||||
#include "MelodyStrategy.h"
|
|
||||||
#include "LuckyStrategy.h"
|
|
||||||
#include "ArpStrategy.h"
|
|
||||||
#include "EuclideanStrategy.h"
|
|
||||||
#include "MarkovStrategy.h"
|
|
||||||
#include "CellularAutomataStrategy.h"
|
|
||||||
#include "LSystemStrategy.h"
|
|
||||||
#include "DroneStrategy.h"
|
|
||||||
#include "MidiDriver.h"
|
#include "MidiDriver.h"
|
||||||
#include "UIManager.h"
|
#include "UIManager.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "UIThread.h"
|
#include "UIThread.h"
|
||||||
#include "SharedState.h"
|
#include "SharedState.h"
|
||||||
|
#include "SequenceGenerator.h"
|
||||||
extern volatile int trackIntensity[NUM_TRACKS];
|
#include "Persistence.h"
|
||||||
|
|
||||||
static Step local_sequence[NUM_TRACKS][NUM_STEPS];
|
static Step local_sequence[NUM_TRACKS][NUM_STEPS];
|
||||||
|
|
||||||
static void handleInput();
|
static void handleInput();
|
||||||
static int scaleEditSelection = 0;
|
|
||||||
static int scaleEditNoteIndex = 0;
|
|
||||||
static void drawUI();
|
static void drawUI();
|
||||||
static void updateLeds();
|
static void updateLeds();
|
||||||
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]);
|
|
||||||
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]);
|
|
||||||
static void savePatch(int bank, int slot);
|
|
||||||
static void loadPatch(int bank, int slot);
|
|
||||||
|
|
||||||
void saveSequence(bool quiet) {
|
void saveSequence(bool quiet) {
|
||||||
midi.lock();
|
Persistence::saveSequence(quiet);
|
||||||
int addr = 0;
|
|
||||||
EEPROM.put(addr, EEPROM_MAGIC); addr += sizeof(EEPROM_MAGIC);
|
|
||||||
int channels[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) channels[i] = midiChannels[i];
|
|
||||||
EEPROM.put(addr, channels); addr += sizeof(channels);
|
|
||||||
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
|
|
||||||
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
|
||||||
bool mutes[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
|
|
||||||
EEPROM.put(addr, mutes); addr += sizeof(mutes);
|
|
||||||
EEPROM.put(addr, (int)tempo); addr += sizeof(int);
|
|
||||||
int intensities[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) intensities[i] = (int)trackIntensity[i];
|
|
||||||
EEPROM.put(addr, intensities); addr += sizeof(intensities);
|
|
||||||
int steps[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) steps[i] = numSteps[i];
|
|
||||||
EEPROM.put(addr, steps); addr += sizeof(steps);
|
|
||||||
|
|
||||||
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
for (int i = 0; i<12; i++) {
|
|
||||||
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
}
|
|
||||||
|
|
||||||
EEPROM.put(addr, sequence); addr += sizeof(sequence);
|
|
||||||
midi.unlock();
|
|
||||||
EEPROM.commit();
|
|
||||||
if (!quiet) ui.showMessage("SAVED!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool loadSequence() {
|
bool loadSequence() {
|
||||||
midi.lock();
|
return Persistence::loadSequence();
|
||||||
int addr = 0;
|
|
||||||
uint32_t magic;
|
|
||||||
EEPROM.get(addr, magic); addr += sizeof(magic);
|
|
||||||
if (magic != EEPROM_MAGIC) {
|
|
||||||
midi.unlock();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int channels[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, channels); addr += sizeof(channels);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
midiChannels[i] = channels[i];
|
|
||||||
if (midiChannels[i] < 1) midiChannels[i] = 1;
|
|
||||||
if (midiChannels[i] > 16) midiChannels[i] = 16;
|
|
||||||
}
|
|
||||||
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
|
|
||||||
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
if (currentStrategyIndices[i] < 0 || currentStrategyIndices[i] >= numStrategies) currentStrategyIndices[i] = 0;
|
|
||||||
}
|
|
||||||
bool mutes[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, mutes); addr += sizeof(mutes);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
|
|
||||||
int t;
|
|
||||||
EEPROM.get(addr, t); addr += sizeof(int);
|
|
||||||
tempo = t;
|
|
||||||
if (tempo < 40) tempo = 40;
|
|
||||||
if (tempo > 240) tempo = 240;
|
|
||||||
int intensities[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, intensities); addr += sizeof(intensities);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
trackIntensity[i] = intensities[i];
|
|
||||||
if (trackIntensity[i] < 1) trackIntensity[i] = 1;
|
|
||||||
if (trackIntensity[i] > 10) trackIntensity[i] = 10;
|
|
||||||
}
|
|
||||||
int steps[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, steps); addr += sizeof(steps);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
numSteps[i] = steps[i];
|
|
||||||
if (numSteps[i] <= 0 || numSteps[i] > NUM_STEPS) {
|
|
||||||
numSteps[i] = NUM_STEPS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
if (numScaleNotes < 0 || numScaleNotes > 12) numScaleNotes = 0;
|
|
||||||
for (int i = 0; i<12; i++) {
|
|
||||||
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
if (scaleNotes[i] < 0) scaleNotes[i] = 0;
|
|
||||||
if (scaleNotes[i] > 11) scaleNotes[i] = 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
EEPROM.get(addr, sequence); addr += sizeof(sequence);
|
|
||||||
midi.unlock();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void savePatch(int bank, int slot) {
|
|
||||||
int patchIndex = bank * 4 + slot;
|
|
||||||
int addr = 512 + patchIndex * 256; // Start after main save, 256 bytes per patch
|
|
||||||
|
|
||||||
midi.lock();
|
|
||||||
EEPROM.put(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
for (int i = 0; i < 12; i++) {
|
|
||||||
EEPROM.put(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
}
|
|
||||||
EEPROM.put(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
|
||||||
EEPROM.put(addr, melodySeeds); addr += sizeof(melodySeeds);
|
|
||||||
int steps[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) steps[i] = numSteps[i];
|
|
||||||
EEPROM.put(addr, steps); addr += sizeof(steps);
|
|
||||||
bool mutes[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) mutes[i] = trackMute[i];
|
|
||||||
EEPROM.put(addr, mutes); addr += sizeof(mutes);
|
|
||||||
int intensities[NUM_TRACKS];
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) intensities[i] = trackIntensity[i];
|
|
||||||
EEPROM.put(addr, intensities); addr += sizeof(intensities);
|
|
||||||
midi.unlock();
|
|
||||||
EEPROM.commit();
|
|
||||||
ui.showMessage("SAVED!");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void loadPatch(int bank, int slot) {
|
|
||||||
int patchIndex = bank * 4 + slot;
|
|
||||||
int addr = 512 + patchIndex * 256;
|
|
||||||
|
|
||||||
midi.lock();
|
|
||||||
EEPROM.get(addr, numScaleNotes); addr += sizeof(numScaleNotes);
|
|
||||||
if (numScaleNotes < 0 || numScaleNotes > 12) numScaleNotes = 0;
|
|
||||||
for (int i = 0; i < 12; i++) {
|
|
||||||
EEPROM.get(addr, scaleNotes[i]); addr += sizeof(int);
|
|
||||||
if (scaleNotes[i] < 0) scaleNotes[i] = 0;
|
|
||||||
if (scaleNotes[i] > 11) scaleNotes[i] = 11;
|
|
||||||
}
|
|
||||||
EEPROM.get(addr, currentStrategyIndices); addr += sizeof(currentStrategyIndices);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
if (currentStrategyIndices[i] < 0 || currentStrategyIndices[i] >= numStrategies) currentStrategyIndices[i] = 0;
|
|
||||||
}
|
|
||||||
EEPROM.get(addr, melodySeeds); addr += sizeof(melodySeeds);
|
|
||||||
int steps[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, steps); addr += sizeof(steps);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
numSteps[i] = steps[i];
|
|
||||||
if (numSteps[i] <= 0 || numSteps[i] > NUM_STEPS) {
|
|
||||||
numSteps[i] = NUM_STEPS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mutes[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, mutes); addr += sizeof(mutes);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) trackMute[i] = mutes[i];
|
|
||||||
|
|
||||||
int intensities[NUM_TRACKS];
|
|
||||||
EEPROM.get(addr, intensities); addr += sizeof(intensities);
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
trackIntensity[i] = intensities[i];
|
|
||||||
if (trackIntensity[i] < 1) trackIntensity[i] = 1;
|
|
||||||
if (trackIntensity[i] > 10) trackIntensity[i] = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
generateSequenceData(currentThemeIndex, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
} else {
|
|
||||||
generateSequenceData(currentThemeIndex, local_sequence);
|
|
||||||
memcpy(sequence, local_sequence, sizeof(local_sequence));
|
|
||||||
}
|
|
||||||
midi.unlock();
|
|
||||||
ui.showMessage("LOADED!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void factoryReset() {
|
void factoryReset() {
|
||||||
ui.showMessage("RESETTING...");
|
Persistence::factoryReset();
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
trackIntensity[i] = 10;
|
|
||||||
}
|
|
||||||
uint32_t magic = 0;
|
|
||||||
EEPROM.put(0, magic);
|
|
||||||
EEPROM.commit();
|
|
||||||
delay(500);
|
|
||||||
rp2040.reboot();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void generateTrackData(int track, int themeType, Step (*target)[NUM_STEPS]) {
|
|
||||||
randomSeed(melodySeeds[track] + themeType * 12345);
|
|
||||||
strategies[currentStrategyIndices[track]]->generate(target, track, numSteps[track], scaleNotes, numScaleNotes, melodySeeds[track] + themeType * 12345, trackIntensity[track]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void generateRandomScale() {
|
void generateRandomScale() {
|
||||||
Serial.println(F("Generating new scale."));
|
SequenceGenerator::generateRandomScale();
|
||||||
// All tracks share the same scale for now
|
|
||||||
strategies[currentStrategyIndices[0]]->generateScale(scaleNotes, numScaleNotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void generateSequenceData(int themeType, Step (*target)[NUM_STEPS]) {
|
|
||||||
Serial.println(F("Generating sequence."));
|
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
|
||||||
generateTrackData(i, themeType, target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void generateTheme(int themeType) {
|
void generateTheme(int themeType) {
|
||||||
generateSequenceData(themeType, local_sequence);
|
SequenceGenerator::pickRandomScaleType(themeType);
|
||||||
|
SequenceGenerator::generateSequenceData(themeType, local_sequence);
|
||||||
|
|
||||||
Serial.println(F("Generating theme."));
|
Serial.println(F("Generating theme."));
|
||||||
midi.lock();
|
midi.lock();
|
||||||
@ -242,11 +50,7 @@ void generateTheme(int themeType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void mutateSequence(Step (*target)[NUM_STEPS]) {
|
void mutateSequence(Step (*target)[NUM_STEPS]) {
|
||||||
for(int i=0; i<NUM_TRACKS; i++) {
|
SequenceGenerator::mutateSequence(target);
|
||||||
if (random(100) < (trackIntensity[i] * 10)) {
|
|
||||||
strategies[currentStrategyIndices[i]]->mutate(target, i, numSteps[i], scaleNotes, numScaleNotes, trackIntensity[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleInput() {
|
static void handleInput() {
|
||||||
@ -289,6 +93,16 @@ static void handleInput() {
|
|||||||
if (numSteps[randomizeTrack] < 1) numSteps[randomizeTrack] = 1;
|
if (numSteps[randomizeTrack] < 1) numSteps[randomizeTrack] = 1;
|
||||||
if (numSteps[randomizeTrack] > NUM_STEPS) numSteps[randomizeTrack] = NUM_STEPS;
|
if (numSteps[randomizeTrack] > NUM_STEPS) numSteps[randomizeTrack] = NUM_STEPS;
|
||||||
break;
|
break;
|
||||||
|
case UI_EDIT_ROOT:
|
||||||
|
currentRoot += delta;
|
||||||
|
if (currentRoot < 0) currentRoot = 11;
|
||||||
|
if (currentRoot > 11) currentRoot = 0;
|
||||||
|
SequenceGenerator::updateScale();
|
||||||
|
if (isPlaying) {
|
||||||
|
sequenceChangeScheduled = true;
|
||||||
|
SequenceGenerator::generateSequenceData((queuedTheme != -1) ? queuedTheme : currentThemeIndex, nextSequence);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case UI_EDIT_FLAVOUR:
|
case UI_EDIT_FLAVOUR:
|
||||||
{
|
{
|
||||||
currentStrategyIndices[randomizeTrack] += (delta > 0 ? 1 : -1);
|
currentStrategyIndices[randomizeTrack] += (delta > 0 ? 1 : -1);
|
||||||
@ -305,38 +119,6 @@ static void handleInput() {
|
|||||||
trackIntensity[randomizeTrack] = current;
|
trackIntensity[randomizeTrack] = current;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UI_SCALE_EDIT:
|
|
||||||
scaleEditSelection += (delta > 0 ? 1 : -1);
|
|
||||||
if (scaleEditSelection < 0) scaleEditSelection = numScaleNotes + 4;
|
|
||||||
if (scaleEditSelection > numScaleNotes + 4) scaleEditSelection = 0;
|
|
||||||
break;
|
|
||||||
case UI_SCALE_NOTE_EDIT:
|
|
||||||
scaleNotes[scaleEditNoteIndex] += (delta > 0 ? 1 : -1);
|
|
||||||
if (scaleNotes[scaleEditNoteIndex] < 0) scaleNotes[scaleEditNoteIndex] = 11;
|
|
||||||
if (scaleNotes[scaleEditNoteIndex] > 11) scaleNotes[scaleEditNoteIndex] = 0;
|
|
||||||
midi.lock();
|
|
||||||
midi.sendNoteOn(60 + scaleNotes[scaleEditNoteIndex], 100, midiChannels[randomizeTrack]);
|
|
||||||
midi.unlock();
|
|
||||||
delay(50);
|
|
||||||
midi.lock();
|
|
||||||
midi.sendNoteOff(60 + scaleNotes[scaleEditNoteIndex], midiChannels[randomizeTrack]);
|
|
||||||
midi.unlock();
|
|
||||||
break;
|
|
||||||
case UI_SCALE_TRANSPOSE:
|
|
||||||
if (delta != 0) {
|
|
||||||
int shift = delta % 12;
|
|
||||||
if (shift < 0) shift += 12;
|
|
||||||
for(int i=0; i<numScaleNotes; i++) scaleNotes[i] = (scaleNotes[i] + shift) % 12;
|
|
||||||
sortArray(scaleNotes, numScaleNotes);
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
midi.lock();
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
midi.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (currentState == UI_RANDOMIZE_TRACK_EDIT) {
|
if (currentState == UI_RANDOMIZE_TRACK_EDIT) {
|
||||||
randomizeTrack += (delta > 0 ? 1 : -1);
|
randomizeTrack += (delta > 0 ? 1 : -1);
|
||||||
@ -396,18 +178,14 @@ static void handleInput() {
|
|||||||
if (!sequenceChangeScheduled) {
|
if (!sequenceChangeScheduled) {
|
||||||
memcpy(nextSequence, sequence, sizeof(sequence));
|
memcpy(nextSequence, sequence, sizeof(sequence));
|
||||||
}
|
}
|
||||||
generateTrackData(randomizeTrack, theme, nextSequence);
|
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
|
||||||
sequenceChangeScheduled = true;
|
sequenceChangeScheduled = true;
|
||||||
}
|
}
|
||||||
midi.unlock();
|
midi.unlock();
|
||||||
saveSequence(true);
|
saveSequence(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ID_SCALE:
|
case MENU_ID_ROOT: currentState = UI_EDIT_ROOT; break;
|
||||||
currentState = UI_SCALE_EDIT;
|
|
||||||
scaleEditSelection = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break;
|
case MENU_ID_TEMPO: currentState = UI_EDIT_TEMPO; break;
|
||||||
case MENU_ID_STEPS: currentState = UI_EDIT_STEPS; break;
|
case MENU_ID_STEPS: currentState = UI_EDIT_STEPS; break;
|
||||||
|
|
||||||
@ -429,12 +207,19 @@ static void handleInput() {
|
|||||||
case MENU_ID_RESET: factoryReset(); break;
|
case MENU_ID_RESET: factoryReset(); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
if (menuItems[menuSelection].id >= MENU_ID_SCALE_TYPE_CHROMATIC && menuItems[menuSelection].id <= MENU_ID_SCALE_TYPE_CHORD_7) {
|
||||||
|
int typeIndex = menuItems[menuSelection].id - MENU_ID_SCALE_TYPE_CHROMATIC;
|
||||||
|
enabledScaleTypes ^= (1 << typeIndex);
|
||||||
|
if (enabledScaleTypes == 0) enabledScaleTypes = (1 << typeIndex); // Prevent disabling all
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (menuItems[menuSelection].id >= MENU_ID_THEME_1 && menuItems[menuSelection].id <= MENU_ID_THEME_7) {
|
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;
|
const int selectedTheme = menuItems[menuSelection].id - MENU_ID_THEME_1 + 1;
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
queuedTheme = selectedTheme;
|
queuedTheme = selectedTheme;
|
||||||
|
SequenceGenerator::pickRandomScaleType(queuedTheme);
|
||||||
midi.lock();
|
midi.lock();
|
||||||
generateSequenceData(queuedTheme, nextSequence);
|
SequenceGenerator::generateSequenceData(queuedTheme, nextSequence);
|
||||||
sequenceChangeScheduled = true;
|
sequenceChangeScheduled = true;
|
||||||
midi.unlock();
|
midi.unlock();
|
||||||
} else {
|
} else {
|
||||||
@ -448,8 +233,8 @@ static void handleInput() {
|
|||||||
int sub = offset % 8;
|
int sub = offset % 8;
|
||||||
bool isSave = sub >= 4;
|
bool isSave = sub >= 4;
|
||||||
int slot = sub % 4;
|
int slot = sub % 4;
|
||||||
if (isSave) savePatch(bank, slot);
|
if (isSave) Persistence::savePatch(bank, slot);
|
||||||
else loadPatch(bank, slot);
|
else Persistence::loadPatch(bank, slot, local_sequence);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -467,6 +252,10 @@ static void handleInput() {
|
|||||||
currentState = UI_MENU_MAIN;
|
currentState = UI_MENU_MAIN;
|
||||||
saveSequence(true);
|
saveSequence(true);
|
||||||
break;
|
break;
|
||||||
|
case UI_EDIT_ROOT:
|
||||||
|
currentState = UI_MENU_MAIN;
|
||||||
|
saveSequence(true);
|
||||||
|
break;
|
||||||
case UI_EDIT_FLAVOUR:
|
case UI_EDIT_FLAVOUR:
|
||||||
currentState = UI_MENU_MAIN;
|
currentState = UI_MENU_MAIN;
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@ -475,7 +264,7 @@ static void handleInput() {
|
|||||||
if (!sequenceChangeScheduled) {
|
if (!sequenceChangeScheduled) {
|
||||||
memcpy(nextSequence, sequence, sizeof(sequence));
|
memcpy(nextSequence, sequence, sizeof(sequence));
|
||||||
}
|
}
|
||||||
generateTrackData(randomizeTrack, theme, nextSequence);
|
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
|
||||||
sequenceChangeScheduled = true;
|
sequenceChangeScheduled = true;
|
||||||
midi.unlock();
|
midi.unlock();
|
||||||
}
|
}
|
||||||
@ -489,7 +278,7 @@ static void handleInput() {
|
|||||||
if (!sequenceChangeScheduled) {
|
if (!sequenceChangeScheduled) {
|
||||||
memcpy(nextSequence, sequence, sizeof(sequence));
|
memcpy(nextSequence, sequence, sizeof(sequence));
|
||||||
}
|
}
|
||||||
generateTrackData(randomizeTrack, theme, nextSequence);
|
SequenceGenerator::generateTrackData(randomizeTrack, theme, nextSequence);
|
||||||
sequenceChangeScheduled = true;
|
sequenceChangeScheduled = true;
|
||||||
midi.unlock();
|
midi.unlock();
|
||||||
}
|
}
|
||||||
@ -499,71 +288,6 @@ static void handleInput() {
|
|||||||
currentState = UI_MENU_MAIN;
|
currentState = UI_MENU_MAIN;
|
||||||
saveSequence(true);
|
saveSequence(true);
|
||||||
break;
|
break;
|
||||||
case UI_SCALE_EDIT:
|
|
||||||
if (scaleEditSelection == 0) {
|
|
||||||
currentState = UI_MENU_MAIN;
|
|
||||||
saveSequence(true);
|
|
||||||
} else if (scaleEditSelection == 1) {
|
|
||||||
generateRandomScale();
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
midi.lock();
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
midi.unlock();
|
|
||||||
}
|
|
||||||
saveSequence(true);
|
|
||||||
} else if (scaleEditSelection <= numScaleNotes + 1) {
|
|
||||||
scaleEditNoteIndex = scaleEditSelection - 2;
|
|
||||||
currentState = UI_SCALE_NOTE_EDIT;
|
|
||||||
} else if (scaleEditSelection == numScaleNotes + 2) {
|
|
||||||
currentState = UI_SCALE_TRANSPOSE;
|
|
||||||
} else if (scaleEditSelection == numScaleNotes + 3) {
|
|
||||||
if (numScaleNotes < 12) {
|
|
||||||
int next = (numScaleNotes > 0) ? (scaleNotes[numScaleNotes-1] + 1) % 12 : 0;
|
|
||||||
scaleNotes[numScaleNotes] = next;
|
|
||||||
numScaleNotes++;
|
|
||||||
scaleEditSelection--; // Move cursor to the new note
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
midi.lock();
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
midi.unlock();
|
|
||||||
}
|
|
||||||
saveSequence(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (numScaleNotes > 1) {
|
|
||||||
numScaleNotes--;
|
|
||||||
scaleEditSelection--;
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
midi.lock();
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
midi.unlock();
|
|
||||||
}
|
|
||||||
saveSequence(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case UI_SCALE_NOTE_EDIT:
|
|
||||||
sortArray(scaleNotes, numScaleNotes);
|
|
||||||
if (isPlaying) {
|
|
||||||
int theme = (queuedTheme != -1) ? queuedTheme : currentThemeIndex;
|
|
||||||
midi.lock();
|
|
||||||
generateSequenceData(theme, nextSequence);
|
|
||||||
sequenceChangeScheduled = true;
|
|
||||||
midi.unlock();
|
|
||||||
}
|
|
||||||
currentState = UI_SCALE_EDIT;
|
|
||||||
saveSequence(true);
|
|
||||||
break;
|
|
||||||
case UI_SCALE_TRANSPOSE:
|
|
||||||
currentState = UI_SCALE_EDIT;
|
|
||||||
saveSequence(true);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -604,11 +328,7 @@ static void drawUI() {
|
|||||||
local_randomizeTrack = randomizeTrack;
|
local_randomizeTrack = randomizeTrack;
|
||||||
|
|
||||||
local_currentState = currentState;
|
local_currentState = currentState;
|
||||||
if (local_currentState == UI_SCALE_EDIT || local_currentState == UI_SCALE_NOTE_EDIT || local_currentState == UI_SCALE_TRANSPOSE) {
|
local_menuSelection = menuSelection;
|
||||||
local_menuSelection = scaleEditSelection;
|
|
||||||
} else {
|
|
||||||
local_menuSelection = menuSelection;
|
|
||||||
}
|
|
||||||
local_midiChannel = midiChannels[local_randomizeTrack];
|
local_midiChannel = midiChannels[local_randomizeTrack];
|
||||||
local_tempo = tempo;
|
local_tempo = tempo;
|
||||||
local_currentTrackNumSteps = numSteps[local_randomizeTrack];
|
local_currentTrackNumSteps = numSteps[local_randomizeTrack];
|
||||||
@ -675,7 +395,7 @@ static void updateLeds() {
|
|||||||
// It's a TRACK section item (Track, Mute, Flavour, Mutation, Themes)
|
// It's a TRACK section item (Track, Mute, Flavour, Mutation, Themes)
|
||||||
ledDisplayMode = MODE_MONO;
|
ledDisplayMode = MODE_MONO;
|
||||||
}
|
}
|
||||||
} else if (local_currentState == UI_EDIT_STEPS || local_currentState == UI_EDIT_FLAVOUR || local_currentState == UI_RANDOMIZE_TRACK_EDIT || local_currentState == UI_SCALE_EDIT || local_currentState == UI_SCALE_NOTE_EDIT || local_currentState == UI_SCALE_TRANSPOSE) {
|
} 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
|
// These are entered from TRACK section items
|
||||||
ledDisplayMode = MODE_MONO;
|
ledDisplayMode = MODE_MONO;
|
||||||
}
|
}
|
||||||
@ -692,7 +412,8 @@ void loopUI() {
|
|||||||
int nextTheme = random(1, 8); // Themes 1-7
|
int nextTheme = random(1, 8); // Themes 1-7
|
||||||
int repeats = random(1, 9); // 1-8 repeats
|
int repeats = random(1, 9); // 1-8 repeats
|
||||||
|
|
||||||
generateSequenceData(nextTheme, nextSequence);
|
SequenceGenerator::pickRandomScaleType(nextTheme);
|
||||||
|
SequenceGenerator::generateSequenceData(nextTheme, nextSequence);
|
||||||
queuedTheme = nextTheme;
|
queuedTheme = nextTheme;
|
||||||
nextSongRepeats = repeats;
|
nextSongRepeats = repeats;
|
||||||
sequenceChangeScheduled = true;
|
sequenceChangeScheduled = true;
|
||||||
|
|||||||
97
WaveStrategy.h
Normal file
97
WaveStrategy.h
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
#include "MelodyStrategy.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef WAVE_STRATEGY_H
|
||||||
|
#define WAVE_STRATEGY_H
|
||||||
|
|
||||||
|
class WaveStrategy : public MelodyStrategy {
|
||||||
|
public:
|
||||||
|
void generate(Step (*sequence)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int seed, int intensity) override {
|
||||||
|
randomSeed(seed);
|
||||||
|
if (numScaleNotes == 0) return;
|
||||||
|
|
||||||
|
// Wave parameters
|
||||||
|
// Frequency: How many cycles per sequence
|
||||||
|
float freq = (float)random(1, 4) + (intensity * 0.2f);
|
||||||
|
// Phase offset
|
||||||
|
float phase = random(100) / 100.0f * 2.0f * PI;
|
||||||
|
// Waveform type: 0=Sine, 1=Triangle, 2=Saw
|
||||||
|
int type = random(3);
|
||||||
|
|
||||||
|
// Center pitch (note index)
|
||||||
|
int centerIdx = numScaleNotes * 2; // Middle of 4 octaves roughly
|
||||||
|
int amp = numScaleNotes + (intensity); // Amplitude in scale degrees
|
||||||
|
|
||||||
|
for (int i = 0; i < numSteps; i++) {
|
||||||
|
float t = (float)i / (float)numSteps; // 0.0 to 1.0
|
||||||
|
float val = 0.0f;
|
||||||
|
|
||||||
|
if (type == 0) { // Sine
|
||||||
|
val = sin(t * freq * 2.0f * PI + phase);
|
||||||
|
} else if (type == 1) { // Triangle
|
||||||
|
float x = t * freq + phase / (2.0f*PI);
|
||||||
|
x = x - (int)x; // frac
|
||||||
|
val = (x < 0.5f) ? (4.0f * x - 1.0f) : (3.0f - 4.0f * x);
|
||||||
|
} else { // Saw
|
||||||
|
float x = t * freq + phase / (2.0f*PI);
|
||||||
|
x = x - (int)x;
|
||||||
|
val = 2.0f * x - 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map val (-1 to 1) to note index
|
||||||
|
int noteIdxOffset = (int)(val * amp);
|
||||||
|
int totalIdx = centerIdx + noteIdxOffset;
|
||||||
|
|
||||||
|
// Normalize totalIdx
|
||||||
|
while(totalIdx < 0) totalIdx += numScaleNotes;
|
||||||
|
|
||||||
|
int octave = 2 + (totalIdx / numScaleNotes);
|
||||||
|
int scaleIdx = totalIdx % numScaleNotes;
|
||||||
|
|
||||||
|
if (octave < 0) octave = 0;
|
||||||
|
if (octave > 8) octave = 8;
|
||||||
|
|
||||||
|
// Rhythmic density based on intensity
|
||||||
|
if (random(100) < (40 + intensity * 5)) {
|
||||||
|
sequence[track][i].note = 12 * octave + scaleNotes[scaleIdx];
|
||||||
|
sequence[track][i].accent = (val > 0.8f); // Accent peaks
|
||||||
|
sequence[track][i].tie = false;
|
||||||
|
} else {
|
||||||
|
sequence[track][i].note = -1;
|
||||||
|
sequence[track][i].accent = false;
|
||||||
|
sequence[track][i].tie = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
randomSeed(micros());
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateScale(int* scaleNotes, int& numScaleNotes) override {
|
||||||
|
numScaleNotes = random(5, 8);
|
||||||
|
for (int i = 0; i < 12; i++) scaleNotes[i] = i;
|
||||||
|
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)[NUM_STEPS], int track, int numSteps, int* scaleNotes, int numScaleNotes, int intensity) override {
|
||||||
|
// Phase shift the sequence
|
||||||
|
int shift = random(1, 4);
|
||||||
|
Step temp[NUM_STEPS];
|
||||||
|
for(int i=0; i<numSteps; i++) {
|
||||||
|
temp[i] = sequence[track][(i + shift) % numSteps];
|
||||||
|
}
|
||||||
|
for(int i=0; i<numSteps; i++) {
|
||||||
|
sequence[track][i] = temp[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* getName() override {
|
||||||
|
return "Wave";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
Loading…
Reference in New Issue
Block a user