├── .gitignore ├── demo.gif ├── README.md ├── ULPAudio.h ├── lookup_tables.h └── ESP32-Hector.ino /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/ESP32-Hector/HEAD/demo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-Hector 2 | 3 | 4 | 5 | 6 | This project is heavily inspired from the work of Gerard Ferrandez http://codepen.io/ge1doot/details/eWzQBm/ 7 | who developed and implemented a similar drawing function for HTML5 Canvas. 8 | 9 | Sound implementation follows [@Bitluni](github.com/bitluni)'s [ULP example](https://github.com/bitluni/ULPSoundESP32/tree/master/ULPSoundMonoSample) 10 | but is also heavily based on the Audio library from [@charlierobson](https://github.com/charlierobson/)'s [Lasertag project](https://github.com/charlierobson/lasertag/) 11 | 12 | The sound loop is taken from "Les histoires d'amour finissent mal" by "Rita Mistouko" 13 | and was provided by [Gardie-Le-Gueux](https://soundcloud.com/gardie-le-gueux) [Complete track](https://www.youtube.com/watch?v=ln0VwCqMkcA) 14 | 15 | [Demo](https://youtu.be/IG3-20U2HEE) 16 | 17 | Software Requirements: 18 | ---------------------- 19 | - Arduino IDE 20 | - ESP32 SDK 21 | - ESP32-Chimera-Core https://github.com/tobozo/ESP32-Chimera-Core 22 | - M5Stack SD Updater https://github.com/tobozo/M5Stack-SD-Updater 23 | 24 | -------------------------------------------------------------------------------- /ULPAudio.h: -------------------------------------------------------------------------------- 1 | // https://github.com/charlierobson/lasertag/blob/master/lt2/Audio.h 2 | #pragma once 3 | 4 | #ifdef ESP32 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "assets.wav.h" 12 | 13 | // THANKS BITLUNI! 14 | // https://github.com/bitluni/ULPSoundESP32/tree/master/ULPSoundMonoSamples 15 | 16 | unsigned char* samplePointers[2]; 17 | unsigned int sampleLengths[2]; 18 | 19 | enum Sounds { 20 | HECTOR_SOUND 21 | }; 22 | 23 | class SFX { 24 | public: 25 | SFX() : 26 | _lastFilledWord(0), 27 | _sampleDataLen(0) { 28 | samplePointers[HECTOR_SOUND] = (unsigned char*)hector_wav_raw; 29 | sampleLengths[HECTOR_SOUND] = hector_wav_raw_len; 30 | } 31 | 32 | void begin() { 33 | //calculate the actual ULP clock 34 | unsigned long rtc_8md256_period = rtc_clk_cal(RTC_CAL_8MD256, 1000); 35 | unsigned long rtc_fast_freq_hz = 1000000ULL * (1 << RTC_CLK_CAL_FRACT) * 256 / rtc_8md256_period; 36 | 37 | //initialize DACs 38 | dac_output_enable(DAC_CHANNEL_1); 39 | dac_output_enable(DAC_CHANNEL_2); 40 | dac_output_voltage(DAC_CHANNEL_1, 128); 41 | dac_output_voltage(DAC_CHANNEL_2, 128); 42 | 43 | int retAddress1 = 13; 44 | 45 | int loopCycles = 84; 46 | Serial.print("Real RTC clock: "); 47 | Serial.println(rtc_fast_freq_hz); 48 | int dt = (rtc_fast_freq_hz / samplingRate) - loopCycles; 49 | if(dt < 0) 50 | Serial.println("Sampling rate too high"); 51 | Serial.print("dt: "); 52 | Serial.println(dt); 53 | const ulp_insn_t mono[] = { 54 | //reset offset register 55 | I_MOVI(R3, 0), 56 | //delay to get the right sampling rate 57 | I_DELAY(dt), // 6 + dt 58 | //reset sample index 59 | I_MOVI(R0, 0), // 6 60 | //write the index back to memory for the main cpu 61 | I_ST(R0, R3, indexAddress), // 8 62 | //divide index by two since we store two samples in each dword 63 | I_RSHI(R2, R0, 1), // 6 64 | //load the samples 65 | I_LD(R1, R2, bufferStart), // 8 66 | //get if odd or even sample 67 | I_ANDI(R2, R0, 1), // 6 68 | //multiply by 8 69 | I_LSHI(R2, R2, 3), // 6 70 | //shift the bits to have the right sample in the lower 8 bits 71 | I_RSHR(R1, R1, R2), // 6 72 | //mask the lower 8 bits 73 | I_ANDI(R1, R1, 255), // 6 74 | //multiply by 2 75 | I_LSHI(R1, R1, 1), // 6 76 | //add start position 77 | I_ADDI(R1, R1, dacTableStart1),// 6 78 | //jump to the dac opcode 79 | I_BXR(R1), // 4 80 | //here we get back from writing a sample 81 | //increment the sample index 82 | I_ADDI(R0, R0, 1), // 6 83 | //if reached end of the buffer, jump relative to index reset 84 | I_BGE(-13, totalSamples), // 4 85 | //wait to get the right sample rate (2 cycles more to compensate the index reset) 86 | I_DELAY((unsigned int)dt + 2), // 8 + dt 87 | //if not, jump absolute to where index is written to memory 88 | I_BXI(3)}; // 4 89 | 90 | size_t load_addr = 0; 91 | size_t size = sizeof(mono)/sizeof(ulp_insn_t); 92 | ulp_process_macros_and_load(load_addr, mono, &size); 93 | // this is how to get the opcodes 94 | // for(int i = 0; i < size; i++) 95 | // Serial.println(RTC_SLOW_MEM[i], HEX); 96 | 97 | //create DAC opcode tables 98 | for(int i = 0; i < 256; i++) 99 | { 100 | RTC_SLOW_MEM[dacTableStart1 + i * 2] = 0x1D4C0121 | (i << 10); //dac0 101 | RTC_SLOW_MEM[dacTableStart1 + 1 + i * 2] = 0x80000000 + retAddress1 * 4; 102 | } 103 | 104 | //set all samples to 128 (silence) 105 | for(int i = 0; i < totalSampleWords; i++) 106 | RTC_SLOW_MEM[bufferStart + i] = 0x8080; 107 | 108 | //start 109 | RTC_SLOW_MEM[indexAddress] = 0; 110 | ulp_run(0); 111 | while(RTC_SLOW_MEM[indexAddress] == 0) 112 | delay(1); 113 | } 114 | 115 | void playSound(int soundID) { 116 | _sampleDataPtr = samplePointers[soundID]; 117 | _sampleDataLen = sampleLengths[soundID]; 118 | } 119 | 120 | void toggleMute() { 121 | muted = !muted; 122 | if( muted ) { 123 | dac_output_disable(DAC_CHANNEL_1); 124 | dac_output_disable(DAC_CHANNEL_2); 125 | } else { 126 | dac_output_enable(DAC_CHANNEL_1); 127 | dac_output_enable(DAC_CHANNEL_2); 128 | } 129 | } 130 | 131 | void playSoundSync(int soundID) { 132 | playSound(soundID); 133 | while(_sampleDataLen) { 134 | update(); 135 | } 136 | } 137 | 138 | void flush() { 139 | for(int i = 0; i < 400; ++i) { 140 | update(); 141 | delay(1); 142 | } 143 | } 144 | 145 | void update() { 146 | int currentSample = RTC_SLOW_MEM[indexAddress] & 0xffff; 147 | int currentWord = currentSample >> 1; 148 | while(_lastFilledWord != currentWord) 149 | { 150 | unsigned int w = nextSample(); 151 | w |= nextSample() << 8; 152 | RTC_SLOW_MEM[bufferStart + _lastFilledWord] = w; 153 | _lastFilledWord++; 154 | if(_lastFilledWord == totalSampleWords) { 155 | _lastFilledWord = 0; 156 | } 157 | } 158 | } 159 | 160 | unsigned long samplelen = 0; 161 | 162 | private: 163 | unsigned char* _sampleDataPtr; 164 | unsigned int _sampleDataLen; 165 | int _lastFilledWord; 166 | 167 | const unsigned long samplingRate = 44100; 168 | 169 | const int opcodeCount = 17; 170 | const int dacTableStart1 = 2048 - 512; 171 | const int dacTableStart2 = dacTableStart1 - 512; 172 | const int totalSampleWords = 2048 - 512 - (opcodeCount + 1); 173 | const int totalSamples = totalSampleWords * 2; 174 | const int indexAddress = opcodeCount; 175 | const int bufferStart = indexAddress + 1; 176 | 177 | bool muted = false; 178 | unsigned long startsample; 179 | unsigned long endsample; 180 | 181 | 182 | unsigned char nextSample() { 183 | if (!_sampleDataLen) return 0x80; 184 | static long pos = 0; 185 | if(pos >= _sampleDataLen) { 186 | endsample = millis(); 187 | samplelen = endsample - startsample; 188 | pos = 0; 189 | } 190 | if(pos == 0) { 191 | startsample = millis(); 192 | } 193 | return (unsigned char)((int)_sampleDataPtr[pos++] + 128); 194 | } 195 | /* 196 | unsigned char nextSample() 197 | { 198 | if (!_sampleDataLen) return 0x80; 199 | 200 | --_sampleDataLen; 201 | return *_sampleDataPtr++; 202 | }*/ 203 | }; 204 | 205 | #else 206 | 207 | class SFX { 208 | public: 209 | void begin() { 210 | } 211 | 212 | void playSound(int sample) { 213 | } 214 | 215 | void playSoundSync(int sample) { 216 | } 217 | 218 | void flush() { 219 | } 220 | 221 | void update() { 222 | } 223 | }; 224 | 225 | #endif 226 | -------------------------------------------------------------------------------- /lookup_tables.h: -------------------------------------------------------------------------------- 1 | 2 | //#define N 5000 // number of calls to make during test 3 | #define NS 1024 // number of entries in sin table 4 | #define MAXI 32768 // max integer value in table 5 | #define I2F (1./MAXI) // conversion from integer to float 6 | 7 | #ifndef _swap_int16_t 8 | #define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; } 9 | #endif 10 | 11 | 12 | int i ; 13 | long startTime, endTime ; 14 | float dt ; 15 | float d = 0.0 ; // argument to sin function, degrees 16 | float s ; // result of sin function 17 | 18 | // sin table 19 | // values for first quadrant, other quadrants calculated by symetry 20 | const uint16_t sintab[NS] = { 21 | 0, 22 | 50,100,150,201,251, 23 | 301,351,402,452,502, 24 | 552,603,653,703,753, 25 | 804,854,904,954,1005, 26 | 1055,1105,1155,1206,1256, 27 | 1306,1356,1407,1457,1507, 28 | 1557,1607,1658,1708,1758, 29 | 1808,1858,1909,1959,2009, 30 | 2059,2109,2159,2210,2260, 31 | 2310,2360,2410,2460,2510, 32 | 2560,2611,2661,2711,2761, 33 | 2811,2861,2911,2961,3011, 34 | 3061,3111,3161,3211,3261, 35 | 3311,3361,3411,3461,3511, 36 | 3561,3611,3661,3711,3761, 37 | 3811,3861,3911,3961,4011, 38 | 4061,4110,4160,4210,4260, 39 | 4310,4360,4409,4459,4509, 40 | 4559,4609,4658,4708,4758, 41 | 4808,4857,4907,4957,5006, 42 | 5056,5106,5155,5205,5255, 43 | 5304,5354,5403,5453,5503, 44 | 5552,5602,5651,5701,5750, 45 | 5800,5849,5898,5948,5997, 46 | 6047,6096,6146,6195,6244, 47 | 6294,6343,6392,6442,6491, 48 | 6540,6589,6639,6688,6737, 49 | 6786,6835,6884,6934,6983, 50 | 7032,7081,7130,7179,7228, 51 | 7277,7326,7375,7424,7473, 52 | 7522,7571,7620,7669,7717, 53 | 7766,7815,7864,7913,7961, 54 | 8010,8059,8108,8156,8205, 55 | 8254,8302,8351,8400,8448, 56 | 8497,8545,8594,8642,8691, 57 | 8739,8788,8836,8884,8933, 58 | 8981,9029,9078,9126,9174, 59 | 9223,9271,9319,9367,9415, 60 | 9463,9512,9560,9608,9656, 61 | 9704,9752,9800,9848,9896, 62 | 9944,9991,10039,10087,10135, 63 | 10183,10230,10278,10326,10374, 64 | 10421,10469,10517,10564,10612, 65 | 10659,10707,10754,10802,10849, 66 | 10897,10944,10991,11039,11086, 67 | 11133,11181,11228,11275,11322, 68 | 11369,11416,11464,11511,11558, 69 | 11605,11652,11699,11746,11793, 70 | 11839,11886,11933,11980,12027, 71 | 12073,12120,12167,12213,12260, 72 | 12307,12353,12400,12446,12493, 73 | 12539,12586,12632,12678,12725, 74 | 12771,12817,12864,12910,12956, 75 | 13002,13048,13094,13140,13186, 76 | 13232,13278,13324,13370,13416, 77 | 13462,13508,13554,13599,13645, 78 | 13691,13736,13782,13828,13873, 79 | 13919,13964,14010,14055,14100, 80 | 14146,14191,14236,14282,14327, 81 | 14372,14417,14462,14507,14552, 82 | 14598,14642,14687,14732,14777, 83 | 14822,14867,14912,14956,15001, 84 | 15046,15090,15135,15180,15224, 85 | 15269,15313,15357,15402,15446, 86 | 15491,15535,15579,15623,15667, 87 | 15712,15756,15800,15844,15888, 88 | 15932,15976,16019,16063,16107, 89 | 16151,16195,16238,16282,16325, 90 | 16369,16413,16456,16499,16543, 91 | 16586,16630,16673,16716,16759, 92 | 16802,16846,16889,16932,16975, 93 | 17018,17061,17104,17146,17189, 94 | 17232,17275,17317,17360,17403, 95 | 17445,17488,17530,17573,17615, 96 | 17658,17700,17742,17784,17827, 97 | 17869,17911,17953,17995,18037, 98 | 18079,18121,18163,18204,18246, 99 | 18288,18330,18371,18413,18454, 100 | 18496,18537,18579,18620,18662, 101 | 18703,18744,18785,18826,18868, 102 | 18909,18950,18991,19032,19073, 103 | 19113,19154,19195,19236,19276, 104 | 19317,19358,19398,19439,19479, 105 | 19519,19560,19600,19640,19681, 106 | 19721,19761,19801,19841,19881, 107 | 19921,19961,20001,20040,20080, 108 | 20120,20159,20199,20239,20278, 109 | 20318,20357,20396,20436,20475, 110 | 20514,20553,20592,20631,20671, 111 | 20709,20748,20787,20826,20865, 112 | 20904,20942,20981,21020,21058, 113 | 21097,21135,21173,21212,21250, 114 | 21288,21326,21365,21403,21441, 115 | 21479,21517,21555,21592,21630, 116 | 21668,21706,21743,21781,21818, 117 | 21856,21893,21931,21968,22005, 118 | 22042,22080,22117,22154,22191, 119 | 22228,22265,22301,22338,22375, 120 | 22412,22448,22485,22521,22558, 121 | 22594,22631,22667,22703,22740, 122 | 22776,22812,22848,22884,22920, 123 | 22956,22992,23027,23063,23099, 124 | 23134,23170,23205,23241,23276, 125 | 23312,23347,23382,23417,23453, 126 | 23488,23523,23558,23593,23627, 127 | 23662,23697,23732,23766,23801, 128 | 23835,23870,23904,23939,23973, 129 | 24007,24041,24075,24109,24144, 130 | 24177,24211,24245,24279,24313, 131 | 24346,24380,24414,24447,24480, 132 | 24514,24547,24580,24614,24647, 133 | 24680,24713,24746,24779,24812, 134 | 24845,24877,24910,24943,24975, 135 | 25008,25040,25073,25105,25137, 136 | 25169,25201,25234,25266,25298, 137 | 25330,25361,25393,25425,25457, 138 | 25488,25520,25551,25583,25614, 139 | 25645,25677,25708,25739,25770, 140 | 25801,25832,25863,25894,25925, 141 | 25955,25986,26016,26047,26077, 142 | 26108,26138,26169,26199,26229, 143 | 26259,26289,26319,26349,26379, 144 | 26409,26438,26468,26498,26527, 145 | 26557,26586,26615,26645,26674, 146 | 26703,26732,26761,26790,26819, 147 | 26848,26877,26905,26934,26963, 148 | 26991,27020,27048,27076,27105, 149 | 27133,27161,27189,27217,27245, 150 | 27273,27301,27329,27356,27384, 151 | 27411,27439,27466,27494,27521, 152 | 27548,27576,27603,27630,27657, 153 | 27684,27711,27737,27764,27791, 154 | 27817,27844,27870,27897,27923, 155 | 27949,27976,28002,28028,28054, 156 | 28080,28106,28131,28157,28183, 157 | 28208,28234,28259,28285,28310, 158 | 28335,28361,28386,28411,28436, 159 | 28461,28486,28511,28535,28560, 160 | 28585,28609,28634,28658,28682, 161 | 28707,28731,28755,28779,28803, 162 | 28827,28851,28875,28898,28922, 163 | 28946,28969,28993,29016,29039, 164 | 29062,29086,29109,29132,29155, 165 | 29178,29201,29223,29246,29269, 166 | 29291,29314,29336,29359,29381, 167 | 29403,29425,29447,29469,29491, 168 | 29513,29535,29557,29578,29600, 169 | 29621,29643,29664,29686,29707, 170 | 29728,29749,29770,29791,29812, 171 | 29833,29854,29874,29895,29915, 172 | 29936,29956,29977,29997,30017, 173 | 30037,30057,30077,30097,30117, 174 | 30137,30156,30176,30196,30215, 175 | 30235,30254,30273,30292,30312, 176 | 30331,30350,30368,30387,30406, 177 | 30425,30443,30462,30480,30499, 178 | 30517,30535,30554,30572,30590, 179 | 30608,30626,30644,30661,30679, 180 | 30697,30714,30732,30749,30766, 181 | 30784,30801,30818,30835,30852, 182 | 30869,30886,30902,30919,30936, 183 | 30952,30969,30985,31001,31018, 184 | 31034,31050,31066,31082,31098, 185 | 31114,31129,31145,31161,31176, 186 | 31192,31207,31222,31237,31253, 187 | 31268,31283,31298,31312,31327, 188 | 31342,31357,31371,31386,31400, 189 | 31414,31429,31443,31457,31471, 190 | 31485,31499,31513,31526,31540, 191 | 31554,31567,31581,31594,31607, 192 | 31620,31634,31647,31660,31673, 193 | 31685,31698,31711,31723,31736, 194 | 31749,31761,31773,31785,31798, 195 | 31810,31822,31834,31846,31857, 196 | 31869,31881,31892,31904,31915, 197 | 31927,31938,31949,31960,31971, 198 | 31982,31993,32004,32015,32025, 199 | 32036,32047,32057,32067,32078, 200 | 32088,32098,32108,32118,32128, 201 | 32138,32148,32157,32167,32176, 202 | 32186,32195,32205,32214,32223, 203 | 32232,32241,32250,32259,32268, 204 | 32276,32285,32294,32302,32311, 205 | 32319,32327,32335,32343,32351, 206 | 32359,32367,32375,32383,32390, 207 | 32398,32405,32413,32420,32427, 208 | 32435,32442,32449,32456,32463, 209 | 32469,32476,32483,32489,32496, 210 | 32502,32509,32515,32521,32527, 211 | 32533,32539,32545,32551,32557, 212 | 32562,32568,32573,32579,32584, 213 | 32589,32595,32600,32605,32610, 214 | 32615,32619,32624,32629,32633, 215 | 32638,32642,32647,32651,32655, 216 | 32659,32663,32667,32671,32675, 217 | 32679,32682,32686,32689,32693, 218 | 32696,32700,32703,32706,32709, 219 | 32712,32715,32718,32720,32723, 220 | 32726,32728,32730,32733,32735, 221 | 32737,32739,32741,32743,32745, 222 | 32747,32749,32750,32752,32754, 223 | 32755,32756,32758,32759,32760, 224 | 32761,32762,32763,32764,32764, 225 | 32765,32766,32766,32767,32767, 226 | 32767,32767,32767 227 | } ; 228 | 229 | 230 | float TWOPI = PI*2; 231 | float HALFPI = PI*0.5; 232 | float ITWOPI = 1./TWOPI; 233 | 234 | 235 | float dosincos(float x, bool iscos) { 236 | int ix ; // index into sin table 237 | int is ; // value read from sin table 238 | int q ; // quadrant number 0,1,2,3 239 | int j ; 240 | if(iscos) x += HALFPI; 241 | x *= ITWOPI; 242 | x -= (int)x ; 243 | if (x < 0) 244 | x += 1.0 ; // x is now between 0 and 1, representing 0 to 360 degrees 245 | q = x * 4 ; // get quadrant number 246 | ix = (int)(x*4*NS) % NS ; // get index into table 247 | 248 | switch(q) { 249 | case 0: // 0-90 250 | is = sintab[ix]; 251 | break ; 252 | case 1: // 90-180 253 | ix = NS - ix - 1 ; // reflect 254 | is = sintab[ix]; 255 | break ; 256 | case 2: // 180-270 257 | is = -sintab[ix]; // negate 258 | break ; 259 | case 3: // 270-360 260 | ix = NS - ix - 1; // reflect 261 | is = -sintab[ix]; // negate 262 | break ; 263 | } 264 | return((float)is*I2F) ; 265 | } 266 | 267 | float romsin(float x) { 268 | return dosincos(x, false); 269 | } 270 | float romcos(float x) { 271 | return dosincos(x, true); 272 | } 273 | 274 | 275 | 276 | #define LOG_CACHE_SIZE 1024 277 | static float logCache[LOG_CACHE_SIZE]; 278 | float precision = 8.90; // 512 / 115 279 | bool romloginit = false; 280 | 281 | float romlog( float in ) { 282 | int logindex = in*precision; 283 | if( logindex > LOG_CACHE_SIZE ) { 284 | //log_n("Out of cache size at offset %.8f", in); 285 | return log(in); 286 | } 287 | if(!romloginit) { 288 | for( uint16_t i=0;i SQR_CACHE_SIZE ) { 306 | log_n("Out of cache size at offset %.8f", in); 307 | return sqrt(in); 308 | } 309 | if(!romsqrtinit) { 310 | for( uint16_t i=0;i POW_CACHE_SIZE ) { 325 | log_n("Out of cache size at offset %.8f", in); 326 | return in*in; 327 | } 328 | if(!rompowinit) { 329 | for( uint16_t i=0;i // https://github.com/tobozo/ESP32-Chimera-Core 39 | #include // https://github.com/tobozo/M5Stack-SD-Updater 40 | #include "ULPAudio.h" 41 | 42 | #define SIZE 150 43 | #define STEP 2 44 | #define GRID_SIZE SIZE/STEP 45 | #include "lookup_tables.h" 46 | 47 | #define tft M5.Lcd 48 | 49 | TFT_eSprite sprite = TFT_eSprite( &tft ); 50 | TFT_eSprite headersprite = TFT_eSprite( &tft ); 51 | TFT_eSprite gfx = TFT_eSprite(&tft); // Sprite object for graphics write 52 | 53 | SFX* sfx; 54 | 55 | // Accel and gyro data 56 | int32_t gx, gy; 57 | 58 | // sizing the 3d animation 59 | static float size = SIZE; 60 | static float step = STEP*1.2; 61 | static float doublestep = STEP*2; 62 | static float speed = 0.15; // for color rotation 63 | static float tsize = 0.85*size; 64 | static float halfsize = size * 0.5; 65 | static float zoom = 1.33; 66 | static float halfzoom = 0.5 * zoom; 67 | static float k = 0; 68 | static float romcosav, romsinav, romcosah, romsinah; 69 | static float stepdirection = 1; 70 | static float brightnessfactor; 71 | static float z; 72 | 73 | static int num = GRID_SIZE; 74 | static int totalpixels = 0; 75 | static int fps = 0; 76 | 77 | uint16_t wtf[SIZE]; // this is a strange bug, can't get rid of this without breaking the demo 78 | 79 | #define PEAK_SIZE 320 80 | static int16_t peak[PEAK_SIZE*2]; // depth cache, for 'solid' visual effect 81 | static int16_t lastpeak[PEAK_SIZE*2]; // depth cache, for 'solid' visual effect 82 | 83 | static int16_t pathindex, scan_x, scan_y; 84 | static int16_t txlast; 85 | static int16_t tylast; 86 | static int16_t scroll_x = 0; // Keep track of the scrolled position, this is where the origin 87 | static int16_t scroll_y = 0; // (top left) of the gfx Sprite will be 88 | 89 | static uint16_t screenWidth; 90 | static uint16_t screenHeight; 91 | static uint16_t screenHalfWidth; 92 | static uint16_t screenHalfHeight; 93 | static uint16_t spritePosX; 94 | static uint16_t spritePosY; 95 | static uint16_t spriteWidth; 96 | static uint16_t spriteHeight; 97 | 98 | static uint8_t maxrangecolor = 255; 99 | static uint8_t minrangecolor = 0; 100 | static uint8_t basecolor; 101 | static uint8_t green; 102 | static uint8_t red; 103 | static uint8_t blue; 104 | static uint8_t segmentpos = 0; 105 | static uint8_t lastsegmentpos = 0; 106 | 107 | static unsigned long framecount = 0; // count fps 108 | static unsigned long lastviewmodechange = millis(); 109 | static unsigned long viewmodechangetime = 5000; 110 | static unsigned long lastscroll = millis(); 111 | static unsigned long scrolltick = 60; 112 | static unsigned long sampleloopsegment; 113 | 114 | static uint32_t fstart = 0; 115 | 116 | uint16_t *gfxPtr; // Pointer to start of graphics sprite memory area 117 | uint16_t *scrollPtr; // Pointer to start of graphics sprite memory area 118 | uint16_t *mainPtr; // Pointer to start of graphics sprite memory area 119 | 120 | bool sound = true; 121 | 122 | struct Coords { 123 | int16_t x = -1; 124 | int16_t y = -1; 125 | uint16_t color = 0; 126 | }; 127 | 128 | static Coords HectorGrid[GRID_SIZE+1][GRID_SIZE+1]; 129 | 130 | enum DisplayStyle { 131 | DISPLAY_GRID, 132 | DISPLAY_SOLID, 133 | DISPLAY_ZEBRA, 134 | DISPLAY_CHECKBOARD 135 | }; 136 | 137 | enum WaveStyle { 138 | DRIP_WAVE, 139 | SIN_WAVE 140 | }; 141 | 142 | 143 | enum ButtonState { 144 | BUTTON_PAUSE, 145 | BUTTON_PLAY, 146 | BUTTON_SPEAKER_ON, 147 | BUTTON_SPEAKER_OFF 148 | }; 149 | 150 | struct ButtonCoords { 151 | uint16_t x; 152 | uint16_t y; 153 | }; 154 | 155 | 156 | struct UIButton { 157 | ButtonCoords coords; 158 | uint8_t width; 159 | uint8_t height; 160 | 161 | void render(ButtonState state=BUTTON_PAUSE ) { 162 | uint16_t x = coords.x; 163 | uint16_t y = coords.y; 164 | byte anglebox = height / 4; 165 | byte anglesign = height / 15; 166 | byte pheight = (height - 2) - height/5; 167 | byte pwidth = width/5; 168 | byte posX1 = x + (((width-2)/2)/2); 169 | byte posX2 = x + ((((width-2)/2)/2) +1)*2; 170 | byte posY = y + ((height-2)/2 - pheight/2); 171 | uint16_t TFT_GRAY = tft.color565( 64, 64, 64 ); 172 | uint16_t TFT_DARKGRAY = tft.color565( 32, 32, 32 ); 173 | uint16_t TFT_LIGHTGRAY = tft.color565( 128, 128, 128 ); 174 | 175 | tft.drawRoundRect( x, y, width, height, anglebox, TFT_GRAY ); 176 | tft.drawRoundRect( x, y, width-1, height-1, anglebox, TFT_LIGHTGRAY ); 177 | 178 | tft.fillRoundRect( x+1, y+1, width-2, height-2, anglebox, TFT_DARKGRAY ); 179 | 180 | switch( state ) { 181 | case BUTTON_PLAY: 182 | tft.fillTriangle( posX1, posY, posX2+pwidth, posY+pheight/2, posX1, posY+pheight, TFT_LIGHTGRAY ); 183 | break; 184 | case BUTTON_PAUSE: 185 | tft.fillRoundRect( posX1, posY, pwidth, pheight, anglesign, TFT_LIGHTGRAY ); 186 | tft.fillRoundRect( posX2, posY, pwidth, pheight, anglesign, TFT_LIGHTGRAY ); 187 | break; 188 | case BUTTON_SPEAKER_ON: 189 | pwidth/=2; 190 | posX1+=pwidth/2; 191 | posX2+=pwidth/2; 192 | tft.fillRoundRect( posX1, posY+pheight/4, pwidth*2, (pheight/2)+1, anglesign, TFT_LIGHTGRAY ); 193 | tft.fillTriangle( posX1, posY+pheight/2, posX2+pwidth, posY, posX2+pwidth, posY+pheight, TFT_LIGHTGRAY ); 194 | break; 195 | case BUTTON_SPEAKER_OFF: 196 | pwidth/=2; 197 | posX1+=pwidth/2; 198 | posX2+=pwidth/2; 199 | tft.fillRoundRect( posX1, posY+pheight/4, pwidth*2, (pheight/2)+1, anglesign, TFT_GRAY ); 200 | tft.fillTriangle( posX1, posY+pheight/2, posX2+pwidth, posY, posX2+pwidth, posY+pheight, TFT_GRAY ); 201 | break; 202 | } 203 | 204 | } 205 | }; 206 | 207 | 208 | WaveStyle waveStyle = DRIP_WAVE; 209 | WaveStyle oldWaveStyle = SIN_WAVE; 210 | DisplayStyle displayStyle = DISPLAY_GRID; 211 | 212 | 213 | ButtonCoords PlayPauseButtonCoords = { 42, 212 }; 214 | ButtonCoords SpeakerButtonCoords = { 132, 212 }; 215 | 216 | UIButton PlayPauseButton = { PlayPauseButtonCoords, 50, 28 }; 217 | UIButton SpeakerButton = { SpeakerButtonCoords, 50, 28 }; 218 | 219 | 220 | static void ulpLoop(void * pvParameters) { 221 | sfx->begin(); 222 | sfx->playSound(HECTOR_SOUND); 223 | bool lastsoundstate = sound; 224 | while(1) { 225 | if( lastsoundstate != sound ) { 226 | sfx->toggleMute(); 227 | lastsoundstate = sound; 228 | } 229 | if( sound ) { 230 | sfx->flush(); 231 | } 232 | } 233 | } 234 | 235 | 236 | // pointer to sine wave function 237 | float (*surfaceFunction)(float x, float y, float k); 238 | 239 | // f(x,y) equation for sin wave 240 | float sinwave(float x, float y, float k) { 241 | float r = 0.001 * ( rompow(x) + rompow(y) ); 242 | return 100 * romcos(-k + r) / (2 + r); 243 | } 244 | // f(x,y) equation for water drip wave 245 | float dripwave( float x, float y, float k ) { 246 | float r = 1.5*romsqrt( rompow(x) + rompow(y) ); 247 | const float amplitude = 2.5; 248 | const float a = 200.0; 249 | const float b = (amplitude - fmod(k/3, amplitude))-amplitude/2; 250 | return /*100 **/ (a/(1+r)) * romcos((b/romlog(r+2))*r); 251 | } 252 | // projection 253 | void project(float x, float y, float z/*, float ah, float av*/) { 254 | float x1 = x * romcosah - y * romsinah; 255 | float y1 = x * romsinah + y * romcosah; 256 | float z2 = z * romcosav - x1 * romsinav; 257 | if( (tsize - x1) == 0 ) return; 258 | float s = size / (tsize - x1); 259 | HectorGrid[scan_x][scan_y].x = screenHalfWidth - (zoom * (y1 * s)); 260 | HectorGrid[scan_x][scan_y].y = screenHalfHeight - (zoom * (z2 * s)); 261 | } 262 | // handle depth cache while line-drawing 263 | void mySetPixel(int16_t x, int16_t y, uint16_t color) { 264 | if(peak[PEAK_SIZE+x]<=y) { 265 | peak[PEAK_SIZE+x] = y; 266 | totalpixels++; 267 | if( displayStyle == DISPLAY_GRID ) { 268 | sprite.drawPixel(x, screenHeight-y, color); 269 | } 270 | } 271 | } 272 | // draw helpers, just a copy of drawLine, calling mySetPixel() instead of setPixel() 273 | // to handle depth, mainly here to compensate the lack of fill/stroke 274 | void drawOverlap(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) { 275 | int16_t steep = abs(y1 - y0) > abs(x1 - x0); 276 | totalpixels = 0; 277 | if (steep) { 278 | _swap_int16_t(x0, y0); 279 | _swap_int16_t(x1, y1); 280 | } 281 | if (x0 > x1) { 282 | _swap_int16_t(x0, x1); 283 | _swap_int16_t(y0, y1); 284 | } 285 | int16_t dx, dy; 286 | dx = x1 - x0; 287 | dy = abs(y1 - y0); 288 | int16_t err = dx / 2; 289 | int16_t ystep; 290 | if (y0 < y1) { 291 | ystep = 1; 292 | } else { 293 | ystep = -1; 294 | } 295 | for (; x0<=x1; x0++) { 296 | if( steep ) { 297 | mySetPixel(y0, x0, color); 298 | } else { 299 | mySetPixel(x0, y0, color); 300 | } 301 | err -= dy; 302 | if (err < 0) { 303 | y0 += ystep; 304 | err += dx; 305 | } 306 | } 307 | } 308 | // depth-aware drawing/filling 309 | void drawLineOverlap() { 310 | int16_t x0 = HectorGrid[pathindex][scan_y].x; 311 | int16_t x1 = HectorGrid[pathindex][scan_y-1].x; 312 | uint16_t color = HectorGrid[pathindex][scan_y].color; 313 | if( color == 0 ) return; // unset color 314 | int16_t y0 = peak[PEAK_SIZE+x0]; 315 | int16_t y1 = lastpeak[PEAK_SIZE+x1]; 316 | 317 | if( y0==-1 && x0==-1 ) return; // coords aren't set 318 | if( y1==-1 && x1==-1 ) return; // coords aren't set 319 | 320 | switch( displayStyle ) { 321 | case DISPLAY_GRID: 322 | sprite.drawLine( x0, screenHeight-y0, x1, screenHeight-y1, color ); 323 | return; 324 | break; 325 | case DISPLAY_SOLID: 326 | if( pathindex%3 == 0 && scan_y%2 == 1 327 | || pathindex%2 == 1 && scan_y%2 == 0 328 | ) { 329 | return; 330 | } 331 | break; 332 | case DISPLAY_ZEBRA: 333 | if( scan_y%2 == 1-pathindex%2 ) return; 334 | break; 335 | case DISPLAY_CHECKBOARD: 336 | if( pathindex%3 == 0 && scan_y%3 == 1 337 | || pathindex%3 == 1 && scan_y%3 == 2 338 | || pathindex%3 == 2 && scan_y%3 == 0 339 | ) { 340 | // render 341 | } else { 342 | return; 343 | }; 344 | break; 345 | } 346 | if( pathindex < STEP ) return; 347 | int16_t x2 = HectorGrid[pathindex-STEP][scan_y].x; 348 | int16_t y2 = peak[PEAK_SIZE+x2]; 349 | int16_t x3 = HectorGrid[pathindex-STEP][scan_y-1].x; 350 | int16_t y3 = lastpeak[PEAK_SIZE+x3]; 351 | sprite.fillTriangle( x2, screenHeight-y2, x0, screenHeight-y0, x1, screenHeight-y1, color ); 352 | sprite.fillTriangle( x2, screenHeight-y2, x1, screenHeight-y1, x3, screenHeight-y3, color ); 353 | } 354 | 355 | 356 | void drawPath() { 357 | txlast = HectorGrid[0][scan_y].x; 358 | tylast = HectorGrid[0][scan_y].y; 359 | memcpy( lastpeak, peak, sizeof(peak) ); 360 | for (pathindex = 0; pathindex < num; pathindex++) { 361 | drawOverlap(txlast, tylast, HectorGrid[pathindex][scan_y].x, HectorGrid[pathindex][scan_y].y, HectorGrid[pathindex][scan_y].color); 362 | if( totalpixels>0 && pathindex%STEP == 1 ) { 363 | drawLineOverlap(); 364 | } 365 | txlast = HectorGrid[pathindex][scan_y].x; 366 | tylast = HectorGrid[pathindex][scan_y].y; 367 | } 368 | } 369 | 370 | 371 | void sinLoop() { 372 | // reset depth cache 373 | memset(peak,-1,sizeof(peak)); 374 | memset(lastpeak,-1,sizeof(lastpeak)); 375 | totalpixels = 0; 376 | 377 | k += speed; 378 | 379 | setupScale(); 380 | 381 | float ah = ((-0.5 * gx + screenHalfWidth) / screenWidth); 382 | float av = 0.5 * gy / screenHeight; 383 | 384 | romcosav = romcos(av); 385 | romsinav = romsin(av); 386 | romcosah = romcos(ah); 387 | romsinah = romsin(ah); 388 | 389 | sprite.fillSprite( TFT_BLACK ); 390 | 391 | scan_y = 0; 392 | 393 | for (float x = halfsize; x >= -halfsize; x -= doublestep) { 394 | 395 | blue = map( x, -halfsize, halfsize, minrangecolor, maxrangecolor ); 396 | 397 | scan_x = 0; 398 | 399 | for (float y = -halfsize; y <= halfsize; y += step) { 400 | //float z = surface(x, y, k); 401 | float z = surfaceFunction(x, y, k); 402 | green = map( y, -halfsize, halfsize, minrangecolor, maxrangecolor ); 403 | brightnessfactor = float(map(int(z), -50, 50, 100, 20)) / 100.0; 404 | red = maxrangecolor - (green-minrangecolor); 405 | green *= brightnessfactor; 406 | red *= brightnessfactor; 407 | blue *= brightnessfactor; 408 | 409 | HectorGrid[scan_x][scan_y].color = tft.color565(red, green, blue); 410 | project(x, y, z*1.2); 411 | scan_x++; 412 | } 413 | drawPath(); 414 | scan_y++; 415 | } 416 | 417 | unsigned long nowmillis = millis(); 418 | 419 | if(nowmillis - fstart >= 1000) { 420 | fps = (framecount * 1000) / (nowmillis - fstart); 421 | fstart = nowmillis; 422 | tft.setCursor( 240, 211 ); 423 | tft.printf( "fps: %2d", fps ); 424 | framecount = 0; 425 | } else { 426 | framecount++; 427 | } 428 | 429 | if( sfx->samplelen > 0 ) { 430 | 431 | int32_t segment = nowmillis%sfx->samplelen; 432 | segmentpos = map( segment, 0, sfx->samplelen, 0, 16); 433 | if( lastsegmentpos != segmentpos ) { 434 | 435 | tft.setCursor( 240, 231 ); 436 | tft.printf( "seg: %2d", segmentpos ); 437 | 438 | lastsegmentpos = segmentpos; 439 | if( segmentpos%2==0 ) { 440 | switch( displayStyle ) { 441 | case DISPLAY_SOLID: displayStyle = DISPLAY_GRID; break; 442 | case DISPLAY_GRID: displayStyle = DISPLAY_ZEBRA; break; 443 | case DISPLAY_ZEBRA: displayStyle = DISPLAY_CHECKBOARD; break; 444 | case DISPLAY_CHECKBOARD: displayStyle = DISPLAY_SOLID; break; 445 | } 446 | tft.setCursor( 240, 221 ); 447 | tft.printf( "style: %2d", displayStyle ); 448 | } 449 | if( segmentpos == 1 ) { // sound loop reset 450 | switch( waveStyle ) { 451 | case DRIP_WAVE: waveStyle = SIN_WAVE; break; 452 | case SIN_WAVE: waveStyle = DRIP_WAVE; break; 453 | } 454 | } 455 | if( oldWaveStyle != waveStyle ) { 456 | switch( waveStyle ) { 457 | case SIN_WAVE: 458 | size = SIZE; 459 | step = STEP*1.5; 460 | setupScale(); 461 | surfaceFunction = &sinwave; 462 | break; 463 | case DRIP_WAVE: 464 | setupScale(); 465 | surfaceFunction = &dripwave; 466 | break; 467 | } 468 | oldWaveStyle = waveStyle; 469 | } 470 | 471 | } 472 | } 473 | 474 | if( nowmillis > lastscroll + scrolltick ) { 475 | uint16_t chromar = map( 64*romsin(k), -64, 64, 128, 200); 476 | uint16_t chromag = map( 64*romsin(k*2), -64, 64, 128, 200); 477 | uint16_t chromab = map( 64*romsin(k*3), -64, 64, 128, 200); 478 | uint16_t gfxcolor = tft.color565( chromar, chromag, chromab ); 479 | gfx.setBitmapColor(gfxcolor, TFT_BLACK); 480 | waveScroll(-3); 481 | headersprite.pushSprite( spritePosX , 0 ); 482 | lastscroll = nowmillis; 483 | } 484 | 485 | sprite.pushSprite( spritePosX, spritePosY ); 486 | } 487 | 488 | 489 | void resetCoords() { 490 | for( uint16_t x=0;x 0) scroll_x -= width; 519 | 520 | headersprite.fillSprite( TFT_BLACK ); 521 | 522 | for (uint16_t x = 0; x < width; x++) { 523 | uint16_t xpos = (scroll_x+x)%(width); 524 | float xseq = (float)xpos / (float)headersprite.width() * (4*PI); 525 | int8_t margin = height/2 + (float)height/2.0 * romcos( xseq ); 526 | int16_t offsetx = scroll_x + x + dx; 527 | //Serial.println( margin ); 528 | for (uint16_t y = 0; y < height; y++) { 529 | if( offsetx > 0 && offsetx < headersprite.width()) { 530 | headersprite.drawPixel(offsetx, y+margin, gfx.readPixel(x, y)); 531 | } 532 | if( offsetx + width > 0 && offsetx + width < headersprite.width() ) { 533 | headersprite.drawPixel(offsetx + width, y+margin, gfx.readPixel(x, y)); 534 | } 535 | } 536 | } 537 | scroll_x += dx; 538 | } 539 | 540 | 541 | 542 | static void drawLoop( void * param ) { 543 | while(1) { 544 | // TODO: change this to MPU6050 data 545 | gx = romsin(fmod(k*.15,2*PI))*/*amplitude*/150 /*offset*/+300; 546 | gy = -romsin(fmod(k*.25,2*PI))*/*amplitude*/50 /*offset*/-250; 547 | 548 | sinLoop(); 549 | 550 | M5.update(); // read buttons 551 | 552 | if( M5.BtnA.wasPressed() ) { 553 | bool oldsound = sound; 554 | PlayPauseButton.render(BUTTON_PLAY); 555 | sound = false; 556 | while(true) { 557 | M5.update(); 558 | if( M5.BtnA.wasPressed() ) { 559 | sound = oldsound; 560 | PlayPauseButton.render(BUTTON_PAUSE); 561 | break; 562 | } 563 | delay( 100 ); 564 | } 565 | } 566 | if( M5.BtnB.wasPressed() ) { 567 | sound = !sound; 568 | if( sound ) { 569 | SpeakerButton.render(BUTTON_SPEAKER_ON); 570 | } else { 571 | SpeakerButton.render(BUTTON_SPEAKER_OFF); 572 | } 573 | } 574 | vTaskDelay(1); 575 | } 576 | 577 | } 578 | 579 | 580 | 581 | 582 | void setup() { 583 | M5.begin(); 584 | tft.clear(); 585 | 586 | if(digitalRead(BUTTON_A_PIN) == 0) { 587 | Serial.println("Will Load menu binary"); 588 | updateFromFS( SD ); 589 | ESP.restart(); 590 | } 591 | 592 | if( tft.width() > tft.height() ) { // landscape 593 | screenWidth = 300; 594 | screenHeight = 180; 595 | } else { // portrait, not implemented, shit will happen 596 | log_n("Unimplemented orientation, expect problems"); 597 | // scale (width*height must be inferior to 56k to fit in sram) 598 | screenWidth = 240; 599 | screenHeight = 230; 600 | } 601 | 602 | screenHalfWidth = screenWidth/2; 603 | screenHalfHeight = screenHeight/2; 604 | spritePosX = tft.width()/2 - screenHalfWidth; 605 | spritePosY = tft.height()/2 - screenHalfHeight; 606 | 607 | sprite.setColorDepth( 16 ); 608 | headersprite.setColorDepth( 1 ); 609 | gfx.setColorDepth( 1 ); 610 | 611 | if( psramInit() ) { 612 | // make sure the sprite doesn't use psram -> bad for the fps 613 | sprite.setPsram( false ); 614 | headersprite.setPsram( false ); 615 | } 616 | 617 | byte textsize = 2; 618 | 619 | gx = 256 + (k*20); 620 | gy = -305; 621 | 622 | tft.fillRect(0, 0, tft.width(), tft.height(), BLACK); 623 | tft.setTextSize( textsize ); 624 | 625 | const char* scrollText = " . . . If silence is as deep as eternity, then speech is as shallow as time . . . "; 626 | 627 | spriteWidth = tft.textWidth( scrollText ); 628 | spriteHeight = tft.fontHeight(); 629 | 630 | mainPtr = (uint16_t*) sprite.createSprite( screenWidth, screenHeight ); 631 | scrollPtr = (uint16_t*) headersprite.createSprite( screenWidth, spriteHeight*2 ); 632 | gfxPtr = (uint16_t*) gfx.createSprite( spriteWidth, spriteHeight ); 633 | 634 | gfx.setTextColor( TFT_WHITE, TFT_WHITE ); 635 | gfx.setTextSize( textsize ); 636 | gfx.fillSprite( TFT_BLACK ); 637 | gfx.drawString( scrollText, 0, 0 ); 638 | 639 | sprite.setTextColor( TFT_WHITE, TFT_WHITE ); 640 | sprite.setTextSize( 1 ); 641 | 642 | tft.setTextSize(1); 643 | tft.setTextColor( TFT_WHITE, TFT_BLACK ); 644 | 645 | wtf[0] = 1; // <<< guru meditation : for some reason deleting this line makes the rendering fail, deleting the array too 646 | 647 | surfaceFunction = &sinwave; 648 | setupScale(); 649 | 650 | fstart = millis()-1; 651 | 652 | PlayPauseButton.render(BUTTON_PAUSE); 653 | SpeakerButton.render(BUTTON_SPEAKER_ON); 654 | 655 | sfx = new SFX(); 656 | xTaskCreatePinnedToCore(ulpLoop, "ulpLoop", 2048, NULL, 4, NULL, 1); 657 | 658 | xTaskCreatePinnedToCore(drawLoop, "drawLoop", 2048, NULL, 16, NULL, 0); 659 | 660 | } 661 | 662 | 663 | 664 | 665 | 666 | void loop() { 667 | vTaskSuspend(NULL); 668 | } 669 | 670 | 671 | --------------------------------------------------------------------------------