
1/*
2 * =========================================================================
3 * Mega Dracula V2 - ULTIMATE OLED Visualizer
4 * =========================================================================
5 *
6 * 🎵 Audio-Reactive Visualizer synchronized to "Dracula" by JENNIE & Tame Impala
7 *
8 * ✨ Features 9 intense, high-framerate OLED animations that switch
9 * dynamically to the beat and lyrics of the song.
10 *
11 * 👨💻 Created by: @smartenginnerslab (Instagram)
12 *
13 * 🔌 HARDWARE & CONNECTIONS:
14 * -------------------------------------------------------------------------
15 * This code is optimized out-of-the-box for ESP8266 and ESP32.
16 *
17 * Display: 0.96" I2C OLED (SSD1306)
18 *
19 * Standard Connections (ESP8266 / ESP32):
20 * - OLED VCC -> 3.3V
21 * - OLED GND -> GND
22 * - OLED SCL -> Standard SCL (e.g. D1 on ESP8266, G22 on ESP32)
23 * - OLED SDA -> Standard SDA (e.g. D2 on ESP8266, G21 on ESP32)
24 *
25 * 🚀 PROUDLY POWERED BY SHRIKE LITE:
26 * -------------------------------------------------------------------------
27 * This specific build was developed and tested using the awesome
28 * "Shrike Lite" development board by Vicharak!
29 * (An incredibly powerful RP2040 + FPGA hybrid board).
30 *
31 * If you are using the Shrike Lite (or another RP2040 board), the code
32 * will automatically configure the custom I2C pins for you!
33 * - Shrike Lite SCL -> GPIO 29
34 * - Shrike Lite SDA -> GPIO 28
35 *
36 * ANIMATIONS:
37 * 1. Bat Swarm 6. Matrix Code Rain
38 * 2. Geometric Mandala 7. Oscilloscope Sound Wave
39 * 3. Synthwave 3D Grid 8. Lightning Bolts
40 * 4. Wireframe Tunnel + EQ Bars 9. 3D Rotating Wireframe Cube
41 * 5. Twisting Vortex Tunnel
42 *
43 * EFFECTS:
44 * POP, SHAKE, INVERT, GLITCH, ZOOM_IN, TYPEWRITER, BOUNCE
45 * =========================================================================
46 */
47
48#include <Wire.h>
49#include <Adafruit_GFX.h>
50#include <Adafruit_SSD1306.h>
51#include <math.h>
52#include <string.h>
53
54#define SCREEN_WIDTH 128
55#define SCREEN_HEIGHT 64
56#define OLED_RESET -1
57Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
58
59// =========================================================================
60// GLOBAL STATE & VARIABLES
61// =========================================================================
62
63enum BackgroundMode {
64 BG_BATS = 0,
65 BG_MANDALA,
66 BG_SYNTHWAVE,
67 BG_TUNNEL_EQ,
68 BG_VORTEX,
69 BG_MATRIX,
70 BG_OSCILLOSCOPE,
71 BG_LIGHTNING,
72 BG_CUBE
73};
74
75BackgroundMode currentBg = BG_TUNNEL_EQ;
76int lastLyricIdx = -1;
77unsigned long startTime = 0;
78bool isPlaying = true;
79unsigned long TOTAL_LOOP_TIME = 0;
80
81// --- 1. Bat Swarm ---
82#define NUM_BATS 15
83struct Bat {
84 float x, y;
85 float vx, vy;
86 float framePhase;
87 float scale;
88} bats[NUM_BATS];
89
90// --- 3. Geometric Mandala ---
91float mAngle1 = 0.0, mAngle2 = 0.0, mAngle3 = 0.0;
92float expandDist = 0.0;
93
94// --- 4. Synthwave 3D ---
95float gridOffset = 0.0;
96int swStarsX[15], swStarsY[15];
97
98// --- 5. Tunnel & EQ ---
99#define NUM_CIRCLES 7
100float circleRadii[NUM_CIRCLES];
101#define NUM_BARS 16
102int barHeights[NUM_BARS], targetHeights[NUM_BARS];
103struct Shockwave { float radius; bool active; } wave;
104
105// --- 6. Vortex ---
106float vortexTime = 0.0;
107
108// --- 7. Matrix Rain ---
109#define MATRIX_COLS 16
110struct MatrixCol { float y; float speed; int x; int trailLen; } matrixCols[MATRIX_COLS];
111
112// --- 8. Oscilloscope ---
113float oscPhase = 0.0;
114
115// --- 9. Lightning ---
116#define MAX_BOLT_PTS 10
117#define MAX_BOLTS 3
118struct LightningBolt {
119 int bx[MAX_BOLT_PTS], by[MAX_BOLT_PTS];
120 int numPts; int framesLeft; bool active;
121} bolts[MAX_BOLTS];
122
123// --- 10. 3D Cube ---
124float cubeAX = 0.0, cubeAY = 0.0, cubeAZ = 0.0;
125
126// =========================================================================
127// EFFECTS & LYRICS
128// =========================================================================
129
130enum Effect {
131 EFFECT_NONE = 0,
132 EFFECT_POP, // Briefly enlarges word
133 EFFECT_SHAKE, // Earthquake shake
134 EFFECT_INVERT, // Inverts entire screen
135 EFFECT_GLITCH, // Random scan lines and jitter
136 EFFECT_ZOOM_IN, // Grows from small to large
137 EFFECT_TYPEWRITER, // Letters appear one by one
138 EFFECT_BOUNCE // Word drops from above and bounces
139};
140
141struct Lyric {
142 unsigned long delayBeforeMs;
143 unsigned long durationMs;
144 const char* word;
145 Effect effect;
146 uint8_t size;
147 unsigned long calculatedStartTime;
148};
149
150// =========================================================================
151// LYRICS TIMELINE - FAST 2-3 WORD ANIMATION SWITCHING
152// =========================================================================
153Lyric lyrics[] = {
154 // Phrase 1: "The morning light is turning BLUE"
155 // Idx 0-1 -> BG_TUNNEL_EQ (tunnel intro!)
156 { 0, 300, "The", EFFECT_NONE, 2, 0 },
157 { 0, 300, "morning", EFFECT_POP, 2, 0 },
158 // Idx 2-4 -> BG_OSCILLOSCOPE (wave for "light")
159 { 0, 300, "light", EFFECT_NONE, 2, 0 },
160 { 0, 300, "is", EFFECT_NONE, 2, 0 },
161 { 0, 300, "turning", EFFECT_NONE, 2, 0 },
162 // Idx 5 -> BG_LIGHTNING (flash on BLUE!)
163 { 100, 500, "BLUE", EFFECT_INVERT, 3, 0 },
164
165 // Phrase 2: "the feeling is BIZARRE"
166 // Idx 6-7 -> BG_MANDALA
167 { 0, 300, "the", EFFECT_NONE, 2, 0 },
168 { 0, 300, "feeling", EFFECT_BOUNCE, 2, 0 },
169 // Idx 8 -> BG_CUBE (switch!)
170 { 0, 300, "is", EFFECT_NONE, 2, 0 },
171 // Idx 9 -> BG_MATRIX (matrix + typewriter for bizarre)
172 { 0, 800, "BIZARRE", EFFECT_TYPEWRITER, 2, 0 },
173
174 // Phrase 3: "The night is almost over,"
175 // Idx 10-11 -> BG_SYNTHWAVE
176 { 200, 300, "The", EFFECT_NONE, 2, 0 },
177 { 0, 300, "night", EFFECT_POP, 2, 0 },
178 // Idx 12-14 -> BG_TUNNEL_EQ (switch!)
179 { 0, 300, "is", EFFECT_NONE, 2, 0 },
180 { 0, 300, "almost", EFFECT_NONE, 2, 0 },
181 { 0, 500, "over", EFFECT_NONE, 2, 0 },
182
183 // Phrase 4: "I still don't know where you ARE"
184 // Idx 15-16 -> BG_CUBE
185 { 200, 300, "I", EFFECT_NONE, 2, 0 },
186 { 0, 300, "still", EFFECT_BOUNCE, 2, 0 },
187 // Idx 17-18 -> BG_OSCILLOSCOPE (switch!)
188 { 0, 300, "don't", EFFECT_NONE, 2, 0 },
189 { 0, 300, "know", EFFECT_NONE, 2, 0 },
190 // Idx 19-20 -> BG_VORTEX (building!)
191 { 0, 200, "where", EFFECT_NONE, 2, 0 },
192 { 0, 200, "you", EFFECT_NONE, 2, 0 },
193 // Idx 21 -> BG_LIGHTNING (STRIKE on ARE!)
194 { 0, 600, "ARE", EFFECT_SHAKE, 3, 0 },
195
196 // Phrase 5: "The shadows, yeah, they keep me pretty like a movie STAR"
197 // Idx 22-23 -> BG_BATS
198 { 200, 300, "The", EFFECT_NONE, 2, 0 },
199 { 0, 400, "shadows", EFFECT_SHAKE, 2, 0 },
200 // Idx 24-25 -> BG_VORTEX (switch!)
201 { 0, 300, "yeah", EFFECT_NONE, 2, 0 },
202 { 0, 300, "they", EFFECT_NONE, 2, 0 },
203 // Idx 26-27 -> BG_MANDALA (switch!)
204 { 0, 300, "keep", EFFECT_NONE, 2, 0 },
205 { 0, 300, "me", EFFECT_NONE, 2, 0 },
206 // Idx 28-29 -> BG_CUBE (switch!)
207 { 0, 300, "pretty", EFFECT_POP, 2, 0 },
208 { 0, 300, "like", EFFECT_NONE, 2, 0 },
209 // Idx 30-31 -> BG_SYNTHWAVE (switch!)
210 { 0, 300, "a", EFFECT_NONE, 2, 0 },
211 { 0, 300, "movie", EFFECT_NONE, 2, 0 },
212 // Idx 32 -> BG_LIGHTNING (flash on STAR!)
213 { 0, 800, "STAR", EFFECT_INVERT, 3, 0 },
214
215 // Phrase 6: "Daylight makes me feel like"
216 // Idx 33-34 -> BG_OSCILLOSCOPE
217 { 300, 500, "Daylight", EFFECT_TYPEWRITER, 2, 0 },
218 { 0, 300, "makes", EFFECT_NONE, 2, 0 },
219 // Idx 35-36 -> BG_BATS (bats swarm on feel!)
220 { 0, 300, "me", EFFECT_NONE, 2, 0 },
221 { 0, 300, "feel", EFFECT_BOUNCE, 2, 0 },
222 // Idx 37 -> BG_MATRIX (building tension)
223 { 0, 200, "like", EFFECT_NONE, 2, 0 },
224
225 // THE DROP (Idx 38) -> BG_TUNNEL_EQ (MASSIVE CLIMAX!)
226 { 100, 2800, "DRACULA", EFFECT_ZOOM_IN, 3, 0 }
227};
228const int numLyrics = sizeof(lyrics) / sizeof(Lyric);
229
230// =========================================================================
231// SETUP & INITIALIZATION
232// =========================================================================
233
234void setup() {
235 Serial.begin(115200);
236
237#if defined(ARDUINO_ARCH_RP2040)
238 // Custom I2C pins specifically for the Shrike Lite board
239 Wire.setSDA(28);
240 Wire.setSCL(29);
241#endif
242
243 Wire.begin();
244 Wire.setClock(400000); // 400kHz Fast I2C for high framerates
245
246 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
247 for (;;);
248 }
249
250 display.clearDisplay();
251 display.setTextColor(WHITE);
252 display.setTextWrap(false);
253
254 // Init Bats
255 for (int i=0; i<NUM_BATS; i++) {
256 bats[i].x = random(0, SCREEN_WIDTH);
257 bats[i].y = random(0, SCREEN_HEIGHT);
258 bats[i].vx = random(-20, 20) / 10.0;
259 if (bats[i].vx == 0) bats[i].vx = 1.0;
260 bats[i].vy = random(-10, 10) / 10.0;
261 bats[i].framePhase = random(0, 100) / 10.0;
262 bats[i].scale = random(10, 25) / 10.0;
263 }
264
265 // Init Synthwave Stars
266 for (int i = 0; i < 15; i++) {
267 swStarsX[i] = random(0, SCREEN_WIDTH);
268 swStarsY[i] = random(0, 32);
269 }
270
271 // Init Tunnel & EQ
272 for (int i = 0; i < NUM_CIRCLES; i++) circleRadii[i] = i * (100.0 / NUM_CIRCLES);
273 for (int i = 0; i < NUM_BARS; i++) { barHeights[i] = 0; targetHeights[i] = 0; }
274 wave.active = false;
275
276 // Init Matrix Rain columns
277 for (int i = 0; i < MATRIX_COLS; i++) {
278 matrixCols[i].x = i * (SCREEN_WIDTH / MATRIX_COLS) + random(0, 4);
279 matrixCols[i].y = random(-30, SCREEN_HEIGHT);
280 matrixCols[i].speed = random(10, 30) / 10.0;
281 matrixCols[i].trailLen = random(8, 22);
282 }
283
284 // Init Lightning
285 for (int i = 0; i < MAX_BOLTS; i++) bolts[i].active = false;
286
287 // Calculate Timeline
288 unsigned long runningTime = 0;
289 for (int i = 0; i < numLyrics; i++) {
290 runningTime += lyrics[i].delayBeforeMs;
291 lyrics[i].calculatedStartTime = runningTime;
292 runningTime += lyrics[i].durationMs;
293 }
294 TOTAL_LOOP_TIME = runningTime + 3000;
295
296 startTime = millis();
297}
298
299// =========================================================================
300// MAIN RENDERING LOOP
301// =========================================================================
302
303void loop() {
304 if (!isPlaying) return;
305
306 unsigned long now = millis() - startTime;
307 if (now > TOTAL_LOOP_TIME) {
308 startTime = millis();
309 now = 0;
310 lastLyricIdx = -1;
311 currentBg = BG_TUNNEL_EQ;
312 }
313
314 display.clearDisplay();
315
316 int currentLyricIdx = -1;
317 for (int i = 0; i < numLyrics; i++) {
318 if (now >= lyrics[i].calculatedStartTime && now <= (lyrics[i].calculatedStartTime + lyrics[i].durationMs)) {
319 currentLyricIdx = i;
320 break;
321 }
322 }
323
324 Effect currentEffect = EFFECT_NONE;
325 bool lyricActive = false;
326 float progress = 0.0;
327 bool invertScreen = false;
328
329 // --- TRACK LYRIC STATE & FAST SCENE SWITCHING ---
330 if (currentLyricIdx != -1) {
331 lyricActive = true;
332 const Lyric& l = lyrics[currentLyricIdx];
333 currentEffect = l.effect;
334 float elapsedLyric = now - l.calculatedStartTime;
335 progress = elapsedLyric / (float)l.durationMs;
336
337 if (currentLyricIdx != lastLyricIdx) {
338
339 // === FAST 2-3 WORD ANIMATION SWITCHING ===
340 // Phrase 1
341 if (currentLyricIdx <= 1) currentBg = BG_TUNNEL_EQ;
342 else if (currentLyricIdx >= 2 && currentLyricIdx <= 4) currentBg = BG_OSCILLOSCOPE;
343 else if (currentLyricIdx == 5) currentBg = BG_LIGHTNING;
344 // Phrase 2
345 else if (currentLyricIdx >= 6 && currentLyricIdx <= 7) currentBg = BG_MANDALA;
346 else if (currentLyricIdx == 8) currentBg = BG_CUBE;
347 else if (currentLyricIdx == 9) currentBg = BG_MATRIX;
348 // Phrase 3
349 else if (currentLyricIdx >= 10 && currentLyricIdx <= 11) currentBg = BG_SYNTHWAVE;
350 else if (currentLyricIdx >= 12 && currentLyricIdx <= 14) currentBg = BG_TUNNEL_EQ;
351 // Phrase 4
352 else if (currentLyricIdx >= 15 && currentLyricIdx <= 16) currentBg = BG_CUBE;
353 else if (currentLyricIdx >= 17 && currentLyricIdx <= 18) currentBg = BG_OSCILLOSCOPE;
354 else if (currentLyricIdx >= 19 && currentLyricIdx <= 20) currentBg = BG_VORTEX;
355 else if (currentLyricIdx == 21) currentBg = BG_LIGHTNING;
356 // Phrase 5
357 else if (currentLyricIdx >= 22 && currentLyricIdx <= 23) currentBg = BG_BATS;
358 else if (currentLyricIdx >= 24 && currentLyricIdx <= 25) currentBg = BG_VORTEX;
359 else if (currentLyricIdx >= 26 && currentLyricIdx <= 27) currentBg = BG_MANDALA;
360 else if (currentLyricIdx >= 28 && currentLyricIdx <= 29) currentBg = BG_CUBE;
361 else if (currentLyricIdx >= 30 && currentLyricIdx <= 31) currentBg = BG_SYNTHWAVE;
362 else if (currentLyricIdx == 32) currentBg = BG_LIGHTNING;
363 // Phrase 6
364 else if (currentLyricIdx >= 33 && currentLyricIdx <= 34) currentBg = BG_OSCILLOSCOPE;
365 else if (currentLyricIdx >= 35 && currentLyricIdx <= 36) currentBg = BG_BATS;
366 else if (currentLyricIdx == 37) currentBg = BG_MATRIX;
367 // THE DROP
368 else if (currentLyricIdx == 38) currentBg = BG_TUNNEL_EQ;
369
370 // Trigger Shockwave for Tunnel
371 if (currentBg == BG_TUNNEL_EQ && (l.effect == EFFECT_POP || l.effect == EFFECT_INVERT || l.effect == EFFECT_ZOOM_IN || l.effect == EFFECT_SHAKE)) {
372 wave.active = true;
373 wave.radius = 5.0;
374 }
375
376 // Trigger bats on impact words
377 if (currentBg == BG_BATS && (l.effect == EFFECT_POP || l.effect == EFFECT_BOUNCE || l.effect == EFFECT_INVERT || l.effect == EFFECT_SHAKE)) {
378 for (int i=0; i<NUM_BATS; i++) {
379 bats[i].x = SCREEN_WIDTH / 2 + random(-10, 10);
380 bats[i].y = SCREEN_HEIGHT / 2 + random(-10, 10);
381 float angle = random(0, 360) * PI / 180.0;
382 float spd = random(20, 50) / 10.0;
383 bats[i].vx = cos(angle) * spd;
384 bats[i].vy = sin(angle) * spd;
385 }
386 }
387
388 lastLyricIdx = currentLyricIdx;
389 }
390 } else {
391 if (now < lyrics[0].calculatedStartTime) {
392 currentBg = BG_TUNNEL_EQ;
393 } else if (lastLyricIdx == 38) {
394 if (now < lyrics[38].calculatedStartTime + lyrics[38].durationMs + 1500) {
395 currentBg = BG_BATS;
396 } else {
397 currentBg = BG_MATRIX;
398 }
399 }
400 }
401
402 // --- 1. RENDER BACKGROUND ---
403 switch (currentBg) {
404 case BG_BATS: drawBats(currentEffect, lyricActive); break;
405 case BG_MANDALA: drawMandalaBackground(currentEffect, lyricActive); break;
406 case BG_SYNTHWAVE: drawSynthwaveBackground(currentEffect, lyricActive); break;
407 case BG_TUNNEL_EQ: drawTunnel(currentEffect, lyricActive); drawShockwave(); drawEQ(currentEffect, lyricActive); break;
408 case BG_VORTEX: drawVortex(lyricActive); break;
409 case BG_MATRIX: drawMatrixRain(currentEffect, lyricActive); break;
410 case BG_OSCILLOSCOPE: drawOscilloscope(currentEffect, lyricActive); break;
411 case BG_LIGHTNING: drawLightning(currentEffect, lyricActive); break;
412 case BG_CUBE: drawCube(currentEffect, lyricActive); break;
413 }
414
415 // --- 2. RENDER LYRICS ON TOP ---
416 if (currentLyricIdx != -1) {
417 const Lyric& l = lyrics[currentLyricIdx];
418
419 int textSize = l.size;
420 int offsetX = 0;
421 int offsetY = 0;
422
423 // Determine displayed word (TYPEWRITER shows partial)
424 char typeBuf[20];
425 const char* displayWord = l.word;
426 bool showCursor = false;
427
428 if (l.effect == EFFECT_TYPEWRITER) {
429 int totalChars = strlen(l.word);
430 int charsToShow = 1 + (int)(progress * totalChars);
431 if (charsToShow > totalChars) charsToShow = totalChars;
432 strncpy(typeBuf, l.word, charsToShow);
433 typeBuf[charsToShow] = '\0';
434 displayWord = typeBuf;
435 showCursor = (charsToShow < totalChars);
436 }
437
438 // Apply text effects
439 if (l.effect == EFFECT_POP) {
440 if (progress < 0.15) textSize = l.size + 1;
441 } else if (l.effect == EFFECT_BOUNCE) {
442 // Word drops from above and bounces with decay
443 float amp = 22.0 * exp(-5.0 * progress);
444 offsetY = -(int)(amp * abs(cos(progress * PI * 4.0)));
445 } else if (l.effect == EFFECT_SHAKE) {
446 offsetX = random(-3, 4);
447 offsetY = random(-3, 4);
448 } else if (l.effect == EFFECT_INVERT) {
449 invertScreen = true;
450 if (random(10) > 5) { offsetX = random(-2, 3); offsetY = random(-2, 3); }
451 } else if (l.effect == EFFECT_GLITCH) {
452 if (random(10) > 6) {
453 offsetX = random(-6, 6);
454 display.fillRect(0, random(SCREEN_HEIGHT), SCREEN_WIDTH, random(2, 8), WHITE);
455 }
456 if (random(10) > 8) invertScreen = true;
457 } else if (l.effect == EFFECT_ZOOM_IN) {
458 if (progress < 0.05) textSize = l.size > 1 ? l.size - 1 : 1;
459 else if (progress < 0.1) textSize = l.size;
460 else textSize = l.size + 1;
461 if (progress > 0.4) {
462 offsetX = random(-4, 5);
463 offsetY = random(-4, 5);
464 }
465 if (progress > 0.3 && random(10) > 7) invertScreen = true;
466 }
467
468 display.setTextSize(textSize);
469 int16_t x1, y1;
470 uint16_t w, h;
471 display.getTextBounds(displayWord, 0, 0, &x1, &y1, &w, &h);
472
473 if (w > SCREEN_WIDTH) {
474 textSize = 2;
475 display.setTextSize(textSize);
476 display.getTextBounds(displayWord, 0, 0, &x1, &y1, &w, &h);
477 }
478
479 int drawX = (SCREEN_WIDTH - w) / 2 + offsetX;
480 int drawY = (SCREEN_HEIGHT - h) / 2 + offsetY;
481
482 if (currentBg == BG_TUNNEL_EQ) drawY -= 5;
483
484 // Solid readable text box
485 display.fillRoundRect(drawX - 6, drawY - 5, w + 12, h + 10, 3, BLACK);
486 display.drawRoundRect(drawX - 6, drawY - 5, w + 12, h + 10, 3, WHITE);
487
488 if (l.effect == EFFECT_GLITCH && random(10) > 8) {
489 display.setTextColor(BLACK, WHITE);
490 display.fillRoundRect(drawX - 4, drawY - 3, w + 8, h + 6, 2, WHITE);
491 } else {
492 display.setTextColor(WHITE);
493 }
494
495 display.setCursor(drawX, drawY);
496 display.print(displayWord);
497
498 // Blinking typewriter cursor
499 if (showCursor && ((millis() / 150) % 2 == 0)) {
500 display.fillRect(drawX + w + 2, drawY, 2, h, WHITE);
501 }
502 }
503
504 display.invertDisplay(invertScreen);
505 display.display();
506}
507
508// =========================================================================
509// BACKGROUND DRAWING FUNCTIONS
510// =========================================================================
511
512// --- 2. Bat Swarm ---
513void drawBats(Effect eff, bool lyricActive) {
514 float speedMult = lyricActive ? 1.5 : 0.8;
515 if (eff == EFFECT_ZOOM_IN || eff == EFFECT_SHAKE) speedMult = 3.0;
516
517 for (int i=0; i<NUM_BATS; i++) {
518 bats[i].x += bats[i].vx * speedMult;
519 bats[i].y += bats[i].vy * speedMult;
520 bats[i].framePhase += 0.3 * speedMult;
521
522 // Wrap around
523 if (bats[i].x < -10) bats[i].x = SCREEN_WIDTH + 10;
524 if (bats[i].x > SCREEN_WIDTH + 10) bats[i].x = -10;
525 if (bats[i].y < -10) bats[i].y = SCREEN_HEIGHT + 10;
526 if (bats[i].y > SCREEN_HEIGHT + 10) bats[i].y = -10;
527
528 // Draw bat (flapping 'V' shape)
529 int bx = (int)bats[i].x;
530 int by = (int)bats[i].y;
531 int wingSpan = 4 * bats[i].scale;
532 int flap = sin(bats[i].framePhase) * 3 * bats[i].scale;
533
534 // Center body
535 display.drawPixel(bx, by, WHITE);
536 // Wings
537 display.drawLine(bx, by, bx - wingSpan, by - flap, WHITE);
538 display.drawLine(bx, by, bx + wingSpan, by - flap, WHITE);
539 }
540}
541
542// --- 3. Geometric Mandala ---
543void drawRotatedTriangle(int cx, int cy, float radius, float a, bool fill) {
544 int x1 = cx + cos(a) * radius; int y1 = cy + sin(a) * radius;
545 int x2 = cx + cos(a + 2.0944) * radius; int y2 = cy + sin(a + 2.0944) * radius;
546 int x3 = cx + cos(a + 4.1888) * radius; int y3 = cy + sin(a + 4.1888) * radius;
547 if (fill) display.fillTriangle(x1, y1, x2, y2, x3, y3, WHITE);
548 else display.drawTriangle(x1, y1, x2, y2, x3, y3, WHITE);
549}
550
551void drawRotatedSquare(int cx, int cy, float radius, float a) {
552 int sx1 = cx + cos(a) * radius; int sy1 = cy + sin(a) * radius;
553 int sx2 = cx + cos(a + 1.5708) * radius; int sy2 = cy + sin(a + 1.5708) * radius;
554 int sx3 = cx + cos(a + 3.1416) * radius; int sy3 = cy + sin(a + 3.1416) * radius;
555 int sx4 = cx + cos(a + 4.7124) * radius; int sy4 = cy + sin(a + 4.7124) * radius;
556 display.drawLine(sx1, sy1, sx2, sy2, WHITE); display.drawLine(sx2, sy2, sx3, sy3, WHITE);
557 display.drawLine(sx3, sy3, sx4, sy4, WHITE); display.drawLine(sx4, sy4, sx1, sy1, WHITE);
558}
559
560void drawMandalaBackground(Effect eff, bool lyricActive) {
561 int cx = SCREEN_WIDTH / 2, cy = SCREEN_HEIGHT / 2;
562 float sm = (eff == EFFECT_ZOOM_IN) ? 8.0 : (eff == EFFECT_SHAKE || eff == EFFECT_INVERT) ? 4.0 : lyricActive ? 1.5 : 0.4;
563
564 mAngle1 += 0.02 * sm; mAngle2 -= 0.03 * sm; mAngle3 += 0.05 * sm;
565 if (mAngle1 > 2 * PI) mAngle1 -= 2 * PI;
566 if (mAngle2 < -2 * PI) mAngle2 += 2 * PI;
567 if (mAngle3 > 2 * PI) mAngle3 -= 2 * PI;
568
569 float outerSz = (eff == EFFECT_POP || eff == EFFECT_BOUNCE) ? 40.0 : 35.0;
570 drawRotatedTriangle(cx, cy, outerSz, mAngle1, false);
571 drawRotatedTriangle(cx, cy, outerSz, mAngle1 + PI, false);
572 drawRotatedSquare(cx, cy, 22.0, mAngle2);
573 if (lyricActive) drawRotatedSquare(cx, cy, 22.0, mAngle2 + (PI / 4));
574 drawRotatedTriangle(cx, cy, 12.0, mAngle3, false);
575 drawRotatedTriangle(cx, cy, 12.0, mAngle3 + PI, false);
576
577 expandDist += 0.5 * sm;
578 if (expandDist > 60.0) expandDist = 0.0;
579 for (int i = 0; i < 8; i++) {
580 float a = (PI / 4) * i + (mAngle1 * 0.5);
581 int x = cx + cos(a) * (expandDist + 10);
582 int y = cy + sin(a) * (expandDist + 10);
583 if (x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT) {
584 display.drawPixel(x, y, WHITE);
585 if (sm > 2.0) {
586 int tx = cx + cos(a) * (expandDist + 8);
587 int ty = cy + sin(a) * (expandDist + 8);
588 display.drawPixel(tx, ty, WHITE);
589 }
590 }
591 }
592}
593
594// --- 4. Synthwave 3D ---
595void drawSynthwaveBackground(Effect eff, bool lyricActive) {
596 int cy = 34, cx = SCREEN_WIDTH / 2;
597 float speed = (eff == EFFECT_ZOOM_IN || eff == EFFECT_SHAKE) ? 0.15 : (!lyricActive) ? 0.02 : 0.04;
598 gridOffset += speed;
599 if (gridOffset >= 1.0) gridOffset -= 1.0;
600
601 for (int i = 0; i < 15; i++) if (random(10) > 2) display.drawPixel(swStarsX[i], swStarsY[i], WHITE);
602
603 display.fillCircle(cx, cy, 22, WHITE);
604 display.fillRect(0, cy, SCREEN_WIDTH, SCREEN_HEIGHT - cy, BLACK);
605 display.drawFastHLine(cx - 22, cy - 3, 44, BLACK);
606 display.fillRect(cx - 22, cy - 8, 44, 2, BLACK);
607 display.fillRect(cx - 22, cy - 15, 44, 3, BLACK);
608
609 display.drawLine(0, cy, 18, cy - 8, WHITE); display.drawLine(18, cy - 8, 30, cy - 2, WHITE); display.drawLine(30, cy - 2, 42, cy, WHITE);
610 display.drawLine(SCREEN_WIDTH, cy, 110, cy - 10, WHITE); display.drawLine(110, cy - 10, 95, cy - 3, WHITE); display.drawLine(95, cy - 3, 85, cy, WHITE);
611 display.drawFastHLine(0, cy, SCREEN_WIDTH, WHITE);
612
613 for (float i = 0; i < 8; i++) {
614 float z = i - gridOffset;
615 if (z < 0.1) continue;
616 int y = cy + (35.0 / z);
617 if (y <= SCREEN_HEIGHT && y > cy) display.drawFastHLine(0, y, SCREEN_WIDTH, WHITE);
618 }
619 for (float i = -10; i <= 10; i++) {
620 int xTop = cx + (i * 8); int xBot = cx + (i * 45);
621 display.drawLine(xTop, cy, xBot, SCREEN_HEIGHT, WHITE);
622 }
623}
624
625// --- 5. Tunnel & EQ ---
626void drawTunnel(Effect eff, bool lyricActive) {
627 float speed = (eff == EFFECT_ZOOM_IN) ? 8.0 : (eff == EFFECT_SHAKE || eff == EFFECT_GLITCH) ? 4.0 : (!lyricActive) ? 0.5 : 1.0;
628 int cx = SCREEN_WIDTH / 2, cy = SCREEN_HEIGHT / 2;
629 if (eff == EFFECT_SHAKE || eff == EFFECT_ZOOM_IN) { cx += random(-3, 4); cy += random(-3, 4); }
630
631 for (float a = 0; a < 2 * PI; a += PI / 4) {
632 display.drawLine(cx + cos(a) * 5, cy + sin(a) * 5, cx + cos(a) * 120, cy + sin(a) * 120, WHITE);
633 }
634 for (int i = 0; i < NUM_CIRCLES; i++) {
635 circleRadii[i] += speed;
636 if (circleRadii[i] > 120) circleRadii[i] = 1;
637 if (circleRadii[i] > 2) display.drawCircle(cx, cy, circleRadii[i], WHITE);
638 }
639}
640
641void drawEQ(Effect eff, bool lyricActive) {
642 int barW = SCREEN_WIDTH / NUM_BARS;
643 for (int i = 0; i < NUM_BARS; i++) {
644 if (random(10) > 4) {
645 if (lyricActive) {
646 if (eff == EFFECT_ZOOM_IN) targetHeights[i] = random(15, 35);
647 else if (eff == EFFECT_SHAKE) targetHeights[i] = random(10, 25);
648 else targetHeights[i] = random(5, 15);
649 } else targetHeights[i] = random(1, 6);
650 }
651 if (barHeights[i] < targetHeights[i]) barHeights[i] += 4;
652 else if (barHeights[i] > targetHeights[i]) barHeights[i] -= 3;
653 if (barHeights[i] < 1) barHeights[i] = 1;
654 display.fillRect(i * barW + 1, SCREEN_HEIGHT - barHeights[i], barW - 1, barHeights[i], WHITE);
655 }
656}
657
658void drawShockwave() {
659 if (wave.active) {
660 display.drawCircle(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, wave.radius, WHITE);
661 display.drawCircle(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, wave.radius - 1, WHITE);
662 wave.radius += 8.0;
663 if (wave.radius > 140) wave.active = false;
664 }
665}
666
667// --- 6. Vortex ---
668void drawVortex(bool lyricActive) {
669 vortexTime += lyricActive ? 0.12 : 0.06;
670 int cx = SCREEN_WIDTH / 2, cy = SCREEN_HEIGHT / 2;
671
672 for (int i = 0; i < 8; i++) {
673 int prevX = -1, prevY = -1;
674 for (int r = 2; r <= 80; r += 6) {
675 float twist = r * 0.04;
676 float a = (i / 8.0) * PI * 2.0 + twist + vortexTime;
677 int x = cx + cos(a) * r, y = cy + sin(a) * r;
678 if (prevX != -1) display.drawLine(prevX, prevY, x, y, WHITE);
679 prevX = x; prevY = y;
680 }
681 }
682 for (int baseR = 10; baseR <= 80; baseR += 16) {
683 int r = baseR + ((int)(vortexTime * 10) % 16);
684 float twist = r * 0.04;
685 int prevX = -1, prevY = -1, firstX = -1, firstY = -1;
686 for (float a = 0; a <= PI * 2.0; a += 0.4) {
687 float ang = a + twist + vortexTime;
688 int x = cx + cos(ang) * r, y = cy + sin(ang) * r;
689 if (prevX != -1) display.drawLine(prevX, prevY, x, y, WHITE);
690 else { firstX = x; firstY = y; }
691 prevX = x; prevY = y;
692 }
693 display.drawLine(prevX, prevY, firstX, firstY, WHITE);
694 }
695}
696
697// --- 7. Matrix Code Rain ---
698void drawMatrixRain(Effect eff, bool lyricActive) {
699 float sm = lyricActive ? 2.0 : 1.0;
700 if (eff == EFFECT_ZOOM_IN || eff == EFFECT_SHAKE) sm = 4.0;
701 else if (eff == EFFECT_TYPEWRITER || eff == EFFECT_GLITCH) sm = 2.5;
702
703 for (int i = 0; i < MATRIX_COLS; i++) {
704 matrixCols[i].y += matrixCols[i].speed * sm;
705
706 // Reset when past bottom
707 if (matrixCols[i].y > SCREEN_HEIGHT + matrixCols[i].trailLen * 3) {
708 matrixCols[i].y = random(-25, -5);
709 matrixCols[i].speed = random(10, 30) / 10.0;
710 matrixCols[i].trailLen = random(8, 22);
711 }
712
713 int headY = (int)matrixCols[i].y;
714 int x = matrixCols[i].x;
715
716 // Draw bright "head" pixel
717 if (headY >= 0 && headY < SCREEN_HEIGHT) {
718 display.fillRect(x, headY, 3, 3, WHITE);
719 }
720
721 // Draw trail behind head
722 for (int j = 1; j < matrixCols[i].trailLen; j++) {
723 int ty = headY - j * 3;
724 if (ty >= 0 && ty < SCREEN_HEIGHT) {
725 if (j < matrixCols[i].trailLen / 3) {
726 display.fillRect(x, ty, 2, 2, WHITE); // Bright near head
727 } else {
728 display.drawPixel(x + (j % 2), ty, WHITE); // Dim far away
729 }
730 }
731 }
732 }
733}
734
735// --- 8. Oscilloscope / Sound Wave ---
736void drawOscilloscope(Effect eff, bool lyricActive) {
737 int cy = SCREEN_HEIGHT / 2;
738
739 // Subtle grid dots like a real oscilloscope screen
740 for (int gx = 0; gx < SCREEN_WIDTH; gx += 16) {
741 for (int gy = 0; gy < SCREEN_HEIGHT; gy += 16) {
742 display.drawPixel(gx, gy, WHITE);
743 }
744 }
745 // Dotted center reference line
746 for (int x = 0; x < SCREEN_WIDTH; x += 4) display.drawPixel(x, cy, WHITE);
747
748 // Wave parameters
749 float amplitude = lyricActive ? 18.0 : 8.0;
750 float frequency = lyricActive ? 2.5 : 1.5;
751 if (eff == EFFECT_ZOOM_IN) { amplitude = 28.0; frequency = 5.0; }
752 else if (eff == EFFECT_SHAKE) { amplitude = 22.0; frequency = 3.5; }
753 else if (eff == EFFECT_BOUNCE || eff == EFFECT_TYPEWRITER) { amplitude = 15.0; frequency = 2.0; }
754
755 oscPhase += lyricActive ? 0.15 : 0.05;
756
757 // Primary wave
758 int prevY1 = cy;
759 for (int x = 0; x < SCREEN_WIDTH; x++) {
760 float t = (float)x / SCREEN_WIDTH;
761 float y = cy + amplitude * sin(t * frequency * PI * 2.0 + oscPhase);
762 y += (amplitude * 0.3) * sin(t * frequency * 2.0 * PI * 2.0 + oscPhase * 1.7);
763 if (eff == EFFECT_SHAKE || eff == EFFECT_ZOOM_IN) y += random(-2, 3);
764 int iy = constrain((int)y, 0, SCREEN_HEIGHT - 1);
765 if (x > 0) display.drawLine(x - 1, prevY1, x, iy, WHITE);
766 prevY1 = iy;
767 }
768
769 // Secondary counter-wave for complexity
770 int prevY2 = cy;
771 for (int x = 0; x < SCREEN_WIDTH; x++) {
772 float t = (float)x / SCREEN_WIDTH;
773 float y = cy + (amplitude * 0.5) * sin(t * frequency * PI * 2.0 - oscPhase * 0.8 + PI / 3.0);
774 int iy = constrain((int)y, 0, SCREEN_HEIGHT - 1);
775 if (x > 0) display.drawLine(x - 1, prevY2, x, iy, WHITE);
776 prevY2 = iy;
777 }
778}
779
780// --- 9. Lightning Bolts ---
781void generateBolt(int idx) {
782 bolts[idx].numPts = MAX_BOLT_PTS;
783 bolts[idx].bx[0] = SCREEN_WIDTH / 2 + random(-25, 26);
784 bolts[idx].by[0] = 0;
785 for (int i = 1; i < MAX_BOLT_PTS; i++) {
786 bolts[idx].by[i] = bolts[idx].by[i - 1] + (SCREEN_HEIGHT / (MAX_BOLT_PTS - 1));
787 bolts[idx].bx[i] = bolts[idx].bx[i - 1] + random(-14, 15);
788 if (bolts[idx].bx[i] < 3) bolts[idx].bx[i] = 3;
789 if (bolts[idx].bx[i] > SCREEN_WIDTH - 3) bolts[idx].bx[i] = SCREEN_WIDTH - 3;
790 }
791 bolts[idx].framesLeft = random(3, 8);
792 bolts[idx].active = true;
793}
794
795void drawLightning(Effect eff, bool lyricActive) {
796 // Spawn bolts
797 int spawnChance = lyricActive ? 35 : 8;
798 if (eff == EFFECT_SHAKE || eff == EFFECT_INVERT || eff == EFFECT_ZOOM_IN) spawnChance = 65;
799
800 if (random(100) < spawnChance) {
801 for (int i = 0; i < MAX_BOLTS; i++) {
802 if (!bolts[i].active) { generateBolt(i); break; }
803 }
804 }
805
806 // Draw bolts
807 for (int i = 0; i < MAX_BOLTS; i++) {
808 if (bolts[i].active) {
809 // Main bolt (thick)
810 for (int j = 0; j < bolts[i].numPts - 1; j++) {
811 display.drawLine(bolts[i].bx[j], bolts[i].by[j], bolts[i].bx[j + 1], bolts[i].by[j + 1], WHITE);
812 display.drawLine(bolts[i].bx[j] + 1, bolts[i].by[j], bolts[i].bx[j + 1] + 1, bolts[i].by[j + 1], WHITE);
813 }
814
815 // Random branch from a midpoint
816 if (bolts[i].numPts > 4) {
817 int bi = random(2, bolts[i].numPts - 2);
818 int brX = bolts[i].bx[bi] + random(-22, 23);
819 int brY = bolts[i].by[bi] + random(5, 15);
820 display.drawLine(bolts[i].bx[bi], bolts[i].by[bi], brX, brY, WHITE);
821 // Second branch
822 int b2i = random(1, bolts[i].numPts - 1);
823 int br2X = bolts[i].bx[b2i] + random(-18, 19);
824 int br2Y = bolts[i].by[b2i] + random(3, 12);
825 display.drawLine(bolts[i].bx[b2i], bolts[i].by[b2i], br2X, br2Y, WHITE);
826 }
827
828 bolts[i].framesLeft--;
829 if (bolts[i].framesLeft <= 0) bolts[i].active = false;
830 }
831 }
832
833 // Ambient electrical sparkle
834 int sparkles = lyricActive ? 8 : 3;
835 for (int i = 0; i < sparkles; i++) {
836 display.drawPixel(random(SCREEN_WIDTH), random(SCREEN_HEIGHT), WHITE);
837 }
838}
839
840// --- 10. 3D Rotating Wireframe Cube ---
841void drawCube(Effect eff, bool lyricActive) {
842 float sm = lyricActive ? 1.5 : 0.5;
843 if (eff == EFFECT_ZOOM_IN) sm = 5.0;
844 else if (eff == EFFECT_SHAKE || eff == EFFECT_INVERT) sm = 3.0;
845 else if (eff == EFFECT_BOUNCE || eff == EFFECT_POP) sm = 2.0;
846
847 cubeAX += 0.02 * sm;
848 cubeAY += 0.03 * sm;
849 cubeAZ += 0.01 * sm;
850 if (cubeAX > 2 * PI) cubeAX -= 2 * PI;
851 if (cubeAY > 2 * PI) cubeAY -= 2 * PI;
852 if (cubeAZ > 2 * PI) cubeAZ -= 2 * PI;
853
854 float sz = (eff == EFFECT_POP || eff == EFFECT_BOUNCE) ? 22.0 : 18.0;
855 int cx = SCREEN_WIDTH / 2, cy = SCREEN_HEIGHT / 2;
856
857 // 8 vertices of a unit cube scaled by sz
858 float verts[8][3] = {
859 {-1, -1, -1}, { 1, -1, -1}, { 1, 1, -1}, {-1, 1, -1},
860 {-1, -1, 1}, { 1, -1, 1}, { 1, 1, 1}, {-1, 1, 1}
861 };
862
863 int proj[8][2];
864 for (int i = 0; i < 8; i++) {
865 float x = verts[i][0] * sz;
866 float y = verts[i][1] * sz;
867 float z = verts[i][2] * sz;
868
869 // Rotate X
870 float y1 = y * cos(cubeAX) - z * sin(cubeAX);
871 float z1 = y * sin(cubeAX) + z * cos(cubeAX);
872 y = y1; z = z1;
873 // Rotate Y
874 float x1 = x * cos(cubeAY) + z * sin(cubeAY);
875 z1 = -x * sin(cubeAY) + z * cos(cubeAY);
876 x = x1; z = z1;
877 // Rotate Z
878 x1 = x * cos(cubeAZ) - y * sin(cubeAZ);
879 y1 = x * sin(cubeAZ) + y * cos(cubeAZ);
880 x = x1; y = y1;
881
882 // Perspective projection
883 float fov = 60.0;
884 float scale = fov / (fov + z);
885 proj[i][0] = cx + (int)(x * scale);
886 proj[i][1] = cy + (int)(y * scale);
887 }
888
889 // 12 edges: back face, front face, connecting
890 int edges[12][2] = {
891 {0, 1}, {1, 2}, {2, 3}, {3, 0},
892 {4, 5}, {5, 6}, {6, 7}, {7, 4},
893 {0, 4}, {1, 5}, {2, 6}, {3, 7}
894 };
895 for (int i = 0; i < 12; i++) {
896 display.drawLine(proj[edges[i][0]][0], proj[edges[i][0]][1],
897 proj[edges[i][1]][0], proj[edges[i][1]][1], WHITE);
898 }
899
900 // Draw vertex dots for extra visual punch
901 for (int i = 0; i < 8; i++) {
902 display.fillCircle(proj[i][0], proj[i][1], 2, WHITE);
903 }
904}
905The Dracula OLED Music Visualizer is an advanced OLED animation project that recreates synchronized visual effects inspired by the song "Dracula" by JENNIE and Tame Impala.
The system displays dynamic lyrics, animated backgrounds, and visual effects on a 0.96-inch SSD1306 OLED display. Multiple visual scenes automatically switch according to a predefined timeline, creating the illusion of a music-synchronized OLED animation.
This project demonstrates advanced graphics programming, animation techniques, and OLED rendering on microcontrollers.
Words appear on the OLED according to a carefully timed sequence.
The visualizer includes:
Several text effects are included:
128×64)128×64I2CSSD1306| OLED Pin | ESP32 Pin |
| -------- | --------- |
| VCC | 3.3V |
| GND | GND |
| SDA | GPIO21 |
| SCL | GPIO22 |
OLED VCC → 3.3V
OLED GND → GND
OLED SDA → GPIO21
OLED SCL → GPIO22
Verify your OLED I2C address before uploading the code. Most displays use 0x3C.
Install the following libraries through Arduino IDE Library Manager:
Connect the ESP32 board to your computer.
Open the Arduino IDE.
Install all required libraries.
Select:
ESP32 Dev ModuleUpload the sketch.
After reset, the animation sequence will start automatically.
The project contains a predefined lyric timeline.
Each lyric contains:
The visualizer dynamically switches between animation modes based on the active lyric.
Every frame:
This process creates smooth real-time animation.
Check the I2C address and verify SDA/SCL wiring.
Confirm the OLED uses the SSD1306 driver and receives 3.3V.
Ensure all required Adafruit libraries are installed.
Use an ESP32. Arduino UNO does not have sufficient memory for this project.
After completing this project you will understand:
The Dracula OLED Music Visualizer is a high-performance graphics project that transforms a small SSD1306 OLED display into a dynamic animation engine. By combining timed lyrics, multiple animation systems, and visual effects, the project creates an immersive music-inspired OLED experience using an ESP32 microcontroller.