arduino-audio-tools
MetaDataID3.h
1 
8 #pragma once
9 #include <string.h>
10 #include <stdint.h>
11 #include <ctype.h>
12 #include "AbstractMetaData.h"
13 
14 #ifdef IS_RENESAS
15 // This is needed for renesas
16 int isascii(int c);
17 #endif
18 
26 namespace audio_tools {
27 
28 // String array with genres
29 static const char *genres[] = { "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alternative Rock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Insdustiral", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native US", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic","Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhytmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", "SynthPop" };
30 
32 enum ParseStatus { TagNotFound, PartialTagAtTail, TagFoundPartial, TagFoundComplete, TagProcessed};
33 
34 
37 struct ID3v1 {
38  char header[3]; // TAG
39  char title[30];
40  char artist[30];
41  char album[30];
42  char year[4];
43  char comment[30];
44  char zero_byte[1];
45  char track[1];
46  char genre;
47 };
48 
49 
52 struct ID3v1Enhanced {
53  char header[4]; // TAG+
54  char title[60];
55  char artist[60];
56  char album[60];
57  char speed;
58  char genre[30];
59  char start[6];
60  char end[6];
61 };
62 
63 
72  public:
73 
74  MetaDataID3Base() = default;
75 
76  void setCallback(void (*fn)(MetaDataType info, const char* str, int len)) {
77  callback = fn;
78  armed = fn!=nullptr;
79  }
80 
81  protected:
82  void (*callback)(MetaDataType info, const char* title, int len);
83  bool armed = false;
84 
86  int findTag(const char* tag, const char*str, size_t len){
87  if (str==nullptr || len<=0) return -1;
88  // The tags are usally in the first 500 bytes - we limit the search
89  if (len>1600){
90  len = 1600;
91  }
92  size_t tag_len = strlen(tag);
93  for (size_t j=0;j<=len-tag_len-1;j++){
94  if (memcmp(str+j,tag, tag_len)==0){
95  return j;
96  }
97  }
98  return -1;
99  }
100 
101 };
102 
103 
112  public:
113 
114  MetaDataID3V1() = default;
115 
117  void begin() {
118  end();
119  status = TagNotFound;
120  use_bytes_of_next_write = 0;
121  memset(tag_str, 0, 5);
122  }
123 
125  void end() {
126  if (tag!=nullptr){
127  delete tag;
128  tag = nullptr;
129  }
130  if (tag_ext!=nullptr){
131  delete tag_ext;
132  tag_ext = nullptr;
133  }
134  }
135 
137  size_t write(const uint8_t* data, size_t len){
138  if (armed){
139  switch(status){
140  case TagNotFound:
141  processTagNotFound(data,len);
142  break;
143  case PartialTagAtTail:
144  processPartialTagAtTail(data,len);
145  break;
146  case TagFoundPartial:
147  processTagFoundPartial(data,len);
148  break;
149  default:
150  // do nothing
151  break;
152  }
153  }
154  return len;
155  }
156 
157  protected:
158  int use_bytes_of_next_write = 0;
159  char tag_str[5] = "";
160  ID3v1 *tag = nullptr;
161  ID3v1Enhanced *tag_ext = nullptr;
162  ParseStatus status = TagNotFound;
163 
164 
166  void processTagNotFound(const uint8_t* data, size_t len) {
167  // find tags
168  int pos = findTag("TAG+",(const char*) data, len);
169  if (pos>0){
170  tag_ext = new ID3v1Enhanced();
171  if (tag_ext!=nullptr){
172  if (len-pos>=sizeof(ID3v1Enhanced)){
173  memcpy(tag,data+pos,sizeof(ID3v1Enhanced));
175  } else {
176  use_bytes_of_next_write = min(sizeof(ID3v1Enhanced), len-pos);
177  memcpy(tag_ext, data+pos, use_bytes_of_next_write);
178  status = TagFoundPartial;
179  }
180  }
181  } else {
182  pos = findTag("TAG", (const char*) data, len);
183  if (pos>0){
184  tag = new ID3v1();
185  if (tag!=nullptr){
186  if (len-pos>=sizeof(ID3v1)){
187  memcpy(tag,data+pos,sizeof(ID3v1));
189  } else {
190  use_bytes_of_next_write = min(sizeof(ID3v1), len-pos);
191  memcpy(tag,data+pos,use_bytes_of_next_write);
192  status = TagFoundPartial;
193 
194  }
195  }
196  }
197  }
198 
199  // we did not find a full tag we check if the end might start with a tag
200  if (pos==-1){
201  if (data[len-3] == 'T' && data[len-2] == 'A' && data[len-1] == 'G'){
202  strcpy(tag_str,"TAG");
203  status = TagFoundPartial;
204  } else if (data[len-2] == 'T' && data[len-1] == 'A' ){
205  strcpy(tag_str,"TA");
206  status = TagFoundPartial;
207  } else if (data[len-1] == 'T'){
208  strcpy(tag_str,"T");
209  status = TagFoundPartial;
210  }
211  }
212  }
213 
215  void processPartialTagAtTail(const uint8_t* data, size_t len) {
216  int tag_len = strlen(tag_str);
217  int missing = 5 - tag_len;
218  strncat((char*)tag_str, (char*)data, missing);
219  if (strncmp((char*)tag_str, "TAG+", 4)==0){
220  tag_ext = new ID3v1Enhanced();
221  memcpy(tag,tag_str, 4);
222  memcpy(tag,data+len,sizeof(ID3v1Enhanced));
224  } else if (strncmp((char*)tag_str,"TAG",3)==0){
225  tag = new ID3v1();
226  memcpy(tag,tag_str, 3);
227  memcpy(tag,data+len,sizeof(ID3v1));
229  }
230  }
231 
233  void processTagFoundPartial(const uint8_t* data, size_t len) {
234  if (tag!=nullptr){
235  int remainder = sizeof(ID3v1) - use_bytes_of_next_write;
236  memcpy(tag,data+use_bytes_of_next_write,remainder);
238  use_bytes_of_next_write = 0;
239  } else if (tag_ext!=nullptr){
240  int remainder = sizeof(ID3v1Enhanced) - use_bytes_of_next_write;
241  memcpy(tag_ext,data+use_bytes_of_next_write,remainder);
243  use_bytes_of_next_write = 0;
244  }
245  }
246 
249  if (callback==nullptr) return;
250 
251  if (tag_ext!=nullptr){
252  callback(Title, tag_ext->title,strnlength(tag_ext->title,60));
253  callback(Artist, tag_ext->artist,strnlength(tag_ext->artist,60));
254  callback(Album, tag_ext->album,strnlength(tag_ext->album,60));
255  callback(Genre, tag_ext->genre,strnlength(tag_ext->genre,30));
256  delete tag_ext;
257  tag_ext = nullptr;
258  status = TagProcessed;
259  }
260 
261  if (tag!=nullptr){
262  callback(Title, tag->title,strnlength(tag->title,30));
263  callback(Artist, tag->artist,strnlength(tag->artist,30));
264  callback(Album, tag->album,strnlength(tag->album,30));
265  uint16_t genre = tag->genre;
266  if (genre < sizeof(genres)){
267  const char* genre_str = genres[genre];
268  callback(Genre, genre_str,strlen(genre_str));
269  }
270  delete tag;
271  tag = nullptr;
272  status = TagProcessed;
273  }
274  }
275 
276 };
277 
278 // -------------------------------------------------------------------------------------------------------------------------------------
279 
280 #define UnsynchronisationFlag 0x40
281 #define ExtendedHeaderFlag 0x20
282 #define ExperimentalIndicatorFlag 0x10
283 
284 // Relevant v2 Tags
285 static const char* id3_v2_tags[] = {"TALB", "TOPE", "TPE1", "TIT2", "TCON"};
286 
287 
288 // ID3 verion 2 TAG Header (10 bytes) @ingroup metadata-id3
289 struct ID3v2 {
290  uint8_t header[3]; // ID3
291  uint8_t version[2];
292  uint8_t flags;
293  uint8_t size[4];
294 };
295 
296 // /// ID3 verion 2 Extended Header
297 // struct ID3v2ExtendedHeader {
298 // uint8_t size[4];
299 // uint16_t flags;
300 // uint32_t padding_size;
301 // };
302 
303 
304 // ID3 verion 2 Tag
305 struct ID3v2Frame {
306  uint8_t id[4];
307  uint8_t size[4];
308  uint16_t flags;
309 };
310 
311 // ID3 verion 2 Tag
313  uint8_t id[4];
314  uint8_t size[4];
315  uint16_t flags;
316 
317 // encoding:
318 // 00 – ISO-8859-1 (ASCII).
319 // 01 – UCS-2 (UTF-16 encoded Unicode with BOM), in ID3v2.2 and ID3v2.3.
320 // 02 – UTF-16BE encoded Unicode without BOM, in ID3v2.4.
321 // 03 – UTF-8 encoded Unicode, in ID3v2.4.
322  uint8_t encoding; // encoding byte for strings
323 };
324 
325 static const int ID3FrameSize = 11;
326 
334  public:
335  MetaDataID3V2() = default;
336 
338  void begin() {
339  status = TagNotFound;
340  use_bytes_of_next_write = 0;
341  actual_tag = nullptr;
342  tag_active = false;
343  tag_processed = false;
344  }
345 
347  void end() {
348  status = TagNotFound;
349  use_bytes_of_next_write = 0;
350  actual_tag = nullptr;
351  tag_active = false;
352  tag_processed = false;
353  }
354 
356  size_t write(const uint8_t* data, size_t len){
357  if (armed){
358  switch(status){
359  case TagNotFound:
360  processTagNotFound(data,len);
361  break;
362  case PartialTagAtTail:
363  processPartialTagAtTail(data,len);
364  break;
365  default:
366  // do nothing
367  break;
368  }
369  }
370  return len;
371  }
372 
375  return tagv2;
376  }
377 
380  return frame_header;
381  }
382 
384  bool isProcessed() {
385  return tag_processed;
386  }
387 
388  protected:
389  ID3v2 tagv2;
390  bool tag_active = false;
391  bool tag_processed = false;
392  ParseStatus status = TagNotFound;
393  const char* actual_tag;
394  ID3v2FrameString frame_header;
395  int use_bytes_of_next_write = 0;
396  char result[256];
397  uint64_t total_len = 0;
398  uint64_t end_len = 0;
399 
400  // calculate the synch save size
401  uint32_t calcSize(uint8_t chars[4]) {
402  uint32_t byte0 = chars[0];
403  uint32_t byte1 = chars[1];
404  uint32_t byte2 = chars[2];
405  uint32_t byte3 = chars[3];
406  return byte0 << 21 | byte1 << 14 | byte2 << 7 | byte3;
407  }
408 
410  void processTagNotFound(const uint8_t* data, size_t len) {
411 
412  // activate tag processing when we have an ID3 Tag
413  if (!tag_active && !tag_processed){
414  int pos = findTag("ID3", (const char*) data, len);
415  if (pos>=0){
416  // fill v2 tag header
417  tag_active = true;
418  // if we have enough data for the header we process it
419  if (len>=pos+sizeof(ID3v2)){
420  memcpy(&tagv2, data+pos, sizeof(ID3v2));
421  end_len = total_len + calcSize(tagv2.size);
422  }
423  }
424  }
425 
426  // deactivate tag processing when we are out of range
427  if (end_len>0 && total_len>end_len){
428  tag_active = false;
429  tag_processed = false;
430  }
431 
432 
433  if (tag_active){
434  // process all tags in current buffer
435  const char* partial_tag = nullptr;
436  for (const char* tag : id3_v2_tags){
437  actual_tag = tag;
438  int tag_pos = findTag(tag, (const char*) data, len);
439 
440  if (tag_pos>0 && tag_pos+sizeof(ID3v2Frame)<=len){
441  // setup tag header
442  memmove(&frame_header, data+tag_pos, sizeof(ID3v2FrameString));
443 
444  // get tag content
445  if(calcSize(frame_header.size) <= len){
446  int l = min(calcSize(frame_header.size)-1, (uint32_t) 256);
447  memset(result,0,256);
448  strncpy((char*)result, (char*) data+tag_pos+ID3FrameSize, l);
449  int checkLen = min(l, 10);
450  if (isAscii(checkLen)){
452  } else {
453  LOGW("TAG %s ignored", tag);
454  }
455  } else {
456  partial_tag = tag;
457  LOGI("%s partial tag", tag);
458  }
459  }
460  }
461 
462  // save partial tag information so that we process the remainder with the next write
463  if (partial_tag!=nullptr){
464  int tag_pos = findTag(partial_tag, (const char*) data, len);
465  memmove(&frame_header, data+tag_pos, sizeof(ID3v2FrameString));
466  int size = min(len - tag_pos, (size_t) calcSize(frame_header.size)-1);
467  strncpy((char*)result, (char*)data+tag_pos+ID3FrameSize, size);
468  use_bytes_of_next_write = size;
469  status = PartialTagAtTail;
470  }
471  }
472 
473  total_len += len;
474 
475  }
476 
478  bool isAscii(int l){
479  // check on first 10 characters
480  int m = l < 5 ? l : 10;
481  for (int j=0; j<m; j++){
482  if (!isascii(result[j])) {
483  return false;
484  }
485  }
486  return true;
487  }
488 
490  void processPartialTagAtTail(const uint8_t* data, size_t len) {
491  int remainder = calcSize(frame_header.size) - use_bytes_of_next_write;
492  memcpy(result+use_bytes_of_next_write, data, remainder);
494 
495  status = TagNotFound;
496  processTagNotFound(data+use_bytes_of_next_write, len-use_bytes_of_next_write);
497  }
498 
501  return frame_header.encoding == 0 || frame_header.encoding == 3;
502  }
503 
504  int strpos(char* str, const char* target) {
505  char* res = strstr(str, target);
506  return (res == nullptr) ? -1 : res - str;
507  }
508 
509 
512  if (callback!=nullptr && actual_tag!=nullptr && encodingIsSupported()){
513  LOGI("callback %s",actual_tag);
514  if (memcmp(actual_tag,"TALB",4)==0)
515  callback(Album, result,strnlength(result, 256));
516  else if (memcmp(actual_tag,"TPE1",4)==0)
517  callback(Artist, result,strnlength(result, 256));
518  else if (memcmp(actual_tag,"TOPE",4)==0)
519  callback(Artist, result,strnlength(result, 256));
520  else if (memcmp(actual_tag,"TIT2",4)==0)
521  callback(Title, result,strnlength(result, 256));
522  else if (memcmp(actual_tag,"TCON",4)==0) {
523  if (result[0]=='('){
524  // convert genre id to string
525  int end_pos = strpos((char*)result, ")");
526  if (end_pos>0){
527  // we just use the first entry
528  result[end_pos]=0;
529  int idx = atoi(result+1);
530  if (idx>=0 && idx< (int)sizeof(genres)){
531  strncpy((char*)result,genres[idx],256);
532  }
533  }
534  }
535  callback(Genre, result,strnlength(result, 256));
536  }
537  }
538  }
539 
540 };
541 
551  public:
552 
553  MetaDataID3() = default;
554 
555  ~MetaDataID3(){
556  end();
557  }
558 
559  void setCallback(void (*fn)(MetaDataType info, const char* str, int len)) {
560  id3v1.setCallback(fn);
561  id3v2.setCallback(fn);
562  }
563 
564  void setFilter(ID3TypeSelection sel) {
565  this->filter = sel;
566  }
567 
568  void begin() {
569  TRACEI();
570  id3v1.begin();
571  id3v2.begin();
572  }
573 
574  void end() {
575  TRACEI();
576  id3v1.end();
577  id3v2.end();
578  }
579 
581  virtual size_t write(const uint8_t *data, size_t length){
582  TRACED();
583  if (filter & SELECT_ID3V2) id3v2.write(data, length);
584  if (!id3v2.isProcessed()) {
585  if (filter & SELECT_ID3V1) id3v1.write(data, length);
586  }
587  return length;
588  }
589 
590  protected:
591  MetaDataID3V1 id3v1;
592  MetaDataID3V2 id3v2;
593  int filter = SELECT_ID3;
594 };
595 
596 } // namespace
Common Metadata methods.
Definition: AbstractMetaData.h:34
ID3 Meta Data Common Functionality.
Definition: MetaDataID3.h:71
int findTag(const char *tag, const char *str, size_t len)
find the tag position in the string - if not found we return -1;
Definition: MetaDataID3.h:86
Simple ID3 Meta Data Parser which supports ID3 V1 and V2 and implements the Stream interface....
Definition: MetaDataID3.h:550
virtual size_t write(const uint8_t *data, size_t length)
Provide tha audio data to the API to parse for Meta Data.
Definition: MetaDataID3.h:581
Simple ID3 Meta Data API which supports ID3 V1.
Definition: MetaDataID3.h:111
void processTagFoundPartial(const uint8_t *data, size_t len)
We have the beginning of the metadata and need to process the remainder.
Definition: MetaDataID3.h:233
void processnotifyAudioChange()
executes the callbacks
Definition: MetaDataID3.h:248
void processPartialTagAtTail(const uint8_t *data, size_t len)
We had part of the start tag at the end of the last write, now we get the full data.
Definition: MetaDataID3.h:215
void end()
Ends the processing and releases the memory.
Definition: MetaDataID3.h:125
void begin()
(re)starts the processing
Definition: MetaDataID3.h:117
void processTagNotFound(const uint8_t *data, size_t len)
try to find the metatdata tag in the provided data
Definition: MetaDataID3.h:166
size_t write(const uint8_t *data, size_t len)
provide the (partial) data which might contain the meta data
Definition: MetaDataID3.h:137
Simple ID3 Meta Data API which supports ID3 V2: We only support the "TALB", "TOPE",...
Definition: MetaDataID3.h:333
ID3v2 header()
provides the ID3v2 header
Definition: MetaDataID3.h:374
bool encodingIsSupported()
For the time beeing we support only ASCII and UTF8.
Definition: MetaDataID3.h:500
bool isProcessed()
returns true if the tag has been provided
Definition: MetaDataID3.h:384
void processnotifyAudioChange()
executes the callbacks
Definition: MetaDataID3.h:511
bool isAscii(int l)
Make sure that the result is a valid ASCII string.
Definition: MetaDataID3.h:478
void processPartialTagAtTail(const uint8_t *data, size_t len)
We have the beginning of the metadata and need to process the remainder.
Definition: MetaDataID3.h:490
void end()
Ends the processing and releases the memory.
Definition: MetaDataID3.h:347
void begin()
(re)starts the processing
Definition: MetaDataID3.h:338
ID3v2FrameString frameHeader()
provides the current frame header
Definition: MetaDataID3.h:379
void processTagNotFound(const uint8_t *data, size_t len)
try to find the metatdata tag in the provided data
Definition: MetaDataID3.h:410
size_t write(const uint8_t *data, size_t len)
provide the (partial) data which might contain the meta data
Definition: MetaDataID3.h:356
ParseStatus
current status of the parsing
Definition: MetaDataID3.h:32
ID3TypeSelection
Enum to filter by type of metadata.
Definition: AbstractMetaData.h:8
MetaDataType
Type of meta info.
Definition: AbstractMetaData.h:11
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition: AnalogAudio.h:10
static size_t strnlength(const char *s, size_t n)
unfortunatly strnlen or strnlen_s is not available in all implementations
Definition: AbstractMetaData.h:22
Definition: MetaDataID3.h:52
Definition: MetaDataID3.h:37
Definition: MetaDataID3.h:305
Definition: MetaDataID3.h:312
Definition: MetaDataID3.h:289