feat: Implement multi-pattern storage and selection with dedicated UI and CV control.
This commit is contained in:
@ -10,13 +10,10 @@ const int MAX_CHAOS = 255;
|
|||||||
|
|
||||||
// EEPROM addrs
|
// EEPROM addrs
|
||||||
const int EEPROM_INIT_ADDR = 0;
|
const int EEPROM_INIT_ADDR = 0;
|
||||||
const int EEPROM_DENS_K = 1;
|
const int EEPROM_CV1 = EEPROM_INIT_ADDR + sizeof(byte);
|
||||||
const int EEPROM_DENS_S = 3;
|
const int EEPROM_CV2 = EEPROM_CV1 + sizeof(int);
|
||||||
const int EEPROM_DENS_H = 5;
|
const int EEPROM_PATTERNS = EEPROM_CV2 + sizeof(int);
|
||||||
const int EEPROM_MAP_X = 7;
|
const byte EEPROM_INIT_FLAG = 0xAF; // Update flag to re-init properly allocated memory
|
||||||
const int EEPROM_MAP_Y = 9;
|
|
||||||
const int EEPROM_CHAOS = 11;
|
|
||||||
const byte EEPROM_INIT_FLAG = 0xAC; // Update flag to re-init
|
|
||||||
|
|
||||||
// EEPROM Delay Save
|
// EEPROM Delay Save
|
||||||
const unsigned long SAVE_DELAY_MS = 5000;
|
const unsigned long SAVE_DELAY_MS = 5000;
|
||||||
@ -24,10 +21,16 @@ bool eeprom_needs_save = false;
|
|||||||
unsigned long last_param_change = 0;
|
unsigned long last_param_change = 0;
|
||||||
|
|
||||||
// Menus and State
|
// Menus and State
|
||||||
enum MenuPage {
|
struct PatternState {
|
||||||
PAGE_PATTERN = 0,
|
int inst_density[3] = {128, 128, 128};
|
||||||
PAGE_GLOBAL = 1,
|
int map_x = 0;
|
||||||
|
int map_y = 127;
|
||||||
|
int chaos_amount = 0;
|
||||||
};
|
};
|
||||||
|
PatternState patterns[5];
|
||||||
|
|
||||||
|
int selected_slot = 1; // 0 = Global, 1-5 = Pattern A-E
|
||||||
|
int active_pattern = 0; // Tracks playback pattern index (0-4)
|
||||||
|
|
||||||
enum GlobalParam {
|
enum GlobalParam {
|
||||||
PARAM_GLOBAL_CLK_SRC = 0,
|
PARAM_GLOBAL_CLK_SRC = 0,
|
||||||
@ -45,10 +48,10 @@ enum CvDest {
|
|||||||
CV_DEST_CHAOS = 4,
|
CV_DEST_CHAOS = 4,
|
||||||
CV_DEST_MAP_X = 5,
|
CV_DEST_MAP_X = 5,
|
||||||
CV_DEST_MAP_Y = 6,
|
CV_DEST_MAP_Y = 6,
|
||||||
CV_DEST_LAST = 7
|
CV_DEST_PRESET = 7,
|
||||||
|
CV_DEST_LAST = 8
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuPage current_page = PAGE_PATTERN;
|
|
||||||
GlobalParam current_global_param = PARAM_GLOBAL_CLK_SRC;
|
GlobalParam current_global_param = PARAM_GLOBAL_CLK_SRC;
|
||||||
CvDest cv1_dest = CV_DEST_MAP_X;
|
CvDest cv1_dest = CV_DEST_MAP_X;
|
||||||
CvDest cv2_dest = CV_DEST_CHAOS;
|
CvDest cv2_dest = CV_DEST_CHAOS;
|
||||||
@ -74,13 +77,6 @@ const unsigned long REDRAW_DELAY_MS = 30; // ~33fps limit
|
|||||||
// Sequencer State
|
// Sequencer State
|
||||||
int current_step = 0;
|
int current_step = 0;
|
||||||
|
|
||||||
// Engine Parameters (0-255)
|
|
||||||
int inst_density[3] = {128, 128,
|
|
||||||
128}; // Default 50% density for kick, snare, hihat
|
|
||||||
int map_x = 0; // 0 to 255 (0 = House, 127 = Breakbeat, 255 = Hiphop)
|
|
||||||
int map_y = 127; // 0 to 255 (0 = Sparse, 127 = Standard, 255 = Busy)
|
|
||||||
int chaos_amount = 0; // 0 to 255
|
|
||||||
|
|
||||||
volatile int cv1_val = 0;
|
volatile int cv1_val = 0;
|
||||||
volatile int cv2_val = 0;
|
volatile int cv2_val = 0;
|
||||||
|
|
||||||
@ -128,29 +124,17 @@ uint8_t GetThreshold(int inst, int step, int x_pos, int y_pos) {
|
|||||||
|
|
||||||
void LoadState() {
|
void LoadState() {
|
||||||
if (EEPROM.read(EEPROM_INIT_ADDR) == EEPROM_INIT_FLAG) {
|
if (EEPROM.read(EEPROM_INIT_ADDR) == EEPROM_INIT_FLAG) {
|
||||||
EEPROM.get(EEPROM_DENS_K, inst_density[0]);
|
EEPROM.get(EEPROM_CV1, cv1_dest);
|
||||||
EEPROM.get(EEPROM_DENS_S, inst_density[1]);
|
EEPROM.get(EEPROM_CV2, cv2_dest);
|
||||||
EEPROM.get(EEPROM_DENS_H, inst_density[2]);
|
EEPROM.get(EEPROM_PATTERNS, patterns);
|
||||||
EEPROM.get(EEPROM_MAP_X, map_x);
|
|
||||||
EEPROM.get(EEPROM_MAP_Y, map_y);
|
|
||||||
EEPROM.get(EEPROM_CHAOS, chaos_amount);
|
|
||||||
|
|
||||||
// We can just add read for newly added properties.
|
|
||||||
// Wait until later to strictly map out addresses manually
|
|
||||||
// or just let EEPROM.get/put figure it out relative to addresses.
|
|
||||||
// For simplicity, let's keep it safe. We will increment the EEPROM_INIT_FLAG
|
|
||||||
// to 0xAD and append the new values after EEPROM_CHAOS.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveState() {
|
void SaveState() {
|
||||||
EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
|
EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
|
||||||
EEPROM.put(EEPROM_DENS_K, inst_density[0]);
|
EEPROM.put(EEPROM_CV1, cv1_dest);
|
||||||
EEPROM.put(EEPROM_DENS_S, inst_density[1]);
|
EEPROM.put(EEPROM_CV2, cv2_dest);
|
||||||
EEPROM.put(EEPROM_DENS_H, inst_density[2]);
|
EEPROM.put(EEPROM_PATTERNS, patterns);
|
||||||
EEPROM.put(EEPROM_MAP_X, map_x);
|
|
||||||
EEPROM.put(EEPROM_MAP_Y, map_y);
|
|
||||||
EEPROM.put(EEPROM_CHAOS, chaos_amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LFSR random bit generator (returns 0 or 1, fast)
|
// LFSR random bit generator (returns 0 or 1, fast)
|
||||||
@ -184,10 +168,25 @@ void ProcessSequencerTick(uint32_t tick) {
|
|||||||
// Handle new 16th note step
|
// Handle new 16th note step
|
||||||
if (tick % PULSES_PER_16TH == 0) {
|
if (tick % PULSES_PER_16TH == 0) {
|
||||||
|
|
||||||
int active_map_x = map_x;
|
// Evaluate CV Preset changes FIRST so params pull from the right struct
|
||||||
int active_map_y = map_y;
|
int preset_cv = 0;
|
||||||
int active_chaos = chaos_amount;
|
if (cv1_dest == CV_DEST_PRESET) preset_cv += cv1_val;
|
||||||
int active_dens[3] = { inst_density[0], inst_density[1], inst_density[2] };
|
if (cv2_dest == CV_DEST_PRESET) preset_cv += cv2_val;
|
||||||
|
|
||||||
|
if (cv1_dest == CV_DEST_PRESET || cv2_dest == CV_DEST_PRESET) {
|
||||||
|
int mapped_slot = constrain(map(preset_cv, 0, 512, 0, 4), 0, 4);
|
||||||
|
if (active_pattern != mapped_slot) {
|
||||||
|
active_pattern = mapped_slot;
|
||||||
|
selected_slot = mapped_slot + 1;
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PatternState &p = patterns[active_pattern];
|
||||||
|
int active_map_x = p.map_x;
|
||||||
|
int active_map_y = p.map_y;
|
||||||
|
int active_chaos = p.chaos_amount;
|
||||||
|
int active_dens[3] = { p.inst_density[0], p.inst_density[1], p.inst_density[2] };
|
||||||
|
|
||||||
// Apply CV1
|
// Apply CV1
|
||||||
switch (cv1_dest) {
|
switch (cv1_dest) {
|
||||||
@ -263,11 +262,9 @@ void OnEncoderPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OnEncoderPressRotate(int val) {
|
void OnEncoderPressRotate(int val) {
|
||||||
// Toggle between Pattern and Global pages
|
selected_slot = constrain(selected_slot + val, 0, 5);
|
||||||
if (val > 0) {
|
if (selected_slot > 0) {
|
||||||
current_page = PAGE_GLOBAL;
|
active_pattern = selected_slot - 1;
|
||||||
} else {
|
|
||||||
current_page = PAGE_PATTERN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset editing state when swapping pages
|
// Reset editing state when swapping pages
|
||||||
@ -285,14 +282,15 @@ const char* GetCvDestName(CvDest dest) {
|
|||||||
case CV_DEST_CHAOS: return "CHAOS";
|
case CV_DEST_CHAOS: return "CHAOS";
|
||||||
case CV_DEST_MAP_X: return "MAP X";
|
case CV_DEST_MAP_X: return "MAP X";
|
||||||
case CV_DEST_MAP_Y: return "MAP Y";
|
case CV_DEST_MAP_Y: return "MAP Y";
|
||||||
default: return "UNKNOWN";
|
case CV_DEST_PRESET: return "PRESET";
|
||||||
|
default: return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnEncoderRotate(int val) {
|
void OnEncoderRotate(int val) {
|
||||||
if (!editing_param) {
|
if (!editing_param) {
|
||||||
// Navigate menu (clamp to edges, do not wrap)
|
// Navigate menu (clamp to edges, do not wrap)
|
||||||
if (current_page == PAGE_PATTERN) {
|
if (selected_slot > 0) {
|
||||||
int next_param = (int)current_param + val;
|
int next_param = (int)current_param + val;
|
||||||
next_param = constrain(next_param, 0, PARAM_LAST - 1);
|
next_param = constrain(next_param, 0, PARAM_LAST - 1);
|
||||||
current_param = (SelectedParam)next_param;
|
current_param = (SelectedParam)next_param;
|
||||||
@ -303,27 +301,28 @@ void OnEncoderRotate(int val) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Edit parameter
|
// Edit parameter
|
||||||
if (current_page == PAGE_PATTERN) {
|
if (selected_slot > 0) {
|
||||||
int amt = val * 8; // Adjust by 8 values at a time for speed mapping
|
int amt = val * 8; // Adjust by 8 values at a time for speed mapping
|
||||||
|
PatternState &p = patterns[active_pattern];
|
||||||
|
|
||||||
switch (current_param) {
|
switch (current_param) {
|
||||||
case PARAM_KICK_DENS:
|
case PARAM_KICK_DENS:
|
||||||
inst_density[0] = constrain(inst_density[0] + amt, 0, 255);
|
p.inst_density[0] = constrain(p.inst_density[0] + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
case PARAM_SNARE_DENS:
|
case PARAM_SNARE_DENS:
|
||||||
inst_density[1] = constrain(inst_density[1] + amt, 0, 255);
|
p.inst_density[1] = constrain(p.inst_density[1] + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
case PARAM_HIHAT_DENS:
|
case PARAM_HIHAT_DENS:
|
||||||
inst_density[2] = constrain(inst_density[2] + amt, 0, 255);
|
p.inst_density[2] = constrain(p.inst_density[2] + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
case PARAM_CHAOS:
|
case PARAM_CHAOS:
|
||||||
chaos_amount = constrain(chaos_amount + amt, 0, 255);
|
p.chaos_amount = constrain(p.chaos_amount + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
case PARAM_MAP_X:
|
case PARAM_MAP_X:
|
||||||
map_x = constrain(map_x + amt, 0, 255);
|
p.map_x = constrain(p.map_x + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
case PARAM_MAP_Y:
|
case PARAM_MAP_Y:
|
||||||
map_y = constrain(map_y + amt, 0, 255);
|
p.map_y = constrain(p.map_y + amt, 0, 255);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -451,30 +450,31 @@ void DisplayMainArea() {
|
|||||||
String mainText;
|
String mainText;
|
||||||
String subText;
|
String subText;
|
||||||
|
|
||||||
if (current_page == PAGE_PATTERN) {
|
if (selected_slot > 0) {
|
||||||
|
PatternState &p = patterns[active_pattern];
|
||||||
switch (current_param) {
|
switch (current_param) {
|
||||||
case PARAM_KICK_DENS:
|
case PARAM_KICK_DENS:
|
||||||
mainText = String(map(inst_density[0], 0, 255, 0, 100)) + "%";
|
mainText = String(map(p.inst_density[0], 0, 255, 0, 100)) + "%";
|
||||||
subText = "KICK DENS";
|
subText = "KICK DENS";
|
||||||
break;
|
break;
|
||||||
case PARAM_SNARE_DENS:
|
case PARAM_SNARE_DENS:
|
||||||
mainText = String(map(inst_density[1], 0, 255, 0, 100)) + "%";
|
mainText = String(map(p.inst_density[1], 0, 255, 0, 100)) + "%";
|
||||||
subText = "SNAR DENS";
|
subText = "SNAR DENS";
|
||||||
break;
|
break;
|
||||||
case PARAM_HIHAT_DENS:
|
case PARAM_HIHAT_DENS:
|
||||||
mainText = String(map(inst_density[2], 0, 255, 0, 100)) + "%";
|
mainText = String(map(p.inst_density[2], 0, 255, 0, 100)) + "%";
|
||||||
subText = "HHAT DENS";
|
subText = "HHAT DENS";
|
||||||
break;
|
break;
|
||||||
case PARAM_CHAOS:
|
case PARAM_CHAOS:
|
||||||
mainText = String(map(chaos_amount, 0, 255, 0, 100)) + "%";
|
mainText = String(map(p.chaos_amount, 0, 255, 0, 100)) + "%";
|
||||||
subText = "CHAOS";
|
subText = "CHAOS";
|
||||||
break;
|
break;
|
||||||
case PARAM_MAP_X:
|
case PARAM_MAP_X:
|
||||||
mainText = String(map_x);
|
mainText = String(p.map_x);
|
||||||
subText = "MAP X";
|
subText = "MAP X";
|
||||||
break;
|
break;
|
||||||
case PARAM_MAP_Y:
|
case PARAM_MAP_Y:
|
||||||
mainText = String(map_y);
|
mainText = String(p.map_y);
|
||||||
subText = "MAP Y";
|
subText = "MAP Y";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -488,7 +488,7 @@ void DisplayMainArea() {
|
|||||||
"KICK", "SNARE", "HHAT", "CHAOS", "MAP X", "MAP Y"};
|
"KICK", "SNARE", "HHAT", "CHAOS", "MAP X", "MAP Y"};
|
||||||
drawMenuItems(menu_items, PARAM_LAST, (int)current_param);
|
drawMenuItems(menu_items, PARAM_LAST, (int)current_param);
|
||||||
|
|
||||||
} else if (current_page == PAGE_GLOBAL) {
|
} else {
|
||||||
switch (current_global_param) {
|
switch (current_global_param) {
|
||||||
case PARAM_GLOBAL_CLK_SRC:
|
case PARAM_GLOBAL_CLK_SRC:
|
||||||
switch (selected_source) {
|
switch (selected_source) {
|
||||||
@ -550,36 +550,36 @@ void DisplayMainArea() {
|
|||||||
void DisplayBottomBar() {
|
void DisplayBottomBar() {
|
||||||
int boxY = 50;
|
int boxY = 50;
|
||||||
int boxHeight = 14;
|
int boxHeight = 14;
|
||||||
int boxX_Pattern = 18;
|
|
||||||
int boxWidth = 55;
|
|
||||||
int boxX_Global = boxX_Pattern + boxWidth;
|
|
||||||
|
|
||||||
gravity.display.setDrawColor(1);
|
gravity.display.setDrawColor(1);
|
||||||
gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2);
|
gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2);
|
||||||
gravity.display.drawVLine(SCREEN_WIDTH - 1, boxY, boxHeight);
|
gravity.display.drawVLine(SCREEN_WIDTH - 1, boxY, boxHeight);
|
||||||
|
|
||||||
if (current_page == PAGE_PATTERN) {
|
for (int i = 0; i <= 5; i++) {
|
||||||
gravity.display.drawBox(boxX_Pattern, boxY, boxWidth, boxHeight);
|
int x = i * 21;
|
||||||
gravity.display.drawVLine(boxX_Global, boxY, boxHeight);
|
int bw = (i == 5) ? (SCREEN_WIDTH - x) : 21;
|
||||||
} else {
|
|
||||||
gravity.display.drawVLine(boxX_Pattern, boxY, boxHeight);
|
gravity.display.setDrawColor(1);
|
||||||
gravity.display.drawBox(boxX_Global, boxY, boxWidth, boxHeight);
|
if (selected_slot == i) {
|
||||||
|
gravity.display.drawBox(x, boxY, bw, boxHeight);
|
||||||
|
} else {
|
||||||
|
gravity.display.drawVLine(x, boxY, boxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
gravity.display.setDrawColor(2);
|
||||||
|
if (i == 0) {
|
||||||
|
gravity.display.setBitmapMode(1);
|
||||||
|
auto icon = gravity.clock.IsPaused() ? pause_icon : play_icon;
|
||||||
|
int iconX = x + (bw / 2) - (play_icon_width / 2);
|
||||||
|
gravity.display.drawXBMP(iconX, boxY, play_icon_width, play_icon_height, icon);
|
||||||
|
} else {
|
||||||
|
char label[2] = {(char)('A' + i - 1), '\0'};
|
||||||
|
gravity.display.setFont(TEXT_FONT);
|
||||||
|
int patW = gravity.display.getStrWidth(label);
|
||||||
|
int patX = x + (bw / 2) - (patW / 2);
|
||||||
|
gravity.display.drawStr(patX, 64 - 3, label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gravity.display.setDrawColor(2);
|
|
||||||
gravity.display.setBitmapMode(1);
|
|
||||||
auto icon = gravity.clock.IsPaused() ? pause_icon : play_icon;
|
|
||||||
gravity.display.drawXBMP(2, boxY, play_icon_width, play_icon_height, icon);
|
|
||||||
|
|
||||||
gravity.display.setFont(TEXT_FONT);
|
|
||||||
|
|
||||||
int patW = gravity.display.getStrWidth("PATTERN");
|
|
||||||
int patX = boxX_Pattern + (boxWidth / 2) - (patW / 2);
|
|
||||||
gravity.display.drawStr(patX, 64 - 3, "PATTERN");
|
|
||||||
|
|
||||||
int gloW = gravity.display.getStrWidth("GLOBAL");
|
|
||||||
int gloX = boxX_Global + (boxWidth / 2) - (gloW / 2);
|
|
||||||
gravity.display.drawStr(gloX, 64 - 3, "GLOBAL");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateDisplay() {
|
void UpdateDisplay() {
|
||||||
|
|||||||
Reference in New Issue
Block a user