id3.c


1
/* ID3 Reader Version 1.0 (c) 2009 by Malte Marwedel
2
   http://www.marwedels.de/malte
3
4
   Terms of use: GPL Version 2 or later
5
   Example for reading id3 information from mp3 files
6
7
   Supports:
8
     Versions:
9
       ID3 V1.0
10
       ID3 V2.2
11
       ID3 V2.3
12
       ID3 V2.4
13
     Formats:
14
       ISO-8859-1
15
       UTF-16 with BOM
16
       UTF-16 without BOM
17
18
   Partly supports:
19
     Flags:
20
       Jumps over external header
21
     Formats:
22
       UTF-8 is handled as ISO-8859-1
23
24
   Not supports:
25
     Umlauts will be removed
26
     Unsynchronisation
27
     Extended Tag in ID3 V1.x
28
29
Should work on all embedded systems, which support file operation similar to the
30
Unix fread and fseek functions.
31
fseek must support: seek +x bytes from current position
32
                    go to position x (only if you want to use the same opened
33
                    file for mp3 playback afterwards)
34
                    go to position FILE_END - x (only if ID3 V1.0 support
35
                    is wished)
36
malloc and free are used too, but these could be replaced by static buffers.
37
38
compile for PC: gcc -o id3 id3.c -Wall -Wextra -DTEST_PC
39
run on PC: ./id3 <your mp3 base dir>*.mp3
40
*/
41
42
43
#include <stdlib.h>
44
#include <inttypes.h>
45
#include <stdio.h>
46
#include <string.h>
47
48
#ifdef TEST_PC
49
#include <unistd.h>
50
51
#else
52
//add your avr specific includes here
53
54
#endif
55
56
//order must fit with the id3 v1 order too
57
#ifndef ID3_TITLE
58
#define ID3_TITLE 0
59
#endif
60
61
#ifndef ID3_ARTIST
62
#define ID3_ARTIST 1
63
#endif
64
65
#ifndef ID3_ALBUM
66
#define ID3_ALBUM 2
67
#endif
68
69
#ifdef TEST_PC
70
71
//for testing on pc only:
72
#define prog_char char
73
#define memcmp_P memcmp
74
75
FILE * stream_file;
76
77
extern void id3_extract(void);
78
79
80
void * s_cmalloc(unsigned short size) {
81
  void * p = calloc(1, size);
82
  if (p == NULL) {
83
    printf("Out of mem\n");
84
    exit(2);
85
  }
86
  return p;
87
}
88
89
uint8_t pgm_read_byte(void * addr) {
90
  return *(uint8_t*)addr;
91
}
92
93
void state_id3_set(char * text, uint8_t id) {
94
  if (id == ID3_TITLE) {
95
    printf("title: %s\n", text);
96
  }
97
  if (id == ID3_ALBUM) {
98
    printf("album: %s\n", text);
99
  }
100
  if (id == ID3_ARTIST) {
101
    printf("artist: %s\n", text);
102
  }
103
}
104
105
int main(int argc, char ** argv) {
106
  if (argc < 2) {
107
    printf("Give one or more mp3 filenames as param\n");
108
    printf("Returns 0 if parsing done, 1: if no file could be openend, 2: if malloc failed\n");
109
    return 1;
110
  }
111
  int i;
112
  for (i = 1; i < argc; i++) {
113
    stream_file = fopen(argv[i], "rb");
114
    if (stream_file != NULL) {
115
      printf("%s:\n", argv[i]);
116
      id3_extract();
117
      fclose(stream_file);
118
    } else {
119
      printf("could not open file '%s'\n", argv[i]);
120
      return 1;
121
    }
122
  }
123
  return 0;
124
}
125
126
#endif
127
128
//==== the id3 reader =================
129
//you might want to put the #define ID3_ int an header file later
130
131
132
extern FILE * stream_file;
133
134
/*
135
See ID3 format at: http://www.id3.org/id3v2.3.0
136
*/
137
138
static prog_char id3_start[3] = {'I','D','3'};
139
static prog_char id3_tag[3] = {'T','A','G'};
140
141
//first half: id3.2 second half id3.3 and id3.4
142
#define ID3_COMPARES 8
143
//4 byte string, 1 byte ouptut index
144
static prog_char id3_table[ID3_COMPARES][5] = {
145
{'T','A','L',0, ID3_ALBUM},
146
{'T','T','2',0, ID3_TITLE},
147
{'T','P','1',0, ID3_ARTIST},
148
{'T','P','2',0, ID3_ARTIST},
149
{'T','A','L','B', ID3_ALBUM},
150
{'T','I','T','2', ID3_TITLE},
151
{'T','P','E','1', ID3_ARTIST},
152
{'T','P','E','2', ID3_ARTIST}};
153
154
static char * id3_v1text(void) {
155
  char * text = s_cmalloc(sizeof(char)*31); //the text is always 30 chars long
156
  fread(text, sizeof(char), 30, stream_file);
157
  uint8_t rp, wp = 0;
158
  for (rp = 0; rp <= 30; rp++) { //fix files with invalid chars
159
    uint8_t t1 = text[rp];
160
    if ((t1 >= 0x20) && (t1 < 0x7F)) {
161
      text[wp] = t1;
162
      wp++;
163
    }
164
    if (t1 == '\0')
165
      break;
166
  }
167
  text[wp] = '\0';
168
  return text;
169
}
170
171
static char * id3_text(uint16_t framesize) {
172
  //id 3v2 requires to framesize to be at least 1
173
  uint16_t l = 128;
174
  if (l > framesize)
175
    l = framesize;
176
  char * text = s_cmalloc(sizeof(char)*l);
177
  fread(text, sizeof(char), l, stream_file);
178
  /* text encoding defined by text[0]:
179
    $00 – ISO-8859-1 (ASCII).
180
    $01 – UTF-16 with BOM.
181
    $02 – UTF-16BE
182
    $03 – UTF-8: handled as ASCII here, so no special chars may appear
183
  */
184
  uint16_t rp, wp = 0;
185
  if ((text[0] == 1) || (text[0] == 2)) { //utf16, convert
186
    //printf("utf-16\n");
187
    uint16_t rp = 1;
188
    uint8_t bom = 0; //byte order, useful first
189
    if (text[0] == 1) {
190
      rp = 3;
191
      if ((unsigned char)text[1] == 0xfe) {//may be either 0xfe or 0xff
192
        bom = 1; //byte order, useful last
193
        //printf("Info: Rare case with utf16 byte order inverted\n"); //untestet, I never found such a file
194
      }
195
    }
196
    while (rp < l) {
197
      uint8_t t1 = text[rp+bom];
198
      uint8_t t2 = text[rp+1-bom];
199
      if ((t1 >= 0x20) && (t1 < 0x7F) && (!t2)) { //if ascii
200
        text[wp] = t1;
201
        wp++;
202
      }
203
      rp += 2;
204
      if ((!t1) & (!t2)) {
205
        break;
206
      }
207
    }
208
    text[wp] = '\0';
209
  } else { //0 indicate ISO-8859-1
210
    rp = 1;
211
    while (rp < l) {
212
      uint8_t t1 = text[rp];
213
      if ((t1 >= 0x20) && (t1 < 0x7F)) { //filter all ascii
214
        text[wp] = t1;
215
        wp++;
216
      }
217
      rp++;
218
    }
219
  }
220
  if (wp >= l) //only happens if utf16 with wrong format occured
221
    wp = l-1;
222
  text[wp] = '\0'; //just make sure its \0 terminated
223
  if (framesize > l) {
224
    fseek(stream_file, framesize-l,  SEEK_CUR);
225
  }
226
  return text;
227
}
228
229
//supports only ID3 version 2.x
230
void id3_extract(void) {
231
  uint8_t id3temp[10];
232
  fread(id3temp, sizeof(uint8_t), 10, stream_file);
233
  uint8_t subversion = id3temp[3];
234
  if ((memcmp_P(id3temp, id3_start, 3) == 0) && (subversion <= 4)) {
235
    //printf("Info: ID3 version 2.%i\n", subversion);
236
    uint32_t headersize = 0;
237
    uint8_t i;
238
    for (i = 0; i < 4; i++) {
239
      headersize <<= 7;
240
      headersize += id3temp[i+6] & 0x7F;
241
    }
242
    if (id3temp[5] & 0x80) {
243
      //printf("Warning: Unsynchronisation must be used, not supported yet, could result in errors in very rare cases\n");
244
    }
245
    if (id3temp[5] & 0x40) { //only very few mp3s use this for a CRC check sum
246
      //printf("Warning: Extended header support is incomplete\n");
247
      fread(id3temp, sizeof(uint8_t), 10, stream_file);
248
      fseek(stream_file, id3temp[3]-6, SEEK_CUR); //in version 2.3, the size is either 10 or 6 and nothing else
249
    }
250
    //printf("Header size: %u\n", headersize); //sould be %lu on the AVR platform
251
    //decode information
252
    uint8_t frameheadersize = 10;
253
    uint8_t arrayoffset = ID3_COMPARES/2;
254
    uint8_t cmplen = 4;
255
    if (subversion < 3) {
256
      frameheadersize = 6;
257
      id3temp[9] = 0x00; //handle things as compression, group or discharged is 0 later
258
      arrayoffset = 0;
259
      cmplen = 3;
260
    }
261
    uint32_t parsed = 0;
262
    while (parsed < headersize) { //usually does one loop for every frame
263
      if (fread(id3temp, sizeof(uint8_t), frameheadersize, stream_file) != frameheadersize) {
264
        //printf("Error: File end!\n");
265
        break; //ouch! file end
266
      }
267
      if (id3temp[0] == 0) { //must be ascii to be valid
268
        //either wrong file format, or padding reached
269
        break;
270
      }
271
      //calculate frame size
272
      uint32_t framesize = 0;
273
      uint8_t i;
274
      for (i = 0; i < 4; i++) {
275
        if ((subversion <= 2) && (i < 3)) { //id3 v 2.2: has only 3 bytes frame size
276
          framesize <<= 8;
277
          framesize += id3temp[i+3];
278
        }
279
        if (subversion == 3) { //id3 v 2.3
280
          framesize <<= 8;
281
          framesize += id3temp[i+4];
282
        }
283
        if (subversion == 4) { //id3 v 2.4
284
          framesize <<= 7;
285
          framesize += id3temp[i+4] & 0x7F;
286
        }
287
        //printf("adding %i\n", id3temp[i+4]);
288
      }
289
      //printf("Framesize %i\n", framesize);
290
      //if compression, or group or discharged, or too large, must be larger than 0
291
      if ((id3temp[9] & 0xE0) || (framesize > 10000) || (!framesize)) {
292
        fseek(stream_file, framesize,  SEEK_CUR);
293
        //printf("Not fitting: %i\n", framesize);
294
      } else {
295
        char * text = NULL;
296
        for (i = 0; i < ID3_COMPARES/2; i++) {
297
          uint8_t type = pgm_read_byte(&id3_table[arrayoffset+i][4]);
298
          if (memcmp_P(id3temp, id3_table[arrayoffset+i], cmplen) == 0) {
299
            text = id3_text(framesize);
300
            state_id3_set(text, type);
301
            free(text);
302
            break;
303
          }
304
        }
305
        if (text == NULL) { //no fitting tag found
306
          fseek(stream_file, framesize,  SEEK_CUR);
307
        }
308
      }
309
      parsed += frameheadersize;
310
      parsed += framesize;
311
    }
312
    //make sure, mp3 playing starts at the right position, even if the frames did contain invalid data or padding was used
313
    fseek(stream_file, headersize+10,  SEEK_SET); //not needed if file is re-opened for playing anyway
314
  } else { //no id3 v2 header, look for id3 v1
315
    fseek(stream_file, -128, SEEK_END);
316
    fread(id3temp, sizeof(uint8_t), 3, stream_file);
317
    if (memcmp_P(id3temp, id3_tag, 3) == 0) { //if there is an id3 tag
318
      //printf("Info: ID3 version 1\n");
319
      uint8_t i;
320
      for (i = 0; i < 3; i++) {
321
        char * text = id3_v1text();
322
        if (strlen(text) > 0)
323
          state_id3_set(text, i);
324
        free(text);
325
      }
326
    }
327
    //make sure, mp3 playing starts from the beginning
328
    fseek(stream_file, 0, SEEK_SET); //not needed if file is re-opened for playing anyway
329
  }
330
}