From a70b567a330b6b21ecd8fe3c80a25d41ec091418 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 17 Mar 2026 21:11:47 -0700 Subject: [PATCH] feat: Introduce global menu for clock and CV destination parameters, integrate CV modulation, and refactor display rendering. --- firmware/Rhythm/Rhythm.ino | 473 ++++++++++++++++++++++++------- firmware/Rhythm/display_assets.h | 90 ++++++ 2 files changed, 456 insertions(+), 107 deletions(-) create mode 100644 firmware/Rhythm/display_assets.h diff --git a/firmware/Rhythm/Rhythm.ino b/firmware/Rhythm/Rhythm.ino index 6c73e9b..3b6b20d 100644 --- a/firmware/Rhythm/Rhythm.ino +++ b/firmware/Rhythm/Rhythm.ino @@ -23,6 +23,37 @@ const unsigned long SAVE_DELAY_MS = 5000; bool eeprom_needs_save = false; unsigned long last_param_change = 0; +// Menus and State +enum MenuPage { + PAGE_PATTERN = 0, + PAGE_GLOBAL = 1, +}; + +enum GlobalParam { + PARAM_GLOBAL_CLK_SRC = 0, + PARAM_GLOBAL_BPM = 1, + PARAM_GLOBAL_CV1_DEST = 2, + PARAM_GLOBAL_CV2_DEST = 3, + PARAM_GLOBAL_LAST = 4 +}; + +enum CvDest { + CV_DEST_NONE = 0, + CV_DEST_KICK_DENS = 1, + CV_DEST_SNARE_DENS = 2, + CV_DEST_HHAT_DENS = 3, + CV_DEST_CHAOS = 4, + CV_DEST_MAP_X = 5, + CV_DEST_MAP_Y = 6, + CV_DEST_LAST = 7 +}; + +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; +Clock::Source selected_source = Clock::SOURCE_INTERNAL; + // UI & Navigation enum SelectedParam { PARAM_KICK_DENS = 0, @@ -42,7 +73,6 @@ const unsigned long REDRAW_DELAY_MS = 30; // ~33fps limit // Sequencer State int current_step = 0; -bool is_playing = false; // Engine Parameters (0-255) int inst_density[3] = {128, 128, @@ -104,6 +134,12 @@ void LoadState() { 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. } } @@ -148,13 +184,37 @@ void ProcessSequencerTick(uint32_t tick) { // Handle new 16th note step if (tick % PULSES_PER_16TH == 0) { - int mod_map_x = constrain(map_x + (cv1_val / 2), 0, 255); - int active_chaos = constrain(chaos_amount + (cv2_val / 2), 0, 255); + 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] }; + + // Apply CV1 + switch (cv1_dest) { + case CV_DEST_KICK_DENS: active_dens[0] = constrain(active_dens[0] + (cv1_val / 4), 0, 255); break; + case CV_DEST_SNARE_DENS: active_dens[1] = constrain(active_dens[1] + (cv1_val / 4), 0, 255); break; + case CV_DEST_HHAT_DENS: active_dens[2] = constrain(active_dens[2] + (cv1_val / 4), 0, 255); break; + case CV_DEST_CHAOS: active_chaos = constrain(active_chaos + (cv1_val / 4), 0, 255); break; + case CV_DEST_MAP_X: active_map_x = constrain(active_map_x + (cv1_val / 4), 0, 255); break; + case CV_DEST_MAP_Y: active_map_y = constrain(active_map_y + (cv1_val / 4), 0, 255); break; + default: break; + } + + // Apply CV2 + switch (cv2_dest) { + case CV_DEST_KICK_DENS: active_dens[0] = constrain(active_dens[0] + (cv2_val / 4), 0, 255); break; + case CV_DEST_SNARE_DENS: active_dens[1] = constrain(active_dens[1] + (cv2_val / 4), 0, 255); break; + case CV_DEST_HHAT_DENS: active_dens[2] = constrain(active_dens[2] + (cv2_val / 4), 0, 255); break; + case CV_DEST_CHAOS: active_chaos = constrain(active_chaos + (cv2_val / 4), 0, 255); break; + case CV_DEST_MAP_X: active_map_x = constrain(active_map_x + (cv2_val / 4), 0, 255); break; + case CV_DEST_MAP_Y: active_map_y = constrain(active_map_y + (cv2_val / 4), 0, 255); break; + default: break; + } // Evaluate hits for Kick, Snare, HiHats for (int inst = 0; inst < 3; inst++) { - uint8_t threshold = GetThreshold(inst, current_step, mod_map_x, map_y); - int active_density = inst_density[inst]; + uint8_t threshold = GetThreshold(inst, current_step, active_map_x, active_map_y); + int active_density = active_dens[inst]; // Inject chaos if (active_chaos > 0) { @@ -187,14 +247,13 @@ void ProcessSequencerTick(uint32_t tick) { } void OnPlayPress() { - if (is_playing) { + if (!gravity.clock.IsPaused()) { gravity.clock.Stop(); for (int i = 0; i < 6; i++) gravity.outputs[i].Low(); } else { gravity.clock.Start(); } - is_playing = !is_playing; needs_redraw = true; } @@ -203,37 +262,103 @@ void OnEncoderPress() { needs_redraw = true; } +void OnEncoderPressRotate(int val) { + // Toggle between Pattern and Global pages + if (val > 0) { + current_page = PAGE_GLOBAL; + } else { + current_page = PAGE_PATTERN; + } + + // Reset editing state when swapping pages + editing_param = false; + needs_redraw = true; +} + +// Map CV Destination enum string labels +const char* GetCvDestName(CvDest dest) { + switch(dest) { + case CV_DEST_NONE: return "NONE"; + case CV_DEST_KICK_DENS: return "KICK DENS"; + case CV_DEST_SNARE_DENS: return "SNAR DENS"; + case CV_DEST_HHAT_DENS: return "HHAT DENS"; + 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"; + } +} + void OnEncoderRotate(int val) { if (!editing_param) { // Navigate menu (clamp to edges, do not wrap) - int next_param = (int)current_param + val; - next_param = constrain(next_param, 0, PARAM_LAST - 1); - current_param = (SelectedParam)next_param; + if (current_page == PAGE_PATTERN) { + int next_param = (int)current_param + val; + next_param = constrain(next_param, 0, PARAM_LAST - 1); + current_param = (SelectedParam)next_param; + } else { + int next_param = (int)current_global_param + val; + next_param = constrain(next_param, 0, PARAM_GLOBAL_LAST - 1); + current_global_param = (GlobalParam)next_param; + } } else { // Edit parameter - int amt = val * 8; // Adjust by 8 values at a time for speed mapping + if (current_page == PAGE_PATTERN) { + int amt = val * 8; // Adjust by 8 values at a time for speed mapping - switch (current_param) { - case PARAM_KICK_DENS: - inst_density[0] = constrain(inst_density[0] + amt, 0, 255); - break; - case PARAM_SNARE_DENS: - inst_density[1] = constrain(inst_density[1] + amt, 0, 255); - break; - case PARAM_HIHAT_DENS: - inst_density[2] = constrain(inst_density[2] + amt, 0, 255); - break; - case PARAM_CHAOS: - chaos_amount = constrain(chaos_amount + amt, 0, 255); - break; - case PARAM_MAP_X: - map_x = constrain(map_x + amt, 0, 255); - break; - case PARAM_MAP_Y: - map_y = constrain(map_y + amt, 0, 255); - break; - default: - break; + switch (current_param) { + case PARAM_KICK_DENS: + inst_density[0] = constrain(inst_density[0] + amt, 0, 255); + break; + case PARAM_SNARE_DENS: + inst_density[1] = constrain(inst_density[1] + amt, 0, 255); + break; + case PARAM_HIHAT_DENS: + inst_density[2] = constrain(inst_density[2] + amt, 0, 255); + break; + case PARAM_CHAOS: + chaos_amount = constrain(chaos_amount + amt, 0, 255); + break; + case PARAM_MAP_X: + map_x = constrain(map_x + amt, 0, 255); + break; + case PARAM_MAP_Y: + map_y = constrain(map_y + amt, 0, 255); + break; + default: + break; + } + } else { + // Edit Global Parameter + switch (current_global_param) { + case PARAM_GLOBAL_CLK_SRC: { + int src = (int)selected_source + val; + src = constrain(src, 0, Clock::SOURCE_LAST - 1); + selected_source = (Clock::Source)src; + gravity.clock.SetSource(selected_source); + break; + } + case PARAM_GLOBAL_BPM: { + if (!gravity.clock.ExternalSource()) { + gravity.clock.SetTempo(gravity.clock.Tempo() + val); + } + break; + } + case PARAM_GLOBAL_CV1_DEST: { + int dest = (int)cv1_dest + val; + dest = constrain(dest, 0, CV_DEST_LAST - 1); + cv1_dest = (CvDest)dest; + break; + } + case PARAM_GLOBAL_CV2_DEST: { + int dest = (int)cv2_dest + val; + dest = constrain(dest, 0, CV_DEST_LAST - 1); + cv2_dest = (CvDest)dest; + break; + } + default: + break; + } } eeprom_needs_save = true; @@ -242,92 +367,225 @@ void OnEncoderRotate(int val) { needs_redraw = true; } -void DrawBarGraph(int y, const char *label, int value, bool is_selected) { - // Reset draw color to default foreground - gravity.display.setDrawColor(1); - gravity.display.setCursor(0, y); +#include "display_assets.h" - if (is_selected) { - gravity.display.print(">"); - if (editing_param) { - // Draw solid white box behind the label - gravity.display.drawBox(6, y - 8, 26, 10); - // Switch to black text to 'cut out' the label from the box - gravity.display.setDrawColor(0); - } - } else { - gravity.display.print(" "); +constexpr uint8_t SCREEN_CENTER_X = 32; +constexpr uint8_t MAIN_TEXT_Y = 26; +constexpr uint8_t SUB_TEXT_Y = 40; +constexpr uint8_t VISIBLE_MENU_ITEMS = 3; +constexpr uint8_t MENU_ITEM_HEIGHT = 14; +constexpr uint8_t MENU_BOX_PADDING = 4; +constexpr uint8_t MENU_BOX_WIDTH = 64; + +void drawCenteredText(const char *text, int y, const uint8_t *font) { + gravity.display.setFont(font); + int textWidth = gravity.display.getStrWidth(text); + gravity.display.drawStr(SCREEN_CENTER_X - (textWidth / 2), y, text); +} + +void drawRightAlignedText(const char *text, int y) { + int textWidth = gravity.display.getStrWidth(text); + int drawX = (SCREEN_WIDTH - textWidth) - MENU_BOX_PADDING; + gravity.display.drawStr(drawX, y, text); +} + +void drawMainSelection() { + gravity.display.setDrawColor(1); + const int tickSize = 3; + const int mainWidth = SCREEN_WIDTH / 2; + const int mainHeight = 49; + gravity.display.drawLine(0, 0, tickSize, 0); + gravity.display.drawLine(0, 0, 0, tickSize); + gravity.display.drawLine(mainWidth, 0, mainWidth - tickSize, 0); + gravity.display.drawLine(mainWidth, 0, mainWidth, tickSize); + gravity.display.drawLine(mainWidth, mainHeight, mainWidth, + mainHeight - tickSize); + gravity.display.drawLine(mainWidth, mainHeight, mainWidth - tickSize, + mainHeight); + gravity.display.drawLine(0, mainHeight, tickSize, mainHeight); + gravity.display.drawLine(0, mainHeight, 0, mainHeight - tickSize); +} + +void drawMenuItems(String menu_items[], int menu_size, int current_item) { + gravity.display.setFont(TEXT_FONT); + + int selectedBoxY = 0; + if (menu_size >= VISIBLE_MENU_ITEMS && current_item == menu_size - 1) { + selectedBoxY = MENU_ITEM_HEIGHT * min(2, current_item); + } else if (current_item > 0) { + selectedBoxY = MENU_ITEM_HEIGHT; } - gravity.display.setCursor(6, y); - gravity.display.print(label); + int boxX = MENU_BOX_WIDTH + 1; + int boxY = selectedBoxY + 2; + int boxWidth = MENU_BOX_WIDTH - 1; + int boxHeight = MENU_ITEM_HEIGHT + 1; - // Restore draw color to white for the bar and text gravity.display.setDrawColor(1); + if (editing_param) { + gravity.display.drawBox(boxX, boxY, boxWidth, boxHeight); + drawMainSelection(); + } else { + gravity.display.drawFrame(boxX, boxY, boxWidth, boxHeight); + } - // Draw Bar - int barLen = map(value, 0, 255, 0, 60); - gravity.display.drawFrame(34, y - 8, 60, 8); - gravity.display.drawBox(34, y - 8, barLen, 8); + gravity.display.setDrawColor(2); + int start_index = 0; + if (menu_size >= VISIBLE_MENU_ITEMS && current_item == menu_size - 1) { + start_index = menu_size - VISIBLE_MENU_ITEMS; + } else if (current_item > 0) { + start_index = current_item - 1; + } - // Draw value percentage - gravity.display.setCursor(98, y); - int pct = map(value, 0, 255, 0, 100); - gravity.display.print(pct); - gravity.display.print("%"); + 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); + } +} + +void DisplayMainArea() { + gravity.display.setFontMode(1); + gravity.display.setDrawColor(2); + + String mainText; + String subText; + + if (current_page == PAGE_PATTERN) { + switch (current_param) { + case PARAM_KICK_DENS: + mainText = String(map(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)) + "%"; + subText = "SNAR DENS"; + break; + case PARAM_HIHAT_DENS: + mainText = String(map(inst_density[2], 0, 255, 0, 100)) + "%"; + subText = "HHAT DENS"; + break; + case PARAM_CHAOS: + mainText = String(map(chaos_amount, 0, 255, 0, 100)) + "%"; + subText = "CHAOS"; + break; + case PARAM_MAP_X: + mainText = String(map_x); + subText = "MAP X"; + break; + case PARAM_MAP_Y: + mainText = String(map_y); + subText = "MAP Y"; + break; + default: + break; + } + + drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); + drawCenteredText(subText.c_str(), 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); + + } else if (current_page == PAGE_GLOBAL) { + switch (current_global_param) { + case PARAM_GLOBAL_CLK_SRC: + switch (selected_source) { + case Clock::SOURCE_INTERNAL: + mainText = F("INT"); + subText = F("CLOCK"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_24: + mainText = F("EXT"); + subText = F("24 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_4: + mainText = F("EXT"); + subText = F("4 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_2: + mainText = F("EXT"); + subText = F("2 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_1: + mainText = F("EXT"); + subText = F("1 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_MIDI: + mainText = F("EXT"); + subText = F("MIDI"); + break; + } + break; + case PARAM_GLOBAL_BPM: + if (gravity.clock.ExternalSource()) { + mainText = F("EXT"); + } else { + mainText = String(gravity.clock.Tempo()); + } + subText = F("BPM"); + break; + case PARAM_GLOBAL_CV1_DEST: + mainText = F("CV1"); + subText = GetCvDestName(cv1_dest); + break; + case PARAM_GLOBAL_CV2_DEST: + mainText = F("CV2"); + 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); + + String menu_items[PARAM_GLOBAL_LAST] = { + "SOURCE", "TEMPO", "CV1 DEST", "CV2 DEST"}; + drawMenuItems(menu_items, PARAM_GLOBAL_LAST, (int)current_global_param); + } +} + +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); + } + + 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() { gravity.display.setFontMode(1); - gravity.display.setDrawColor(1); - gravity.display.setFont(u8g2_font_5x7_tf); - - // Header - gravity.display.setCursor(0, 7); - if (is_playing) - gravity.display.print("[>] PLAY"); - else - gravity.display.print("[||] PAUS"); - - gravity.display.setCursor(55, 7); - gravity.display.print("BPM:"); - gravity.display.print(gravity.clock.Tempo()); - - gravity.display.drawHLine(0, 10, 128); - - // Parameters List (Scrollable window of 5 items) - int y_start = 20; - int y_spacing = 9; - - // Calculate window start index - int window_start = max(0, min((int)current_param - 2, PARAM_LAST - 5)); - - for (int i = 0; i < 5; i++) { - int param_idx = window_start + i; - int y_pos = y_start + (y_spacing * i); - bool is_sel = (current_param == param_idx); - - switch (param_idx) { - case PARAM_KICK_DENS: - DrawBarGraph(y_pos, "KICK", inst_density[0], is_sel); - break; - case PARAM_SNARE_DENS: - DrawBarGraph(y_pos, "SNAR", inst_density[1], is_sel); - break; - case PARAM_HIHAT_DENS: - DrawBarGraph(y_pos, "HHAT", inst_density[2], is_sel); - break; - case PARAM_CHAOS: - DrawBarGraph(y_pos, "CHAO", chaos_amount, is_sel); - break; - case PARAM_MAP_X: - DrawBarGraph(y_pos, "MAPX", map_x, is_sel); - break; - case PARAM_MAP_Y: - DrawBarGraph(y_pos, "MAPY", map_y, is_sel); - break; - } - } + DisplayMainArea(); + DisplayBottomBar(); } void setup() { @@ -337,6 +595,7 @@ void setup() { gravity.play_button.AttachPressHandler(OnPlayPress); gravity.encoder.AttachPressHandler(OnEncoderPress); gravity.encoder.AttachRotateHandler(OnEncoderRotate); + gravity.encoder.AttachPressRotateHandler(OnEncoderPressRotate); gravity.clock.AttachIntHandler(ProcessSequencerTick); // Default to 120 BPM internal diff --git a/firmware/Rhythm/display_assets.h b/firmware/Rhythm/display_assets.h new file mode 100644 index 0000000..8acdf9d --- /dev/null +++ b/firmware/Rhythm/display_assets.h @@ -0,0 +1,90 @@ +/* + * Font: velvetscreen.bdf 9pt + * https://stncrn.github.io/u8g2-unifont-helper/ + * "%/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + */ +const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = + "\64\0\2\2\3\3\2\3\4\5\5\0\0\5\0\5\0\0\221\0\0\1\230 \4\200\134%\11\255tT" + "R\271RI(\6\252\334T\31)\7\252\134bJ\12+\7\233\345\322J\0,\5\221T\4-\5\213" + "f\6.\5\211T\2/" + "\6\244\354c\33\60\10\254\354T\64\223\2\61\7\353\354\222\254\6\62\11\254l" + "\66J*" + "\217\0\63\11\254l\66J\32\215\4\64\10\254l\242\34\272\0\65\11\254l\206\336h" + "$\0\66" + "\11\254\354T^\61)\0\67\10\254lF\216u\4\70\11\254\354TL*&" + "\5\71\11\254\354TL;" + ")\0:\6\231UR\0A\10\254\354T\34S\6B\11\254lV\34)\216\4C\11\254\354T\324\61" + ")\0D\10\254lV\64G\2E\10\254l\206\36z\4F\10\254l\206^\71\3G\11\254\354TN" + "\63)" + "\0H\10\254l\242\34S\6I\6\251T\206\0J\10\254\354k\231\24\0K\11\254l\242J\62" + "\225\1L\7\254lr{\4M\11\255t\362ZI\353\0N\11\255t\362TI\356\0O\10\254\354T" + "\64\223\2P\11\254lV\34)" + "g\0Q\10\254\354T\264b\12R\10\254lV\34\251\31S\11\254\354" + "FF\32\215\4T\7\253dVl\1U\10\254l\242\63)\0V\11\255t\262Ne\312\21W\12\255" + "t\262J*\251.\0X\11\254l\242L*\312\0Y\12\255tr\252\63\312(\2Z\7\253df*" + "\7p\10\255\364V\266\323\2q\7\255\364\216\257\5r\10\253d\242\32*" + "\2t\6\255t\376#w\11" + "\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0"; + +/* + * Font: STK-L.bdf 36pt + * https://stncrn.github.io/u8g2-unifont-helper/ + * "%/0123456789ABCDEFILNORSTUVXx" + */ +const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") = + "\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL" + "\64B\214\30\22\223\220)" + "Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14" + "\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:" + "fW\207\320\210\60\42\304\204\30D\247" + "\214\331\354\20\11%" + "\212\314\0\61\24z\275\245a\244\12\231\71\63b\214\220q\363\377(E\6\62\33|" + "\373\35ShT\20:fl\344\14\211\231\301\306T\71\202#g\371\340\201\1\63\34|" + "\373\35ShT" + "\20:fl\344@r\264\263\222\344,\215\35\42\241\6\225\31\0\64 " + "|\373-!\203\206\214!\62\204" + "\314\220A#\10\215\30\65b\324\210Q\306\354\354\1\213\225\363\1\65\32|" + "\373\15\25[\214\234/\10)" + "Y\61j\350\310Y\32;DB\15*\63\0\66\33}\33\236SiV\14;gt^\230Y\302\202\324" + "\71\273;EbM\252\63\0\67\23|\373\205\25\17R\316\207\344\350p\312\201#" + "\347\35\0\70 |\373" + "\35ShT\20:f\331!\22D\310 " + ":\205\206\10\11B\307\354\354\20\11\65\250\314\0\71\32|\373" + "\35ShT\20:fg\207H,Q\223r\276\30DB\15*\63\0A\26}\33\246r\247\322P\62" + "j\310\250\21\343\354\335\203\357\354w\3B$}" + "\33\206Dj\226\214\42\61l\304\260\21\303F\14\33\61" + "\212\304\222MF\221\30v\316\236=\10\301b\11\0C\27}" + "\33\236Si\226\20Bft\376O\211\215" + " Db\215\42$\0D\33}\33\206Dj\226\214\32\62l\304\260\21\343\354\177vl\304(" + "\22K\324" + "$\2E\22|\373\205\17R\316KD\30\215\234_>x`\0F\20|" + "\373\205\17R\316\227i\262\31" + "\71\377\22\0I\7s\333\204\77HL\15{\333\205\201\363\377\77|\360`\0N$}" + "\33\6\201\346\314" + "\35;\206\12U\242D&\306\230\30cd\210\221!fF\230\31a(+\314\256\63\67\0O\26}" + "\33" + "\236Si\226\214\32\61\316\376\277\33\61j\310\232Tg\0R\61\216;\6Ek\230\14#" + "\61n\304\270" + "\21\343F\214\33\61n\304\60\22\243\210\60Q\224j\310\260\61\243\306\20\232" + "\325\230QD\206\221\30\67b" + "\334\301\1S\42\216;\236c\211\226\220\42\61n\304\270\21c\307R\232,[" + "\262\203\307\216\65h\16\25" + "\21&\253\320\0T\15}\33\206\17R\15\235\377\377\25\0U\21|" + "\373\205a\366\377\237\215\30\64D\15" + "*\63\0V\26\177\371\205\221\366\377\313\21\343\206\220\42C\25\11r'" + "\313\16\3X)~;\206\201\6" + "\217\221\30\66\204\20\31\42\244\206\14Cg\320$Q\222\6\315!" + "\33\62\212\10\31BD\206\215 v\320" + "\302\1x\24\312\272\205A\206\216\220@c\212\224\31$" + "S\14\262h\0\0\0\0\4\377\377\0"; + +#define play_icon_width 14 +#define play_icon_height 14 +static const unsigned char play_icon[28] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x7C, 0x00, 0xFC, 0x00, + 0xFC, 0x03, 0xFC, 0x0F, 0xFC, 0x0F, 0xFC, 0x03, 0xFC, 0x00, + 0x7C, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const unsigned char pause_icon[28] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, + 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, + 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x00, 0x00};