Fix for ff/rw in long MP3 files.
[kugel-rb.git] / tools / songdb.pl
blob40d23d780d5bd07f92848d6dfae7eeb83169231c
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;
21 while($ARGV[0]) {
22 if($ARGV[0] eq "--db") {
23 $db = $ARGV[1];
24 shift @ARGV;
25 shift @ARGV;
27 elsif($ARGV[0] eq "--path") {
28 $dir = $ARGV[1];
29 shift @ARGV;
30 shift @ARGV;
32 elsif($ARGV[0] eq "--strip") {
33 $strip = $ARGV[1];
34 shift @ARGV;
35 shift @ARGV;
37 elsif($ARGV[0] eq "--add") {
38 $add = $ARGV[1];
39 shift @ARGV;
40 shift @ARGV;
42 elsif($ARGV[0] eq "--verbose") {
43 $verbose = 1;
44 shift @ARGV;
46 elsif($ARGV[0] eq "--dirisalbum") {
47 $dirisalbum = 1;
48 shift @ARGV;
50 elsif($ARGV[0] eq "--dirisalbumname") {
51 $dirisalbumname = 1;
52 shift @ARGV;
54 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
55 $help = 1;
56 shift @ARGV;
58 else {
59 shift @ARGV;
62 my %entries;
63 my %genres;
64 my %albums;
65 my %years;
66 my %filename;
68 my %lcartists;
69 my %lcalbums;
71 my %dir2albumname;
73 my $dbver = 3;
75 if(! -d $dir or $help) {
76 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
77 print "songdb --path <dir> [--dirisalbum] [--dirisalbumname] [--db <file>] [--strip <path>] [--add <path>] [--verbose] [--help]\n";
78 exit;
81 sub get_oggtag {
82 my $fn = shift;
83 my %hash;
85 my $ogg = vorbiscomm->new($fn);
87 my $h= $ogg->load;
89 # Convert this format into the same format used by the id3 parser hash
91 foreach my $k ($ogg->comment_tags())
93 foreach my $cmmt ($ogg->comment($k))
95 my $n;
96 if($k =~ /^artist$/i) {
97 $n = 'ARTIST';
99 elsif($k =~ /^album$/i) {
100 $n = 'ALBUM';
102 $hash{$n}=$cmmt if($n);
106 return \%hash;
109 sub get_ogginfo {
110 my $fn = shift;
111 my %hash;
113 my $ogg = vorbiscomm->new($fn);
115 my $h= $ogg->load;
117 return $ogg->{'INFO'};
120 # return ALL directory entries in the given dir
121 sub getdir {
122 my ($dir) = @_;
124 $dir =~ s|/$|| if ($dir ne "/");
126 if (opendir(DIR, $dir)) {
127 my @all = readdir(DIR);
128 closedir DIR;
129 return @all;
131 else {
132 warn "can't opendir $dir: $!\n";
136 sub extractmp3 {
137 my ($dir, @files) = @_;
138 my @mp3;
139 for(@files) {
140 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
141 push @mp3, $_;
144 return @mp3;
147 sub extractdirs {
148 my ($dir, @files) = @_;
149 $dir =~ s|/$||;
150 my @dirs;
151 for(@files) {
152 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
153 push @dirs, $_;
156 return @dirs;
159 # CRC32 32KB of data (use less if there isn't 32KB available)
161 sub crc32 {
162 my ($filename, $index) = @_;
164 my $len = 32*1024;
166 if(!open(FILE, "<$filename")) {
167 print "failed to open \"$filename\" $!\n";
168 return -2;
171 # read $data from index $index to $buffer from the file, may return fewer
172 # bytes when dealing with a very small file.
174 # TODO: make sure we don't include a trailer with metadata when doing this.
175 # Like a id3v1 tag.
176 my $nread = sysread FILE, $buffer, $len, $index;
178 close(FILE);
180 my @crc_table =
181 ( # CRC32 lookup table for polynomial 0x04C11DB7
182 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B,
183 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
184 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 0x4C11DB70, 0x48D0C6C7,
185 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75,
186 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3,
187 0x709F7B7A, 0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039,
188 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58, 0xBAEA46EF,
189 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D,
190 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB,
191 0xCEB42022, 0xCA753D95, 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1,
192 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0,
193 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072,
194 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4,
195 0x0808D07D, 0x0CC9CDCA, 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE,
196 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08,
197 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA,
198 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC,
199 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6,
200 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 0xE0B41DE7, 0xE4750050,
201 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2,
202 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34,
203 0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637,
204 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 0x4F040D56, 0x4BC510E1,
205 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53,
206 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5,
207 0x3F9B762C, 0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF,
208 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E, 0xF5EE4BB9,
209 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B,
210 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD,
211 0xCDA1F604, 0xC960EBB3, 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7,
212 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71,
213 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3,
214 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2,
215 0x470CDD2B, 0x43CDC09C, 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8,
216 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E,
217 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC,
218 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A,
219 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0,
220 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 0xE3A1CBC1, 0xE760D676,
221 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4,
222 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662,
223 0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668,
224 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4
227 my $crc = 0xffffffff;
228 for ($i = 0; $i < $nread; $i++) {
229 # get the numeric for the byte of the $i index
230 $buf = ord(substr($buffer, $i, 1));
232 $crc = ($crc << 8) ^ $crc_table[(($crc >> 24) ^ $buf) & 0xFF];
234 # printf("%08x\n", $crc);
237 return $crc;
241 sub singlefile {
242 my ($file) = @_;
243 my $hash;
244 my $info;
246 if($file =~ /\.ogg$/i) {
247 $hash = get_oggtag($file);
249 $info = get_ogginfo($file);
251 $hash->{FILECRC} = crc32($file, $info->{audio_offset});
253 else {
254 $hash = get_mp3tag($file);
256 $info = get_mp3info($file);
258 $hash->{FILECRC} = crc32($file, $info->{headersize});
261 return $hash; # a hash reference
264 my $maxsongperalbum;
266 sub dodir {
267 my ($dir)=@_;
269 print "$dir\n";
271 # getdir() returns all entries in the given dir
272 my @a = getdir($dir);
274 # extractmp3 filters out only the mp3 files from all given entries
275 my @m = extractmp3($dir, @a);
277 my $f;
279 for $f (sort @m) {
281 my $id3 = singlefile("$dir/$f");
283 # ARTIST
284 # COMMENT
285 # ALBUM
286 # TITLE
287 # GENRE
288 # TRACKNUM
289 # YEAR
291 # don't index songs without tags
292 # um. yes we do.
293 if (not defined $$id3{'ARTIST'} and
294 not defined $$id3{'ALBUM'} and
295 not defined $$id3{'TITLE'})
297 next;
300 #printf "Artist: %s\n", $id3->{'ARTIST'};
301 my $path = "$dir/$f";
302 if ($strip ne "" and $path =~ /^$strip(.*)/) {
303 $path = $1;
306 if ($add ne "") {
307 $path = $add . $path;
310 # Only use one case-variation of each album/artist
311 if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
312 # if another album with different case exists
313 # use that case (store it in $$id3{'ALBUM'}
314 $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
316 else {
317 # else create a new entry in the hash
318 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
321 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
322 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
324 else {
325 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
328 $entries{$path}= $id3;
329 $artists{$id3->{'ARTIST'}}++ if($id3->{'ARTIST'});
330 $genres{$id3->{'GENRE'}}++ if($id3->{'GENRE'});
331 $years{$id3->{'YEAR'}}++ if($id3->{'YEAR'});
333 # fallback names
334 $$id3{'ARTIST'} = "<no artist tag>" if ($$id3{'ARTIST'} eq "");
335 # Fall back on the directory name (not full path dirname),
336 # if no album tag
337 if ($dirisalbum) {
338 if($dir2albumname{$dir} eq "") {
339 $dir2albumname{$dir} = $$id3{'ALBUM'};
341 elsif($dir2albumname{$dir} ne $$id3{'ALBUM'}) {
342 $dir2albumname{$dir} = (split m[/], $dir)[-1];
345 # if no directory
346 if ($dirisalbumname) {
347 $$id3{'ALBUM'} = (split m[/], $dir)[-1] if ($$id3{'ALBUM'} eq "");
349 $$id3{'ALBUM'} = "<no album tag>" if ($$id3{'ALBUM'} eq "");
350 # fall back on basename of the file if no title tag.
351 my $base;
352 ($base = $f) =~ s/\.\w+$//;
353 $$id3{'TITLE'} = $base if ($$id3{'TITLE'} eq "");
355 # Append dirname, to handle multi-artist albums
356 $$id3{'DIR'} = $dir;
357 my $albumid;
358 if ($dirisalbum) {
359 $albumid=$$id3{'DIR'};
361 else {
362 $albumid= $id3->{'ALBUM'}."___".$$id3{'DIR'};
364 #printf "album id: %s\n", $albumid;
366 # if($id3->{'ALBUM'}."___".$id3->{'DIR'} ne "<no album tag>___<no artist tag>") {
367 my $num = ++$albums{$albumid};
368 if($num > $maxsongperalbum) {
369 $maxsongperalbum = $num;
370 $longestalbum = $albumid;
372 $album2songs{$albumid}{$$id3{TITLE}} = $id3;
373 if($dirisalbum) {
374 $artist2albums{$$id3{ARTIST}}{$$id3{DIR}} = $id3;
376 else {
377 $artist2albums{$$id3{ARTIST}}{$$id3{ALBUM}} = $id3;
382 if($dirisalbum and $dir2albumname{$dir} eq "") {
383 $dir2albumname{$dir} = (split m[/], $dir)[-1];
384 printf "%s\n", $dir2albumname{$dir};
387 # extractdirs filters out only subdirectories from all given entries
388 my @d = extractdirs($dir, @a);
390 for $d (sort @d) {
391 $dir =~ s|/$||;
392 dodir("$dir/$d");
397 dodir($dir);
398 print "\n";
400 print "File name table\n" if ($verbose);
401 for(sort keys %entries) {
402 printf(" %s\n", $_) if ($verbose);
403 my $l = length($_);
404 if($l > $maxfilelen) {
405 $maxfilelen = $l;
406 $longestfilename = $_;
409 $maxfilelen++; # include zero termination byte
410 while($maxfilelen&3) {
411 $maxfilelen++;
414 my $maxsonglen = 0;
415 my $sc;
416 print "\nSong title table\n" if ($verbose);
418 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
419 printf(" %s\n", $entries{$_}->{'TITLE'} ) if ($verbose);
420 my $l = length($entries{$_}->{'TITLE'});
421 if($l > $maxsonglen) {
422 $maxsonglen = $l;
423 $longestsong = $entries{$_}->{'TITLE'};
426 $maxsonglen++; # include zero termination byte
427 while($maxsonglen&3) {
428 $maxsonglen++;
431 my $maxartistlen = 0;
432 print "\nArtist table\n" if ($verbose);
433 my $i=0;
434 my %artistcount;
435 for(sort {uc($a) cmp uc($b)} keys %artists) {
436 printf(" %s: %d\n", $_, $i) if ($verbose);
437 $artistcount{$_}=$i++;
438 my $l = length($_);
439 if($l > $maxartistlen) {
440 $maxartistlen = $l;
441 $longestartist = $_;
444 $l = scalar keys %{$artist2albums{$_}};
445 if ($l > $maxalbumsperartist) {
446 $maxalbumsperartist = $l;
447 $longestartistalbum = $_;
450 $maxartistlen++; # include zero termination byte
451 while($maxartistlen&3) {
452 $maxartistlen++;
455 print "\nGenre table\n" if ($verbose);
456 for(sort keys %genres) {
457 my $l = length($_);
458 if($l > $maxgenrelen) {
459 $maxgenrelen = $l;
460 $longestgenrename = $_;
464 $maxgenrelen++; #include zero termination byte
465 while($maxgenrelen&3) {
466 $maxgenrelen++;
470 if ($verbose) {
471 print "\nYear table\n";
472 for(sort keys %years) {
473 printf(" %s\n", $_);
477 print "\nAlbum table\n" if ($verbose);
478 my $maxalbumlen = 0;
479 my %albumcount;
480 $i=0;
481 my @albumssort;
482 if($dirisalbum) {
483 @albumssort = sort {uc($dir2albumname{$a}) cmp uc($dir2albumname{$b})} keys %albums;
485 else {
486 @albumssort = sort {uc($a) cmp uc($b)} keys %albums;
488 for(@albumssort) {
489 my @moo=split(/___/, $_);
490 printf(" %s\n", $moo[0]) if ($verbose);
491 $albumcount{$_} = $i++;
492 my $l;
493 if($dirisalbum) {
494 $l = length($dir2albumname{$_});
496 else {
497 $l = length($moo[0]);
499 if($l > $maxalbumlen) {
500 $maxalbumlen = $l;
501 if($dirisalbum) {
502 $longestalbumname = $dir2albumname{$_};
504 else {
505 $longestalbumname = $moo[0];
509 $maxalbumlen++; # include zero termination byte
510 while($maxalbumlen&3) {
511 $maxalbumlen++;
516 sub dumpshort {
517 my ($num)=@_;
519 # print "int: $num\n";
521 print DB pack "n", $num;
524 sub dumpint {
525 my ($num)=@_;
527 # print "int: $num\n";
529 print DB pack "N", $num;
532 if (!scalar keys %entries) {
533 print "No songs found. Did you specify the right --path ?\n";
534 print "Use the --help parameter to see all options.\n";
535 exit;
538 if ($db) {
539 my $songentrysize = $maxsonglen + 12 + $maxgenrelen+ 12;
540 my $albumentrysize = $maxalbumlen + 4 + $maxsongperalbum*4;
541 my $artistentrysize = $maxartistlen + $maxalbumsperartist*4;
542 my $fileentrysize = $maxfilelen + 12;
544 printf "Number of artists : %d\n", scalar keys %artists;
545 printf "Number of albums : %d\n", scalar keys %albums;
546 printf "Number of songs / files : %d\n", scalar keys %entries;
547 print "Max artist length : $maxartistlen ($longestartist)\n";
548 print "Max album length : $maxalbumlen ($longestalbumname)\n";
549 print "Max song length : $maxsonglen ($longestsong)\n";
550 print "Max songs per album : $maxsongperalbum ($longestalbum)\n";
551 print "Max albums per artist: $maxalbumsperartist ($longestartistalbum)\n";
552 print "Max genre length : $maxgenrelen ($longestgenrename)\n";
553 print "Max file length : $maxfilelen ($longestfilename)\n";
554 print "Database version: $dbver\n" if ($verbose);
555 print "Song Entry Size : $songentrysize ($maxsonglen + 12 + $maxgenrelen + 4)\n" if ($verbose);
556 print "Album Entry Size: $albumentrysize ($maxalbumlen + 4 + $maxsongperalbum * 4)\n" if ($verbose);
557 print "Artist Entry Size: $artistentrysize ($maxartistlen + $maxalbumsperartist * 4)\n" if ($verbose);
558 print "File Entry Size: $fileentrysize ($maxfilelen + 12)\n" if ($verbose);
561 open(DB, ">$db") || die "couldn't make $db";
562 binmode(DB);
563 printf DB "RDB%c", $dbver;
565 $pathindex = 68; # paths always start at index 68
567 $artistindex = $pathindex;
569 # set total size of song title table
570 $sc = scalar(keys %entries) * $songentrysize;
571 my $ac = scalar(keys %albums) * $albumentrysize;
572 my $arc = scalar(keys %artists) * $artistentrysize;
573 $albumindex = $artistindex + $arc; # arc is size of all artists
574 $songindex = $albumindex + $ac; # ac is size of all albums
575 my $fileindex = $songindex + $sc; # sc is size of all songs
577 dumpint($artistindex); # file position index of artist table
578 dumpint($albumindex); # file position index of album table
579 dumpint($songindex); # file position index of song table
580 dumpint($fileindex); # file position index of file table
581 dumpint(scalar(keys %artists)); # number of artists
582 dumpint(scalar(keys %albums)); # number of albums
583 dumpint(scalar(keys %entries)); # number of songs
584 dumpint(scalar(keys %entries)); # number of files
585 dumpint($maxartistlen); # length of artist name field
586 dumpint($maxalbumlen); # length of album name field
587 dumpint($maxsonglen); # length of song name field
588 dumpint($maxgenrelen); #length of genre field
589 dumpint($maxfilelen); # length of file field
590 dumpint($maxsongperalbum); # number of entries in songs-per-album array
591 dumpint($maxalbumsperartist); # number of entries in albums-per-artist array
592 dumpint(-1); # rundb dirty
594 #### TABLE of artists ###
595 # name of artist1
596 # pointers to albums of artist1
598 for (sort {uc($a) cmp uc($b)} keys %artists) {
599 my $artist = $_;
600 my $str = $_."\x00" x ($maxartistlen - length($_));
601 print DB $str;
603 for (sort keys %{$artist2albums{$artist}}) {
604 my $id3 = $artist2albums{$artist}{$_};
605 my $a;
606 if($dirisalbum) {
607 $a = $albumcount{"$$id3{'DIR'}"} * $albumentrysize;
609 else {
610 $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'DIR'}"} * $albumentrysize;
612 dumpint($a + $albumindex);
615 for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) {
616 print DB "\x00\x00\x00\x00";
621 ### Build song offset info.
622 my $offset = $songindex;
623 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
624 my $id3 = $entries{$_};
625 $$id3{'songoffset'} = $offset;
626 $offset += $songentrysize;
630 #### TABLE of albums ###
631 # name of album1
632 # pointers to artists of album1
633 # pointers to songs on album1
635 for(@albumssort) {
636 my $albumid = $_;
637 my @moo=split(/___/, $_);
638 my $t;
639 my $str;
640 if($dirisalbum) {
641 $t = $dir2albumname{$albumid};
643 else {
644 $t = $moo[0];
646 $str = $t."\x00" x ($maxalbumlen - length($t));
647 print DB $str;
649 my @songlist = keys %{$album2songs{$albumid}};
650 my $id3 = $album2songs{$albumid}{$songlist[0]};
652 #printf "(d) albumid: %s artist: %s\n",$albumid, $id3->{'ARTIST'};
654 my $aoffset = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
655 dumpint($aoffset + $artistindex); # pointer to artist of this album
657 if (defined $id3->{'TRACKNUM'}) {
658 @songlist = sort {
659 $album2songs{$albumid}{$a}->{'TRACKNUM'} <=>
660 $album2songs{$albumid}{$b}->{'TRACKNUM'}
661 } @songlist;
663 else {
664 @songlist = sort @songlist;
667 for (@songlist) {
668 my $id3 = $album2songs{$albumid}{$_};
669 dumpint($$id3{'songoffset'});
672 for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) {
673 print DB "\x00\x00\x00\x00";
677 #### Build filename offset info
678 my $l=$fileindex;
679 my %filenamepos;
680 for $f (sort {uc($a) cmp uc($b)} keys %entries) {
681 $filenamepos{$f}= $l;
682 $l += $fileentrysize;
685 #### TABLE of songs ###
686 # title of song1
687 # pointer to artist of song1
688 # pointer to album of song1
689 # pointer to filename of song1
691 for(sort {uc($entries{$a}->{'TITLE'}) cmp uc($entries{$b}->{'TITLE'})} keys %entries) {
692 my $f = $_;
693 my $id3 = $entries{$f};
694 my $t = $id3->{'TITLE'};
695 my $g = $id3->{'GENRE'};
696 my $str = $t."\x00" x ($maxsonglen- length($t));
698 print DB $str; # title
699 $str = $g."\x00" x ($maxgenrelen - length($g));
701 my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
702 dumpint($a + $artistindex); # pointer to artist of this song
704 if($dirisalbum) {
705 $a = $albumcount{"$$id3{DIR}"} * $albumentrysize;
707 else {
708 $a = $albumcount{"$$id3{ALBUM}___$$id3{DIR}"} * $albumentrysize;
710 dumpint($a + $albumindex); # pointer to album of this song
712 # pointer to filename of this song
713 dumpint($filenamepos{$f});
714 print DB $str; #genre
715 dumpshort(-1);
716 dumpshort($id3->{'YEAR'});
717 dumpint(-1);
718 dumpshort($id3->{'TRACKNUM'});
719 dumpshort(-1);
722 #### TABLE of file names ###
723 # path1
725 for $f (sort {uc($a) cmp uc($b)} %entries) {
726 my $str = $f."\x00" x ($maxfilelen- length($f));
727 my $id3 = $entries{$f};
728 print DB $str;
729 #print STDERR "CRC: ".."\n";
730 dumpint($id3->{'FILECRC'}); # CRC32 of the song data
731 dumpint($id3->{'songoffset'}); # offset to song data
732 dumpint(-1); # offset to rundb data. always set to -1. this is updated by rockbox code on the player.
735 close(DB);
739 ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor
740 ### Modified by Björn Stenberg to remove use of external libraries
743 our(
744 @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION,
745 @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder,
746 @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields,
747 @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD,
748 @mp3_info_fields
751 @ISA = 'Exporter';
752 @EXPORT = qw(
753 set_mp3tag get_mp3tag get_mp3info remove_mp3tag
754 use_winamp_genres
756 @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8);
757 %EXPORT_TAGS = (
758 genres => [qw(@mp3_genres %mp3_genres)],
759 utf8 => [qw(use_mp3_utf8)],
760 all => [@EXPORT, @EXPORT_OK]
763 # $Id$
764 ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
765 $VERSION = '1.02';
767 =pod
769 =head1 NAME
771 MP3::Info - Manipulate / fetch info from MP3 audio files
773 =head1 SYNOPSIS
775 #!perl -w
776 use MP3::Info;
777 my $file = 'Pearls_Before_Swine.mp3';
778 set_mp3tag($file, 'Pearls Before Swine', q"77's",
779 'Sticks and Stones', '1990',
780 q"(c) 1990 77's LTD.", 'rock & roll');
782 my $tag = get_mp3tag($file) or die "No TAG info";
783 $tag->{GENRE} = 'rock';
784 set_mp3tag($file, $tag);
786 my $info = get_mp3info($file);
787 printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
789 =cut
792 my $c = -1;
793 # set all lower-case and regular-cased versions of genres as keys
794 # with index as value of each key
795 %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres;
797 # do it again for winamp genres
798 $c = -1;
799 %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
802 =pod
804 my $mp3 = new MP3::Info $file;
805 $mp3->title('Perls Before Swine');
806 printf "$file length is %s, title is %s\n",
807 $mp3->time, $mp3->title;
810 =head1 DESCRIPTION
812 =over 4
814 =item $mp3 = MP3::Info-E<gt>new(FILE)
816 OOP interface to the rest of the module. The same keys
817 available via get_mp3info and get_mp3tag are available
818 via the returned object (using upper case or lower case;
819 but note that all-caps "VERSION" will return the module
820 version, not the MP3 version).
822 Passing a value to one of the methods will set the value
823 for that tag in the MP3 file, if applicable.
825 =cut
827 sub new {
828 my($pack, $file) = @_;
830 my $info = get_mp3info($file) or return undef;
831 my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names };
832 my %self = (
833 FILE => $file,
834 TRY_HARDER => 0
837 @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
838 @{$info}{@mp3_info_fields},
839 @{$tags}{@v1_tag_names},
840 $file
843 return bless \%self, $pack;
846 sub can {
847 my $self = shift;
848 return $self->SUPER::can(@_) unless ref $self;
849 my $name = uc shift;
850 return sub { $self->$name(@_) } if exists $self->{$name};
851 return undef;
854 sub AUTOLOAD {
855 my($self) = @_;
856 (my $name = uc $AUTOLOAD) =~ s/^.*://;
858 if (exists $self->{$name}) {
859 my $sub = exists $v1_tag_fields{$name}
860 ? sub {
861 if (defined $_[1]) {
862 $_[0]->{$name} = $_[1];
863 set_mp3tag($_[0]->{FILE}, $_[0]);
865 return $_[0]->{$name};
867 : sub {
868 return $_[0]->{$name}
871 *{$AUTOLOAD} = $sub;
872 goto &$AUTOLOAD;
874 } else {
875 warn(sprintf "No method '$name' available in package %s.",
876 __PACKAGE__);
880 sub DESTROY {
885 =item use_mp3_utf8([STATUS])
887 Tells MP3::Info to (or not) return TAG info in UTF-8.
888 TRUE is 1, FALSE is 0. Default is FALSE.
890 Will only be able to it on if Unicode::String is available. ID3v2
891 tags will be converted to UTF-8 according to the encoding specified
892 in each tag; ID3v1 tags will be assumed Latin-1 and converted
893 to UTF-8.
895 Function returns status (TRUE/FALSE). If no argument is supplied,
896 or an unaccepted argument is supplied, function merely returns status.
898 This function is not exported by default, but may be exported
899 with the C<:utf8> or C<:all> export tag.
901 =cut
903 my $unicode_module = eval { require Unicode::String };
904 my $UNICODE = 0;
906 sub use_mp3_utf8 {
907 my($val) = @_;
908 if ($val == 1) {
909 $UNICODE = 1 if $unicode_module;
910 } elsif ($val == 0) {
911 $UNICODE = 0;
913 return $UNICODE;
916 =pod
918 =item use_winamp_genres()
920 Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
921 (adds 68 additional genres to the default list of 80).
922 This is a separate function because these are non-standard
923 genres, but they are included because they are widely used.
925 You can import the data structures with one of:
927 use MP3::Info qw(:genres);
928 use MP3::Info qw(:DEFAULT :genres);
929 use MP3::Info qw(:all);
931 =cut
933 sub use_winamp_genres {
934 %mp3_genres = %winamp_genres;
935 @mp3_genres = @winamp_genres;
936 return 1;
939 =pod
941 =pod
943 =item get_mp3tag (FILE [, VERSION, RAW_V2])
945 Returns hash reference containing tag information in MP3 file. The keys
946 returned are the same as those supplied for C<set_mp3tag>, except in the
947 case of RAW_V2 being set.
949 If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
950 If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
951 If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
952 then, if present, the ID3v2 tag information will override any existing ID3v1
953 tag info.
955 If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
956 of text encoding. The key name is the same as the frame ID (ID to name mappings
957 are in the global %v2_tag_names).
959 If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
960 necessary, etc. It also takes multiple values for a given key (such as comments)
961 and puts them in an arrayref.
963 If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
964 not be read.
966 Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
967 (unless RAW_V2 is C<1>).
969 Also returns a TAGVERSION key, containing the ID3 version used for the returned
970 data (if TAGVERSION argument is C<0>, may contain two versions).
972 =cut
974 sub get_mp3tag {
975 my($file, $ver, $raw_v2) = @_;
976 my($tag, $v1, $v2, $v2h, %info, @array, $fh);
977 $raw_v2 ||= 0;
978 $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0;
980 if (not (defined $file && $file ne '')) {
981 $@ = "No file specified";
982 return undef;
985 if (not -s $file) {
986 $@ = "File is empty";
987 return undef;
990 if (ref $file) { # filehandle passed
991 $fh = $file;
992 } else {
993 $fh = gensym;
994 if (not open $fh, "< $file\0") {
995 $@ = "Can't open $file: $!";
996 return undef;
1000 binmode $fh;
1002 if ($ver < 2) {
1003 seek $fh, -128, 2;
1004 while(defined(my $line = <$fh>)) { $tag .= $line }
1006 if ($tag =~ /^TAG/) {
1007 $v1 = 1;
1008 if (substr($tag, -3, 2) =~ /\000[^\000]/) {
1009 (undef, @info{@v1_tag_names}) =
1010 (unpack('a3a30a30a30a4a28', $tag),
1011 ord(substr($tag, -2, 1)),
1012 $mp3_genres[ord(substr $tag, -1)]);
1013 $info{TAGVERSION} = 'ID3v1.1';
1014 } else {
1015 (undef, @info{@v1_tag_names[0..4, 6]}) =
1016 (unpack('a3a30a30a30a4a30', $tag),
1017 $mp3_genres[ord(substr $tag, -1)]);
1018 $info{TAGVERSION} = 'ID3v1';
1020 if ($UNICODE) {
1021 for my $key (keys %info) {
1022 next unless $info{$key};
1023 my $u = Unicode::String::latin1($info{$key});
1024 $info{$key} = $u->utf8;
1027 } elsif ($ver == 1) {
1028 _close($file, $fh);
1029 $@ = "No ID3v1 tag found";
1030 return undef;
1034 ($v2, $v2h) = _get_v2tag($fh);
1036 unless ($v1 || $v2) {
1037 _close($file, $fh);
1038 $@ = "No ID3 tag found";
1039 return undef;
1042 if (($ver == 0 || $ver == 2) && $v2) {
1043 if ($raw_v2 == 1 && $ver == 2) {
1044 %info = %$v2;
1045 $info{TAGVERSION} = $v2h->{version};
1046 } else {
1047 my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names;
1048 for my $id (keys %$hash) {
1049 if (exists $v2->{$id}) {
1050 if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) {
1051 $info{$hash->{$id}} = $mp3_genres[$1];
1052 } else {
1053 my $data1 = $v2->{$id};
1055 # this is tricky ... if this is an arrayref, we want
1056 # to only return one, so we pick the first one. but
1057 # if it is a comment, we pick the first one where the
1058 # first charcter after the language is NULL and not an
1059 # additional sub-comment, because that is most likely
1060 # to be the user-supplied comment
1062 if (ref $data1 && !$raw_v2) {
1063 if ($id =~ /^COMM?$/) {
1064 my($newdata) = grep /^(....\000)/, @{$data1};
1065 $data1 = $newdata || $data1->[0];
1066 } else {
1067 $data1 = $data1->[0];
1071 $data1 = [ $data1 ] if ! ref $data1;
1073 for my $data (@$data1) {
1074 $data =~ s/^(.)//; # strip first char (text encoding)
1075 my $encoding = $1;
1076 my $desc;
1077 if ($id =~ /^COM[M ]?$/) {
1078 $data =~ s/^(?:...)//; # strip language
1079 $data =~ s/^(.*?)\000+//; # strip up to first NULL(s),
1080 # for sub-comment
1081 $desc = $1;
1084 if ($UNICODE) {
1085 if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE
1086 my $u = Unicode::String::utf16($data);
1087 $data = $u->utf8;
1088 $data =~ s/^\xEF\xBB\xBF//; # strip BOM
1089 } elsif ($encoding eq "\000") {
1090 my $u = Unicode::String::latin1($data);
1091 $data = $u->utf8;
1095 if ($raw_v2 == 2 && $desc) {
1096 $data = { $desc => $data };
1099 if ($raw_v2 == 2 && exists $info{$hash->{$id}}) {
1100 if (ref $info{$hash->{$id}} eq 'ARRAY') {
1101 push @{$info{$hash->{$id}}}, $data;
1102 } else {
1103 $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ];
1105 } else {
1106 $info{$hash->{$id}} = $data;
1112 if ($ver == 0 && $info{TAGVERSION}) {
1113 $info{TAGVERSION} .= ' / ' . $v2h->{version};
1114 } else {
1115 $info{TAGVERSION} = $v2h->{version};
1120 unless ($raw_v2 && $ver == 2) {
1121 foreach my $key (keys %info) {
1122 if (defined $info{$key}) {
1123 $info{$key} =~ s/\000+.*//g;
1124 $info{$key} =~ s/\s+$//;
1128 for (@v1_tag_names) {
1129 $info{$_} = '' unless defined $info{$_};
1133 if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) {
1134 $info{GENRE} = '';
1137 _close($file, $fh);
1139 return keys %info ? {%info} : undef;
1142 sub _get_v2tag {
1143 my($fh) = @_;
1144 my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num);
1145 $h = {};
1147 $v2 = _get_v2head($fh) or return;
1148 if ($v2->{major_version} < 2) {
1149 warn "This is $v2->{version}; " .
1150 "ID3v2 versions older than ID3v2.2.0 not supported\n"
1151 if $^W;
1152 return;
1155 if ($v2->{major_version} == 2) {
1156 $hlen = 6;
1157 $num = 3;
1158 } else {
1159 $hlen = 10;
1160 $num = 4;
1163 $myseek = sub {
1164 seek $fh, $off, 0;
1165 read $fh, my($bytes), $hlen;
1166 return unless $bytes =~ /^([A-Z0-9]{$num})/
1167 || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes
1168 my($id, $size) = ($1, $hlen);
1169 my @bytes = reverse unpack "C$num", substr($bytes, $num, $num);
1170 for my $i (0 .. ($num - 1)) {
1171 $size += $bytes[$i] * 256 ** $i;
1173 return($id, $size);
1176 $off = $v2->{ext_header_size} + 10;
1178 while ($off < $v2->{tag_size}) {
1179 my($id, $size) = &$myseek or last;
1180 seek $fh, $off + $hlen, 0;
1181 read $fh, my($bytes), $size - $hlen;
1182 if (exists $h->{$id}) {
1183 if (ref $h->{$id} eq 'ARRAY') {
1184 push @{$h->{$id}}, $bytes;
1185 } else {
1186 $h->{$id} = [$h->{$id}, $bytes];
1188 } else {
1189 $h->{$id} = $bytes;
1191 $off += $size;
1194 return($h, $v2);
1198 =pod
1200 =item get_mp3info (FILE)
1202 Returns hash reference containing file information for MP3 file.
1203 This data cannot be changed. Returned data:
1205 VERSION MPEG audio version (1, 2, 2.5)
1206 LAYER MPEG layer description (1, 2, 3)
1207 STEREO boolean for audio is in stereo
1209 VBR boolean for variable bitrate
1210 BITRATE bitrate in kbps (average for VBR files)
1211 FREQUENCY frequency in kHz
1212 SIZE bytes in audio stream
1214 SECS total seconds
1215 MM minutes
1216 SS leftover seconds
1217 MS leftover milliseconds
1218 TIME time in MM:SS
1220 COPYRIGHT boolean for audio is copyrighted
1221 PADDING boolean for MP3 frames are padded
1222 MODE channel mode (0 = stereo, 1 = joint stereo,
1223 2 = dual channel, 3 = single channel)
1224 FRAMES approximate number of frames
1225 FRAME_LENGTH approximate length of a frame
1226 VBR_SCALE VBR scale from VBR header
1228 On error, returns nothing and sets C<$@>.
1230 =cut
1232 sub get_mp3info {
1233 my($file) = @_;
1234 my($off, $myseek, $byte, $eof, $h, $tot, $fh);
1236 if (not (defined $file && $file ne '')) {
1237 $@ = "No file specified";
1238 return undef;
1241 if (not -s $file) {
1242 $@ = "File is empty";
1243 return undef;
1246 if (ref $file) { # filehandle passed
1247 $fh = $file;
1248 } else {
1249 $fh = gensym;
1250 if (not open $fh, "< $file\0") {
1251 $@ = "Can't open $file: $!";
1252 return undef;
1256 $off = 0;
1257 $tot = 4096;
1259 $myseek = sub {
1260 seek $fh, $off, 0;
1261 read $fh, $byte, 4;
1264 binmode $fh;
1265 &$myseek;
1267 if ($off == 0) {
1268 if (my $id3v2 = _get_v2head($fh)) {
1269 $tot += $off += $id3v2->{tag_size};
1270 &$myseek;
1274 $h = _get_head($byte);
1275 until (_is_mp3($h)) {
1276 $off++;
1277 &$myseek;
1278 $h = _get_head($byte);
1279 if ($off > $tot && !$try_harder) {
1280 _close($file, $fh);
1281 $@ = "Couldn't find MP3 header (perhaps set " .
1282 '$MP3::Info::try_harder and retry)';
1283 return undef;
1287 my $vbr = _get_vbr($fh, $h, \$off);
1289 $h->{headersize}=$off; # data size prepending the actual mp3 data
1291 seek $fh, 0, 2;
1292 $eof = tell $fh;
1293 seek $fh, -128, 2;
1294 $off += 128 if <$fh> =~ /^TAG/ ? 1 : 0;
1296 _close($file, $fh);
1298 $h->{size} = $eof - $off;
1300 return _get_info($h, $vbr);
1303 sub _get_info {
1304 my($h, $vbr) = @_;
1305 my $i;
1307 $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 :
1308 $h->{IDR} == 0 ? 2.5 : 0;
1309 $i->{LAYER} = 4 - $h->{layer};
1310 $i->{VBR} = defined $vbr ? 1 : 0;
1312 $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0;
1313 $i->{PADDING} = $h->{padding_bit} ? 1 : 0;
1314 $i->{STEREO} = $h->{mode} == 3 ? 0 : 1;
1315 $i->{MODE} = $h->{mode};
1317 $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size};
1319 my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000);
1320 $i->{FRAMES} = int($vbr && $vbr->{frames}
1321 ? $vbr->{frames}
1322 : $i->{SIZE} / $h->{bitrate} / $mfs
1325 if ($vbr) {
1326 $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale};
1327 $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs;
1328 if (not $h->{bitrate}) {
1329 $@ = "Couldn't determine VBR bitrate";
1330 return undef;
1334 $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10;
1335 $i->{SECS} = $h->{'length'} / 100;
1336 $i->{MM} = int $i->{SECS} / 60;
1337 $i->{SS} = int $i->{SECS} % 60;
1338 $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000);
1339 # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS});
1340 # int($i->{MS} / 100 * 75); # is this right?
1341 $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'};
1343 $i->{BITRATE} = int $h->{bitrate};
1344 # should we just return if ! FRAMES?
1345 $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES};
1346 $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}];
1348 $i->{headersize} = $h->{headersize};
1350 return $i;
1353 sub _get_head {
1354 my($byte) = @_;
1355 my($bytes, $h);
1357 $bytes = _unpack_head($byte);
1358 @$h{qw(IDR ID layer protection_bit
1359 bitrate_index sampling_freq padding_bit private_bit
1360 mode mode_extension copyright original
1361 emphasis version_index bytes)} = (
1362 ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1,
1363 ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1,
1364 ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1,
1365 $bytes&3, ($bytes>>19)&3, $bytes
1368 $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}];
1369 $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}];
1371 return $h;
1374 sub _is_mp3 {
1375 my $h = $_[0] or return undef;
1376 return ! ( # all below must be false
1377 $h->{bitrate_index} == 0
1379 $h->{version_index} == 1
1381 ($h->{bytes} & 0xFFE00000) != 0xFFE00000
1383 !$h->{fs}
1385 !$h->{bitrate}
1387 $h->{bitrate_index} == 15
1389 !$h->{layer}
1391 $h->{sampling_freq} == 3
1393 $h->{emphasis} == 2
1395 !$h->{bitrate_index}
1397 ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000
1399 ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1)
1401 ($h->{mode_extension} != 0 && $h->{mode} != 1)
1405 sub _get_vbr {
1406 my($fh, $h, $roff) = @_;
1407 my($off, $bytes, @bytes, $myseek, %vbr);
1409 $off = $$roff;
1410 @_ = (); # closure confused if we don't do this
1412 $myseek = sub {
1413 my $n = $_[0] || 4;
1414 seek $fh, $off, 0;
1415 read $fh, $bytes, $n;
1416 $off += $n;
1419 $off += 4;
1421 if ($h->{ID}) { # MPEG1
1422 $off += $h->{mode} == 3 ? 17 : 32;
1423 } else { # MPEG2
1424 $off += $h->{mode} == 3 ? 9 : 17;
1427 &$myseek;
1428 return unless $bytes eq 'Xing';
1430 &$myseek;
1431 $vbr{flags} = _unpack_head($bytes);
1433 if ($vbr{flags} & 1) {
1434 &$myseek;
1435 $vbr{frames} = _unpack_head($bytes);
1438 if ($vbr{flags} & 2) {
1439 &$myseek;
1440 $vbr{bytes} = _unpack_head($bytes);
1443 if ($vbr{flags} & 4) {
1444 $myseek->(100);
1445 # Not used right now ...
1446 # $vbr{toc} = _unpack_head($bytes);
1449 if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst)
1450 &$myseek;
1451 $vbr{scale} = _unpack_head($bytes);
1452 } else {
1453 $vbr{scale} = -1;
1456 $$roff = $off;
1457 return \%vbr;
1460 sub _get_v2head {
1461 my $fh = $_[0] or return;
1462 my($h, $bytes, @bytes);
1464 # check first three bytes for 'ID3'
1465 seek $fh, 0, 0;
1466 read $fh, $bytes, 3;
1467 return unless $bytes eq 'ID3';
1469 # get version
1470 read $fh, $bytes, 2;
1471 $h->{version} = sprintf "ID3v2.%d.%d",
1472 @$h{qw[major_version minor_version]} =
1473 unpack 'c2', $bytes;
1475 # get flags
1476 read $fh, $bytes, 1;
1477 if ($h->{major_version} == 2) {
1478 @$h{qw[unsync compression]} =
1479 (unpack 'b8', $bytes)[7, 6];
1480 $h->{ext_header} = 0;
1481 $h->{experimental} = 0;
1482 } else {
1483 @$h{qw[unsync ext_header experimental]} =
1484 (unpack 'b8', $bytes)[7, 6, 5];
1487 # get ID3v2 tag length from bytes 7-10
1488 $h->{tag_size} = 10; # include ID3v2 header size
1489 read $fh, $bytes, 4;
1490 @bytes = reverse unpack 'C4', $bytes;
1491 foreach my $i (0 .. 3) {
1492 # whoaaaaaa nellllllyyyyyy!
1493 $h->{tag_size} += $bytes[$i] * 128 ** $i;
1496 # get extended header size
1497 $h->{ext_header_size} = 0;
1498 if ($h->{ext_header}) {
1499 $h->{ext_header_size} += 10;
1500 read $fh, $bytes, 4;
1501 @bytes = reverse unpack 'C4', $bytes;
1502 for my $i (0..3) {
1503 $h->{ext_header_size} += $bytes[$i] * 256 ** $i;
1507 return $h;
1510 sub _unpack_head {
1511 unpack('l', pack('L', unpack('N', $_[0])));
1514 sub _close {
1515 my($file, $fh) = @_;
1516 unless (ref $file) { # filehandle not passed
1517 close $fh or warn "Problem closing '$file': $!";
1521 BEGIN {
1522 @mp3_genres = (
1523 'Blues',
1524 'Classic Rock',
1525 'Country',
1526 'Dance',
1527 'Disco',
1528 'Funk',
1529 'Grunge',
1530 'Hip-Hop',
1531 'Jazz',
1532 'Metal',
1533 'New Age',
1534 'Oldies',
1535 'Other',
1536 'Pop',
1537 'R&B',
1538 'Rap',
1539 'Reggae',
1540 'Rock',
1541 'Techno',
1542 'Industrial',
1543 'Alternative',
1544 'Ska',
1545 'Death Metal',
1546 'Pranks',
1547 'Soundtrack',
1548 'Euro-Techno',
1549 'Ambient',
1550 'Trip-Hop',
1551 'Vocal',
1552 'Jazz+Funk',
1553 'Fusion',
1554 'Trance',
1555 'Classical',
1556 'Instrumental',
1557 'Acid',
1558 'House',
1559 'Game',
1560 'Sound Clip',
1561 'Gospel',
1562 'Noise',
1563 'AlternRock',
1564 'Bass',
1565 'Soul',
1566 'Punk',
1567 'Space',
1568 'Meditative',
1569 'Instrumental Pop',
1570 'Instrumental Rock',
1571 'Ethnic',
1572 'Gothic',
1573 'Darkwave',
1574 'Techno-Industrial',
1575 'Electronic',
1576 'Pop-Folk',
1577 'Eurodance',
1578 'Dream',
1579 'Southern Rock',
1580 'Comedy',
1581 'Cult',
1582 'Gangsta',
1583 'Top 40',
1584 'Christian Rap',
1585 'Pop/Funk',
1586 'Jungle',
1587 'Native American',
1588 'Cabaret',
1589 'New Wave',
1590 'Psychadelic',
1591 'Rave',
1592 'Showtunes',
1593 'Trailer',
1594 'Lo-Fi',
1595 'Tribal',
1596 'Acid Punk',
1597 'Acid Jazz',
1598 'Polka',
1599 'Retro',
1600 'Musical',
1601 'Rock & Roll',
1602 'Hard Rock',
1605 @winamp_genres = (
1606 @mp3_genres,
1607 'Folk',
1608 'Folk-Rock',
1609 'National Folk',
1610 'Swing',
1611 'Fast Fusion',
1612 'Bebob',
1613 'Latin',
1614 'Revival',
1615 'Celtic',
1616 'Bluegrass',
1617 'Avantgarde',
1618 'Gothic Rock',
1619 'Progressive Rock',
1620 'Psychedelic Rock',
1621 'Symphonic Rock',
1622 'Slow Rock',
1623 'Big Band',
1624 'Chorus',
1625 'Easy Listening',
1626 'Acoustic',
1627 'Humour',
1628 'Speech',
1629 'Chanson',
1630 'Opera',
1631 'Chamber Music',
1632 'Sonata',
1633 'Symphony',
1634 'Booty Bass',
1635 'Primus',
1636 'Porn Groove',
1637 'Satire',
1638 'Slow Jam',
1639 'Club',
1640 'Tango',
1641 'Samba',
1642 'Folklore',
1643 'Ballad',
1644 'Power Ballad',
1645 'Rhythmic Soul',
1646 'Freestyle',
1647 'Duet',
1648 'Punk Rock',
1649 'Drum Solo',
1650 'Acapella',
1651 'Euro-House',
1652 'Dance Hall',
1653 'Goa',
1654 'Drum & Bass',
1655 'Club-House',
1656 'Hardcore',
1657 'Terror',
1658 'Indie',
1659 'BritPop',
1660 'Negerpunk',
1661 'Polsk Punk',
1662 'Beat',
1663 'Christian Gangsta Rap',
1664 'Heavy Metal',
1665 'Black Metal',
1666 'Crossover',
1667 'Contemporary Christian',
1668 'Christian Rock',
1669 'Merengue',
1670 'Salsa',
1671 'Thrash Metal',
1672 'Anime',
1673 'JPop',
1674 'Synthpop',
1677 @t_bitrate = ([
1678 [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
1679 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
1680 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
1682 [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
1683 [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
1684 [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
1687 @t_sampling_freq = (
1688 [11025, 12000, 8000],
1689 [undef, undef, undef], # reserved
1690 [22050, 24000, 16000],
1691 [44100, 48000, 32000]
1694 @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 }
1695 map { @$_ } @t_sampling_freq;
1697 @mp3_info_fields = qw(
1698 VERSION
1699 LAYER
1700 STEREO
1702 BITRATE
1703 FREQUENCY
1704 SIZE
1705 SECS
1709 TIME
1710 COPYRIGHT
1711 PADDING
1712 MODE
1713 FRAMES
1714 FRAME_LENGTH
1715 VBR_SCALE
1718 %v1_tag_fields =
1719 (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4);
1721 @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
1723 %v2_to_v1_names = (
1724 # v2.2 tags
1725 'TT2' => 'TITLE',
1726 'TP1' => 'ARTIST',
1727 'TAL' => 'ALBUM',
1728 'TYE' => 'YEAR',
1729 'COM' => 'COMMENT',
1730 'TRK' => 'TRACKNUM',
1731 'TCO' => 'GENRE', # not clean mapping, but ...
1732 # v2.3 tags
1733 'TIT2' => 'TITLE',
1734 'TPE1' => 'ARTIST',
1735 'TALB' => 'ALBUM',
1736 'TYER' => 'YEAR',
1737 'COMM' => 'COMMENT',
1738 'TRCK' => 'TRACKNUM',
1739 'TCON' => 'GENRE',
1742 %v2_tag_names = (
1743 # v2.2 tags
1744 'BUF' => 'Recommended buffer size',
1745 'CNT' => 'Play counter',
1746 'COM' => 'Comments',
1747 'CRA' => 'Audio encryption',
1748 'CRM' => 'Encrypted meta frame',
1749 'ETC' => 'Event timing codes',
1750 'EQU' => 'Equalization',
1751 'GEO' => 'General encapsulated object',
1752 'IPL' => 'Involved people list',
1753 'LNK' => 'Linked information',
1754 'MCI' => 'Music CD Identifier',
1755 'MLL' => 'MPEG location lookup table',
1756 'PIC' => 'Attached picture',
1757 'POP' => 'Popularimeter',
1758 'REV' => 'Reverb',
1759 'RVA' => 'Relative volume adjustment',
1760 'SLT' => 'Synchronized lyric/text',
1761 'STC' => 'Synced tempo codes',
1762 'TAL' => 'Album/Movie/Show title',
1763 'TBP' => 'BPM (Beats Per Minute)',
1764 'TCM' => 'Composer',
1765 'TCO' => 'Content type',
1766 'TCR' => 'Copyright message',
1767 'TDA' => 'Date',
1768 'TDY' => 'Playlist delay',
1769 'TEN' => 'Encoded by',
1770 'TFT' => 'File type',
1771 'TIM' => 'Time',
1772 'TKE' => 'Initial key',
1773 'TLA' => 'Language(s)',
1774 'TLE' => 'Length',
1775 'TMT' => 'Media type',
1776 'TOA' => 'Original artist(s)/performer(s)',
1777 'TOF' => 'Original filename',
1778 'TOL' => 'Original Lyricist(s)/text writer(s)',
1779 'TOR' => 'Original release year',
1780 'TOT' => 'Original album/Movie/Show title',
1781 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
1782 'TP2' => 'Band/Orchestra/Accompaniment',
1783 'TP3' => 'Conductor/Performer refinement',
1784 'TP4' => 'Interpreted, remixed, or otherwise modified by',
1785 'TPA' => 'Part of a set',
1786 'TPB' => 'Publisher',
1787 'TRC' => 'ISRC (International Standard Recording Code)',
1788 'TRD' => 'Recording dates',
1789 'TRK' => 'Track number/Position in set',
1790 'TSI' => 'Size',
1791 'TSS' => 'Software/hardware and settings used for encoding',
1792 'TT1' => 'Content group description',
1793 'TT2' => 'Title/Songname/Content description',
1794 'TT3' => 'Subtitle/Description refinement',
1795 'TXT' => 'Lyricist/text writer',
1796 'TXX' => 'User defined text information frame',
1797 'TYE' => 'Year',
1798 'UFI' => 'Unique file identifier',
1799 'ULT' => 'Unsychronized lyric/text transcription',
1800 'WAF' => 'Official audio file webpage',
1801 'WAR' => 'Official artist/performer webpage',
1802 'WAS' => 'Official audio source webpage',
1803 'WCM' => 'Commercial information',
1804 'WCP' => 'Copyright/Legal information',
1805 'WPB' => 'Publishers official webpage',
1806 'WXX' => 'User defined URL link frame',
1808 # v2.3 tags
1809 'AENC' => 'Audio encryption',
1810 'APIC' => 'Attached picture',
1811 'COMM' => 'Comments',
1812 'COMR' => 'Commercial frame',
1813 'ENCR' => 'Encryption method registration',
1814 'EQUA' => 'Equalization',
1815 'ETCO' => 'Event timing codes',
1816 'GEOB' => 'General encapsulated object',
1817 'GRID' => 'Group identification registration',
1818 'IPLS' => 'Involved people list',
1819 'LINK' => 'Linked information',
1820 'MCDI' => 'Music CD identifier',
1821 'MLLT' => 'MPEG location lookup table',
1822 'OWNE' => 'Ownership frame',
1823 'PCNT' => 'Play counter',
1824 'POPM' => 'Popularimeter',
1825 'POSS' => 'Position synchronisation frame',
1826 'PRIV' => 'Private frame',
1827 'RBUF' => 'Recommended buffer size',
1828 'RVAD' => 'Relative volume adjustment',
1829 'RVRB' => 'Reverb',
1830 'SYLT' => 'Synchronized lyric/text',
1831 'SYTC' => 'Synchronized tempo codes',
1832 'TALB' => 'Album/Movie/Show title',
1833 'TBPM' => 'BPM (beats per minute)',
1834 'TCOM' => 'Composer',
1835 'TCON' => 'Content type',
1836 'TCOP' => 'Copyright message',
1837 'TDAT' => 'Date',
1838 'TDLY' => 'Playlist delay',
1839 'TENC' => 'Encoded by',
1840 'TEXT' => 'Lyricist/Text writer',
1841 'TFLT' => 'File type',
1842 'TIME' => 'Time',
1843 'TIT1' => 'Content group description',
1844 'TIT2' => 'Title/songname/content description',
1845 'TIT3' => 'Subtitle/Description refinement',
1846 'TKEY' => 'Initial key',
1847 'TLAN' => 'Language(s)',
1848 'TLEN' => 'Length',
1849 'TMED' => 'Media type',
1850 'TOAL' => 'Original album/movie/show title',
1851 'TOFN' => 'Original filename',
1852 'TOLY' => 'Original lyricist(s)/text writer(s)',
1853 'TOPE' => 'Original artist(s)/performer(s)',
1854 'TORY' => 'Original release year',
1855 'TOWN' => 'File owner/licensee',
1856 'TPE1' => 'Lead performer(s)/Soloist(s)',
1857 'TPE2' => 'Band/orchestra/accompaniment',
1858 'TPE3' => 'Conductor/performer refinement',
1859 'TPE4' => 'Interpreted, remixed, or otherwise modified by',
1860 'TPOS' => 'Part of a set',
1861 'TPUB' => 'Publisher',
1862 'TRCK' => 'Track number/Position in set',
1863 'TRDA' => 'Recording dates',
1864 'TRSN' => 'Internet radio station name',
1865 'TRSO' => 'Internet radio station owner',
1866 'TSIZ' => 'Size',
1867 'TSRC' => 'ISRC (international standard recording code)',
1868 'TSSE' => 'Software/Hardware and settings used for encoding',
1869 'TXXX' => 'User defined text information frame',
1870 'TYER' => 'Year',
1871 'UFID' => 'Unique file identifier',
1872 'USER' => 'Terms of use',
1873 'USLT' => 'Unsychronized lyric/text transcription',
1874 'WCOM' => 'Commercial information',
1875 'WCOP' => 'Copyright/Legal information',
1876 'WOAF' => 'Official audio file webpage',
1877 'WOAR' => 'Official artist/performer webpage',
1878 'WOAS' => 'Official audio source webpage',
1879 'WORS' => 'Official internet radio station homepage',
1880 'WPAY' => 'Payment',
1881 'WPUB' => 'Publishers official webpage',
1882 'WXXX' => 'User defined URL link frame',
1884 # v2.4 additional tags
1885 # note that we don't restrict tags from 2.3 or 2.4,
1886 'ASPI' => 'Audio seek point index',
1887 'EQU2' => 'Equalisation (2)',
1888 'RVA2' => 'Relative volume adjustment (2)',
1889 'SEEK' => 'Seek frame',
1890 'SIGN' => 'Signature frame',
1891 'TDEN' => 'Encoding time',
1892 'TDOR' => 'Original release time',
1893 'TDRC' => 'Recording time',
1894 'TDRL' => 'Release time',
1895 'TDTG' => 'Tagging time',
1896 'TIPL' => 'Involved people list',
1897 'TMCL' => 'Musician credits list',
1898 'TMOO' => 'Mood',
1899 'TPRO' => 'Produced notice',
1900 'TSOA' => 'Album sort order',
1901 'TSOP' => 'Performer sort order',
1902 'TSOT' => 'Title sort order',
1903 'TSST' => 'Set subtitle',
1905 # grrrrrrr
1906 'COM ' => 'Broken iTunes comments',
1912 __END__
1914 =pod
1916 =back
1918 =head1 TROUBLESHOOTING
1920 If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
1921 If you cannot figure out why it does not work for you, please put the MP3 file in
1922 a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
1923 mail regarding where I can get the file, with a detailed description of the problem.
1925 If I download the file, after debugging the problem I will not keep the MP3 file
1926 if it is not legal for me to have it. Just let me know if it is legal for me to
1927 keep it or not.
1930 =head1 TODO
1932 =over 4
1934 =item ID3v2 Support
1936 Still need to do more for reading tags, such as using Compress::Zlib to decompress
1937 compressed tags. But until I see this in use more, I won't bother. If something
1938 does not work properly with reading, follow the instructions above for
1939 troubleshooting.
1941 ID3v2 I<writing> is coming soon.
1943 =item Get data from scalar
1945 Instead of passing a file spec or filehandle, pass the
1946 data itself. Would take some work, converting the seeks, etc.
1948 =item Padding bit ?
1950 Do something with padding bit.
1952 =item Test suite
1954 Test suite could use a bit of an overhaul and update. Patches very welcome.
1956 =over 4
1958 =item *
1960 Revamp getset.t. Test all the various get_mp3tag args.
1962 =item *
1964 Test Unicode.
1966 =item *
1968 Test OOP API.
1970 =item *
1972 Test error handling, check more for missing files, bad MP3s, etc.
1974 =back
1976 =item Other VBR
1978 Right now, only Xing VBR is supported.
1980 =back
1983 =head1 THANKS
1985 Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>,
1986 Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>,
1987 Michael Blakeley E<lt>mike@blakeley.comE<gt>,
1988 Per Bolmstedt E<lt>tomten@kol14.comE<gt>,
1989 Tony Bowden E<lt>tony@tmtm.comE<gt>,
1990 Tom Brown E<lt>thecap@usa.netE<gt>,
1991 Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>,
1992 Chris Dawson E<lt>cdawson@webiphany.comE<gt>,
1993 Luke Drumm E<lt>lukedrumm@mypad.comE<gt>,
1994 Kyle Farrell E<lt>kyle@cantametrix.comE<gt>,
1995 Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>,
1996 brian d foy E<lt>comdog@panix.comE<gt>,
1997 Ben Gertzfield E<lt>che@debian.orgE<gt>,
1998 Brian Goodwin E<lt>brian@fuddmain.comE<gt>,
1999 Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>,
2000 Todd Harris E<lt>harris@cshl.orgE<gt>,
2001 Woodrow Hill E<lt>asim@mindspring.comE<gt>,
2002 Kee Hinckley E<lt>nazgul@somewhere.comE<gt>,
2003 Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>,
2004 Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>,
2005 Johann Lindvall,
2006 Peter Marschall E<lt>peter.marschall@mayn.deE<gt>,
2007 Trond Michelsen E<lt>mike@crusaders.noE<gt>,
2008 Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>,
2009 Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>,
2010 Jake Palmer E<lt>jake.palmer@db.comE<gt>,
2011 Andrew Phillips E<lt>asp@wasteland.orgE<gt>,
2012 David Reuteler E<lt>reuteler@visi.comE<gt>,
2013 John Ruttenberg E<lt>rutt@chezrutt.comE<gt>,
2014 Matthew Sachs E<lt>matthewg@zevils.comE<gt>,
2015 E<lt>scfc_de@users.sf.netE<gt>,
2016 Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>,
2017 Chris Sidi E<lt>sidi@angband.orgE<gt>,
2018 Roland Steinbach E<lt>roland@support-system.comE<gt>,
2019 Stuart E<lt>schneis@users.sourceforge.netE<gt>,
2020 Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>,
2021 Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>,
2022 Bogdan Surdu E<lt>tim@go.roE<gt>,
2023 E<lt>tim@tim-landscheidt.deE<gt>,
2024 Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>,
2025 Tobias Wagener E<lt>tobias@wagener.nuE<gt>,
2026 Ronan Waide E<lt>waider@stepstone.ieE<gt>,
2027 Andy Waite E<lt>andy@mailroute.comE<gt>,
2028 Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>,
2029 Meng Weng Wong E<lt>mengwong@pobox.comE<gt>.
2032 =head1 AUTHOR AND COPYRIGHT
2034 Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
2036 Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is
2037 free software; you can redistribute it and/or modify it under the terms
2038 of the Artistic License, distributed with Perl.
2041 =head1 SEE ALSO
2043 =over 4
2045 =item MP3::Info Project Page
2047 http://projects.pudge.net/
2049 =item mp3tools
2051 http://www.zevils.com/linux/mp3tools/
2053 =item mpgtools
2055 http://www.dv.co.yu/mpgscript/mpgtools.htm
2056 http://www.dv.co.yu/mpgscript/mpeghdr.htm
2058 =item mp3tool
2060 http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
2062 =item ID3v2
2064 http://www.id3.org/
2066 =item Xing Variable Bitrate
2068 http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
2070 =item MP3Ext
2072 http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
2074 =item Xmms
2076 http://www.xmms.org/
2079 =back
2081 =head1 VERSION
2083 v1.02, Sunday, March 2, 2003
2085 =cut