// feb 12 1215am 2026
// added file overflow protection
//
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
// ---------------- AUDIO ----------------
AudioPlaySdWav playWav;
AudioRecordQueue recordQueueL;
AudioRecordQueue recordQueueR;
AudioOutputI2S i2s_out;
AudioInputI2S i2s_in;
AudioControlSGTL5000 sgtl5000;
AudioAnalyzePeak peakL;
AudioAnalyzePeak peakR;
AudioAnalyzeFFT1024 fft1;
AudioMixer4 mixerL;
AudioMixer4 mixerR;
// Audio connections
////AudioConnection patch1(playWav, 0, i2s_out, 0); // for playing
////AudioConnection patch2(playWav, 1, i2s_out, 1);
AudioConnection patch5(i2s_in, 0, recordQueueL, 0); // for recording
AudioConnection patch6(i2s_in, 1, recordQueueR, 0);
AudioConnection patchPeakL(i2s_in, 0, peakL, 0);
AudioConnection patchPeakR(i2s_in, 1, peakR, 0);
// playback to mixer
AudioConnection patchPlayL(playWav, 0, mixerL, 0);
AudioConnection patchPlayR(playWav, 1, mixerR, 0);
// mixer output
AudioConnection patchOutL(mixerL, 0, i2s_out, 0);
AudioConnection patchOutR(mixerR, 0, i2s_out, 1);
// mixer FFT
AudioConnection patchFFT(mixerL, 0, fft1, 0);
// ---------------- STATES ----------------
enum PlayerState { STOPPED,
PLAY,
SETTINGS,
MONITOR,
RECORD
};
PlayerState state = STOPPED;
PlayerState lastState = STOPPED; //// is this needed?
// ---------------- BUTTONS ----------------
#define BTN_STOP 37
#define BTN_PLAY 38
#define BTN_SETTINGS 39
#define BTN_MONITOR 40
#define BTN_RECORD 41
#define ENCODER_A 33
#define ENCODER_B 35
unsigned long lastPress[5] = { 0, 0, 0, 0, 0 };
const unsigned long debounce = 100;
//------------------ TFT ------------------
#define TFT_CS 28
#define TFT_DC 25
#define TFT_RST 24
#define SCREEN_WIDTH 160
#define SCREEN_HEIGHT 128
#define STATUS_HEIGHT 20
#define ST7735_PURPLE 0x780F
// 0x780F deep purple (good for status bar)
// 0x981F brighter purple
// 0x7016 darker purple
// 0xA0BF magenta-purple
// ---------------- SD & FILES ----------------
////#define SD_CS_PIN 10
#define MAX_FILES 200
char fileList[MAX_FILES][13];
int numFiles = 0;
int currentFile = -1;
bool isRecording = false;
File recordFile;
// Use SPI1 (pins 26 = MOSI, 27 = SCK)
Adafruit_ST7735 tft = Adafruit_ST7735(&SPI1, TFT_CS, TFT_DC, TFT_RST);
// ---------------- SD WRITE TIMING DEBUG ----------------
unsigned long sdWriteStart = 0;
unsigned long sdWriteMaxUs = 0;
unsigned long sdWriteTotalUs = 0;
unsigned long sdWriteCount = 0;
// block tracking
elapsedMicros timer; // recording elapsed time. to keep track of how many blocks should have come in
unsigned long blocksReceived = 0;
unsigned long blocksWritten = 0;
#define SWEEP_SECONDS 10
#define SWEEP_SAMPLES (SWEEP_SECONDS * 34) // ~34 peak updates per second at 44.1kHz
float peakSweep[SWEEP_SAMPLES]; // Stores combined L/R peak history
int sweepIndex = 0;
bool sweepFull = false;
unsigned long lastSweepUpdate = 0;
// ------------------FFT ----------------------
const int FFT_BARS = 128;
const int FFT_HEIGHT = 77;
const int FFT_Y = 52;
float fftFall[FFT_BARS];
float fftPeak[FFT_BARS];
// ---------------- PROTOTYPES ----------------
void stateStopped();
void statePlay();
void stateSettings();
void stateMonitor();
void stateRecord();
void scanFiles();
void listFiles(void);
void showStoppedUI(void);
void saveRecording();
void displayStatusLine(const char* prefix, const char* name);
void printNoExt(const char* n);
void createWavPlaceholder(File& f);
void finalizeWav(File& f);
int findNextRecNumber();
void drawPeakSweepMeter();
void updatePeakSweep();
int readEncoder();
void displayHeader();
void applySetting(int);
void drawInputBadge();
void drawInputParameter();
// ------- MISC. GLOBAL VARIABLES ---------
volatile uint32_t recordedSamples = 0;
bool useMic = false; // starts as Line In
bool bassBoostOn = false;
bool eqSmileyOn = false;
bool hpfOn = false;
bool autoVolOn = false;
int micGainLevel = 25; // Initial gain for AOM-6738P-R
bool micBiasOn = true; // Electret mics need this ON
// ---------------- SETUP ----------------
void setup() {
Serial.begin(115200);
// Audio
AudioMemory(1000);
sgtl5000.enable();
delay(100); // Critical: give codec time to initialize
sgtl5000.adcHighPassFilterDisable();
sgtl5000.volume(0.6);
//sgtl5000.lineInLevel(5); // Range 015 (0 = max gain ~+30 dB, 15 = min gain 0 dB, default is 5)
sgtl5000.lineInLevel(13);
sgtl5000.unmuteHeadphone();
////sgtl5000.volume(0.8);
////sgtl5000.dacVolume(1.0); // Full DAC level
////sgtl5000.lineOutLevel(13); // Loudest line out (13 = max)
sgtl5000.inputSelect(AUDIO_INPUT_LINEIN);
mixerL.gain(0, 1.0); // full volume for left channel
fft1.windowFunction(AudioWindowHanning1024);
// or AudioWindowBlackmanNuttall1024 for sharper peaks
// I2C
Wire.setSDA(18);
Wire.setSCL(19);
// TFT
SPI1.begin(); // Start SPI1 explicitly
tft.initR(INITR_BLACKTAB);
tft.setRotation(3);
// Common ground pin for the buttons
pinMode(36, OUTPUT);
digitalWrite(36, LOW); // Set to ground for the buttons
pinMode(34, OUTPUT);
digitalWrite(34, LOW); // Pin 35 becomes GND for the encoder
// Button pins with internal pull-up resistors
pinMode(BTN_STOP, INPUT_PULLUP);
pinMode(BTN_PLAY, INPUT_PULLUP);
pinMode(BTN_SETTINGS, INPUT_PULLUP);
pinMode(BTN_MONITOR, INPUT_PULLUP);
pinMode(BTN_RECORD, INPUT_PULLUP);
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
// blink LED twice on startup
pinMode(LED_BUILTIN, OUTPUT); // Built-in LED on pin 13
for (int i = 0; i < 2; i++) {
digitalWrite(LED_BUILTIN, HIGH);
delay(200); // On for 200ms
digitalWrite(LED_BUILTIN, LOW);
delay(200); // Off for 200ms
}
// SD card
if (!SD.begin(BUILTIN_SDCARD)) {
Serial.println("SD card failed");
tft.fillScreen(ST7735_BLACK);
tft.setTextColor(ST7735_RED);
tft.setTextSize(2);
tft.setCursor(0, 40);
tft.println(" NO CARD");
while (1)
;
}
Serial.println("SD is OK"); ////
}
// ---------------- LOOP ----------------
void loop() {
switch (state) {
case STOPPED: stateStopped(); break;
case PLAY: statePlay(); break;
case SETTINGS: stateSettings(); break;
case MONITOR: stateMonitor(); break;
case RECORD: stateRecord(); break;
}
}
// ---------------- STATES ----------------
void stateStopped() {
Serial.println("Stopped");
displayHeader();
// Scan files at entry
scanFiles(); // determines number of files (numFiles)
// Ensure there is a valid selection
if (currentFile < 0 && numFiles > 0) currentFile = 0;
tft.setTextColor(ST7735_YELLOW);
tft.setTextSize(1);
showStoppedUI(); // show list on TFT at entry
// Warn if running low on filespace
if (numFiles >= MAX_FILES-20 && MAX_FILES > 20) {
tft.setTextColor(ST7735_YELLOW);
tft.setCursor(80, 26);
tft.print(MAX_FILES - numFiles);
tft.setCursor(80, 36);
tft.print("RECORDINGS");
tft.setCursor(80, 46);
tft.print("LEFT");
}
// stopped loop
while (1) {
unsigned long now = millis();
// CHECK THE BUTTONS
if (digitalRead(BTN_PLAY) == LOW && now - lastPress[1] > debounce) {
lastPress[1] = now;
if (numFiles > 0) {
state = PLAY;
return;
}
}
if (digitalRead(BTN_SETTINGS) == LOW && now - lastPress[2] > debounce) {
lastPress[2] = now;
state = SETTINGS;
return;
}
if (digitalRead(BTN_MONITOR) == LOW && now - lastPress[3] > debounce) {
lastPress[3] = now;
state = MONITOR;
return;
}
if (digitalRead(BTN_RECORD) == LOW && now - lastPress[4] > debounce) {
lastPress[4] = now;
state = RECORD;
return;
}
int step = readEncoder();
if (step != 0) {
if (step > 0) {
// Clockwise scroll down
if (currentFile < numFiles - 1) {
currentFile++;
}
} else {
// Counter-clockwise scroll up
if (currentFile > 0) {
currentFile--;
}
}
showStoppedUI(); // Redraw list
}
// SERIAL
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
s.trim();
if (s == "l") {
listFiles();
} else if (s == "v") {
verifyWav();
} else
{
int n = s.toInt();
if (n > 0 && n <= numFiles) {
currentFile = n - 1;
state = PLAY;
return;
} // file count check
} // play command
} // serial commands
} // loop
} // function
// -----------------------------------------------------------
void statePlay() {
// ENTRY (runs once)
displayHeader();
if (playWav.isPlaying()) playWav.stop();
playWav.play(fileList[currentFile]);
delay(5); // A brief delay for the library to read WAV header
// display file name
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(1);
tft.setCursor(0, 26);
//tft.print(prefix);
printNoExt(fileList[currentFile]);
Serial.print("Playing ");
Serial.println(fileList[currentFile]);
// Get total length in milliseconds (WAV header has it)
unsigned long totalMs = playWav.lengthMillis();
unsigned long totalSec = totalMs / 1000;
unsigned long totalMin = totalSec / 60;
unsigned long totalS = totalSec % 60;
unsigned long startTime = millis(); // record starting time
unsigned long lastElapsedUpdate = 0; // tracks when we last updated time
// play loop
while (1) {
unsigned long now = millis();
// auto-exit when playback ends
if (!playWav.isPlaying()) {
playWav.stop();
Serial.println("end of song ");
state = STOPPED;
return;
}
// Check buttons and commands
// STOP
if (digitalRead(BTN_STOP) == LOW && now - lastPress[1] > debounce) {
lastPress[1] = now;
playWav.stop();
state = STOPPED;
return;
}
// SERIAL
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
s.trim();
if (s == "s") {
playWav.stop();
state = STOPPED;
return;
}
}
// ---------- UPDATE ELAPSED TIME EVERY SECOND ----------
if (now - lastElapsedUpdate >= 100) {
lastElapsedUpdate = now;
uint32_t elapsedSec = (now - startTime) / 1000;
;
uint32_t min = elapsedSec / 60;
uint32_t sec = elapsedSec % 60;
tft.fillRect(0, 36, SCREEN_WIDTH, 20, ST7735_BLACK); // clear previous time line
tft.setCursor(0, 38);
tft.print(min);
tft.print(":");
if (sec < 10) tft.print("0"); // 2 digits
tft.print(sec);
tft.print(" / ");
tft.print(totalMin);
tft.print(":");
if (totalS < 10) tft.print("0"); // 2 digits
tft.println(totalSec % 60);
delay(5); // yield to background processes
}
static unsigned long lastAvailable = 0;
static int countAvailable = 0;
if (fft1.available()) {
countAvailable++;
unsigned long now = millis();
if (now - lastAvailable > 0) {
Serial.print("FFT available after ");
Serial.print(now - lastAvailable);
Serial.print(" ms (count: ");
Serial.print(countAvailable);
Serial.println(")");
lastAvailable = now;
}
drawFFT();
}
}
}
//-----------------------------------------------------------------------
void stateMonitor() {
displayHeader();
Serial.println("Monitor Mode");
drawInputBadge();
tft.setTextSize(1);
tft.setCursor(0, 25);
// Reset sweep for fresh graph
sweepIndex = 0;
sweepFull = false;
memset(peakSweep, 0, sizeof(peakSweep));
unsigned long lastDraw = 0;
while (true) {
updatePeakSweep();
if (millis() - lastDraw > 100) {
lastDraw = millis();
drawPeakSweepMeter();
}
if (digitalRead(BTN_STOP) == LOW) {
delay(200);
state = STOPPED;
return;
}
if (digitalRead(BTN_PLAY) == LOW) {
delay(200);
state = PLAY;
return;
}
if (digitalRead(BTN_RECORD) == LOW) {
delay(200);
state = RECORD;
return;
}
delay(10);
}
}
//-------------------------------------------------------------------------
void stateRecord() {
// ---------- ENTRY ----------
displayHeader();
// Make sure the system doesn't crash
if (numFiles >= MAX_FILES) {
tft.setTextColor(ST7735_RED);
tft.setTextSize(1);
tft.setCursor(0, 26);
tft.print("FILE LIMIT REACHED");
Serial.println("FILE LIMIT REACHED");
delay(5000);
state = STOPPED;
return;
}
drawInputBadge();
int n = findNextRecNumber();
if (n == 0) {
Serial.println("No free filenames!");
state = STOPPED;
return;
}
char name[13];
sprintf(name, "REC%04d.WAV", n);
tft.setTextColor(ST7735_RED);
tft.setTextSize(1);
tft.setCursor(2, 22);
tft.print(name);
Serial.print("Recording ");
Serial.println(name);
recordFile = SD.open(name, FILE_WRITE);
createWavPlaceholder(recordFile);
unsigned long startTime = millis(); // record starting time
unsigned long lastElapsedUpdate = 0; // tracks when we last updated time
recordQueueL.begin();
recordQueueR.begin();
timer = 0;
isRecording = true;
sdWriteMaxUs = 0;
sdWriteTotalUs = 0;
sdWriteCount = 0;
// ---------- RUN ----------
while (1) {
unsigned long now = millis();
saveRecording();
// ---------- UPDATE ELAPSED TIME EVERY SECOND ----------
if (now - lastElapsedUpdate >= 200) {
lastElapsedUpdate = now;
uint32_t elapsedSec = (millis() - startTime) / 1000;
;
uint32_t min = elapsedSec / 60;
uint32_t sec = elapsedSec % 60;
tft.fillRect(0, 35, 50, 10, ST7735_BLACK); // clear previous time line
tft.setCursor(0, 36);
tft.print(min);
tft.print(":");
if (sec < 10) tft.print("0");
tft.println(sec);
updatePeakSweep();
drawPeakSweepMeter();
}
// Check buttons and serial to end recording and determine next state
// STOP button
if (digitalRead(BTN_STOP) == LOW && now - lastPress[1] > debounce) {
lastPress[1] = now;
state = STOPPED;
break;
} // PLAY button
if (digitalRead(BTN_PLAY) == LOW && now - lastPress[2] > debounce) {
lastPress[1] = now;
state = PLAY;
break;
}
// MONITOR button
if (digitalRead(BTN_MONITOR) == LOW && now - lastPress[3] > debounce) {
lastPress[3] = now;
state = MONITOR;
break;
}
// SERIAL stop
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
s.trim();
if (s == "s") {
state = STOPPED;
break;
}
}
}
// ---------- EXIT ----------
recordQueueL.end();
recordQueueR.end();
saveRecording();
finalizeWav(recordFile);
uint32_t size = recordFile.size();
recordFile.close();
isRecording = false;
Serial.print("Recording stopped. Bytes: ");
Serial.println(size);
if (sdWriteCount > 0) {
float avg = (float)sdWriteTotalUs / sdWriteCount;
Serial.print("SD write max: ");
Serial.print(sdWriteMaxUs);
Serial.print(" us, avg: ");
Serial.println(avg, 1);
}
//// TEST
scanFiles(); // Refresh the list to include the new file
//currentFile = 0; // Default to first file if not found (safety)
for (int i = 0; i < numFiles; i++) {
if (strcmp(fileList[i], name) == 0) {
currentFile = i;
break;
}
}
// state = STOPPED;
return;
}
//---------------------------------------------------------
void listFiles(void) {
scanFiles();
Serial.println("\nFiles:");
for (int i = 0; i < numFiles; i++) {
File f = SD.open(fileList[i]);
Serial.print(i + 1);
Serial.print(": ");
Serial.print(fileList[i]);
Serial.print(" ");
Serial.println(f.size());
f.close();
}
}
//-------------------------------------------------------------------
void saveRecording() {
static uint8_t sdBuffer[512];
static uint16_t sdIndex = 0;
unsigned long startTime = millis();
while (recordQueueL.available() && recordQueueR.available()) {
int16_t* l = recordQueueL.readBuffer();
int16_t* r = recordQueueR.readBuffer();
recordQueueL.freeBuffer();
recordQueueR.freeBuffer();
blocksWritten++;
for (int i = 0; i < 128; i++) {
// interleave L/R samples (16-bit each)
sdBuffer[sdIndex++] = l[i] & 0xFF;
sdBuffer[sdIndex++] = (l[i] >> 8) & 0xFF;
sdBuffer[sdIndex++] = r[i] & 0xFF;
sdBuffer[sdIndex++] = (r[i] >> 8) & 0xFF;
// write full SD block
if (sdIndex >= 512) {
recordFile.write(sdBuffer, 512);
sdIndex = 0;
}
}
//if(digitalReadFast(BTN_STOP) == LOW) return; // this seems to work
if (millis() > startTime + 200) return; // solves both problems
}
}
// ---------------- FILES & UI ----------------
void scanFiles() {
numFiles = 0;
File root = SD.open("/");
while (true) {
File f = root.openNextFile();
if (!f) break;
if (!f.isDirectory() && f.size() >= 44) strncpy(fileList[numFiles++], f.name(), 12);
f.close();
}
root.close();
}
//------------------------------------------------
void printNoExt(const char* n) {
char b[13];
strncpy(b, n, 12);
b[12] = 0;
char* d = strrchr(b, '.');
if (d) *d = 0;
tft.print(b);
}
// ---------------- WAV ----------------
void createWavPlaceholder(File & f) {
uint8_t h[44] = { 'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ', 16, 0, 0, 0, 1, 0, 2, 0, 0x44, 0xAC, 0, 0, 0x10, 0xB1, 2, 0, 4, 0, 16, 0, 'd', 'a', 't', 'a', 0, 0, 0, 0 };
f.write(h, 44);
}
//--------------------------------------
void finalizeWav(File & f) {
uint32_t size = f.size();
uint32_t data = size - 44;
uint32_t riff = size - 8;
f.seek(4);
f.write((uint8_t*)&riff, 4);
f.seek(40);
f.write((uint8_t*)&data, 4);
}
//-------------------------------------
int findNextRecNumber() {
for (int i = 1; i <= 9999; i++) {
char n[13];
sprintf(n, "REC%04d.WAV", i);
if (!SD.exists(n)) return i;
}
return 0;
}
//------------------------------------
void verifyWav() {
Serial.println("\n=== WAV Header Verification ===");
String input = Serial.readStringUntil('\n');
input.trim();
int fileIndex = -1;
bool useCurrent = (input.length() == 0 || input == "v");
if (!useCurrent) {
fileIndex = input.toInt() - 1;
if (fileIndex < 0 || fileIndex >= numFiles) {
Serial.println("Invalid file number.");
return;
}
} else {
if (currentFile < 0 || currentFile >= numFiles) {
Serial.println("No file selected.");
return;
}
fileIndex = currentFile;
}
const char* filename = fileList[fileIndex];
Serial.print("File: ");
Serial.println(filename);
File file = SD.open(filename);
if (!file) {
Serial.println("ERROR: Cannot open file.");
return;
}
unsigned long fileSize = file.size();
Serial.print("Reported file size (SD): ");
Serial.print(fileSize);
Serial.println(" bytes");
if (fileSize < 44) {
Serial.println("ERROR: File too small for WAV header.");
file.close();
return;
}
uint8_t header[44];
file.read(header, 44);
file.close();
bool valid = true;
if (memcmp(header, "RIFF", 4) != 0) {
valid = false;
Serial.println("FAIL: Missing 'RIFF'");
}
else Serial.println("OK: 'RIFF' chunk");
uint32_t riffSize = header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24);
Serial.print("RIFF size field: ");
Serial.print(riffSize);
Serial.print(" (0x");
Serial.print(riffSize, HEX);
Serial.println(") implied total file size = RIFF + 8");
if (memcmp(header + 8, "WAVE", 4) != 0) {
valid = false;
Serial.println("FAIL: Missing 'WAVE'");
}
else Serial.println("OK: 'WAVE' format");
// Skip fmt checks for brevity (keep your existing ones)
if (memcmp(header + 36, "data", 4) != 0) {
valid = false;
Serial.println("FAIL: Missing 'data' chunk");
}
else Serial.println("OK: 'data' chunk present");
uint32_t dataSize = header[40] | (header[41] << 8) | (header[42] << 16) | (header[43] << 24);
Serial.print("Data size field: ");
Serial.print(dataSize);
Serial.print(" (0x");
Serial.print(dataSize, HEX);
Serial.println(") implied audio bytes");
// Compare sizes
long diff = riffSize - dataSize;
Serial.print("Difference (RIFF - data): ");
Serial.print(diff);
Serial.print(" bytes");
if (diff == 36) {
Serial.println(" EXACTLY 36 (standard, no extra chunks)");
} else {
Serial.print(" NOT 36 (");
if (diff > 36) Serial.print("extra chunks or padding");
else Serial.print("data field too small");
Serial.println(")");
}
if (fileSize == 44) {
Serial.println("ERROR: No audio data. Header only");
}
Serial.print("\n=== RESULT: ");
if (valid) {
Serial.println("VALID CD-QUALITY WAV HEADER ===");
} else {
Serial.println("INVALID HEADER ===");
}
}
//-----------------------------------------
void updatePeakSweep() {
unsigned long now = millis();
if (now - lastSweepUpdate < 29) return; // ~34 Hz update
lastSweepUpdate = now;
if (peakL.available() && peakR.available()) {
float linear = max(peakL.read(), peakR.read()); // 0.01.0
// Convert to dB (-60 dB to 0 dB range)
float db = (linear > 0.0001) ? 20.0 * log10(linear) : -60.0;
// Normalize to 0.0 (quiet) to 1.0 (0 dB)
float normalized = max(0.0, (db + 60.0) / 60.0);
////float normalized = 0.0; // TEST: Force absolute zero line at bottom////
peakSweep[sweepIndex] = normalized;
sweepIndex++;
if (sweepIndex >= SWEEP_SAMPLES) {
sweepIndex = 0;
sweepFull = true;
}
}
}
// --- Draw with clipping highlight ---
void drawPeakSweepMeter() {
const int x = 4;
const int y = 60; // Start lower to leave room for text above
const int w = SCREEN_WIDTH - 8;
const int h = 60; // Height fits below text
// Clear only the graph area
tft.fillRect(x, y - 5, w, h, ST7735_BLACK);
// Draw blue border box
// tft.drawRect(x, y, w, h, ST7735_BLUE);
// Draw blue border OUTSIDE the graphing area
tft.drawRect(x - 1, y - 4, w + 4, h + 4, ST7735_BLUE);
////tft.drawRect(x - 1, y - 1, w + 2, h + 2, ST7735_BLUE); // Border outside graphing area
int samples = sweepFull ? SWEEP_SAMPLES : sweepIndex;
int start = sweepFull ? sweepIndex : 0;
for (int i = 1; i < samples; i++) {
int idx1 = (start + i - 1) % SWEEP_SAMPLES;
int idx2 = (start + i) % SWEEP_SAMPLES;
int px1 = x + (i - 1) * w / SWEEP_SAMPLES;
int px2 = x + i * w / SWEEP_SAMPLES;
// Pull quiet line 2 pixels above bottom
int py1 = y + h - (int)(peakSweep[idx1] * h) - 2;
int py2 = y + h - (int)(peakSweep[idx2] * h) - 2;
uint16_t color = ST7735_YELLOW;
if (peakSweep[idx2] >= 0.95) color = ST7735_RED;
tft.drawLine(px1, py1, px2, py2, color);
}
// Current peak marker erase old, draw new (no trail)
if (samples > 0) {
static int lastX = -1;
static int lastY = -1;
int currentIdx = (start + samples - 1) % SWEEP_SAMPLES;
int currentX = x + (samples - 1) * w / SWEEP_SAMPLES;
int currentY = y + h - (int)(peakSweep[currentIdx] * h) - 2;
// Keep marker inside graph
currentX = constrain(currentX, x + 4, x + w - 4);
currentY = constrain(currentY, y + 4, y + h - 4);
// Erase previous marker
if (lastX != -1) {
tft.fillCircle(lastX, lastY, 5, ST7735_BLACK); // Slightly larger erase
}
// Draw new marker
uint16_t markerColor = (peakSweep[currentIdx] >= 0.95) ? ST7735_RED : ST7735_CYAN;
tft.fillCircle(currentX, currentY, 3, markerColor);
lastX = currentX;
lastY = currentY;
}
}
//-------------------------------------------
void showStoppedUI() {
const int MAX_VISIBLE = 9;
const int LINE_HEIGHT = 12;
const int START_Y = 22;
// Auto-center selection
int idealOffset = currentFile - MAX_VISIBLE / 2;
idealOffset = max(0, min(idealOffset, numFiles - MAX_VISIBLE));
int scrollOffset = idealOffset;
tft.fillRect(0, START_Y, SCREEN_WIDTH, SCREEN_HEIGHT - START_Y, ST7735_BLACK);
tft.setTextSize(1);
tft.setTextColor(ST7735_WHITE);
int y = START_Y;
int startIdx = scrollOffset;
int endIdx = min(numFiles, scrollOffset + MAX_VISIBLE);
for (int i = startIdx; i < endIdx; i++) {
tft.setCursor(4, y);
if (i == currentFile) {
tft.setTextColor(ST7735_GREEN);
tft.print("> ");
} else {
tft.setTextColor(ST7735_WHITE);
tft.print(" ");
}
printNoExt(fileList[i]);
tft.println();
y += LINE_HEIGHT;
}
// Scroll indicators
if (scrollOffset > 0) {
...
This file has been truncated, please download it to see its full contents.
Comments