diff --git a/firmware/Rhythm/Rhythm.ino b/firmware/Rhythm/Rhythm.ino index 3b6b20d..2f0beaa 100644 --- a/firmware/Rhythm/Rhythm.ino +++ b/firmware/Rhythm/Rhythm.ino @@ -10,13 +10,10 @@ const int MAX_CHAOS = 255; // EEPROM addrs const int EEPROM_INIT_ADDR = 0; -const int EEPROM_DENS_K = 1; -const int EEPROM_DENS_S = 3; -const int EEPROM_DENS_H = 5; -const int EEPROM_MAP_X = 7; -const int EEPROM_MAP_Y = 9; -const int EEPROM_CHAOS = 11; -const byte EEPROM_INIT_FLAG = 0xAC; // Update flag to re-init +const int EEPROM_CV1 = EEPROM_INIT_ADDR + sizeof(byte); +const int EEPROM_CV2 = EEPROM_CV1 + sizeof(int); +const int EEPROM_PATTERNS = EEPROM_CV2 + sizeof(int); +const byte EEPROM_INIT_FLAG = 0xAF; // Update flag to re-init properly allocated memory // EEPROM Delay Save const unsigned long SAVE_DELAY_MS = 5000; @@ -24,10 +21,16 @@ bool eeprom_needs_save = false; unsigned long last_param_change = 0; // Menus and State -enum MenuPage { - PAGE_PATTERN = 0, - PAGE_GLOBAL = 1, +struct PatternState { + int inst_density[3] = {128, 128, 128}; + 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 { PARAM_GLOBAL_CLK_SRC = 0, @@ -45,10 +48,10 @@ enum CvDest { CV_DEST_CHAOS = 4, CV_DEST_MAP_X = 5, 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; CvDest cv1_dest = CV_DEST_MAP_X; CvDest cv2_dest = CV_DEST_CHAOS; @@ -74,13 +77,6 @@ const unsigned long REDRAW_DELAY_MS = 30; // ~33fps limit // Sequencer State 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 cv2_val = 0; @@ -128,29 +124,17 @@ uint8_t GetThreshold(int inst, int step, int x_pos, int y_pos) { void LoadState() { if (EEPROM.read(EEPROM_INIT_ADDR) == EEPROM_INIT_FLAG) { - EEPROM.get(EEPROM_DENS_K, inst_density[0]); - EEPROM.get(EEPROM_DENS_S, inst_density[1]); - EEPROM.get(EEPROM_DENS_H, inst_density[2]); - 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. + EEPROM.get(EEPROM_CV1, cv1_dest); + EEPROM.get(EEPROM_CV2, cv2_dest); + EEPROM.get(EEPROM_PATTERNS, patterns); } } void SaveState() { EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG); - EEPROM.put(EEPROM_DENS_K, inst_density[0]); - EEPROM.put(EEPROM_DENS_S, inst_density[1]); - EEPROM.put(EEPROM_DENS_H, inst_density[2]); - EEPROM.put(EEPROM_MAP_X, map_x); - EEPROM.put(EEPROM_MAP_Y, map_y); - EEPROM.put(EEPROM_CHAOS, chaos_amount); + EEPROM.put(EEPROM_CV1, cv1_dest); + EEPROM.put(EEPROM_CV2, cv2_dest); + EEPROM.put(EEPROM_PATTERNS, patterns); } // LFSR random bit generator (returns 0 or 1, fast) @@ -184,10 +168,25 @@ void ProcessSequencerTick(uint32_t tick) { // Handle new 16th note step if (tick % PULSES_PER_16TH == 0) { - int active_map_x = map_x; - int active_map_y = map_y; - int active_chaos = chaos_amount; - int active_dens[3] = { inst_density[0], inst_density[1], inst_density[2] }; + // Evaluate CV Preset changes FIRST so params pull from the right struct + int preset_cv = 0; + if (cv1_dest == CV_DEST_PRESET) preset_cv += cv1_val; + 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 switch (cv1_dest) { @@ -263,11 +262,9 @@ void OnEncoderPress() { } void OnEncoderPressRotate(int val) { - // Toggle between Pattern and Global pages - if (val > 0) { - current_page = PAGE_GLOBAL; - } else { - current_page = PAGE_PATTERN; + selected_slot = constrain(selected_slot + val, 0, 5); + if (selected_slot > 0) { + active_pattern = selected_slot - 1; } // Reset editing state when swapping pages @@ -285,14 +282,15 @@ const char* GetCvDestName(CvDest dest) { case CV_DEST_CHAOS: return "CHAOS"; case CV_DEST_MAP_X: return "MAP X"; case CV_DEST_MAP_Y: return "MAP Y"; - default: return "UNKNOWN"; + case CV_DEST_PRESET: return "PRESET"; + default: return ""; } } void OnEncoderRotate(int val) { if (!editing_param) { // Navigate menu (clamp to edges, do not wrap) - if (current_page == PAGE_PATTERN) { + if (selected_slot > 0) { int next_param = (int)current_param + val; next_param = constrain(next_param, 0, PARAM_LAST - 1); current_param = (SelectedParam)next_param; @@ -303,27 +301,28 @@ void OnEncoderRotate(int val) { } } else { // 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 + PatternState &p = patterns[active_pattern]; switch (current_param) { 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; 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; 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; case PARAM_CHAOS: - chaos_amount = constrain(chaos_amount + amt, 0, 255); + p.chaos_amount = constrain(p.chaos_amount + amt, 0, 255); break; case PARAM_MAP_X: - map_x = constrain(map_x + amt, 0, 255); + p.map_x = constrain(p.map_x + amt, 0, 255); break; case PARAM_MAP_Y: - map_y = constrain(map_y + amt, 0, 255); + p.map_y = constrain(p.map_y + amt, 0, 255); break; default: break; @@ -451,30 +450,31 @@ void DisplayMainArea() { String mainText; String subText; - if (current_page == PAGE_PATTERN) { + if (selected_slot > 0) { + PatternState &p = patterns[active_pattern]; switch (current_param) { 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"; break; 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"; break; 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"; break; case PARAM_CHAOS: - mainText = String(map(chaos_amount, 0, 255, 0, 100)) + "%"; + mainText = String(map(p.chaos_amount, 0, 255, 0, 100)) + "%"; subText = "CHAOS"; break; case PARAM_MAP_X: - mainText = String(map_x); + mainText = String(p.map_x); subText = "MAP X"; break; case PARAM_MAP_Y: - mainText = String(map_y); + mainText = String(p.map_y); subText = "MAP Y"; break; default: @@ -488,7 +488,7 @@ void DisplayMainArea() { "KICK", "SNARE", "HHAT", "CHAOS", "MAP X", "MAP Y"}; drawMenuItems(menu_items, PARAM_LAST, (int)current_param); - } else if (current_page == PAGE_GLOBAL) { + } else { switch (current_global_param) { case PARAM_GLOBAL_CLK_SRC: switch (selected_source) { @@ -550,36 +550,36 @@ void DisplayMainArea() { void DisplayBottomBar() { int boxY = 50; int boxHeight = 14; - int boxX_Pattern = 18; - int boxWidth = 55; - int boxX_Global = boxX_Pattern + boxWidth; gravity.display.setDrawColor(1); gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2); gravity.display.drawVLine(SCREEN_WIDTH - 1, boxY, boxHeight); - if (current_page == PAGE_PATTERN) { - gravity.display.drawBox(boxX_Pattern, boxY, boxWidth, boxHeight); - gravity.display.drawVLine(boxX_Global, boxY, boxHeight); - } else { - gravity.display.drawVLine(boxX_Pattern, boxY, boxHeight); - gravity.display.drawBox(boxX_Global, boxY, boxWidth, boxHeight); + for (int i = 0; i <= 5; i++) { + int x = i * 21; + int bw = (i == 5) ? (SCREEN_WIDTH - x) : 21; + + gravity.display.setDrawColor(1); + 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() {