├── .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 |
--------------------------------------------------------------------------------