Gave up on trying to extract cover art from mp3 files and removed debug logs. Updated firmware version to match variant type

This commit is contained in:
pelgraine
2026-02-13 23:27:32 +11:00
parent 1f46bc1970
commit 546ce55c2b
4 changed files with 251 additions and 36 deletions

View File

@@ -12,7 +12,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.9.0"
#define FIRMWARE_VERSION "Meck v0.8.7A"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

View File

@@ -188,8 +188,7 @@ private:
pinMode(41, OUTPUT);
digitalWrite(41, HIGH);
if (!_dacPowered) {
delay(50); // Let DAC power rail stabilise on first enable
Serial.println("AB: GPIO 41 (DAC power) -> HIGH");
delay(50);
}
_dacPowered = true;
}
@@ -197,23 +196,15 @@ private:
void disableDAC() {
digitalWrite(41, LOW);
_dacPowered = false;
Serial.println("AB: GPIO 41 (DAC power) -> LOW");
}
void ensureI2SInit() {
if (!_i2sInitialized && _audio) {
// Configure I2S output pins
// Try with MCLK=0 (ESP32-S3 may need explicit MCLK even if PCM5102A
// uses internal PLL — setting 0 lets the driver auto-assign or skip)
bool ok = _audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT, 0);
Serial.printf("AB: setPinout(BCLK=%d, LRC=%d, DOUT=%d, MCLK=0) -> %s\n",
BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT,
ok ? "OK" : "FAILED");
if (!ok) {
// Retry without MCLK (original behavior)
ok = _audio->setPinout(BOARD_I2S_BCLK, BOARD_I2S_LRC, BOARD_I2S_DOUT);
Serial.printf("AB: setPinout retry without MCLK -> %s\n", ok ? "OK" : "FAILED");
}
if (!ok) Serial.println("AB: setPinout FAILED");
_i2sInitialized = true;
}
}
@@ -449,7 +440,20 @@ private:
yield(); // Feed WDT after metadata parse
decodeCoverArt(file);
yield(); // Feed WDT after cover decode
} else if (lower.endsWith(".mp3")) {
_metadata.parseID3v2(file);
yield(); // Feed WDT after metadata parse
decodeCoverArt(file);
yield(); // Feed WDT after cover decode
// Fall back to filename for title if ID3 had none
if (_metadata.title[0] == '\0') {
String base = filename;
int dot = base.lastIndexOf('.');
if (dot > 0) base = base.substring(0, dot);
strncpy(_metadata.title, base.c_str(), M4B_MAX_TITLE - 1);
}
} else {
// Other audio formats — use filename as title
_metadata.clear();
String base = filename;
int dot = base.lastIndexOf('.');
@@ -509,16 +513,11 @@ private:
_audio->connecttoFS(SD, fullPath.c_str());
_audio->setVolume(_volume);
Serial.printf("AB: Volume=%d, isRunning=%d, getVolume=%d\n",
_volume, _audio->isRunning(), _audio->getVolume());
// DON'T seek immediately — the library hasn't parsed headers yet.
// Store pending seek; apply once stream reports ready (has duration).
_streamReady = false;
if (_currentPosSec > 5) {
_pendingSeekSec = (_currentPosSec > 3) ? _currentPosSec - 3 : 0;
Serial.printf("AB: Deferred seek to %us (bookmark was %us)\n",
_pendingSeekSec, _currentPosSec);
} else {
_pendingSeekSec = 0;
}
@@ -527,7 +526,7 @@ private:
_isPaused = false;
_lastPositionSave = millis();
Serial.println("AB: Playback started");
Serial.printf("AB: Playing '%s'\n", _currentFile.c_str());
}
void stopPlayback() {
@@ -548,7 +547,7 @@ private:
// Force I2S re-init for next file (sample rate may differ)
_i2sInitialized = false;
Serial.println("AB: Playback stopped");
Serial.println("AB: Stopped");
}
void togglePause() {
@@ -558,11 +557,9 @@ private:
_audio->pauseResume();
_isPaused = true;
saveBookmark();
Serial.println("AB: Paused");
} else if (_isPaused) {
_audio->pauseResume();
_isPaused = false;
Serial.println("AB: Resumed");
} else {
// Not playing yet — start from bookmark
startPlayback();
@@ -581,7 +578,6 @@ private:
_audio->setTimeOffset((uint32_t)target);
_currentPosSec = (uint32_t)target;
Serial.printf("AB: Seek %+ds -> %us\n", seconds, (uint32_t)target);
}
void seekToChapter(int chapterIdx) {
@@ -595,8 +591,6 @@ private:
}
_currentPosSec = targetSec;
_currentChapter = chapterIdx;
Serial.printf("AB: Jump to chapter %d '%s' at %us\n",
chapterIdx, _metadata.chapters[chapterIdx].name, targetSec);
}
// ---- Rendering Helpers ----
@@ -863,23 +857,15 @@ public:
uint32_t pos = _audio->getAudioCurrentTime();
if (pos > 0) _currentPosSec = pos;
// Get duration from library once available
if (_durationSec == 0) {
uint32_t dur = _audio->getAudioFileDuration();
if (dur > 0) {
_durationSec = dur;
Serial.printf("AB: Duration from library: %us\n", dur);
}
if (dur > 0) _durationSec = dur;
}
// Apply deferred seek once stream is ready
// Stream is ready when the library reports a valid duration
if (!_streamReady && _durationSec > 0) {
_streamReady = true;
Serial.printf("AB: Stream ready! isRunning=%d, duration=%us\n",
_audio->isRunning(), _durationSec);
if (_pendingSeekSec > 0) {
Serial.printf("AB: Applying deferred seek to %us\n", _pendingSeekSec);
_audio->setTimeOffset(_pendingSeekSec);
_currentPosSec = _pendingSeekSec;
_pendingSeekSec = 0;
@@ -1019,7 +1005,6 @@ public:
if (_volume < 21) {
_volume++;
if (_audio) _audio->setVolume(_volume);
Serial.printf("AB: Volume -> %d\n", _volume);
}
return true; // Always consume & refresh (show current volume)
}
@@ -1029,7 +1014,6 @@ public:
if (_volume > 0) {
_volume--;
if (_audio) _audio->setVolume(_volume);
Serial.printf("AB: Volume -> %d\n", _volume);
}
return true; // Always consume & refresh
}

