refactor: Replace LFSR with Xorshift PRNG, store menu strings in PROGMEM, and ensure atomic CV value updates.

This commit is contained in:
2026-03-18 13:00:19 -07:00
parent 59fbd37524
commit e363c05823

View File

@ -81,8 +81,16 @@ volatile int cv1_val = 0;
volatile int cv2_val = 0;
int last_mapped_slot = -1;
// LFSR State for Chaos
uint16_t lfsr = 0xACE1;
// PRNG State for Chaos
uint16_t xorshift_seed = 0xACE1;
// Fast 16-bit xorshift PRNG
uint16_t xorshift16() {
xorshift_seed ^= xorshift_seed << 7;
xorshift_seed ^= xorshift_seed >> 9;
xorshift_seed ^= xorshift_seed << 8;
return xorshift_seed;
}
// Math Helper: 1D Linear Interpolation between two bytes
uint8_t lerp(uint8_t a, uint8_t b, uint8_t t) {
@ -95,21 +103,17 @@ uint8_t GetThreshold(int inst, int step, int x_pos, int y_pos) {
// x_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
// y_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
int x_idx = x_pos / 85;
int y_idx = y_pos / 85;
int x_idx = 0;
uint8_t x_frac = 0;
if (x_pos < 85) { x_idx = 0; x_frac = x_pos * 3; }
else if (x_pos < 170) { x_idx = 1; x_frac = (x_pos - 85) * 3; }
else { x_idx = 2; x_frac = x_pos == 255 ? 255 : (x_pos - 170) * 3; }
uint8_t x_frac = (x_pos % 85) * 3; // scale remainder 0-84 up to 0-255
uint8_t y_frac = (y_pos % 85) * 3;
// Guard against out of bounds if exactly 255
if (x_idx >= 3) {
x_idx = 2;
x_frac = 255;
}
if (y_idx >= 3) {
y_idx = 2;
y_frac = 255;
}
int y_idx = 0;
uint8_t y_frac = 0;
if (y_pos < 85) { y_idx = 0; y_frac = y_pos * 3; }
else if (y_pos < 170) { y_idx = 1; y_frac = (y_pos - 85) * 3; }
else { y_idx = 2; y_frac = y_pos == 255 ? 255 : (y_pos - 170) * 3; }
// Read 4 corners from PROGMEM
uint8_t p00 = pgm_read_byte(&PATTERN_MAPS[x_idx][y_idx][inst][step]);
@ -138,20 +142,13 @@ void SaveState() {
EEPROM.put(EEPROM_PATTERNS, patterns);
}
// LFSR random bit generator (returns 0 or 1, fast)
uint8_t GetRandomBit() {
uint8_t bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1;
lfsr = (lfsr >> 1) | (bit << 15);
return bit;
return xorshift16() & 1;
}
// Get 8-bit pseudo-random number
uint8_t GetRandomByte() {
uint8_t r = 0;
for (int i = 0; i < 8; i++) {
r = (r << 1) | GetRandomBit();
}
return r;
return xorshift16() & 0xFF;
}
void ProcessSequencerTick(uint32_t tick) {
@ -428,7 +425,21 @@ void drawMainSelection() {
gravity.display.drawLine(0, mainHeight, 0, mainHeight - tickSize);
}
void drawMenuItems(String menu_items[], int menu_size, int current_item) {
const char str_kick[] PROGMEM = "KICK";
const char str_snare[] PROGMEM = "SNARE";
const char str_hhat[] PROGMEM = "HHAT";
const char str_chaos[] PROGMEM = "CHAOS";
const char str_mapx[] PROGMEM = "MAP X";
const char str_mapy[] PROGMEM = "MAP Y";
const char* const param_menu_items[] PROGMEM = {str_kick, str_snare, str_hhat, str_chaos, str_mapx, str_mapy};
const char str_source[] PROGMEM = "SOURCE";
const char str_tempo[] PROGMEM = "TEMPO";
const char str_cv1dest[] PROGMEM = "CV1 DEST";
const char str_cv2dest[] PROGMEM = "CV2 DEST";
const char* const global_menu_items[] PROGMEM = {str_source, str_tempo, str_cv1dest, str_cv2dest};
void drawMenuItems(const char* const menu_items[], int menu_size, int current_item) {
gravity.display.setFont(TEXT_FONT);
int selectedBoxY = 0;
@ -461,8 +472,9 @@ void drawMenuItems(String menu_items[], int menu_size, int current_item) {
for (int i = 0; i < min(menu_size, (int)VISIBLE_MENU_ITEMS); ++i) {
int idx = start_index + i;
drawRightAlignedText(menu_items[idx].c_str(),
MENU_ITEM_HEIGHT * (i + 1) - 1);
char buffer[16];
strcpy_P(buffer, (char*)pgm_read_ptr(&(menu_items[idx])));
drawRightAlignedText(buffer, MENU_ITEM_HEIGHT * (i + 1) - 1);
}
}
@ -470,8 +482,8 @@ void DisplayMainArea() {
gravity.display.setFontMode(1);
gravity.display.setDrawColor(2);
String mainText;
String subText;
char mainText[16] = "";
char subText[16] = "";
if (selected_slot > 0) {
PatternState &p = patterns[active_pattern];
@ -494,96 +506,96 @@ void DisplayMainArea() {
switch (current_param) {
case PARAM_KICK_DENS:
mainText = String(map(act_val, 0, 255, 0, 100)) + "%";
subText = "KICK DENS";
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "KICK DENS");
break;
case PARAM_SNARE_DENS:
mainText = String(map(act_val, 0, 255, 0, 100)) + "%";
subText = "SNAR DENS";
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "SNAR DENS");
break;
case PARAM_HIHAT_DENS:
mainText = String(map(act_val, 0, 255, 0, 100)) + "%";
subText = "HHAT DENS";
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "HHAT DENS");
break;
case PARAM_CHAOS:
mainText = String(map(act_val, 0, 255, 0, 100)) + "%";
subText = "CHAOS";
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "CHAOS");
break;
case PARAM_MAP_X:
mainText = String(act_val);
subText = "MAP X";
itoa(act_val, mainText, 10);
strcpy(subText, "MAP X");
break;
case PARAM_MAP_Y:
mainText = String(act_val);
subText = "MAP Y";
itoa(act_val, mainText, 10);
strcpy(subText, "MAP Y");
break;
default:
break;
}
drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
String menu_items[PARAM_LAST] = {
"KICK", "SNARE", "HHAT", "CHAOS", "MAP X", "MAP Y"};
drawMenuItems(menu_items, PARAM_LAST, (int)current_param);
drawMenuItems(param_menu_items, PARAM_LAST, (int)current_param);
} else {
switch (current_global_param) {
case PARAM_GLOBAL_CLK_SRC:
switch (selected_source) {
case Clock::SOURCE_INTERNAL:
mainText = F("INT");
subText = F("CLOCK");
strcpy_P(mainText, PSTR("INT"));
strcpy_P(subText, PSTR("CLOCK"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_24:
mainText = F("EXT");
subText = F("24 PPQN");
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("24 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_4:
mainText = F("EXT");
subText = F("4 PPQN");
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("4 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_2:
mainText = F("EXT");
subText = F("2 PPQN");
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("2 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_1:
mainText = F("EXT");
subText = F("1 PPQN");
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("1 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_MIDI:
mainText = F("EXT");
subText = F("MIDI");
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("MIDI"));
break;
}
break;
case PARAM_GLOBAL_BPM:
if (gravity.clock.ExternalSource()) {
mainText = F("EXT");
strcpy_P(mainText, PSTR("EXT"));
} else {
mainText = String(gravity.clock.Tempo());
itoa(gravity.clock.Tempo(), mainText, 10);
}
subText = F("BPM");
strcpy_P(subText, PSTR("BPM"));
break;
case PARAM_GLOBAL_CV1_DEST:
mainText = F("CV1");
subText = GetCvDestName(cv1_dest);
strcpy_P(mainText, PSTR("CV1"));
strcpy(subText, GetCvDestName(cv1_dest));
break;
case PARAM_GLOBAL_CV2_DEST:
mainText = F("CV2");
subText = GetCvDestName(cv2_dest);
strcpy_P(mainText, PSTR("CV2"));
strcpy(subText, GetCvDestName(cv2_dest));
break;
default:
break;
}
drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
String menu_items[PARAM_GLOBAL_LAST] = {
"SOURCE", "TEMPO", "CV1 DEST", "CV2 DEST"};
drawMenuItems(menu_items, PARAM_GLOBAL_LAST, (int)current_global_param);
drawMenuItems(global_menu_items, PARAM_GLOBAL_LAST, (int)current_global_param);
}
}
@ -653,8 +665,13 @@ void loop() {
static int last_cv1_val = 0;
static int last_cv2_val = 0;
cv1_val = gravity.cv1.Read(); // -512 to 512
cv2_val = gravity.cv2.Read();
int cv1_temp = gravity.cv1.Read(); // -512 to 512
int cv2_temp = gravity.cv2.Read();
noInterrupts();
cv1_val = cv1_temp;
cv2_val = cv2_temp;
interrupts();
// Trigger redraw if we're not editing and the CV modulating the selected param changes
if (!editing_param && selected_slot > 0) {