// Bad Apple!! but it's a bunch of MAX7219s
#include "Bad_Apple.h"
#include "Bad_Apple_English_lyrics.h"
#include <LiquidCrystal_I2C.h>
#include <MATRIX7219.h>
#define LED_BUILTIN 2
#define DIO 19
#define CS 5
#define CLK 18
#define FPS 15
#define NUM_MODULES 12
// Global buffer: 12 modules × 8 rows
uint8_t rowBuffer[12][8] = {0};
const int dataPin = 19;
const int clockPin = 18;
const int latchPin = 5;
const int numModules = 12;
LiquidCrystal_I2C lcd(0x27, 20, 4); // Adjust I2C address if needed
MATRIX7219 matrix(DIO, CS, CLK, NUM_MODULES); // 3 matrices
const bool debug = false;
const bool lyrics = false; // true; // false for MAX7219
const int framePause = 1000.0 / FPS + 0.5;
const int frameCount = sizeof(frames) / sizeof(frames[0]);
const int chantCount = sizeof(chant) / sizeof(chant[0]);
unsigned long playStart = millis();
int frameDelay = 27; // Initial delay in milliseconds
// float framesPerMeasure = (FPS * 60.0) / BPM * beatsPerMeasure;
float framesPerMeasure = (FPS * 60.0) / 138 * 4;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
delay(100);
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();
matrix.begin();
matrix.setBrightness(5); // Optional: 0–15
matrix.clear();
pinMode(2, OUTPUT);
pinMode(dataPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(latchPin, OUTPUT);
playStart = millis();
}
void loop() {
// setup
static int frame = 0;
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
// frame += 1; // for testing all frames
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
}
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)
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;
}
}
}
}
// clear frame
drawFrame3(frame);
// // 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);
// 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));
}
}
}
void sendFullRow(uint8_t rowIndex, uint8_t moduleMasks[12]) {
digitalWrite(latchPin, LOW);
for (int i = 0; i < 12; i++) {
shiftOut(dataPin, clockPin, MSBFIRST, rowIndex); // Row address
shiftOut(dataPin, clockPin, MSBFIRST, moduleMasks[i]); // Unique mask per module
// shiftOut(dataPin, clockPin, MSBFIRST, reverseBits(moduleMasks[i])); // Unique mask per module
// Serial.print(String(moduleMasks[i]) + ',');
// Serial.print("Module "); Serial.print(i);
// Serial.print(" → Row "); Serial.print(rowIndex);
// Serial.print(" → Mask "); Serial.println(moduleMasks[i], BIN);
}
digitalWrite(latchPin, HIGH);
}
uint8_t reverseBits(uint8_t b) {
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; // Swap nibbles
b = (b & 0xCC) >> 2 | (b & 0x33) << 2; // Swap bit pairs
b = (b & 0xAA) >> 1 | (b & 0x55) << 1; // Swap individual bits
return b;
}
void drawFrame3(int frameIndex) {
for (uint8_t rowInModule = 1; rowInModule <= 8; rowInModule++) {
uint8_t moduleMasks[12] = {0};
for (uint8_t moduleIndex = 0; moduleIndex < 12; moduleIndex++) {
uint8_t moduleRow = moduleIndex / 4;
uint8_t moduleCol = moduleIndex % 4;
uint8_t rowIndex = moduleRow * 8 + (rowInModule - 1);
for (uint8_t byteIndex = 0; byteIndex < 4; byteIndex++) {
uint8_t slice = frames[frameIndex][rowIndex][byteIndex];
slice = reverseBits(slice); // if needed
for (uint8_t bit = 0; bit < 8; bit++) {
if (slice & (1 << bit)) {
uint8_t pixelIndex = byteIndex * 8 + bit;
uint8_t colModule = pixelIndex / 8;
if (colModule == moduleCol) {
uint8_t bitPos = pixelIndex % 8;
moduleMasks[moduleIndex] |= (1 << bitPos);
}
}
}
}
}
sendFullRow(rowInModule, moduleMasks);
}
}