micro222
Published © GPL3+

Teensy Stereo Audio Recorder

Based on the Teensy 4.1 microcontroller board and the SGTL5000 audio codec. CD quality sound. Simple to use.

IntermediateWork in progress8 hours5
Teensy Stereo Audio Recorder

Things used in this project

Hardware components

Teensy 4.1
Teensy 4.1
×1
Teensy Audio Shield
×1
TFT display - 1.8" SPI
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
×1
rotary encoder - PEC12R-3020F-N0024
×1

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Recorder

Code

recorder.ino

C/C++
Created using the Arduino IDE with the Teensy libraries
// 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.

Credits

micro222
1 project • 0 followers

Comments