From e363c058234173e65d4fbc93991b78107da5f529 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 18 Mar 2026 13:00:19 -0700 Subject: [PATCH] refactor: Replace LFSR with Xorshift PRNG, store menu strings in PROGMEM, and ensure atomic CV value updates. --- firmware/Rhythm/Rhythm.ino | 163 ++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 73 deletions(-) diff --git a/firmware/Rhythm/Rhythm.ino b/firmware/Rhythm/Rhythm.ino index b4e33a4..7f279aa 100644 --- a/firmware/Rhythm/Rhythm.ino +++ b/firmware/Rhythm/Rhythm.ino @@ -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) {