First start for "Manage Settings" and "Browse Themes" (FS#4997 by Mark Bright)
[Rockbox.git] / tools / songdb.pl
blobdad7e10496f30bc0159bfce399937ef0f17491ba
1 #!/usr/bin/perl
3 # Rockbox song database docs:
4 # http://www.rockbox.org/twiki/bin/view/Main/TagDatabase
6 # MP3::Info by Chris Nandor is included verbatim in this script to make
7 # it runnable standalone on removable drives. See below.
10 use vorbiscomm;
12 my $db = "rockbox.tagdb";
13 my $dir;
14 my $strip;
15 my $add;
16 my $verbose;
17 my $help;
18 my $dirisalbum;
19 my $dirisalbumname;
20 my $crc = 1;
22 while($ARGV[0]) {
23 if($ARGV[0] eq "--db") {
24 $db = $ARGV[1];
25 shift @ARGV;
26 shift @ARGV;
28 elsif($ARGV[0] eq "--path") {
29 $dir = $ARGV[1];
30 shift @ARGV;
31 shift @ARGV;
33 elsif($ARGV[0] eq "--strip") {
34 $strip = $ARGV[1];
35 shift @ARGV;
36 shift @ARGV;
38 elsif($ARGV[0] eq "--add") {
39 $add = $ARGV[1];
40 shift @ARGV;
41 shift @ARGV;
43 elsif($ARGV[0] eq "--verbose") {
44 $verbose = 1;
45 shift @ARGV;
47 elsif($ARGV[0] eq "--nocrc") {
48 $crc = 0;
49 shift @ARGV;
51 elsif($ARGV[0] eq "--dirisalbum") {
52 $dirisalbum = 1;
53 shift @ARGV;
55 elsif($ARGV[0] eq "--dirisalbumname") {
56 $dirisalbumname = 1;
57 shift @ARGV;
59 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
60 $help = 1;
61 shift @ARGV;
63 else {
64 shift @ARGV;
67 my %entries;
68 my %genres;
69 my %albums;
70 my %years;
71 my %filename;
73 my %lcartists;
74 my %lcalbums;
76 my %dir2albumname;
78 my $dbver = 3;
80 if(! -d $dir or $help) {
81 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
82 print <<MOO
84 songdb --path <dir> [--dirisalbum] [--dirisalbumname] [--db <file>] [--strip <path>] [--add <path>] [--verbose] [--help]
86 Options:
88 --path <dir> Where your music collection is found
89 --dirisalbum Use dir name as album name if the album name is missing in the
90 tags
91 --dirisalbumname Uh, isn\'t this the same as the above?
92 --db <file> What to call the output file. Defaults to rockbox.tagdb
93 --strip <path> Removes this string from the left of all file names
94 --add <path> Adds this string to the left of all file names
95 --nocrc Disables the CRC32 checksums. It makes the output database not
96 suitable for runtimedb but it makes this script run much
97 faster.
98 --verbose Shows more details while working
99 --help This text
102 exit;
105 sub get_oggtag {
106 my $fn = shift;
107 my %hash;
109 my $ogg = vorbiscomm->new($fn);
111 my $h= $ogg->load;
113 # Convert this format into the same format used by the id3 parser hash
115 foreach my $k ($ogg->comment_tags())
117 foreach my $cmmt ($ogg->comment($k))
119 my $n;
120 if($k =~ /^artist$/i) {
121 $n = 'ARTIST';
123 elsif($k =~ /^album$/i) {
124 $n = 'ALBUM';
126 elsif($k =~ /^title$/i) {
127 $n = 'TITLE';
129 $hash{$n}=$cmmt if($n);
133 return \%hash;
136 sub get_ogginfo {
137 my $fn = shift;
138 my %hash;
140 my $ogg = vorbiscomm->new($fn);
142 my $h= $ogg->load;
144 return $ogg->{'INFO'};
147 # return ALL directory entries in the given dir
148 sub getdir {
149 my ($dir) = @_;
151 $dir =~ s|/$|| if ($dir ne "/");
153 if (opendir(DIR, $dir)) {
154 my @all = readdir(DIR);
155 closedir DIR;
156 return @all;
158 else {
159 warn "can't opendir $dir: $!\n";
163 sub extractmp3 {
164 my ($dir, @files) = @_;
165 my @mp3;
166 for(@files) {
167 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
168 push @mp3, $_;
171 return @mp3;
174 sub extractdirs {
175 my ($dir, @files) = @_;
176 $dir =~ s|/$||;
177 my @dirs;
178 for(@files) {
179 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
180 push @dirs, $_;
183 return @dirs;
186 # CRC32 32KB of data (use less if there isn't 32KB available)
188 sub crc32 {
189 my ($filename, $index) = @_;
191 my $len = 32*1024;
193 if(!$crc) {
194 return 0; # fixed bad CRC when disabled!
195 # The runtimedb treats a CRC zero as CRC disabled!
198 if(!open(FILE, "<$filename")) {
199 print "failed to open \"$filename\" $!\n";
200 return 0;
203 # read $data from index $index to $buffer from the file, may return fewer
204 # bytes when dealing with a very small file.
206 # TODO: make sure we don't include a trailer with metadata when doing this.
207 # Like a id3v1 tag.
208 my $nread = sysread FILE, $buffer, $len, $index;
210 close(FILE);
212 my @crc_table =
213 ( # CRC32 lookup table for polynomial 0x04C11DB7
214 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B,
215 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
216 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 0x4C11DB70, 0x48D0C6C7,
217 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75,
218 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3,
219 0x709F7B7A, 0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039,
220 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58, 0xBAEA46EF,
221 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D,
222 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB,
223 0xCEB42022, 0xCA753D95, 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1,
224 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0,
225 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072,
226 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4,
227 0x0808D07D, 0x0CC9CDCA, 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE,
228 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08,
229 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
230 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC,
231 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6,
232 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 0xE0B41DE7, 0xE4750050,
233 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2,
234 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34,
235 0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637,
236 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 0x4F040D56, 0x4BC510E1,
237 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53,
238 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5,
239 0x3F9B762C, 0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF,
240 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E, 0xF5EE4BB9,
241 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B,
242 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD,
243 0xCDA1F604, 0xC960EBB3, 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7,
244 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71,
245 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3,
246 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2,
247 0x470CDD2B, 0x43CDC09C, 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8,
248 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E,
249 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
250 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A,
251 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0,
252 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 0xE3A1CBC1, 0xE760D676,
253 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4,
254 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662,
255 0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668,
256 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4
259 my $crc = 0xffffffff;
260 for ($i = 0; $i < $nread; $i++) {
261 # get the numeric for the byte of the $i index
262 $buf = ord(substr($buffer, $i, 1));
264 $crc = ($crc << 8) ^ $crc_table[(($crc >> 24) ^ $buf) & 0xFF];
266 # printf("%08x\n", $crc);
269 if($crc == 0) {
270 # rule out the very small risk that this actually returns a zero, as
271 # the current rockbox code assumes a zero CRC means it is disabled!
272 # TODO: fix the Rockbox code. This is just a hack.
273 return 1;
276 return $crc;
279 sub singlefile {
280 my ($file) = @_;
281 my $hash;
282 my $info;
284 if($file =~ /\.ogg$/i) {
285 $hash = get_oggtag($file);
287 $info = get_ogginfo($file);
289 $hash->{FILECRC} = crc32($file, $info->{audio_offset});
291 else {
292 $hash = get_mp3tag($file);
294 $info = get_mp3info($file);
296 $hash->{FILECRC} = crc32($file, $info->{headersize});
299 return $hash; # a hash reference
302 my $maxsongperalbum;
304 sub dodir {
305 my ($dir)=@_;
307 print "$dir\n";
309 # getdir() returns all entries in the given dir
310 my @a = getdir($dir);
312 # extractmp3 filters out only the mp3 files from all given entries
313 my @m = extractmp3($dir, @a);
315 my $f;
317 for $f (sort @m) {
319 my $id3 = singlefile("$dir/$f");
321 # ARTIST
322 # COMMENT
323 # ALBUM
324 # TITLE
325 # GENRE
326 # TRACKNUM
327 # YEAR
329 # don't index songs without tags
330 # um. yes we do.
331 if (not defined $$id3{'ARTIST'} and
332 not defined $$id3{'ALBUM'} and
333 not defined $$id3{'TITLE'})
335 next;
338 #printf "Artist: %s\n", $id3->{'ARTIST'};
339 my $path = "$dir/$f";
340 if ($strip ne "" and $path =~ /^$strip(.*)/) {
341 $path = $1;
344 if ($add ne "") {
345 $path = $add . $path;
348 # Only use one case-variation of each album/artist
349 if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
350 # if another album with different case exists
351 # use that case (store it in $$id3{'ALBUM'}
352 $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
354 else {
355 # else create a new entry in the hash
356 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
359 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
360 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
362 else {
363 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
366 $entries{$path}= $id3;
367 $artists{$id3->{'ARTIST'}}++ if($id3->{'ARTIST'});
368 $genres{$id3->{'GENRE'}}++ if($id3->{'GENRE'});
369 $years{$id3->{'YEAR'}}++ if($id3->{'YEAR'});
371 # fallback names
372 $$id3{'ARTIST'} = "<no artist tag>" if ($$id3{'ARTIST'} eq "");
373 # Fall back on the directory name (not full path dirname),
374 # if no album tag
375 if ($dirisalbum) {
376 if($dir2albumname{$dir} eq "") {
377 $dir2albumname{$dir} = $$id3{'ALBUM'};
379 elsif($dir2albumname{$dir} ne $$id3{'ALBUM'}) {
380 $dir2albumname{$dir} = (split m[/], $dir)[-1];
383 # if no directory
384 if ($dirisalbumname) {
385 $$id3{'ALBUM'} = (split m[/], $dir)[-1] if ($$id3{'ALBUM'} eq "");
387 $$id3{'ALBUM'} = "<no album tag>" if ($$id3{'ALBUM'} eq "");
388 # fall back on basename of the file if no title tag.
389 my $base;
390 ($base = $f) =~ s/\.\w+$//;
391 $$id3{'TITLE'} = $base if ($$id3{'TITLE'} eq "");
393 # Append dirname, to handle multi-artist albums
394 $$id3{'DIR'} = $dir;
395 my $albumid;
396 if ($dirisalbum) {
397 $albumid=$$id3{'DIR'};
399 else {
400 $albumid= $id3->{'ALBUM'}."___".$$id3{'DIR'};
402 #printf "album id: %s\n", $albumid;
404 # if($id3->{'ALBUM'}."___".$id3->{'DIR'} ne "<no album tag>___<no artist tag>") {
405 my $num = ++$albums{$albumid};
406 if($num > $maxsongperalbum) {
407 $maxsongperalbum = $num;
408 $longestalbum = $albumid;
410 $album2songs{$albumid}{$$id3{TITLE}} = $id3;
411 if($dirisalbum) {
412 $artist2albums{$$id3{ARTIST}}{$$id3{DIR}} = $id3;
414 else {
415 $artist2albums{$$id3{ARTIST}}{$$id3{ALBUM}} = $id3;
420 if($dirisalbum and $dir2albumname{$dir} eq "") {
421 $dir2albumname{$dir} = (split m[/], $dir)[-1];
422 printf "%s\n", $dir2albumname{$dir};
425 # extractdirs filters out only subdirectories from all given entries
426 my @d = extractdirs($dir, @a);
428 for $d (sort @d) {
429 $dir =~ s|/$||;
430 dodir("$dir/$d");
435 dodir($dir);
436 print "\n";
438 print "File name table\n" if ($verbose);
439 for(sort keys %entries) {
440 printf(" %s\n", $_) if ($verbose);
441 my $l = length($_);
442 if($l > $maxfilelen) {
443 $maxfilelen = $l;
444 $longestfilename = $_;
447 $maxfilelen++; # include zero termination byte
448 while($maxfilelen&3) {
449 $maxfilelen++;
452 my $maxsonglen = 0;
453 my $sc;
454 print "\nSong title table\n" if ($verbose);
456 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
457 printf(" %s\n", $entries{$_}->{'TITLE'} ) if ($verbose);
458 my $l = length($entries{$_}->{'TITLE'});
459 if($l > $maxsonglen) {
460 $maxsonglen = $l;
461 $longestsong = $entries{$_}->{'TITLE'};
464 $maxsonglen++; # include zero termination byte
465 while($maxsonglen&3) {
466 $maxsonglen++;
469 my $maxartistlen = 0;
470 print "\nArtist table\n" if ($verbose);
471 my $i=0;
472 my %artistcount;
473 for(sort {uc($a) cmp uc($b)} keys %artists) {
474 printf(" %s: %d\n", $_, $i) if ($verbose);
475 $artistcount{$_}=$i++;
476 my $l = length($_);
477 if($l > $maxartistlen) {
478 $maxartistlen = $l;
479 $longestartist = $_;
482 $l = scalar keys %{$artist2albums{$_}};
483 if ($l > $maxalbumsperartist) {
484 $maxalbumsperartist = $l;
485 $longestartistalbum = $_;
488 $maxartistlen++; # include zero termination byte
489 while($maxartistlen&3) {
490 $maxartistlen++;
493 print "\nGenre table\n" if ($verbose);
494 for(sort keys %genres) {
495 my $l = length($_);
496 if($l > $maxgenrelen) {
497 $maxgenrelen = $l;
498 $longestgenrename = $_;
502 $maxgenrelen++; #include zero termination byte
503 while($maxgenrelen&3) {
504 $maxgenrelen++;
508 if ($verbose) {
509 print "\nYear table\n";
510 for(sort keys %years) {
511 printf(" %s\n", $_);
515 print "\nAlbum table\n" if ($verbose);
516 my $maxalbumlen = 0;
517 my %albumcount;
518 $i=0;
519 my @albumssort;
520 if($dirisalbum) {
521 @albumssort = sort {uc($dir2albumname{$a}) cmp uc($dir2albumname{$b})} keys %albums;
523 else {
524 @albumssort = sort {uc($a) cmp uc($b)} keys %albums;
526 for(@albumssort) {
527 my @moo=split(/___/, $_);
528 printf(" %s\n", $moo[0]) if ($verbose);
529 $albumcount{$_} = $i++;
530 my $l;
531 if($dirisalbum) {
532 $l = length($dir2albumname{$_});
534 else {
535 $l = length($moo[0]);
537 if($l > $maxalbumlen) {
538 $maxalbumlen = $l;
539 if($dirisalbum) {
540 $longestalbumname = $dir2albumname{$_};
542 else {
543 $longestalbumname = $moo[0];
547 $maxalbumlen++; # include zero termination byte
548 while($maxalbumlen&3) {
549 $maxalbumlen++;
554 sub dumpshort {
555 my ($num)=@_;
557 # print "int: $num\n";
559 print DB pack "n", $num;
562 sub dumpint {
563 my ($num)=@_;
565 # print "int: $num\n";
567 print DB pack "N", $num;
570 if (!scalar keys %entries) {
571 print "No songs found. Did you specify the right --path ?\n";
572 print "Use the --help parameter to see all options.\n";
573 exit;
576 if ($db) {
577 my $songentrysize = $maxsonglen + 12 + $maxgenrelen+ 12;
578 my $albumentrysize = $maxalbumlen + 4 + $maxsongperalbum*4;
579 my $artistentrysize = $maxartistlen + $maxalbumsperartist*4;
580 my $fileentrysize = $maxfilelen + 12;
582 printf "Number of artists : %d\n", scalar keys %artists;
583 printf "Number of albums : %d\n", scalar keys %albums;
584 printf "Number of songs / files : %d\n", scalar keys %entries;
585 print "Max artist length : $maxartistlen ($longestartist)\n";
586 print "Max album length : $maxalbumlen ($longestalbumname)\n";
587 print "Max song length : $maxsonglen ($longestsong)\n";
588 print "Max songs per album : $maxsongperalbum ($longestalbum)\n";
589 print "Max albums per artist: $maxalbumsperartist ($longestartistalbum)\n";
590 print "Max genre length : $maxgenrelen ($longestgenrename)\n";
591 print "Max file length : $maxfilelen ($longestfilename)\n";
592 print "Database version: $dbver\n" if ($verbose);
593 print "Song Entry Size : $songentrysize ($maxsonglen + 12 + $maxgenrelen + 4)\n" if ($verbose);
594 print "Album Entry Size: $albumentrysize ($maxalbumlen + 4 + $maxsongperalbum * 4)\n" if ($verbose);
595 print "Artist Entry Size: $artistentrysize ($maxartistlen + $maxalbumsperartist * 4)\n" if ($verbose);
596 print "File Entry Size: $fileentrysize ($maxfilelen + 12)\n" if ($verbose);
599 open(DB, ">$db") || die "couldn't make $db";
600 binmode(DB);
601 printf DB "RDB%c", $dbver;
603 $pathindex = 68; # paths always start at index 68
605 $artistindex = $pathindex;
607 # set total size of song title table
608 $sc = scalar(keys %entries) * $songentrysize;
609 my $ac = scalar(keys %albums) * $albumentrysize;
610 my $arc = scalar(keys %artists) * $artistentrysize;
611 $albumindex = $artistindex + $arc; # arc is size of all artists
612 $songindex = $albumindex + $ac; # ac is size of all albums
613 my $fileindex = $songindex + $sc; # sc is size of all songs
615 dumpint($artistindex); # file position index of artist table
616 dumpint($albumindex); # file position index of album table
617 dumpint($songindex); # file position index of song table
618 dumpint($fileindex); # file position index of file table
619 dumpint(scalar(keys %artists)); # number of artists
620 dumpint(scalar(keys %albums)); # number of albums
621 dumpint(scalar(keys %entries)); # number of songs
622 dumpint(scalar(keys %entries)); # number of files
623 dumpint($maxartistlen); # length of artist name field
624 dumpint($maxalbumlen); # length of album name field
625 dumpint($maxsonglen); # length of song name field
626 dumpint($maxgenrelen); #length of genre field
627 dumpint($maxfilelen); # length of file field
628 dumpint($maxsongperalbum); # number of entries in songs-per-album array
629 dumpint($maxalbumsperartist); # number of entries in albums-per-artist array
630 dumpint(-1); # rundb dirty
632 #### TABLE of artists ###
633 # name of artist1
634 # pointers to albums of artist1
636 for (sort {uc($a) cmp uc($b)} keys %artists) {
637 my $artist = $_;
638 my $str = $_."\x00" x ($maxartistlen - length($_));
639 print DB $str;
641 for (sort keys %{$artist2albums{$artist}}) {
642 my $id3 = $artist2albums{$artist}{$_};
643 my $a;
644 if($dirisalbum) {
645 $a = $albumcount{"$$id3{'DIR'}"} * $albumentrysize;
647 else {
648 $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'DIR'}"} * $albumentrysize;
650 dumpint($a + $albumindex);
653 for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) {
654 print DB "\x00\x00\x00\x00";
659 ### Build song offset info.
660 my $offset = $songindex;
661 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
662 my $id3 = $entries{$_};
663 $$id3{'songoffset'} = $offset;
664 $offset += $songentrysize;
668 #### TABLE of albums ###
669 # name of album1
670 # pointers to artists of album1
671 # pointers to songs on album1
673 for(@albumssort) {
674 my $albumid = $_;
675 my @moo=split(/___/, $_);
676 my $t;
677 my $str;
678 if($dirisalbum) {
679 $t = $dir2albumname{$albumid};
681 else {
682 $t = $moo[0];
684 $str = $t."\x00" x ($maxalbumlen - length($t));
685 print DB $str;
687 my @songlist = keys %{$album2songs{$albumid}};
688 my $id3 = $album2songs{$albumid}{$songlist[0]};
690 #printf "(d) albumid: %s artist: %s\n",$albumid, $id3->{'ARTIST'};
692 my $aoffset = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
693 dumpint($aoffset + $artistindex); # pointer to artist of this album
695 if (defined $id3->{'TRACKNUM'}) {
696 @songlist = sort {
697 $album2songs{$albumid}{$a}->{'TRACKNUM'} <=>
698 $album2songs{$albumid}{$b}->{'TRACKNUM'}
699 } @songlist;
701 else {
702 @songlist = sort @songlist;
705 for (@songlist) {
706 my $id3 = $album2songs{$albumid}{$_};
707 dumpint($$id3{'songoffset'});
710 for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) {
711 print DB "\x00\x00\x00\x00";
715 #### Build filename offset info
716 my $l=$fileindex;
717 my %filenamepos;
718 for $f (sort {uc($a) cmp uc($b)} keys %entries) {
719 $filenamepos{$f}= $l;
720 $l += $fileentrysize;
723 #### TABLE of songs ###
724 # title of song1
725 # pointer to artist of song1
726 # pointer to album of song1
727 # pointer to filename of song1
729 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
730 my $f = $_;
731 my $id3 = $entries{$f};
732 my $t = $id3->{'TITLE'};
733 my $g = $id3->{'GENRE'};
734 my $str = $t."\x00" x ($maxsonglen- length($t));
736 print DB $str; # title
737 $str = $g."\x00" x ($maxgenrelen - length($g));
739 my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
740 dumpint($a + $artistindex); # pointer to artist of this song
742 if($dirisalbum) {
743 $a = $albumcount{"$$id3{DIR}"} * $albumentrysize;
745 else {
746 $a = $albumcount{"$$id3{ALBUM}___$$id3{DIR}"} * $albumentrysize;
748 dumpint($a + $albumindex); # pointer to album of this song
750 # pointer to filename of this song
751 dumpint($filenamepos{$f});
752 print DB $str; #genre
753 dumpshort(-1);
754 dumpshort($id3->{'YEAR'});
755 dumpint(-1);
756 dumpshort($id3->{'TRACKNUM'});
757 dumpshort(-1);
760 #### TABLE of file names ###
761 # path1
763 for $f (sort {uc($a) cmp uc($b)} %entries) {
764 my $str = $f."\x00" x ($maxfilelen- length($f));
765 my $id3 = $entries{$f};
766 print DB $str;
767 #print STDERR "CRC: ".."\n";
768 dumpint($id3->{'FILECRC'}); # CRC32 of the song data
769 dumpint($id3->{'songoffset'}); # offset to song data
770 dumpint(-1); # offset to rundb data. always set to -1. this is updated by rockbox code on the player.
773 close(DB);
777 ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor
778 ### Modified by Björn Stenberg to remove use of external libraries
781 our(
782 @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION,
783 @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder,
784 @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields,
785 @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD,
786 @mp3_info_fields
789 @ISA = 'Exporter';
790 @EXPORT = qw(
791 set_mp3tag get_mp3tag get_mp3info remove_mp3tag
792 use_winamp_genres
794 @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8);
795 %EXPORT_TAGS = (
796 genres => [qw(@mp3_genres %mp3_genres)],
797 utf8 => [qw(use_mp3_utf8)],
798 all => [@EXPORT, @EXPORT_OK]
801 # $Id$
802 ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
803 $VERSION = '1.02';
805 =pod
807 =head1 NAME
809 MP3::Info - Manipulate / fetch info from MP3 audio files
811 =head1 SYNOPSIS
813 #!perl -w
814 use MP3::Info;
815 my $file = 'Pearls_Before_Swine.mp3';
816 set_mp3tag($file, 'Pearls Before Swine', q"77's",
817 'Sticks and Stones', '1990',
818 q"(c) 1990 77's LTD.", 'rock & roll');
820 my $tag = get_mp3tag($file) or die "No TAG info";
821 $tag->{GENRE} = 'rock';
822 set_mp3tag($file, $tag);
824 my $info = get_mp3info($file);
825 printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
827 =cut
830 my $c = -1;
831 # set all lower-case and regular-cased versions of genres as keys
832 # with index as value of each key
833 %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres;
835 # do it again for winamp genres
836 $c = -1;
837 %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
840 =pod
842 my $mp3 = new MP3::Info $file;
843 $mp3->title('Perls Before Swine');
844 printf "$file length is %s, title is %s\n",
845 $mp3->time, $mp3->title;
848 =head1 DESCRIPTION
850 =over 4
852 =item $mp3 = MP3::Info-E<gt>new(FILE)
854 OOP interface to the rest of the module. The same keys
855 available via get_mp3info and get_mp3tag are available
856 via the returned object (using upper case or lower case;
857 but note that all-caps "VERSION" will return the module
858 version, not the MP3 version).
860 Passing a value to one of the methods will set the value
861 for that tag in the MP3 file, if applicable.
863 =cut
865 sub new {
866 my($pack, $file) = @_;
868 my $info = get_mp3info($file) or return undef;
869 my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names };
870 my %self = (
871 FILE => $file,
872 TRY_HARDER => 0
875 @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
876 @{$info}{@mp3_info_fields},
877 @{$tags}{@v1_tag_names},
878 $file
881 return bless \%self, $pack;
884 sub can {
885 my $self = shift;
886 return $self->SUPER::can(@_) unless ref $self;
887 my $name = uc shift;
888 return sub { $self->$name(@_) } if exists $self->{$name};
889 return undef;
892 sub AUTOLOAD {
893 my($self) = @_;
894 (my $name = uc $AUTOLOAD) =~ s/^.*://;
896 if (exists $self->{$name}) {
897 my $sub = exists $v1_tag_fields{$name}
898 ? sub {
899 if (defined $_[1]) {
900 $_[0]->{$name} = $_[1];
901 set_mp3tag($_[0]->{FILE}, $_[0]);
903 return $_[0]->{$name};
905 : sub {
906 return $_[0]->{$name}
909 *{$AUTOLOAD} = $sub;
910 goto &$AUTOLOAD;
912 } else {
913 warn(sprintf "No method '$name' available in package %s.",
914 __PACKAGE__);
918 sub DESTROY {
923 =item use_mp3_utf8([STATUS])
925 Tells MP3::Info to (or not) return TAG info in UTF-8.
926 TRUE is 1, FALSE is 0. Default is FALSE.
928 Will only be able to it on if Unicode::String is available. ID3v2
929 tags will be converted to UTF-8 according to the encoding specified
930 in each tag; ID3v1 tags will be assumed Latin-1 and converted
931 to UTF-8.
933 Function returns status (TRUE/FALSE). If no argument is supplied,
934 or an unaccepted argument is supplied, function merely returns status.
936 This function is not exported by default, but may be exported
937 with the C<:utf8> or C<:all> export tag.
939 =cut
941 my $unicode_module = eval { require Unicode::String };
942 my $UNICODE = 0;
944 sub use_mp3_utf8 {
945 my($val) = @_;
946 if ($val == 1) {
947 $UNICODE = 1 if $unicode_module;
948 } elsif ($val == 0) {
949 $UNICODE = 0;
951 return $UNICODE;
954 =pod
956 =item use_winamp_genres()
958 Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
959 (adds 68 additional genres to the default list of 80).
960 This is a separate function because these are non-standard
961 genres, but they are included because they are widely used.
963 You can import the data structures with one of:
965 use MP3::Info qw(:genres);
966 use MP3::Info qw(:DEFAULT :genres);
967 use MP3::Info qw(:all);
969 =cut
971 sub use_winamp_genres {
972 %mp3_genres = %winamp_genres;
973 @mp3_genres = @winamp_genres;
974 return 1;
977 =pod
979 =pod
981 =item get_mp3tag (FILE [, VERSION, RAW_V2])
983 Returns hash reference containing tag information in MP3 file. The keys
984 returned are the same as those supplied for C<set_mp3tag>, except in the
985 case of RAW_V2 being set.
987 If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
988 If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
989 If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
990 then, if present, the ID3v2 tag information will override any existing ID3v1
991 tag info.
993 If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
994 of text encoding. The key name is the same as the frame ID (ID to name mappings
995 are in the global %v2_tag_names).
997 If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
998 necessary, etc. It also takes multiple values for a given key (such as comments)
999 and puts them in an arrayref.
1001 If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
1002 not be read.
1004 Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
1005 (unless RAW_V2 is C<1>).
1007 Also returns a TAGVERSION key, containing the ID3 version used for the returned
1008 data (if TAGVERSION argument is C<0>, may contain two versions).
1010 =cut
1012 sub get_mp3tag {
1013 my($file, $ver, $raw_v2) = @_;
1014 my($tag, $v1, $v2, $v2h, %info, @array, $fh);
1015 $raw_v2 ||= 0;
1016 $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0;
1018 if (not (defined $file && $file ne '')) {
1019 $@ = "No file specified";
1020 return undef;
1023 if (not -s $file) {
1024 $@ = "File is empty";
1025 return undef;
1028 if (ref $file) { # filehandle passed
1029 $fh = $file;
1030 } else {
1031 $fh = gensym;
1032 if (not open $fh, "< $file\0") {
1033 $@ = "Can't open $file: $!";
1034 return undef;
1038 binmode $fh;
1040 if ($ver < 2) {
1041 seek $fh, -128, 2;
1042 while(defined(my $line = <$fh>)) { $tag .= $line }
1044 if ($tag =~ /^TAG/) {
1045 $v1 = 1;
1046 if (substr($tag, -3, 2) =~ /\000[^\000]/) {
1047 (undef, @info{@v1_tag_names}) =
1048 (unpack('a3a30a30a30a4a28', $tag),
1049 ord(substr($tag, -2, 1)),
1050 $mp3_genres[ord(substr $tag, -1)]);
1051 $info{TAGVERSION} = 'ID3v1.1';
1052 } else {
1053 (undef, @info{@v1_tag_names[0..4, 6]}) =
1054 (unpack('a3a30a30a30a4a30', $tag),
1055 $mp3_genres[ord(substr $tag, -1)]);
1056 $info{TAGVERSION} = 'ID3v1';
1058 if ($UNICODE) {
1059 for my $key (keys %info) {
1060 next unless $info{$key};
1061 my $u = Unicode::String::latin1($info{$key});
1062 $info{$key} = $u->utf8;
1065 } elsif ($ver == 1) {
1066 _close($file, $fh);
1067 $@ = "No ID3v1 tag found";
1068 return undef;
1072 ($v2, $v2h) = _get_v2tag($fh);
1074 unless ($v1 || $v2) {
1075 _close($file, $fh);
1076 $@ = "No ID3 tag found";
1077 return undef;
1080 if (($ver == 0 || $ver == 2) && $v2) {
1081 if ($raw_v2 == 1 && $ver == 2) {
1082 %info = %$v2;
1083 $info{TAGVERSION} = $v2h->{version};
1084 } else {
1085 my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names;
1086 for my $id (keys %$hash) {
1087 if (exists $v2->{$id}) {
1088 if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) {
1089 $info{$hash->{$id}} = $mp3_genres[$1];
1090 } else {
1091 my $data1 = $v2->{$id};
1093 # this is tricky ... if this is an arrayref, we want
1094 # to only return one, so we pick the first one. but
1095 # if it is a comment, we pick the first one where the
1096 # first charcter after the language is NULL and not an
1097 # additional sub-comment, because that is most likely
1098 # to be the user-supplied comment
1100 if (ref $data1 && !$raw_v2) {
1101 if ($id =~ /^COMM?$/) {
1102 my($newdata) = grep /^(....\000)/, @{$data1};
1103 $data1 = $newdata || $data1->[0];
1104 } else {
1105 $data1 = $data1->[0];
1109 $data1 = [ $data1 ] if ! ref $data1;
1111 for my $data (@$data1) {
1112 $data =~ s/^(.)//; # strip first char (text encoding)
1113 my $encoding = $1;
1114 my $desc;
1115 if ($id =~ /^COM[M ]?$/) {
1116 $data =~ s/^(?:...)//; # strip language
1117 $data =~ s/^(.*?)\000+//; # strip up to first NULL(s),
1118 # for sub-comment
1119 $desc = $1;
1122 if ($UNICODE) {
1123 if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE
1124 my $u = Unicode::String::utf16($data);
1125 $data = $u->utf8;
1126 $data =~ s/^\xEF\xBB\xBF//; # strip BOM
1127 } elsif ($encoding eq "\000") {
1128 my $u = Unicode::String::latin1($data);
1129 $data = $u->utf8;
1133 if ($raw_v2 == 2 && $desc) {
1134 $data = { $desc => $data };
1137 if ($raw_v2 == 2 && exists $info{$hash->{$id}}) {
1138 if (ref $info{$hash->{$id}} eq 'ARRAY') {
1139 push @{$info{$hash->{$id}}}, $data;
1140 } else {
1141 $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ];
1143 } else {
1144 $info{$hash->{$id}} = $data;
1150 if ($ver == 0 && $info{TAGVERSION}) {
1151 $info{TAGVERSION} .= ' / ' . $v2h->{version};
1152 } else {
1153 $info{TAGVERSION} = $v2h->{version};
1158 unless ($raw_v2 && $ver == 2) {
1159 foreach my $key (keys %info) {
1160 if (defined $info{$key}) {
1161 $info{$key} =~ s/\000+.*//g;
1162 $info{$key} =~ s/\s+$//;
1166 for (@v1_tag_names) {
1167 $info{$_} = '' unless defined $info{$_};
1171 if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) {
1172 $info{GENRE} = '';
1175 _close($file, $fh);
1177 return keys %info ? {%info} : undef;
1180 sub _get_v2tag {
1181 my($fh) = @_;
1182 my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num);
1183 $h = {};
1185 $v2 = _get_v2head($fh) or return;
1186 if ($v2->{major_version} < 2) {
1187 warn "This is $v2->{version}; " .
1188 "ID3v2 versions older than ID3v2.2.0 not supported\n"
1189 if $^W;
1190 return;
1193 if ($v2->{major_version} == 2) {
1194 $hlen = 6;
1195 $num = 3;
1196 } else {
1197 $hlen = 10;
1198 $num = 4;
1201 $myseek = sub {
1202 seek $fh, $off, 0;
1203 read $fh, my($bytes), $hlen;
1204 return unless $bytes =~ /^([A-Z0-9]{$num})/
1205 || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes
1206 my($id, $size) = ($1, $hlen);
1207 my @bytes = reverse unpack "C$num", substr($bytes, $num, $num);
1208 for my $i (0 .. ($num - 1)) {
1209 $size += $bytes[$i] * 256 ** $i;
1211 return($id, $size);
1214 $off = $v2->{ext_header_size} + 10;
1216 while ($off < $v2->{tag_size}) {
1217 my($id, $size) = &$myseek or last;
1218 seek $fh, $off + $hlen, 0;
1219 read $fh, my($bytes), $size - $hlen;
1220 if (exists $h->{$id}) {
1221 if (ref $h->{$id} eq 'ARRAY') {
1222 push @{$h->{$id}}, $bytes;
1223 } else {
1224 $h->{$id} = [$h->{$id}, $bytes];
1226 } else {
1227 $h->{$id} = $bytes;
1229 $off += $size;
1232 return($h, $v2);
1236 =pod
1238 =item get_mp3info (FILE)
1240 Returns hash reference containing file information for MP3 file.
1241 This data cannot be changed. Returned data:
1243 VERSION MPEG audio version (1, 2, 2.5)
1244 LAYER MPEG layer description (1, 2, 3)
1245 STEREO boolean for audio is in stereo
1247 VBR boolean for variable bitrate
1248 BITRATE bitrate in kbps (average for VBR files)
1249 FREQUENCY frequency in kHz
1250 SIZE bytes in audio stream
1252 SECS total seconds
1253 MM minutes
1254 SS leftover seconds
1255 MS leftover milliseconds
1256 TIME time in MM:SS
1258 COPYRIGHT boolean for audio is copyrighted
1259 PADDING boolean for MP3 frames are padded
1260 MODE channel mode (0 = stereo, 1 = joint stereo,
1261 2 = dual channel, 3 = single channel)
1262 FRAMES approximate number of frames
1263 FRAME_LENGTH approximate length of a frame
1264 VBR_SCALE VBR scale from VBR header
1266 On error, returns nothing and sets C<$@>.
1268 =cut
1270 sub get_mp3info {
1271 my($file) = @_;
1272 my($off, $myseek, $byte, $eof, $h, $tot, $fh);
1274 if (not (defined $file && $file ne '')) {
1275 $@ = "No file specified";
1276 return undef;
1279 if (not -s $file) {
1280 $@ = "File is empty";
1281 return undef;
1284 if (ref $file) { # filehandle passed
1285 $fh = $file;
1286 } else {
1287 $fh = gensym;
1288 if (not open $fh, "< $file\0") {
1289 $@ = "Can't open $file: $!";
1290 return undef;
1294 $off = 0;
1295 $tot = 4096;
1297 $myseek = sub {
1298 seek $fh, $off, 0;
1299 read $fh, $byte, 4;
1302 binmode $fh;
1303 &$myseek;
1305 if ($off == 0) {
1306 if (my $id3v2 = _get_v2head($fh)) {
1307 $tot += $off += $id3v2->{tag_size};
1308 &$myseek;
1312 $h = _get_head($byte);
1313 until (_is_mp3($h)) {
1314 $off++;
1315 &$myseek;
1316 $h = _get_head($byte);
1317 if ($off > $tot && !$try_harder) {
1318 _close($file, $fh);
1319 $@ = "Couldn't find MP3 header (perhaps set " .
1320 '$MP3::Info::try_harder and retry)';
1321 return undef;
1325 my $vbr = _get_vbr($fh, $h, \$off);
1327 $h->{headersize}=$off; # data size prepending the actual mp3 data
1329 seek $fh, 0, 2;
1330 $eof = tell $fh;
1331 seek $fh, -128, 2;
1332 $off += 128 if <$fh> =~ /^TAG/ ? 1 : 0;
1334 _close($file, $fh);
1336 $h->{size} = $eof - $off;
1338 return _get_info($h, $vbr);
1341 sub _get_info {
1342 my($h, $vbr) = @_;
1343 my $i;
1345 $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 :
1346 $h->{IDR} == 0 ? 2.5 : 0;
1347 $i->{LAYER} = 4 - $h->{layer};
1348 $i->{VBR} = defined $vbr ? 1 : 0;
1350 $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0;
1351 $i->{PADDING} = $h->{padding_bit} ? 1 : 0;
1352 $i->{STEREO} = $h->{mode} == 3 ? 0 : 1;
1353 $i->{MODE} = $h->{mode};
1355 $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size};
1357 my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000);
1358 $i->{FRAMES} = int($vbr && $vbr->{frames}
1359 ? $vbr->{frames}
1360 : $i->{SIZE} / $h->{bitrate} / $mfs
1363 if ($vbr) {
1364 $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale};
1365 $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs;
1366 if (not $h->{bitrate}) {
1367 $@ = "Couldn't determine VBR bitrate";
1368 return undef;
1372 $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10;
1373 $i->{SECS} = $h->{'length'} / 100;
1374 $i->{MM} = int $i->{SECS} / 60;
1375 $i->{SS} = int $i->{SECS} % 60;
1376 $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000);
1377 # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS});
1378 # int($i->{MS} / 100 * 75); # is this right?
1379 $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'};
1381 $i->{BITRATE} = int $h->{bitrate};
1382 # should we just return if ! FRAMES?
1383 $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES};
1384 $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}];
1386 $i->{headersize} = $h->{headersize};
1388 return $i;
1391 sub _get_head {
1392 my($byte) = @_;
1393 my($bytes, $h);
1395 $bytes = _unpack_head($byte);
1396 @$h{qw(IDR ID layer protection_bit
1397 bitrate_index sampling_freq padding_bit private_bit
1398 mode mode_extension copyright original
1399 emphasis version_index bytes)} = (
1400 ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1,
1401 ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1,
1402 ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1,
1403 $bytes&3, ($bytes>>19)&3, $bytes
1406 $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}];
1407 $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}];
1409 return $h;
1412 sub _is_mp3 {
1413 my $h = $_[0] or return undef;
1414 return ! ( # all below must be false
1415 $h->{bitrate_index} == 0
1417 $h->{version_index} == 1
1419 ($h->{bytes} & 0xFFE00000) != 0xFFE00000
1421 !$h->{fs}
1423 !$h->{bitrate}
1425 $h->{bitrate_index} == 15
1427 !$h->{layer}
1429 $h->{sampling_freq} == 3
1431 $h->{emphasis} == 2
1433 !$h->{bitrate_index}
1435 ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000
1437 ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1)
1439 ($h->{mode_extension} != 0 && $h->{mode} != 1)
1443 sub _get_vbr {
1444 my($fh, $h, $roff) = @_;
1445 my($off, $bytes, @bytes, $myseek, %vbr);
1447 $off = $$roff;
1448 @_ = (); # closure confused if we don't do this
1450 $myseek = sub {
1451 my $n = $_[0] || 4;
1452 seek $fh, $off, 0;
1453 read $fh, $bytes, $n;
1454 $off += $n;
1457 $off += 4;
1459 if ($h->{ID}) { # MPEG1
1460 $off += $h->{mode} == 3 ? 17 : 32;
1461 } else { # MPEG2
1462 $off += $h->{mode} == 3 ? 9 : 17;
1465 &$myseek;
1466 return unless $bytes eq 'Xing';
1468 &$myseek;
1469 $vbr{flags} = _unpack_head($bytes);
1471 if ($vbr{flags} & 1) {
1472 &$myseek;
1473 $vbr{frames} = _unpack_head($bytes);
1476 if ($vbr{flags} & 2) {
1477 &$myseek;
1478 $vbr{bytes} = _unpack_head($bytes);
1481 if ($vbr{flags} & 4) {
1482 $myseek->(100);
1483 # Not used right now ...
1484 # $vbr{toc} = _unpack_head($bytes);
1487 if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst)
1488 &$myseek;
1489 $vbr{scale} = _unpack_head($bytes);
1490 } else {
1491 $vbr{scale} = -1;
1494 $$roff = $off;
1495 return \%vbr;
1498 sub _get_v2head {
1499 my $fh = $_[0] or return;
1500 my($h, $bytes, @bytes);
1502 # check first three bytes for 'ID3'
1503 seek $fh, 0, 0;
1504 read $fh, $bytes, 3;
1505 return unless $bytes eq 'ID3';
1507 # get version
1508 read $fh, $bytes, 2;
1509 $h->{version} = sprintf "ID3v2.%d.%d",
1510 @$h{qw[major_version minor_version]} =
1511 unpack 'c2', $bytes;
1513 # get flags
1514 read $fh, $bytes, 1;
1515 if ($h->{major_version} == 2) {
1516 @$h{qw[unsync compression]} =
1517 (unpack 'b8', $bytes)[7, 6];
1518 $h->{ext_header} = 0;
1519 $h->{experimental} = 0;
1520 } else {
1521 @$h{qw[unsync ext_header experimental]} =
1522 (unpack 'b8', $bytes)[7, 6, 5];
1525 # get ID3v2 tag length from bytes 7-10
1526 $h->{tag_size} = 10; # include ID3v2 header size
1527 read $fh, $bytes, 4;
1528 @bytes = reverse unpack 'C4', $bytes;
1529 foreach my $i (0 .. 3) {
1530 # whoaaaaaa nellllllyyyyyy!
1531 $h->{tag_size} += $bytes[$i] * 128 ** $i;
1534 # get extended header size
1535 $h->{ext_header_size} = 0;
1536 if ($h->{ext_header}) {
1537 $h->{ext_header_size} += 10;
1538 read $fh, $bytes, 4;
1539 @bytes = reverse unpack 'C4', $bytes;
1540 for my $i (0..3) {
1541 $h->{ext_header_size} += $bytes[$i] * 256 ** $i;
1545 return $h;
1548 sub _unpack_head {
1549 unpack('l', pack('L', unpack('N', $_[0])));
1552 sub _close {
1553 my($file, $fh) = @_;
1554 unless (ref $file) { # filehandle not passed
1555 close $fh or warn "Problem closing '$file': $!";
1559 BEGIN {
1560 @mp3_genres = (
1561 'Blues',
1562 'Classic Rock',
1563 'Country',
1564 'Dance',
1565 'Disco',
1566 'Funk',
1567 'Grunge',
1568 'Hip-Hop',
1569 'Jazz',
1570 'Metal',
1571 'New Age',
1572 'Oldies',
1573 'Other',
1574 'Pop',
1575 'R&B',
1576 'Rap',
1577 'Reggae',
1578 'Rock',
1579 'Techno',
1580 'Industrial',
1581 'Alternative',
1582 'Ska',
1583 'Death Metal',
1584 'Pranks',
1585 'Soundtrack',
1586 'Euro-Techno',
1587 'Ambient',
1588 'Trip-Hop',
1589 'Vocal',
1590 'Jazz+Funk',
1591 'Fusion',
1592 'Trance',
1593 'Classical',
1594 'Instrumental',
1595 'Acid',
1596 'House',
1597 'Game',
1598 'Sound Clip',
1599 'Gospel',
1600 'Noise',
1601 'AlternRock',
1602 'Bass',
1603 'Soul',
1604 'Punk',
1605 'Space',
1606 'Meditative',
1607 'Instrumental Pop',
1608 'Instrumental Rock',
1609 'Ethnic',
1610 'Gothic',
1611 'Darkwave',
1612 'Techno-Industrial',
1613 'Electronic',
1614 'Pop-Folk',
1615 'Eurodance',
1616 'Dream',
1617 'Southern Rock',
1618 'Comedy',
1619 'Cult',
1620 'Gangsta',
1621 'Top 40',
1622 'Christian Rap',
1623 'Pop/Funk',
1624 'Jungle',
1625 'Native American',
1626 'Cabaret',
1627 'New Wave',
1628 'Psychadelic',
1629 'Rave',
1630 'Showtunes',
1631 'Trailer',
1632 'Lo-Fi',
1633 'Tribal',
1634 'Acid Punk',
1635 'Acid Jazz',
1636 'Polka',
1637 'Retro',
1638 'Musical',
1639 'Rock & Roll',
1640 'Hard Rock',
1643 @winamp_genres = (
1644 @mp3_genres,
1645 'Folk',
1646 'Folk-Rock',
1647 'National Folk',
1648 'Swing',
1649 'Fast Fusion',
1650 'Bebob',
1651 'Latin',
1652 'Revival',
1653 'Celtic',
1654 'Bluegrass',
1655 'Avantgarde',
1656 'Gothic Rock',
1657 'Progressive Rock',
1658 'Psychedelic Rock',
1659 'Symphonic Rock',
1660 'Slow Rock',
1661 'Big Band',
1662 'Chorus',
1663 'Easy Listening',
1664 'Acoustic',
1665 'Humour',
1666 'Speech',
1667 'Chanson',
1668 'Opera',
1669 'Chamber Music',
1670 'Sonata',
1671 'Symphony',
1672 'Booty Bass',
1673 'Primus',
1674 'Porn Groove',
1675 'Satire',
1676 'Slow Jam',
1677 'Club',
1678 'Tango',
1679 'Samba',
1680 'Folklore',
1681 'Ballad',
1682 'Power Ballad',
1683 'Rhythmic Soul',
1684 'Freestyle',
1685 'Duet',
1686 'Punk Rock',
1687 'Drum Solo',
1688 'Acapella',
1689 'Euro-House',
1690 'Dance Hall',
1691 'Goa',
1692 'Drum & Bass',
1693 'Club-House',
1694 'Hardcore',
1695 'Terror',
1696 'Indie',
1697 'BritPop',
1698 'Negerpunk',
1699 'Polsk Punk',
1700 'Beat',
1701 'Christian Gangsta Rap',
1702 'Heavy Metal',
1703 'Black Metal',
1704 'Crossover',
1705 'Contemporary Christian',
1706 'Christian Rock',
1707 'Merengue',
1708 'Salsa',
1709 'Thrash Metal',
1710 'Anime',
1711 'JPop',
1712 'Synthpop',
1715 @t_bitrate = ([
1716 [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
1717 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
1718 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
1720 [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
1721 [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
1722 [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
1725 @t_sampling_freq = (
1726 [11025, 12000, 8000],
1727 [undef, undef, undef], # reserved
1728 [22050, 24000, 16000],
1729 [44100, 48000, 32000]
1732 @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 }
1733 map { @$_ } @t_sampling_freq;
1735 @mp3_info_fields = qw(
1736 VERSION
1737 LAYER
1738 STEREO
1740 BITRATE
1741 FREQUENCY
1742 SIZE
1743 SECS
1747 TIME
1748 COPYRIGHT
1749 PADDING
1750 MODE
1751 FRAMES
1752 FRAME_LENGTH
1753 VBR_SCALE
1756 %v1_tag_fields =
1757 (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4);
1759 @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
1761 %v2_to_v1_names = (
1762 # v2.2 tags
1763 'TT2' => 'TITLE',
1764 'TP1' => 'ARTIST',
1765 'TAL' => 'ALBUM',
1766 'TYE' => 'YEAR',
1767 'COM' => 'COMMENT',
1768 'TRK' => 'TRACKNUM',
1769 'TCO' => 'GENRE', # not clean mapping, but ...
1770 # v2.3 tags
1771 'TIT2' => 'TITLE',
1772 'TPE1' => 'ARTIST',
1773 'TALB' => 'ALBUM',
1774 'TYER' => 'YEAR',
1775 'COMM' => 'COMMENT',
1776 'TRCK' => 'TRACKNUM',
1777 'TCON' => 'GENRE',
1780 %v2_tag_names = (
1781 # v2.2 tags
1782 'BUF' => 'Recommended buffer size',
1783 'CNT' => 'Play counter',
1784 'COM' => 'Comments',
1785 'CRA' => 'Audio encryption',
1786 'CRM' => 'Encrypted meta frame',
1787 'ETC' => 'Event timing codes',
1788 'EQU' => 'Equalization',
1789 'GEO' => 'General encapsulated object',
1790 'IPL' => 'Involved people list',
1791 'LNK' => 'Linked information',
1792 'MCI' => 'Music CD Identifier',
1793 'MLL' => 'MPEG location lookup table',
1794 'PIC' => 'Attached picture',
1795 'POP' => 'Popularimeter',
1796 'REV' => 'Reverb',
1797 'RVA' => 'Relative volume adjustment',
1798 'SLT' => 'Synchronized lyric/text',
1799 'STC' => 'Synced tempo codes',
1800 'TAL' => 'Album/Movie/Show title',
1801 'TBP' => 'BPM (Beats Per Minute)',
1802 'TCM' => 'Composer',
1803 'TCO' => 'Content type',
1804 'TCR' => 'Copyright message',
1805 'TDA' => 'Date',
1806 'TDY' => 'Playlist delay',
1807 'TEN' => 'Encoded by',
1808 'TFT' => 'File type',
1809 'TIM' => 'Time',
1810 'TKE' => 'Initial key',
1811 'TLA' => 'Language(s)',
1812 'TLE' => 'Length',
1813 'TMT' => 'Media type',
1814 'TOA' => 'Original artist(s)/performer(s)',
1815 'TOF' => 'Original filename',
1816 'TOL' => 'Original Lyricist(s)/text writer(s)',
1817 'TOR' => 'Original release year',
1818 'TOT' => 'Original album/Movie/Show title',
1819 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
1820 'TP2' => 'Band/Orchestra/Accompaniment',
1821 'TP3' => 'Conductor/Performer refinement',
1822 'TP4' => 'Interpreted, remixed, or otherwise modified by',
1823 'TPA' => 'Part of a set',
1824 'TPB' => 'Publisher',
1825 'TRC' => 'ISRC (International Standard Recording Code)',
1826 'TRD' => 'Recording dates',
1827 'TRK' => 'Track number/Position in set',
1828 'TSI' => 'Size',
1829 'TSS' => 'Software/hardware and settings used for encoding',
1830 'TT1' => 'Content group description',
1831 'TT2' => 'Title/Songname/Content description',
1832 'TT3' => 'Subtitle/Description refinement',
1833 'TXT' => 'Lyricist/text writer',
1834 'TXX' => 'User defined text information frame',
1835 'TYE' => 'Year',
1836 'UFI' => 'Unique file identifier',
1837 'ULT' => 'Unsychronized lyric/text transcription',
1838 'WAF' => 'Official audio file webpage',
1839 'WAR' => 'Official artist/performer webpage',
1840 'WAS' => 'Official audio source webpage',
1841 'WCM' => 'Commercial information',
1842 'WCP' => 'Copyright/Legal information',
1843 'WPB' => 'Publishers official webpage',
1844 'WXX' => 'User defined URL link frame',
1846 # v2.3 tags
1847 'AENC' => 'Audio encryption',
1848 'APIC' => 'Attached picture',
1849 'COMM' => 'Comments',
1850 'COMR' => 'Commercial frame',
1851 'ENCR' => 'Encryption method registration',
1852 'EQUA' => 'Equalization',
1853 'ETCO' => 'Event timing codes',
1854 'GEOB' => 'General encapsulated object',
1855 'GRID' => 'Group identification registration',
1856 'IPLS' => 'Involved people list',
1857 'LINK' => 'Linked information',
1858 'MCDI' => 'Music CD identifier',
1859 'MLLT' => 'MPEG location lookup table',
1860 'OWNE' => 'Ownership frame',
1861 'PCNT' => 'Play counter',
1862 'POPM' => 'Popularimeter',
1863 'POSS' => 'Position synchronisation frame',
1864 'PRIV' => 'Private frame',
1865 'RBUF' => 'Recommended buffer size',
1866 'RVAD' => 'Relative volume adjustment',
1867 'RVRB' => 'Reverb',
1868 'SYLT' => 'Synchronized lyric/text',
1869 'SYTC' => 'Synchronized tempo codes',
1870 'TALB' => 'Album/Movie/Show title',
1871 'TBPM' => 'BPM (beats per minute)',
1872 'TCOM' => 'Composer',
1873 'TCON' => 'Content type',
1874 'TCOP' => 'Copyright message',
1875 'TDAT' => 'Date',
1876 'TDLY' => 'Playlist delay',
1877 'TENC' => 'Encoded by',
1878 'TEXT' => 'Lyricist/Text writer',
1879 'TFLT' => 'File type',
1880 'TIME' => 'Time',
1881 'TIT1' => 'Content group description',
1882 'TIT2' => 'Title/songname/content description',
1883 'TIT3' => 'Subtitle/Description refinement',
1884 'TKEY' => 'Initial key',
1885 'TLAN' => 'Language(s)',
1886 'TLEN' => 'Length',
1887 'TMED' => 'Media type',
1888 'TOAL' => 'Original album/movie/show title',
1889 'TOFN' => 'Original filename',
1890 'TOLY' => 'Original lyricist(s)/text writer(s)',
1891 'TOPE' => 'Original artist(s)/performer(s)',
1892 'TORY' => 'Original release year',
1893 'TOWN' => 'File owner/licensee',
1894 'TPE1' => 'Lead performer(s)/Soloist(s)',
1895 'TPE2' => 'Band/orchestra/accompaniment',
1896 'TPE3' => 'Conductor/performer refinement',
1897 'TPE4' => 'Interpreted, remixed, or otherwise modified by',
1898 'TPOS' => 'Part of a set',
1899 'TPUB' => 'Publisher',
1900 'TRCK' => 'Track number/Position in set',
1901 'TRDA' => 'Recording dates',
1902 'TRSN' => 'Internet radio station name',
1903 'TRSO' => 'Internet radio station owner',
1904 'TSIZ' => 'Size',
1905 'TSRC' => 'ISRC (international standard recording code)',
1906 'TSSE' => 'Software/Hardware and settings used for encoding',
1907 'TXXX' => 'User defined text information frame',
1908 'TYER' => 'Year',
1909 'UFID' => 'Unique file identifier',
1910 'USER' => 'Terms of use',
1911 'USLT' => 'Unsychronized lyric/text transcription',
1912 'WCOM' => 'Commercial information',
1913 'WCOP' => 'Copyright/Legal information',
1914 'WOAF' => 'Official audio file webpage',
1915 'WOAR' => 'Official artist/performer webpage',
1916 'WOAS' => 'Official audio source webpage',
1917 'WORS' => 'Official internet radio station homepage',
1918 'WPAY' => 'Payment',
1919 'WPUB' => 'Publishers official webpage',
1920 'WXXX' => 'User defined URL link frame',
1922 # v2.4 additional tags
1923 # note that we don't restrict tags from 2.3 or 2.4,
1924 'ASPI' => 'Audio seek point index',
1925 'EQU2' => 'Equalisation (2)',
1926 'RVA2' => 'Relative volume adjustment (2)',
1927 'SEEK' => 'Seek frame',
1928 'SIGN' => 'Signature frame',
1929 'TDEN' => 'Encoding time',
1930 'TDOR' => 'Original release time',
1931 'TDRC' => 'Recording time',
1932 'TDRL' => 'Release time',
1933 'TDTG' => 'Tagging time',
1934 'TIPL' => 'Involved people list',
1935 'TMCL' => 'Musician credits list',
1936 'TMOO' => 'Mood',
1937 'TPRO' => 'Produced notice',
1938 'TSOA' => 'Album sort order',
1939 'TSOP' => 'Performer sort order',
1940 'TSOT' => 'Title sort order',
1941 'TSST' => 'Set subtitle',
1943 # grrrrrrr
1944 'COM ' => 'Broken iTunes comments',
1950 __END__
1952 =pod
1954 =back
1956 =head1 TROUBLESHOOTING
1958 If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
1959 If you cannot figure out why it does not work for you, please put the MP3 file in
1960 a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
1961 mail regarding where I can get the file, with a detailed description of the problem.
1963 If I download the file, after debugging the problem I will not keep the MP3 file
1964 if it is not legal for me to have it. Just let me know if it is legal for me to
1965 keep it or not.
1968 =head1 TODO
1970 =over 4
1972 =item ID3v2 Support
1974 Still need to do more for reading tags, such as using Compress::Zlib to decompress
1975 compressed tags. But until I see this in use more, I won't bother. If something
1976 does not work properly with reading, follow the instructions above for
1977 troubleshooting.
1979 ID3v2 I<writing> is coming soon.
1981 =item Get data from scalar
1983 Instead of passing a file spec or filehandle, pass the
1984 data itself. Would take some work, converting the seeks, etc.
1986 =item Padding bit ?
1988 Do something with padding bit.
1990 =item Test suite
1992 Test suite could use a bit of an overhaul and update. Patches very welcome.
1994 =over 4
1996 =item *
1998 Revamp getset.t. Test all the various get_mp3tag args.
2000 =item *
2002 Test Unicode.
2004 =item *
2006 Test OOP API.
2008 =item *
2010 Test error handling, check more for missing files, bad MP3s, etc.
2012 =back
2014 =item Other VBR
2016 Right now, only Xing VBR is supported.
2018 =back
2021 =head1 THANKS
2023 Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>,
2024 Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>,
2025 Michael Blakeley E<lt>mike@blakeley.comE<gt>,
2026 Per Bolmstedt E<lt>tomten@kol14.comE<gt>,
2027 Tony Bowden E<lt>tony@tmtm.comE<gt>,
2028 Tom Brown E<lt>thecap@usa.netE<gt>,
2029 Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>,
2030 Chris Dawson E<lt>cdawson@webiphany.comE<gt>,
2031 Luke Drumm E<lt>lukedrumm@mypad.comE<gt>,
2032 Kyle Farrell E<lt>kyle@cantametrix.comE<gt>,
2033 Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>,
2034 brian d foy E<lt>comdog@panix.comE<gt>,
2035 Ben Gertzfield E<lt>che@debian.orgE<gt>,
2036 Brian Goodwin E<lt>brian@fuddmain.comE<gt>,
2037 Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>,
2038 Todd Harris E<lt>harris@cshl.orgE<gt>,
2039 Woodrow Hill E<lt>asim@mindspring.comE<gt>,
2040 Kee Hinckley E<lt>nazgul@somewhere.comE<gt>,
2041 Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>,
2042 Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>,
2043 Johann Lindvall,
2044 Peter Marschall E<lt>peter.marschall@mayn.deE<gt>,
2045 Trond Michelsen E<lt>mike@crusaders.noE<gt>,
2046 Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>,
2047 Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>,
2048 Jake Palmer E<lt>jake.palmer@db.comE<gt>,
2049 Andrew Phillips E<lt>asp@wasteland.orgE<gt>,
2050 David Reuteler E<lt>reuteler@visi.comE<gt>,
2051 John Ruttenberg E<lt>rutt@chezrutt.comE<gt>,
2052 Matthew Sachs E<lt>matthewg@zevils.comE<gt>,
2053 E<lt>scfc_de@users.sf.netE<gt>,
2054 Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>,
2055 Chris Sidi E<lt>sidi@angband.orgE<gt>,
2056 Roland Steinbach E<lt>roland@support-system.comE<gt>,
2057 Stuart E<lt>schneis@users.sourceforge.netE<gt>,
2058 Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>,
2059 Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>,
2060 Bogdan Surdu E<lt>tim@go.roE<gt>,
2061 E<lt>tim@tim-landscheidt.deE<gt>,
2062 Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>,
2063 Tobias Wagener E<lt>tobias@wagener.nuE<gt>,
2064 Ronan Waide E<lt>waider@stepstone.ieE<gt>,
2065 Andy Waite E<lt>andy@mailroute.comE<gt>,
2066 Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>,
2067 Meng Weng Wong E<lt>mengwong@pobox.comE<gt>.
2070 =head1 AUTHOR AND COPYRIGHT
2072 Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
2074 Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is
2075 free software; you can redistribute it and/or modify it under the terms
2076 of the Artistic License, distributed with Perl.
2079 =head1 SEE ALSO
2081 =over 4
2083 =item MP3::Info Project Page
2085 http://projects.pudge.net/
2087 =item mp3tools
2089 http://www.zevils.com/linux/mp3tools/
2091 =item mpgtools
2093 http://www.dv.co.yu/mpgscript/mpgtools.htm
2094 http://www.dv.co.yu/mpgscript/mpeghdr.htm
2096 =item mp3tool
2098 http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
2100 =item ID3v2
2102 http://www.id3.org/
2104 =item Xing Variable Bitrate
2106 http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
2108 =item MP3Ext
2110 http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
2112 =item Xmms
2114 http://www.xmms.org/
2117 =back
2119 =head1 VERSION
2121 v1.02, Sunday, March 2, 2003
2123 =cut