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 
22 namespace audio_tools {
23 
24 // String array with genres
25 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" };
26 
28 enum ParseStatus { TagNotFound, PartialTagAtTail, TagFoundPartial, TagFoundComplete, TagProcessed};
29 
30 
33 struct ID3v1 {
34  char header[3]; // TAG
35  char title[30];
36  char artist[30];
37  char album[30];
38  char year[4];
39  char comment[30];
40  char zero_byte[1];
41  char track[1];
42  char genre;
43 };
44 
45 
48 struct ID3v1Enhanced {
49  char header[4]; // TAG+
50  char title[60];
51  char artist[60];
52  char album[60];
53  char speed;
54  char genre[30];
55  char start[6];
56  char end[6];
57 };
58 
59 
68  public:
69 
70  MetaDataID3Base() = default;
71 
72  void setCallback(void (*fn)(MetaDataType info, const char* str, int len)) {
73  callback = fn;
74  armed = fn!=nullptr;
75  }
76 
77  protected:
78  void (*callback)(MetaDataType info, const char* title, int len);
79  bool armed = false;
80 
82  int findTag(const char* tag, const char*str, size_t len){
83  if (str==nullptr || len<=0) return -1;
84  // The tags are usally in the first 500 bytes - we limit the search
85  if (len>1600){
86  len = 1600;
87  }
88  size_t tag_len = strlen(tag);
89  for (size_t j=0;j<=len-tag_len-1;j++){
90  if (memcmp(str+j,tag, tag_len)==0){
91  return j;
92  }
93  }
94  return -1;
95  }
96 
97 };
98 
99 
108  public:
109 
110  MetaDataID3V1() = default;
111 
113  void begin() {
114  end();
115  status = TagNotFound;
116  use_bytes_of_next_write = 0;
117  memset(tag_str, 0, 5);
118  }
119 
121  void end() {
122  if (tag!=nullptr){
123  delete tag;
124  tag = nullptr;
125  }
126  if (tag_ext!=nullptr){
127  delete tag_ext;
128  tag_ext = nullptr;
129  }
130  }
131 
133  size_t write(const uint8_t* data, size_t len){
134  if (armed){
135  switch(status){
136  case TagNotFound:
137  processTagNotFound(data,len);
138  break;
139  case PartialTagAtTail:
140  processPartialTagAtTail(data,len);
141  break;
142  case TagFoundPartial:
143  processTagFoundPartial(data,len);
144  break;
145  default:
146  // do nothing
147  break;
148  }
149  }
150  return len;
151  }
152 
153  protected:
154  int use_bytes_of_next_write = 0;
155  char tag_str[5] = "";
156  ID3v1 *tag = nullptr;
157  ID3v1Enhanced *tag_ext = nullptr;
158  ParseStatus status = TagNotFound;
159 
160 
162  void processTagNotFound(const uint8_t* data, size_t len) {
163  // find tags
164  int pos = findTag("TAG+",(const char*) data, len);
165  if (pos>0){
166  tag_ext = new ID3v1Enhanced();
167  if (tag_ext!=nullptr){
168  if (len-pos>=sizeof(ID3v1Enhanced)){
169  memcpy(tag,data+pos,sizeof(ID3v1Enhanced));
171  } else {
172  use_bytes_of_next_write = min(sizeof(ID3v1Enhanced), len-pos);
173  memcpy(tag_ext, data+pos, use_bytes_of_next_write);
174  status = TagFoundPartial;
175  }
176  }
177  } else {
178  pos = findTag("TAG", (const char*) data, len);
179  if (pos>0){
180  tag = new ID3v1();
181  if (tag!=nullptr){
182  if (len-pos>=sizeof(ID3v1)){
183  memcpy(tag,data+pos,sizeof(ID3v1));
185  } else {
186  use_bytes_of_next_write = min(sizeof(ID3v1), len-pos);
187  memcpy(tag,data+pos,use_bytes_of_next_write);
188  status = TagFoundPartial;
189 
190  }
191  }
192  }
193  }
194 
195  // we did not find a full tag we check if the end might start with a tag
196  if (pos==-1){
197  if (data[len-3] == 'T' && data[len-2] == 'A' && data[len-1] == 'G'){
198  strcpy(tag_str,"TAG");
199  status = TagFoundPartial;
200  } else if (data[len-2] == 'T' && data[len-1] == 'A' ){
201  strcpy(tag_str,"TA");
202  status = TagFoundPartial;
203  } else if (data[len-1] == 'T'){
204  strcpy(tag_str,"T");
205  status = TagFoundPartial;
206  }
207  }
208  }
209 
211  void processPartialTagAtTail(const uint8_t* data, size_t len) {
212  int tag_len = strlen(tag_str);
213  int missing = 5 - tag_len;
214  strncat((char*)tag_str, (char*)data, missing);
215  if (strncmp((char*)tag_str, "TAG+", 4)==0){
216  tag_ext = new ID3v1Enhanced();
217  memcpy(tag,tag_str, 4);
218  memcpy(tag,data+len,sizeof(ID3v1Enhanced));
220  } else if (strncmp((char*)tag_str,"TAG",3)==0){
221  tag = new ID3v1();
222  memcpy(tag,tag_str, 3);
223  memcpy(tag,data+len,sizeof(ID3v1));
225  }
226  }
227 
229  void processTagFoundPartial(const uint8_t* data, size_t len) {
230  if (tag!=nullptr){
231  int remainder = sizeof(ID3v1) - use_bytes_of_next_write;
232  memcpy(tag,data+use_bytes_of_next_write,remainder);
234  use_bytes_of_next_write = 0;
235  } else if (tag_ext!=nullptr){
236  int remainder = sizeof(ID3v1Enhanced) - use_bytes_of_next_write;
237  memcpy(tag_ext,data+use_bytes_of_next_write,remainder);
239  use_bytes_of_next_write = 0;
240  }
241  }
242 
245  if (callback==nullptr) return;
246 
247  if (tag_ext!=nullptr){
248  callback(Title, tag_ext->title,strnlength(tag_ext->title,60));
249  callback(Artist, tag_ext->artist,strnlength(tag_ext->artist,60));
250  callback(Album, tag_ext->album,strnlength(tag_ext->album,60));
251  callback(Genre, tag_ext->genre,strnlength(tag_ext->genre,30));
252  delete tag_ext;
253  tag_ext = nullptr;
254  status = TagProcessed;
255  }
256 
257  if (tag!=nullptr){
258  callback(Title, tag->title,strnlength(tag->title,30));
259  callback(Artist, tag->artist,strnlength(tag->artist,30));
260  callback(Album, tag->album,strnlength(tag->album,30));
261  uint16_t genre = tag->genre;
262  if (genre < sizeof(genres)){
263  const char* genre_str = genres[genre];
264  callback(Genre, genre_str,strlen(genre_str));
265  }
266  delete tag;
267  tag = nullptr;
268  status = TagProcessed;
269  }
270  }
271 
272 };
273 
274 // -------------------------------------------------------------------------------------------------------------------------------------
275 
276 #define UnsynchronisationFlag 0x40
277 #define ExtendedHeaderFlag 0x20
278 #define ExperimentalIndicatorFlag 0x10
279 
280 // Relevant v2 Tags
281 static const char* id3_v2_tags[] = {"TALB", "TOPE", "TPE1", "TIT2", "TCON"};
282 
283 
284 // ID3 verion 2 TAG Header (10 bytes) @ingroup metadata-id3
285 struct ID3v2 {
286  uint8_t header[3]; // ID3
287  uint8_t version[2];
288  uint8_t flags;
289  uint8_t size[4];
290 };
291 
292 // /// ID3 verion 2 Extended Header
293 // struct ID3v2ExtendedHeader {
294 // uint8_t size[4];
295 // uint16_t flags;
296 // uint32_t padding_size;
297 // };
298 
299 
300 // ID3 verion 2 Tag
301 struct ID3v2Frame {
302  uint8_t id[4];
303  uint8_t size[4];
304  uint16_t flags;
305 };
306 
307 // ID3 verion 2 Tag
309  uint8_t id[4];
310  uint8_t size[4];
311  uint16_t flags;
312 
313 // encoding:
314 // 00 – ISO-8859-1 (ASCII).
315 // 01 – UCS-2 (UTF-16 encoded Unicode with BOM), in ID3v2.2 and ID3v2.3.
316 // 02 – UTF-16BE encoded Unicode without BOM, in ID3v2.4.
317 // 03 – UTF-8 encoded Unicode, in ID3v2.4.
318  uint8_t encoding; // encoding byte for strings
319 };
320 
321 static const int ID3FrameSize = 11;
322 
330  public:
331  MetaDataID3V2() = default;
332 
334  void begin() {
335  status = TagNotFound;
336  use_bytes_of_next_write = 0;
337  actual_tag = nullptr;
338  tag_active = false;
339  tag_processed = false;
340  }
341 
343  void end() {
344  status = TagNotFound;
345  use_bytes_of_next_write = 0;
346  actual_tag = nullptr;
347  tag_active = false;
348  tag_processed = false;
349  }
350 
352  size_t write(const uint8_t* data, size_t len){
353  if (armed){
354  switch(status){
355  case TagNotFound:
356  processTagNotFound(data,len);
357  break;
358  case PartialTagAtTail:
359  processPartialTagAtTail(data,len);
360  break;
361  default:
362  // do nothing
363  break;
364  }
365  }
366  return len;
367  }
368 
371  return tagv2;
372  }
373 
376  return frame_header;
377  }
378 
380  bool isProcessed() {
381  return tag_processed;
382  }
383 
384  protected:
385  ID3v2 tagv2;
386  bool tag_active = false;
387  bool tag_processed = false;
388  ParseStatus status = TagNotFound;
389  const char* actual_tag;
390  ID3v2FrameString frame_header;
391  int use_bytes_of_next_write = 0;
392  char result[256];
393  uint64_t total_len = 0;
394  uint64_t end_len = 0;
395 
396  // calculate the synch save size
397  uint32_t calcSize(uint8_t chars[4]) {
398  uint32_t byte0 = chars[0];
399  uint32_t byte1 = chars[1];
400  uint32_t byte2 = chars[2];
401  uint32_t byte3 = chars[3];
402  return byte0 << 21 | byte1 << 14 | byte2 << 7 | byte3;
403  }
404 
406  void processTagNotFound(const uint8_t* data, size_t len) {
407 
408  // activate tag processing when we have an ID3 Tag
409  if (!tag_active && !tag_processed){
410  int pos = findTag("ID3", (const char*) data, len);
411  if (pos>=0){
412  // fill v2 tag header
413  tag_active = true;
414  // if we have enough data for the header we process it
415  if (len>=pos+sizeof(ID3v2)){
416  memcpy(&tagv2, data+pos, sizeof(ID3v2));
417  end_len = total_len + calcSize(tagv2.size);
418  }
419  }
420  }
421 
422  // deactivate tag processing when we are out of range
423  if (end_len>0 && total_len>end_len){
424  tag_active = false;
425  tag_processed = false;
426  }
427 
428 
429  if (tag_active){
430  // process all tags in current buffer
431  const char* partial_tag = nullptr;
432  for (const char* tag : id3_v2_tags){
433  actual_tag = tag;
434  int tag_pos = findTag(tag, (const char*) data, len);
435 
436  if (tag_pos>0 && tag_pos+sizeof(ID3v2Frame)<=len){
437  // setup tag header
438  memmove(&frame_header, data+tag_pos, sizeof(ID3v2FrameString));
439 
440  // get tag content
441  if(calcSize(frame_header.size) <= len){
442  int l = min(calcSize(frame_header.size)-1, (uint32_t) 256);
443  memset(result,0,256);
444  strncpy((char*)result, (char*) data+tag_pos+ID3FrameSize, l);
445  int checkLen = min(l, 10);
446  if (isAscii(checkLen)){
448  } else {
449  LOGW("TAG %s ignored", tag);
450  }
451  } else {
452  partial_tag = tag;
453  LOGI("%s partial tag", tag);
454  }
455  }
456  }
457 
458  // save partial tag information so that we process the remainder with the next write
459  if (partial_tag!=nullptr){
460  int tag_pos = findTag(partial_tag, (const char*) data, len);
461  memmove(&frame_header, data+tag_pos, sizeof(ID3v2FrameString));
462  int size = min(len - tag_pos, (size_t) calcSize(frame_header.size)-1);
463  strncpy((char*)result, (char*)data+tag_pos+ID3FrameSize, size);
464  use_bytes_of_next_write = size;
465  status = PartialTagAtTail;
466  }
467  }
468 
469  total_len += len;
470 
471  }
472 
473  int isCharAscii(int ch) {return ch >= 0 && ch < 128; }
474 
476  bool isAscii(int l){
477  // check on first 10 characters
478  int m = l < 5 ? l : 10;
479  for (int j=0; j<m; j++){
480  if (!isCharAscii(result[j])) {
481  return false;
482  }
483  }
484  return true;
485  }
486 
488  void processPartialTagAtTail(const uint8_t* data, size_t len) {
489  int remainder = calcSize(frame_header.size) - use_bytes_of_next_write;
490  memcpy(result+use_bytes_of_next_write, data, remainder);
492 
493  status = TagNotFound;
494  processTagNotFound(data+use_bytes_of_next_write, len-use_bytes_of_next_write);
495  }
496 
499  return frame_header.encoding == 0 || frame_header.encoding == 3;
500  }
501 
502  int strpos(char* str, const char* target) {
503  char* res = strstr(str, target);
504  return (res == nullptr) ? -1 : res - str;
505  }
506 
507 
510  if (callback!=nullptr && actual_tag!=nullptr && encodingIsSupported()){
511  LOGI("callback %s",actual_tag);
512  if (memcmp(actual_tag,"TALB",4)==0)
513  callback(Album, result,strnlength(result, 256));
514  else if (memcmp(actual_tag,"TPE1",4)==0)
515  callback(Artist, result,strnlength(result, 256));
516  else if (memcmp(actual_tag,"TOPE",4)==0)
517  callback(Artist, result,strnlength(result, 256));
518  else if (memcmp(actual_tag,"TIT2",4)==0)
519  callback(Title, result,strnlength(result, 256));
520  else if (memcmp(actual_tag,"TCON",4)==0) {
521  if (result[0]=='('){
522  // convert genre id to string
523  int end_pos = strpos((char*)result, ")");
524  if (end_pos>0){
525  // we just use the first entry
526  result[end_pos]=0;
527  int idx = atoi(result+1);
528  if (idx>=0 && idx< (int)sizeof(genres)){
529  strncpy((char*)result,genres[idx],256);
530  }
531  }
532  }
533  callback(Genre, result,strnlength(result, 256));
534  }
535  }
536  }
537 
538 };
539 
549  public:
550 
551  MetaDataID3() = default;
552 
553  ~MetaDataID3(){
554  end();
555  }
556 
557  void setCallback(void (*fn)(MetaDataType info, const char* str, int len)) {
558  id3v1.setCallback(fn);
559  id3v2.setCallback(fn);
560  }
561 
562  void setFilter(ID3TypeSelection sel) {
563  this->filter = sel;
564  }
565 
566  void begin() {
567  TRACEI();
568  id3v1.begin();
569  id3v2.begin();
570  }
571 
572  void end() {
573  TRACEI();
574  id3v1.end();
575  id3v2.end();
576  }
577 
579  virtual size_t write(const uint8_t *data, size_t len){
580  TRACED();
581  if (filter & SELECT_ID3V2) id3v2.write(data, len);
582  if (!id3v2.isProcessed()) {
583  if (filter & SELECT_ID3V1) id3v1.write(data, len);
584  }
585  return len;
586  }
587 
588  protected:
589  MetaDataID3V1 id3v1;
590  MetaDataID3V2 id3v2;
591  int filter = SELECT_ID3;
592 };
593 
594 } // namespace
Common Metadata methods.
Definition: AbstractMetaData.h:34
ID3 Meta Data Common Functionality.
Definition: MetaDataID3.h:67
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:82
Simple ID3 Meta Data Parser which supports ID3 V1 and V2 and implements the Stream interface....
Definition: MetaDataID3.h:548
virtual size_t write(const uint8_t *data, size_t len)
Provide tha audio data to the API to parse for Meta Data.
Definition: MetaDataID3.h:579
Simple ID3 Meta Data API which supports ID3 V1.
Definition: MetaDataID3.h:107
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:229
void processnotifyAudioChange()
executes the callbacks
Definition: MetaDataID3.h:244
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:211
void end()
Ends the processing and releases the memory.
Definition: MetaDataID3.h:121
void begin()
(re)starts the processing
Definition: MetaDataID3.h:113
void processTagNotFound(const uint8_t *data, size_t len)
try to find the metatdata tag in the provided data
Definition: MetaDataID3.h:162
size_t write(const uint8_t *data, size_t len)
provide the (partial) data which might contain the meta data
Definition: MetaDataID3.h:133
Simple ID3 Meta Data API which supports ID3 V2: We only support the "TALB", "TOPE",...
Definition: MetaDataID3.h:329
ID3v2 header()
provides the ID3v2 header
Definition: MetaDataID3.h:370
bool encodingIsSupported()
For the time beeing we support only ASCII and UTF8.
Definition: MetaDataID3.h:498
bool isProcessed()
returns true if the tag has been provided
Definition: MetaDataID3.h:380
void processnotifyAudioChange()
executes the callbacks
Definition: MetaDataID3.h:509
bool isAscii(int l)
Make sure that the result is a valid ASCII string.
Definition: MetaDataID3.h:476
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:488
void end()
Ends the processing and releases the memory.
Definition: MetaDataID3.h:343
void begin()
(re)starts the processing
Definition: MetaDataID3.h:334
ID3v2FrameString frameHeader()
provides the current frame header
Definition: MetaDataID3.h:375
void processTagNotFound(const uint8_t *data, size_t len)
try to find the metatdata tag in the provided data
Definition: MetaDataID3.h:406
size_t write(const uint8_t *data, size_t len)
provide the (partial) data which might contain the meta data
Definition: MetaDataID3.h:352
ParseStatus
current status of the parsing
Definition: MetaDataID3.h:28
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: AudioConfig.h:872
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:48
Definition: MetaDataID3.h:33
Definition: MetaDataID3.h:301
Definition: MetaDataID3.h:308
Definition: MetaDataID3.h:285