#include "Bad_Apple.h"
#include "Bad_Apple_English_lyrics.h"
#include <LiquidCrystal_I2C.h>
//Bad Apple!! but it's an LCD2004
#define LED_BUILTIN 2
LiquidCrystal_I2C lcd(0x27, 20, 4); // Adjust I2C address if needed
const bool debug = false;
const bool lyrics = true;
const int framePause = 1000.0 / 7.3 + 0.5; // 7.3 fps (136.99, rounded to 137)
const int frameCount = sizeof(frames) / sizeof(frames[0]);
const int chantCount = sizeof(chant) / sizeof(chant[0]);
unsigned long playStart = millis();
int frameDelay = 0; // Initial delay in milliseconds (10 for QA009, 15 with lyrics off, 0 for Wokwi)
//float framesPerMeasure = (FPS * 60.0) / BPM * beatsPerMeasure;
float framesPerMeasure = (7.3 * 60.0) / 138 * 4;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
Serial.println("Frame pause: " + String(framePause) + " units, frame delay: " + String(frameDelay) + " ms");
Serial.println("Frame count: " + String(frameCount) + " frames, frames/measure: " + String(framesPerMeasure));
lcd.init();
lcd.backlight();
// Draw glyphs to screen
lcd.setCursor(0, 0); // Top-left corner
//lcd.setCursor(8, 1); // Centered
for (int i = 0; i < 4; i++) {
lcd.write(i); // Display glyph i
}
lcd.setCursor(0, 1);
//lcd.setCursor(8, 2);
for (int i = 4; i < 8; i++) {
lcd.write(i); // Display glyph i
}
playStart = millis();
}
void loop() {
// setup
static int frame = 0;
//static int frame = 20; // temporarily skipping initial dark frames on Wokwi
static int lastFrame = 0;
static float lastMeasure = -2;
static int lastChant = -1;
// calculate frame
frame = (millis() - playStart) / framePause; // dynamically generate frame number based on start time and play duration
if (frame >= frameCount) {
frame = 0;
lastFrame = 0;
lastChant = -1;
Serial.print("Loop to frame 0 ");
delay(300); // keep in time with looping source video
playStart = millis();; // Loop back to start (1601 frames max allowed by compiler)
}
if (frame > lastFrame + 1) {
Serial.print("skip " + String(frame - (lastFrame + 1)) + " "); // display skipped frame count
}
if (frame < lastFrame + 1) {
Serial.print("dupe " + String((lastFrame + 1) - frame) + " "); // display duplicated frame count
}
lastFrame = frame;
if (debug) {
// optional on-screen diagnostics (suggest lowering frameDelay from 14 to 1)
lcd.setCursor(10, 0);
char frameStr[5]; // 4 digits + null terminator
sprintf(frameStr, "%04d", frame);
lcd.print("Frame ");
lcd.print(frameStr);
//
lcd.setCursor(10, 1);
unsigned long elapsed = millis() - playStart;
int minutes = elapsed / 60000;
int seconds = (elapsed % 60000) / 1000;
int tenths = (elapsed % 1000) / 100;
char timeStr[11]; // "TS m:ss.t" + null terminator
sprintf(timeStr, "TS %d:%02d.%d", minutes, seconds, tenths);
lcd.print(timeStr);
}
// if (frame % 10 == 0) { // replaced with measures below
// Serial.println(frame);
// }
float currentMeasureFloat = (float)frame / framesPerMeasure - 0.8; // 10-frame lead-in
int currentEighthNote = currentMeasureFloat * 8;
float currentMeasure = currentEighthNote / 8.0;
if (currentMeasure != lastMeasure) {
if (currentMeasure == (int)currentMeasure) {
Serial.println("Measure " + String(currentMeasure) + " frame " + String(frame));
digitalWrite(LED_BUILTIN, !((int)currentMeasure % 2));
}
lastMeasure = currentMeasure;
if (lyrics) {
// display lyrics
for (int i = lastChant + 1; i < chantCount; i++) { //starting at 0 every time gets slow by the end of the song
if (chant[i].measure == currentMeasure) {
//lcd.clear();
lcd.setCursor(5, 0); lcd.print(chant[i].line1);
lcd.setCursor(5, 1); lcd.print(chant[i].line2);
lcd.setCursor(5, 2); lcd.print(chant[i].line3);
lcd.setCursor(5, 3); lcd.print(chant[i].line4);
lastChant = i;
break;
}
}
}
}
// Load 8 glyphs into CGRAM
for (int i = 0; i < 8; i++) {
lcd.createChar(i, frames[frame][i]); // Each glyph is 8 bytes
}
// // hardcoded pause between frames (replaced with "calculate frame" above)
// frame++;
// if (frame % 6 == 1) { // ESP32 is slow (original method, replaced with millis() check below)
// frame++;
// }
// //frame += 5; // Wokwi is REALLY slow
// pause between frames to avoid "calculate frame" excessively skip or duplicate frames
delay(frameDelay); // 7.4 FPS is slightly faster then LCD2004 refresh rate, causing frames to need to be drawn twice to keep in sync
// Check for serial input for manual real-time adjustments
if (Serial.available() > 0) {
String input = Serial.readStringUntil('\n');
int serialMessage = input.toInt();
if (serialMessage >= 1 && serialMessage <= 1000) { // loop ms delay
frameDelay = serialMessage;
Serial.print("⏳ Frame delay updated to: ");
Serial.println(frameDelay);
} else if (serialMessage >= (0 - frameCount) && serialMessage <= -1) { // frame number
if (serialMessage == -1) serialMessage = 0;
playStart = millis() - (abs(serialMessage) * framePause); // adjust start time to reach desired frame
frame = (millis() - playStart) / framePause; // dynamically generate frame number based on start time and play duration
lastChant = -1;
Serial.print("🎬 Next frame updated to: ");
Serial.println(frame);
} else {
Serial.println("⚠️ Invalid value. Must be between 1-1000 ms delay, or -1 to " + String(0 - frameCount) + " for frame number (-1 to sync audio with source video).");
Serial.println("🛈 Current values: Frame delay: " + String(frameDelay) + " ms, frame " + String(frame));
}
}
}