View File

@@ -371,6 +371,237 @@ private:
}
}
// =====================================================================
// ID3v2 Parser for MP3 files
// =====================================================================
public:
// Parse ID3v2 tags from an MP3 file. Extracts title (TIT2), artist
// (TPE1), and cover art (APIC). Fills the same metadata fields as
// the M4B parser so decodeCoverArt() works unchanged.
bool parseID3v2(File& file) {
clear();
if (!file || file.size() < 10) return false;
file.seek(0);
uint8_t hdr[10];
if (file.read(hdr, 10) != 10) return false;
// Verify "ID3" magic
if (hdr[0] != 'I' || hdr[1] != 'D' || hdr[2] != '3') {
Serial.println("ID3: No ID3v2 header found");
return false;
}
uint8_t versionMajor = hdr[3]; // 3 = ID3v2.3, 4 = ID3v2.4
bool v24 = (versionMajor == 4);
bool hasExtHeader = (hdr[5] & 0x40) != 0;
// Tag size is syncsafe integer (4 x 7-bit bytes)
uint32_t tagSize = ((uint32_t)(hdr[6] & 0x7F) << 21) |
((uint32_t)(hdr[7] & 0x7F) << 14) |
((uint32_t)(hdr[8] & 0x7F) << 7) |
(hdr[9] & 0x7F);
uint32_t tagEnd = 10 + tagSize;
if (tagEnd > file.size()) tagEnd = file.size();
Serial.printf("ID3: v2.%d, %u bytes\n", versionMajor, tagSize);
// Skip extended header if present
uint32_t pos = 10;
if (hasExtHeader && pos + 4 < tagEnd) {
file.seek(pos);
uint32_t extSize;
if (v24) {
uint8_t eb[4];
file.read(eb, 4);
extSize = ((uint32_t)(eb[0] & 0x7F) << 21) |
((uint32_t)(eb[1] & 0x7F) << 14) |
((uint32_t)(eb[2] & 0x7F) << 7) |
(eb[3] & 0x7F);
} else {
extSize = readU32BE(file) + 4;
}
pos += extSize;
}
// Walk ID3v2 frames
bool foundTitle = false, foundArtist = false, foundCover = false;
while (pos + 10 < tagEnd) {
file.seek(pos);
uint8_t fhdr[10];
if (file.read(fhdr, 10) != 10) break;
if (fhdr[0] == 0) break;
char frameId[5] = { (char)fhdr[0], (char)fhdr[1],
(char)fhdr[2], (char)fhdr[3], '\0' };
uint32_t frameSize;
if (v24) {
frameSize = ((uint32_t)(fhdr[4] & 0x7F) << 21) |
((uint32_t)(fhdr[5] & 0x7F) << 14) |
((uint32_t)(fhdr[6] & 0x7F) << 7) |
(fhdr[7] & 0x7F);
} else {
frameSize = ((uint32_t)fhdr[4] << 24) | ((uint32_t)fhdr[5] << 16) |
((uint32_t)fhdr[6] << 8) | fhdr[7];
}
if (frameSize == 0 || pos + 10 + frameSize > tagEnd) break;
}
uint32_t dataStart = pos + 10;
// --- TIT2 (Title) ---
if (!foundTitle && strcmp(frameId, "TIT2") == 0 && frameSize > 1) {
id3ExtractText(file, dataStart, frameSize, title, M4B_MAX_TITLE);
foundTitle = (title[0] != '\0');
}
// --- TPE1 (Artist/Author) ---
if (!foundArtist && strcmp(frameId, "TPE1") == 0 && frameSize > 1) {
id3ExtractText(file, dataStart, frameSize, author, M4B_MAX_AUTHOR);
foundArtist = (author[0] != '\0');
}
// --- APIC (Attached Picture) ---
if (!foundCover && strcmp(frameId, "APIC") == 0 && frameSize > 20) {
id3ExtractAPIC(file, dataStart, frameSize);
foundCover = hasCoverArt;
}
pos = dataStart + frameSize;
// Early exit once we have everything
if (foundTitle && foundArtist && foundCover) break;
}
if (foundTitle) Serial.printf("ID3: Title: %s\n", title);
if (foundArtist) Serial.printf("ID3: Author: %s\n", author);
return (foundTitle || foundCover);
}
private:
// Extract text from a TIT2/TPE1 frame.
// Format: encoding(1) + text data
void id3ExtractText(File& file, uint32_t offset, uint32_t size,
char* dest, int maxLen) {
file.seek(offset);
uint8_t encoding = file.read();
uint32_t textLen = size - 1;
if (textLen == 0) return;
if (encoding == 0 || encoding == 3) {
// ISO-8859-1 or UTF-8 — read directly
uint32_t readLen = (textLen < (uint32_t)(maxLen - 1))
? textLen : (uint32_t)(maxLen - 1);
file.read((uint8_t*)dest, readLen);
dest[readLen] = '\0';
// Strip trailing nulls
while (readLen > 0 && dest[readLen - 1] == '\0') readLen--;
dest[readLen] = '\0';
}
else if (encoding == 1 || encoding == 2) {
// UTF-16 (with or without BOM) — crude ASCII extraction
// Static buffer to avoid stack overflow (loopTask has limited stack)
static uint8_t u16buf[128];
uint32_t readLen = (textLen > sizeof(u16buf)) ? sizeof(u16buf) : textLen;
file.read(u16buf, readLen);
uint32_t srcStart = 0;
// Skip BOM if present
if (readLen >= 2 && ((u16buf[0] == 0xFF && u16buf[1] == 0xFE) ||
(u16buf[0] == 0xFE && u16buf[1] == 0xFF))) {
srcStart = 2;
}
bool littleEndian = (srcStart >= 2 && u16buf[0] == 0xFF);
int dstIdx = 0;
for (uint32_t i = srcStart; i + 1 < readLen && dstIdx < maxLen - 1; i += 2) {
uint8_t lo = littleEndian ? u16buf[i] : u16buf[i + 1];
uint8_t hi = littleEndian ? u16buf[i + 1] : u16buf[i];
if (lo == 0 && hi == 0) break; // null terminator
if (hi == 0 && lo >= 0x20 && lo < 0x7F) {
dest[dstIdx++] = (char)lo;
} else {
dest[dstIdx++] = '?';
}
}
dest[dstIdx] = '\0';
}
}
// Extract APIC (cover art) frame.
// Format: encoding(1) + MIME(null-term) + picType(1) + desc(null-term) + imageData
void id3ExtractAPIC(File& file, uint32_t offset, uint32_t frameSize) {
file.seek(offset);
uint8_t encoding = file.read();
// Read MIME type (null-terminated ASCII)
char mime[32] = {0};
int mimeLen = 0;
while (mimeLen < 31) {
int b = file.read();
if (b < 0) return; // Read error
if (b == 0) break; // Null terminator = end of MIME string
mime[mimeLen++] = (char)b;
}
mime[mimeLen] = '\0';
// Picture type (1 byte)
uint8_t picType = file.read();
(void)picType;
// Skip description (null-terminated, encoding-dependent)
if (encoding == 0 || encoding == 3) {
// Single-byte null terminator
while (true) {
int b = file.read();
if (b < 0) return; // Read error
if (b == 0) break; // Null terminator
}
} else {
// UTF-16: double-null terminator
while (true) {
int b1 = file.read();
int b2 = file.read();
if (b1 < 0 || b2 < 0) return; // Read error
if (b1 == 0 && b2 == 0) break; // Double-null terminator
}
}
// Everything from here to end of frame is image data
uint32_t imgOffset = file.position();
uint32_t imgEnd = offset + frameSize;
if (imgOffset >= imgEnd) return;
uint32_t imgSize = imgEnd - imgOffset;
// Determine format from MIME type
bool isJpeg = (strstr(mime, "jpeg") || strstr(mime, "jpg"));
bool isPng = (strstr(mime, "png") != nullptr);
// Also detect by magic bytes if MIME is generic
if (!isJpeg && !isPng && imgSize > 4) {
file.seek(imgOffset);
uint8_t magic[4];
file.read(magic, 4);
if (magic[0] == 0xFF && magic[1] == 0xD8) isJpeg = true;
else if (magic[0] == 0x89 && magic[1] == 'P' &&
magic[2] == 'N' && magic[3] == 'G') isPng = true;
}
coverOffset = imgOffset;
coverSize = imgSize;
coverFormat = isJpeg ? 13 : (isPng ? 14 : 0);
hasCoverArt = (imgSize > 100 && (isJpeg || isPng));
if (hasCoverArt) {
Serial.printf("ID3: Cover %s, %u bytes\n",
isJpeg ? "JPEG" : "PNG", imgSize);
}
}
// Parse Nero-style chapter list (chpl atom).
void parseChpl(File& file, uint32_t offset, uint32_t size) {
if (size < 9) return;

View File

@@ -80,7 +80,7 @@ build_flags =
-D PIN_DISPLAY_BL=45
-D PIN_USER_BTN=0
-D CST328_PIN_RST=38
-D FIRMWARE_VERSION='"Meck v0.9.0"'
-D FIRMWARE_VERSION='"Meck v0.8.7A"'
-D ARDUINO_LOOP_STACK_SIZE=16384
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/LilyGo_TDeck_Pro>