replace the calls to id3_unsyncsafe() by __builtin_bswap32()
Absolutely correct, it works like a charm now!
Code:
#include <SD.h>
#define SDCARD_CS_PIN BUILTIN_SDCARD
SdFat sd;
SdFile dir;
File file;
#include "SPI.h"
char itemToOpen[256];
String title;
String artist;
String tracklen;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
if (!(SD.sdfs.begin(SdioConfig(FIFO_SDIO)))) {
while (1) {
Serial.println("Unable to access the SD card");
delay(500);
}
}
strcpy(itemToOpen, "/MusicBee/Music/Deadmau5/5 years of mau5/1-04 - Some Chords.mp3");
getTags();
strcpy(itemToOpen, "/MusicBee/Music/Deadmau5/At Play 2/1-01 - Outta My Life (Touch Mix).mp3");
getTags();
strcpy(itemToOpen, "/MusicBee/Music/Deadmau5/For Lack Of A Better Name/1-03 - Ghosts 'n Stuff (feat. Rob Swire).mp3");
getTags();
strcpy(itemToOpen, "/MusicBee/Music/Deadmau5/Album Title Goes Here/1-03 - The Veldt (feat. Chris James) [8 Minute Edit].mp3");
getTags();
}
void getTags(){
title = ""; //clear tag strings
artist = "";
Serial.print("Opening file ");
Serial.println(itemToOpen);
file = SD.open(itemToOpen);
if (!file) {
Serial.println("File failed to open");
}
id3Read();
file.close();
Serial.print("Title: ");
Serial.println(title);
Serial.print("Artist: ");
Serial.println(artist);
Serial.print("Track length (ms): ");
Serial.println(tracklen);
}
void loop() {
}
String UTF16UTF8(const char* buf, const uint32_t len)
{
// converts unicode in UTF-8, buff contains the string to be converted up to len
// range U+1 ... U+FFFF
//if no BOM found, BE is default
String out;
out.reserve(len);
auto *tmpbuf = (uint8_t*)malloc(len + 1);
if (!tmpbuf)
return String(); // out of memory;
auto *t = tmpbuf;
auto bitorder = false; //Default to BE
auto *p = (uint16_t*) buf;
const auto *pe = (uint16_t*) &buf[len];
auto code = *p;
if (code == 0xFEFF) {
bitorder = false;
p++;
} // LSB/MSB
else if (code == 0xFFFE) {
bitorder = true;
p++;
} // MSB/LSB
while (p < pe) {
code = *p++;
if (bitorder == true)
code = __builtin_bswap16(code);
if (code < 0X80) {
*t++ = code & 0xff;
}
else if (code < 0X800) {
*t++ = ((code >> 6) | 0XC0);
*t++ = ((code & 0X3F) | 0X80);
}
else {
*t++ = ((code >> 12) | 0XE0);
*t++ = (((code >> 6) & 0X3F) | 0X80);
*t++ = ((code & 0X3F) | 0X80);
}
}
*t = 0;
out = (char*)tmpbuf;
free(tmpbuf);
return out;
}
// https://id3.org/id3v2.4.0-structure
typedef struct
{
char id[3]; //"ID3"
uint8_t version[2]; //04 00 Version 4
uint8_t flag_unsynchronization: 1;
uint8_t flag_extendedheader : 1;
uint8_t flag_experimental : 1;
uint8_t flag_footerpresent : 1;
uint8_t flags_zero: 4;
uint32_t size;
} __attribute__((packed)) ID3TAG;
typedef struct
{
uint32_t size;
uint8_t numFlagBytes;
uint8_t flag_zero1: 1;
uint8_t flag_update: 1;
uint8_t flag_crc: 1;
uint8_t flag_restrictions: 1;
uint8_t flag_zero2: 4;
} __attribute__((packed)) ID3TAG_EXTHEADER;
typedef struct
{
char id[4]; //Tag ID
uint32_t size;
uint8_t flags[2]; //Several bits.. not needed here
} __attribute__((packed)) ID3FRAME;
//for ID3V2.4 only,
//if using ID3V4, replace calls to __builtin_bswap32() in later functions with calls this function
/*static uint32_t id3_unsyncsafe(uint32_t data)
{
data = __builtin_bswap32(data);
auto out = 0;
auto mask = 0x7f000000ul;
do {
out >>= 1;
out |= data & mask;
mask >>= 8;
} while (mask);
return out;
}
*/
/*
id3_getString
The first byte tells the encoding:
$00 ISO-8859-1 [ISO-8859-1]. Terminated with $00.
$01 UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM. All
strings in the same frame SHALL have the same byteorder.
Terminated with $00 00.
$02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
Terminated with $00 00.
$03 UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00.
*/
static String id3_getString(File file, unsigned int len)
{
if (len > 127) len = 127;
char buf[len + 1];
file.readBytes(buf, len);
buf[len] = 0;
const auto encoding = buf[0];
auto *s = &buf[1];
len--;
switch (encoding) {
case 0x00:
Serial.println("Encoding is ISO-8859-1");
break;
// return latin1UTF8(s);
case 0x01:
Serial.println("Encoding is UTF-16 with BOM");
return UTF16UTF8(s, len);
case 0x02:
Serial.println("Encoding is UTF-16BE without BOM");
return UTF16UTF8(s, len);
case 0x03:
Serial.println("Encoding is UTF-8");
break;
// return String(s);
}
return String(s);
}
void id3Read()
{
Serial.println("Reading ID3 header");
//Try to skip or parse ID3 Tags - fill buffer
bool ID3present = false;
auto numTagstoFind = 3;
ID3TAG id3;
auto lenRead = 0;
lenRead += file.readBytes((char*)&id3, sizeof(id3));
if (strncmp (id3.id, "ID3", 3) != 0) {
Serial.println("No ID3 header found");
return; //Identifier not found
}
//size is a "syncsafe" integer
id3.size = __builtin_bswap32(id3.size) + 10;
// log_e("ID3 size: %u", id3.size);
if (id3.version[0] < 2 || id3.version[0] == 0xff || id3.version[1] == 0xff || id3.flags_zero != 0) {
Serial.println("Abort!");
return;
}
// log_d("ID3v%u.%u found", (unsigned)id3.version[0], (unsigned)id3.version[1] );
//TODO: Handle ID3V2
if (id3.version[0] == 2 || id3.version[0] > 4 ) {
// log_w("Can't handle this ID3 version");
Serial.println("Can't handle this ID3 version");
goto end;
}
if (id3.flag_extendedheader) {
// log_v("Has extended header. Skipping.");
Serial.println("Has extended header. Skipping.");
ID3TAG_EXTHEADER extheader;
lenRead += file.readBytes((char*)&extheader, sizeof(extheader));
extheader.size = __builtin_bswap32(extheader.size);
if (extheader.size < 6) goto end; //error
lenRead += extheader.size;
}
//ID3 Frames:
file.seek(lenRead);
Serial.println("Searching for frames");
do {
ID3FRAME frameheader;
lenRead += file.readBytes((char*)&frameheader, sizeof(frameheader));
frameheader.size = __builtin_bswap32(frameheader.size);
if (frameheader.id[0] == 0 || frameheader.id[1] == 0 || frameheader.id[2] == 0 ) {
Serial.println("Blank data, break");
break;
}
if (lenRead + frameheader.size >= id3.size) {
Serial.println("End of ID3, break");
break;
}
Serial.printf("ID3 Frame: %c%c%c%c size:%u\n", frameheader.id[0], frameheader.id[1], frameheader.id[2] , frameheader.id[3], frameheader.size);
if ( strncmp("TIT2", frameheader.id, 4) == 0) {
Serial.print("Found TIT2 - ");
title = id3_getString(file, frameheader.size);
numTagstoFind--;
}
else if ( strncmp("TPE1", frameheader.id, 4) == 0) {
Serial.print("Found TPE1 - ");
artist = id3_getString(file, frameheader.size);
numTagstoFind--;
}
else if ( strncmp("TLEN", frameheader.id, 4) == 0) {
Serial.print("Found TLEN - ");
tracklen = id3_getString(file, frameheader.size);
numTagstoFind--;
}
if (numTagstoFind == 0) {
Serial.println("Found all searched tags");
break;
}
lenRead += frameheader.size;
file.seek(lenRead);
} while (1);
end:
//Skip to first mp3/aac frame:
file.seek(id3.size);
}
Now returns as expected:
Code:
Opening file /MusicBee/Music/Deadmau5/5 years of mau5/1-04 - Some Chords.mp3
Reading ID3 header
Searching for frames
ID3 Frame: TALB size:33
ID3 Frame: TIT2 size:25
Found TIT2 - Encoding is UTF-16 with BOM
ID3 Frame: TPE1 size:19
Found TPE1 - Encoding is UTF-16 with BOM
ID3 Frame: TPE2 size:19
ID3 Frame: TRCK size:11
ID3 Frame: TPOS size:9
ID3 Frame: APIC size:269246
ID3 Frame: TYER size:11
ID3 Frame: TCOP size:67
ID3 Frame: PRIV size:984
ID3 Frame: TCON size:15
ID3 Frame: COMM size:10
Blank data, break
Title: Some Chords
Artist: deadmau5
Track length (ms):
Opening file /MusicBee/Music/Deadmau5/At Play 2/1-01 - Outta My Life (Touch Mix).mp3
Reading ID3 header
Searching for frames
ID3 Frame: MCDI size:94
ID3 Frame: TENC size:32
ID3 Frame: TSSE size:23
ID3 Frame: TXXX size:20
ID3 Frame: TDAT size:5
ID3 Frame: TXXX size:15
ID3 Frame: TXXX size:17
ID3 Frame: TXXX size:28
ID3 Frame: TXXX size:20
ID3 Frame: PRIV size:27
ID3 Frame: TXXX size:19
ID3 Frame: APIC size:131011
ID3 Frame: COMM size:95
ID3 Frame: TIT2 size:26
Found TIT2 - Encoding is ISO-8859-1
ID3 Frame: TYER size:5
ID3 Frame: TIT1 size:10
ID3 Frame: PRIV size:39
ID3 Frame: PRIV size:41
ID3 Frame: PRIV size:31
ID3 Frame: PRIV size:138
ID3 Frame: TLAN size:4
ID3 Frame: TPUB size:13
ID3 Frame: TCON size:18
ID3 Frame: TALB size:10
ID3 Frame: TPE2 size:9
ID3 Frame: PRIV size:34
ID3 Frame: PRIV size:39
ID3 Frame: PRIV size:20
ID3 Frame: TPOS size:4
ID3 Frame: TRCK size:2
ID3 Frame: TCOM size:34
ID3 Frame: TPE1 size:9
Found TPE1 - Encoding is ISO-8859-1
ID3 Frame: TLEN size:7
Found TLEN - Encoding is ISO-8859-1
Found all searched tags
Title: Outta My Life (Touch Mix)
Artist: Deadmau5
Track length (ms): 368120
Opening file /MusicBee/Music/Deadmau5/For Lack Of A Better Name/1-03 - Ghosts 'n Stuff (feat. Rob Swire).mp3
Reading ID3 header
Searching for frames
ID3 Frame: UFID size:92
ID3 Frame: TIT2 size:34
Found TIT2 - Encoding is ISO-8859-1
ID3 Frame: TALB size:26
ID3 Frame: TCON size:11
ID3 Frame: TRCK size:2
ID3 Frame: TYER size:5
ID3 Frame: TPE2 size:9
ID3 Frame: TENC size:12
ID3 Frame: TPOS size:4
ID3 Frame: APIC size:9054
ID3 Frame: PRIV size:41
ID3 Frame: PRIV size:39
ID3 Frame: PRIV size:20
ID3 Frame: PRIV size:31
ID3 Frame: PRIV size:34
ID3 Frame: PRIV size:39
ID3 Frame: TPUB size:7
ID3 Frame: PRIV size:138
ID3 Frame: TCOM size:15
ID3 Frame: TPE1 size:9
Found TPE1 - Encoding is ISO-8859-1
ID3 Frame: COMM size:36
Blank data, break
Title: Ghosts 'n Stuff (feat. Rob Swire)
Artist: Deadmau5
Track length (ms): 368120
Opening file /MusicBee/Music/Deadmau5/Album Title Goes Here/1-03 - The Veldt (feat. Chris James) [8 Minute Edit].mp3
Reading ID3 header
Searching for frames
ID3 Frame: TALB size:53
ID3 Frame: TPE1 size:19
Found TPE1 - Encoding is UTF-16 with BOM
ID3 Frame: TYER size:11
ID3 Frame: TPOS size:5
ID3 Frame: TCON size:13
ID3 Frame: TIT2 size:93
Found TIT2 - Encoding is UTF-16 with BOM
ID3 Frame: TXXX size:27
ID3 Frame: TXXX size:31
ID3 Frame: TRCK size:7
ID3 Frame: TPE2 size:19
Blank data, break
Title: The Veldt (feat. Chris James) [8 Minute Edit]
Artist: Deadmau5
Track length (ms): 368120
And as you can see I've added an extra field to search for (TLEN - track length in milliseconds), and it works perfectly first time. You're an absolute legend Frank, thank you very much!
why isn't that backwards compatible?
Yes it's rather a pain. Perhaps this explains why the ID3 homepage says that ID3V4 "...has not achieved popular status due to some disagreements on some of the revisions..."
It's also just come to my attention that ID3 is only used for MP3s, and that the M4A standard is a whole other Apple-masterminded issue...I think that's a headache for another day.