summaryrefslogtreecommitdiff
path: root/tools/songdb.pl
diff options
context:
space:
mode:
Diffstat (limited to 'tools/songdb.pl')
-rwxr-xr-xtools/songdb.pl2123
1 files changed, 0 insertions, 2123 deletions
diff --git a/tools/songdb.pl b/tools/songdb.pl
deleted file mode 100755
index dad7e10496..0000000000
--- a/tools/songdb.pl
+++ /dev/null
@@ -1,2123 +0,0 @@
1#!/usr/bin/perl
2#
3# Rockbox song database docs:
4# http://www.rockbox.org/twiki/bin/view/Main/TagDatabase
5#
6# MP3::Info by Chris Nandor is included verbatim in this script to make
7# it runnable standalone on removable drives. See below.
8#
9
10use vorbiscomm;
11
12my $db = "rockbox.tagdb";
13my $dir;
14my $strip;
15my $add;
16my $verbose;
17my $help;
18my $dirisalbum;
19my $dirisalbumname;
20my $crc = 1;
21
22while($ARGV[0]) {
23 if($ARGV[0] eq "--db") {
24 $db = $ARGV[1];
25 shift @ARGV;
26 shift @ARGV;
27 }
28 elsif($ARGV[0] eq "--path") {
29 $dir = $ARGV[1];
30 shift @ARGV;
31 shift @ARGV;
32 }
33 elsif($ARGV[0] eq "--strip") {
34 $strip = $ARGV[1];
35 shift @ARGV;
36 shift @ARGV;
37 }
38 elsif($ARGV[0] eq "--add") {
39 $add = $ARGV[1];
40 shift @ARGV;
41 shift @ARGV;
42 }
43 elsif($ARGV[0] eq "--verbose") {
44 $verbose = 1;
45 shift @ARGV;
46 }
47 elsif($ARGV[0] eq "--nocrc") {
48 $crc = 0;
49 shift @ARGV;
50 }
51 elsif($ARGV[0] eq "--dirisalbum") {
52 $dirisalbum = 1;
53 shift @ARGV;
54 }
55 elsif($ARGV[0] eq "--dirisalbumname") {
56 $dirisalbumname = 1;
57 shift @ARGV;
58 }
59 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
60 $help = 1;
61 shift @ARGV;
62 }
63 else {
64 shift @ARGV;
65 }
66}
67my %entries;
68my %genres;
69my %albums;
70my %years;
71my %filename;
72
73my %lcartists;
74my %lcalbums;
75
76my %dir2albumname;
77
78my $dbver = 3;
79
80if(! -d $dir or $help) {
81 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
82 print <<MOO
83
84songdb --path <dir> [--dirisalbum] [--dirisalbumname] [--db <file>] [--strip <path>] [--add <path>] [--verbose] [--help]
85
86Options:
87
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
100MOO
101;
102 exit;
103}
104
105sub get_oggtag {
106 my $fn = shift;
107 my %hash;
108
109 my $ogg = vorbiscomm->new($fn);
110
111 my $h= $ogg->load;
112
113 # Convert this format into the same format used by the id3 parser hash
114
115 foreach my $k ($ogg->comment_tags())
116 {
117 foreach my $cmmt ($ogg->comment($k))
118 {
119 my $n;
120 if($k =~ /^artist$/i) {
121 $n = 'ARTIST';
122 }
123 elsif($k =~ /^album$/i) {
124 $n = 'ALBUM';
125 }
126 elsif($k =~ /^title$/i) {
127 $n = 'TITLE';
128 }
129 $hash{$n}=$cmmt if($n);
130 }
131 }
132
133 return \%hash;
134}
135
136sub get_ogginfo {
137 my $fn = shift;
138 my %hash;
139
140 my $ogg = vorbiscomm->new($fn);
141
142 my $h= $ogg->load;
143
144 return $ogg->{'INFO'};
145}
146
147# return ALL directory entries in the given dir
148sub getdir {
149 my ($dir) = @_;
150
151 $dir =~ s|/$|| if ($dir ne "/");
152
153 if (opendir(DIR, $dir)) {
154 my @all = readdir(DIR);
155 closedir DIR;
156 return @all;
157 }
158 else {
159 warn "can't opendir $dir: $!\n";
160 }
161}
162
163sub extractmp3 {
164 my ($dir, @files) = @_;
165 my @mp3;
166 for(@files) {
167 if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
168 push @mp3, $_;
169 }
170 }
171 return @mp3;
172}
173
174sub extractdirs {
175 my ($dir, @files) = @_;
176 $dir =~ s|/$||;
177 my @dirs;
178 for(@files) {
179 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
180 push @dirs, $_;
181 }
182 }
183 return @dirs;
184}
185
186# CRC32 32KB of data (use less if there isn't 32KB available)
187
188sub crc32 {
189 my ($filename, $index) = @_;
190
191 my $len = 32*1024;
192
193 if(!$crc) {
194 return 0; # fixed bad CRC when disabled!
195 # The runtimedb treats a CRC zero as CRC disabled!
196 }
197
198 if(!open(FILE, "<$filename")) {
199 print "failed to open \"$filename\" $!\n";
200 return 0;
201 }
202
203 # read $data from index $index to $buffer from the file, may return fewer
204 # bytes when dealing with a very small file.
205 #
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;
209
210 close(FILE);
211
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
257 );
258
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));
263
264 $crc = ($crc << 8) ^ $crc_table[(($crc >> 24) ^ $buf) & 0xFF];
265
266 # printf("%08x\n", $crc);
267 }
268
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;
274 }
275
276 return $crc;
277}
278
279sub singlefile {
280 my ($file) = @_;
281 my $hash;
282 my $info;
283
284 if($file =~ /\.ogg$/i) {
285 $hash = get_oggtag($file);
286
287 $info = get_ogginfo($file);
288
289 $hash->{FILECRC} = crc32($file, $info->{audio_offset});
290 }
291 else {
292 $hash = get_mp3tag($file);
293
294 $info = get_mp3info($file);
295
296 $hash->{FILECRC} = crc32($file, $info->{headersize});
297 }
298
299 return $hash; # a hash reference
300}
301
302my $maxsongperalbum;
303
304sub dodir {
305 my ($dir)=@_;
306
307 print "$dir\n";
308
309 # getdir() returns all entries in the given dir
310 my @a = getdir($dir);
311
312 # extractmp3 filters out only the mp3 files from all given entries
313 my @m = extractmp3($dir, @a);
314
315 my $f;
316
317 for $f (sort @m) {
318
319 my $id3 = singlefile("$dir/$f");
320
321 # ARTIST
322 # COMMENT
323 # ALBUM
324 # TITLE
325 # GENRE
326 # TRACKNUM
327 # YEAR
328
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'})
334 {
335 next;
336 }
337
338 #printf "Artist: %s\n", $id3->{'ARTIST'};
339 my $path = "$dir/$f";
340 if ($strip ne "" and $path =~ /^$strip(.*)/) {
341 $path = $1;
342 }
343
344 if ($add ne "") {
345 $path = $add . $path;
346 }
347
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'})};
353 }
354 else {
355 # else create a new entry in the hash
356 $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
357 }
358
359 if (exists($lcartists{lc($$id3{'ARTIST'})})) {
360 $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
361 }
362 else {
363 $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
364 }
365
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'});
370
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'};
378 }
379 elsif($dir2albumname{$dir} ne $$id3{'ALBUM'}) {
380 $dir2albumname{$dir} = (split m[/], $dir)[-1];
381 }
382 }
383 # if no directory
384 if ($dirisalbumname) {
385 $$id3{'ALBUM'} = (split m[/], $dir)[-1] if ($$id3{'ALBUM'} eq "");
386 }
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 "");
392
393 # Append dirname, to handle multi-artist albums
394 $$id3{'DIR'} = $dir;
395 my $albumid;
396 if ($dirisalbum) {
397 $albumid=$$id3{'DIR'};
398 }
399 else {
400 $albumid= $id3->{'ALBUM'}."___".$$id3{'DIR'};
401 }
402 #printf "album id: %s\n", $albumid;
403
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;
409 }
410 $album2songs{$albumid}{$$id3{TITLE}} = $id3;
411 if($dirisalbum) {
412 $artist2albums{$$id3{ARTIST}}{$$id3{DIR}} = $id3;
413 }
414 else {
415 $artist2albums{$$id3{ARTIST}}{$$id3{ALBUM}} = $id3;
416 }
417# }
418 }
419
420 if($dirisalbum and $dir2albumname{$dir} eq "") {
421 $dir2albumname{$dir} = (split m[/], $dir)[-1];
422 printf "%s\n", $dir2albumname{$dir};
423 }
424
425 # extractdirs filters out only subdirectories from all given entries
426 my @d = extractdirs($dir, @a);
427
428 for $d (sort @d) {
429 $dir =~ s|/$||;
430 dodir("$dir/$d");
431 }
432}
433
434
435dodir($dir);
436print "\n";
437
438print "File name table\n" if ($verbose);
439for(sort keys %entries) {
440 printf(" %s\n", $_) if ($verbose);
441 my $l = length($_);
442 if($l > $maxfilelen) {
443 $maxfilelen = $l;
444 $longestfilename = $_;
445 }
446}
447$maxfilelen++; # include zero termination byte
448while($maxfilelen&3) {
449 $maxfilelen++;
450}
451
452my $maxsonglen = 0;
453my $sc;
454print "\nSong title table\n" if ($verbose);
455
456for(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'};
462 }
463}
464$maxsonglen++; # include zero termination byte
465while($maxsonglen&3) {
466 $maxsonglen++;
467}
468
469my $maxartistlen = 0;
470print "\nArtist table\n" if ($verbose);
471my $i=0;
472my %artistcount;
473for(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 = $_;
480 }
481
482 $l = scalar keys %{$artist2albums{$_}};
483 if ($l > $maxalbumsperartist) {
484 $maxalbumsperartist = $l;
485 $longestartistalbum = $_;
486 }
487}
488$maxartistlen++; # include zero termination byte
489while($maxartistlen&3) {
490 $maxartistlen++;
491}
492
493print "\nGenre table\n" if ($verbose);
494for(sort keys %genres) {
495 my $l = length($_);
496 if($l > $maxgenrelen) {
497 $maxgenrelen = $l;
498 $longestgenrename = $_;
499 }
500}
501
502$maxgenrelen++; #include zero termination byte
503while($maxgenrelen&3) {
504 $maxgenrelen++;
505}
506
507
508if ($verbose) {
509 print "\nYear table\n";
510 for(sort keys %years) {
511 printf(" %s\n", $_);
512 }
513}
514
515print "\nAlbum table\n" if ($verbose);
516my $maxalbumlen = 0;
517my %albumcount;
518$i=0;
519my @albumssort;
520if($dirisalbum) {
521 @albumssort = sort {uc($dir2albumname{$a}) cmp uc($dir2albumname{$b})} keys %albums;
522}
523else {
524 @albumssort = sort {uc($a) cmp uc($b)} keys %albums;
525}
526for(@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{$_});
533 }
534 else {
535 $l = length($moo[0]);
536 }
537 if($l > $maxalbumlen) {
538 $maxalbumlen = $l;
539 if($dirisalbum) {
540 $longestalbumname = $dir2albumname{$_};
541 }
542 else {
543 $longestalbumname = $moo[0];
544 }
545 }
546}
547$maxalbumlen++; # include zero termination byte
548while($maxalbumlen&3) {
549 $maxalbumlen++;
550}
551
552
553
554sub dumpshort {
555 my ($num)=@_;
556
557 # print "int: $num\n";
558
559 print DB pack "n", $num;
560}
561
562sub dumpint {
563 my ($num)=@_;
564
565# print "int: $num\n";
566
567 print DB pack "N", $num;
568}
569
570if (!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;
574}
575
576if ($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;
581
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);
597
598
599 open(DB, ">$db") || die "couldn't make $db";
600 binmode(DB);
601 printf DB "RDB%c", $dbver;
602
603 $pathindex = 68; # paths always start at index 68
604
605 $artistindex = $pathindex;
606
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
614
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
631
632 #### TABLE of artists ###
633 # name of artist1
634 # pointers to albums of artist1
635
636 for (sort {uc($a) cmp uc($b)} keys %artists) {
637 my $artist = $_;
638 my $str = $_."\x00" x ($maxartistlen - length($_));
639 print DB $str;
640
641 for (sort keys %{$artist2albums{$artist}}) {
642 my $id3 = $artist2albums{$artist}{$_};
643 my $a;
644 if($dirisalbum) {
645 $a = $albumcount{"$$id3{'DIR'}"} * $albumentrysize;
646 }
647 else {
648 $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'DIR'}"} * $albumentrysize;
649 }
650 dumpint($a + $albumindex);
651 }
652
653 for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) {
654 print DB "\x00\x00\x00\x00";
655 }
656
657 }
658
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;
665 }
666
667
668 #### TABLE of albums ###
669 # name of album1
670 # pointers to artists of album1
671 # pointers to songs on album1
672
673 for(@albumssort) {
674 my $albumid = $_;
675 my @moo=split(/___/, $_);
676 my $t;
677 my $str;
678 if($dirisalbum) {
679 $t = $dir2albumname{$albumid};
680 }
681 else {
682 $t = $moo[0];
683 }
684 $str = $t."\x00" x ($maxalbumlen - length($t));
685 print DB $str;
686
687 my @songlist = keys %{$album2songs{$albumid}};
688 my $id3 = $album2songs{$albumid}{$songlist[0]};
689
690 #printf "(d) albumid: %s artist: %s\n",$albumid, $id3->{'ARTIST'};
691
692 my $aoffset = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
693 dumpint($aoffset + $artistindex); # pointer to artist of this album
694
695 if (defined $id3->{'TRACKNUM'}) {
696 @songlist = sort {
697 $album2songs{$albumid}{$a}->{'TRACKNUM'} <=>
698 $album2songs{$albumid}{$b}->{'TRACKNUM'}
699 } @songlist;
700 }
701 else {
702 @songlist = sort @songlist;
703 }
704
705 for (@songlist) {
706 my $id3 = $album2songs{$albumid}{$_};
707 dumpint($$id3{'songoffset'});
708 }
709
710 for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) {
711 print DB "\x00\x00\x00\x00";
712 }
713 }
714
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;
721 }
722
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
728
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));
735
736 print DB $str; # title
737 $str = $g."\x00" x ($maxgenrelen - length($g));
738
739 my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
740 dumpint($a + $artistindex); # pointer to artist of this song
741
742 if($dirisalbum) {
743 $a = $albumcount{"$$id3{DIR}"} * $albumentrysize;
744 }
745 else {
746 $a = $albumcount{"$$id3{ALBUM}___$$id3{DIR}"} * $albumentrysize;
747 }
748 dumpint($a + $albumindex); # pointer to album of this song
749
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);
758 }
759
760 #### TABLE of file names ###
761 # path1
762
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.
771 }
772
773 close(DB);
774}
775
776###
777### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor
778### Modified by Björn Stenberg to remove use of external libraries
779###
780
781our(
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
787);
788
789@ISA = 'Exporter';
790@EXPORT = qw(
791 set_mp3tag get_mp3tag get_mp3info remove_mp3tag
792 use_winamp_genres
793);
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]
799);
800
801# $Id$
802($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
803$VERSION = '1.02';
804
805=pod
806
807=head1 NAME
808
809MP3::Info - Manipulate / fetch info from MP3 audio files
810
811=head1 SYNOPSIS
812
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');
819
820 my $tag = get_mp3tag($file) or die "No TAG info";
821 $tag->{GENRE} = 'rock';
822 set_mp3tag($file, $tag);
823
824 my $info = get_mp3info($file);
825 printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
826
827=cut
828
829{
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;
834
835 # do it again for winamp genres
836 $c = -1;
837 %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
838}
839
840=pod
841
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;
846
847
848=head1 DESCRIPTION
849
850=over 4
851
852=item $mp3 = MP3::Info-E<gt>new(FILE)
853
854OOP interface to the rest of the module. The same keys
855available via get_mp3info and get_mp3tag are available
856via the returned object (using upper case or lower case;
857but note that all-caps "VERSION" will return the module
858version, not the MP3 version).
859
860Passing a value to one of the methods will set the value
861for that tag in the MP3 file, if applicable.
862
863=cut
864
865sub new {
866 my($pack, $file) = @_;
867
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
873 );
874
875 @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
876 @{$info}{@mp3_info_fields},
877 @{$tags}{@v1_tag_names},
878 $file
879 );
880
881 return bless \%self, $pack;
882}
883
884sub 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;
890}
891
892sub AUTOLOAD {
893 my($self) = @_;
894 (my $name = uc $AUTOLOAD) =~ s/^.*://;
895
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]);
902 }
903 return $_[0]->{$name};
904 }
905 : sub {
906 return $_[0]->{$name}
907 };
908
909 *{$AUTOLOAD} = $sub;
910 goto &$AUTOLOAD;
911
912 } else {
913 warn(sprintf "No method '$name' available in package %s.",
914 __PACKAGE__);
915 }
916}
917
918sub DESTROY {
919
920}
921
922
923=item use_mp3_utf8([STATUS])
924
925Tells MP3::Info to (or not) return TAG info in UTF-8.
926TRUE is 1, FALSE is 0. Default is FALSE.
927
928Will only be able to it on if Unicode::String is available. ID3v2
929tags will be converted to UTF-8 according to the encoding specified
930in each tag; ID3v1 tags will be assumed Latin-1 and converted
931to UTF-8.
932
933Function returns status (TRUE/FALSE). If no argument is supplied,
934or an unaccepted argument is supplied, function merely returns status.
935
936This function is not exported by default, but may be exported
937with the C<:utf8> or C<:all> export tag.
938
939=cut
940
941my $unicode_module = eval { require Unicode::String };
942my $UNICODE = 0;
943
944sub use_mp3_utf8 {
945 my($val) = @_;
946 if ($val == 1) {
947 $UNICODE = 1 if $unicode_module;
948 } elsif ($val == 0) {
949 $UNICODE = 0;
950 }
951 return $UNICODE;
952}
953
954=pod
955
956=item use_winamp_genres()
957
958Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
959(adds 68 additional genres to the default list of 80).
960This is a separate function because these are non-standard
961genres, but they are included because they are widely used.
962
963You can import the data structures with one of:
964
965 use MP3::Info qw(:genres);
966 use MP3::Info qw(:DEFAULT :genres);
967 use MP3::Info qw(:all);
968
969=cut
970
971sub use_winamp_genres {
972 %mp3_genres = %winamp_genres;
973 @mp3_genres = @winamp_genres;
974 return 1;
975}
976
977=pod
978
979=pod
980
981=item get_mp3tag (FILE [, VERSION, RAW_V2])
982
983Returns hash reference containing tag information in MP3 file. The keys
984returned are the same as those supplied for C<set_mp3tag>, except in the
985case of RAW_V2 being set.
986
987If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
988If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
989If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
990then, if present, the ID3v2 tag information will override any existing ID3v1
991tag info.
992
993If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
994of text encoding. The key name is the same as the frame ID (ID to name mappings
995are in the global %v2_tag_names).
996
997If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
998necessary, etc. It also takes multiple values for a given key (such as comments)
999and puts them in an arrayref.
1000
1001If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
1002not be read.
1003
1004Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
1005(unless RAW_V2 is C<1>).
1006
1007Also returns a TAGVERSION key, containing the ID3 version used for the returned
1008data (if TAGVERSION argument is C<0>, may contain two versions).
1009
1010=cut
1011
1012sub 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;
1017
1018 if (not (defined $file && $file ne '')) {
1019 $@ = "No file specified";
1020 return undef;
1021 }
1022
1023 if (not -s $file) {
1024 $@ = "File is empty";
1025 return undef;
1026 }
1027
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;
1035 }
1036 }
1037
1038 binmode $fh;
1039
1040 if ($ver < 2) {
1041 seek $fh, -128, 2;
1042 while(defined(my $line = <$fh>)) { $tag .= $line }
1043
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';
1057 }
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;
1063 }
1064 }
1065 } elsif ($ver == 1) {
1066 _close($file, $fh);
1067 $@ = "No ID3v1 tag found";
1068 return undef;
1069 }
1070 }
1071
1072 ($v2, $v2h) = _get_v2tag($fh);
1073
1074 unless ($v1 || $v2) {
1075 _close($file, $fh);
1076 $@ = "No ID3 tag found";
1077 return undef;
1078 }
1079
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};
1092
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
1099
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];
1106 }
1107 }
1108
1109 $data1 = [ $data1 ] if ! ref $data1;
1110
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;
1120 }
1121
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;
1130 }
1131 }
1132
1133 if ($raw_v2 == 2 && $desc) {
1134 $data = { $desc => $data };
1135 }
1136
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 ];
1142 }
1143 } else {
1144 $info{$hash->{$id}} = $data;
1145 }
1146 }
1147 }
1148 }
1149 }
1150 if ($ver == 0 && $info{TAGVERSION}) {
1151 $info{TAGVERSION} .= ' / ' . $v2h->{version};
1152 } else {
1153 $info{TAGVERSION} = $v2h->{version};
1154 }
1155 }
1156 }
1157
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+$//;
1163 }
1164 }
1165
1166 for (@v1_tag_names) {
1167 $info{$_} = '' unless defined $info{$_};
1168 }
1169 }
1170
1171 if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) {
1172 $info{GENRE} = '';
1173 }
1174
1175 _close($file, $fh);
1176
1177 return keys %info ? {%info} : undef;
1178}
1179
1180sub _get_v2tag {
1181 my($fh) = @_;
1182 my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num);
1183 $h = {};
1184
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;
1191 }
1192
1193 if ($v2->{major_version} == 2) {
1194 $hlen = 6;
1195 $num = 3;
1196 } else {
1197 $hlen = 10;
1198 $num = 4;
1199 }
1200
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;
1210 }
1211 return($id, $size);
1212 };
1213
1214 $off = $v2->{ext_header_size} + 10;
1215
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];
1225 }
1226 } else {
1227 $h->{$id} = $bytes;
1228 }
1229 $off += $size;
1230 }
1231
1232 return($h, $v2);
1233}
1234
1235
1236=pod
1237
1238=item get_mp3info (FILE)
1239
1240Returns hash reference containing file information for MP3 file.
1241This data cannot be changed. Returned data:
1242
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
1246
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
1251
1252 SECS total seconds
1253 MM minutes
1254 SS leftover seconds
1255 MS leftover milliseconds
1256 TIME time in MM:SS
1257
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
1265
1266On error, returns nothing and sets C<$@>.
1267
1268=cut
1269
1270sub get_mp3info {
1271 my($file) = @_;
1272 my($off, $myseek, $byte, $eof, $h, $tot, $fh);
1273
1274 if (not (defined $file && $file ne '')) {
1275 $@ = "No file specified";
1276 return undef;
1277 }
1278
1279 if (not -s $file) {
1280 $@ = "File is empty";
1281 return undef;
1282 }
1283
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;
1291 }
1292 }
1293
1294 $off = 0;
1295 $tot = 4096;
1296
1297 $myseek = sub {
1298 seek $fh, $off, 0;
1299 read $fh, $byte, 4;
1300 };
1301
1302 binmode $fh;
1303 &$myseek;
1304
1305 if ($off == 0) {
1306 if (my $id3v2 = _get_v2head($fh)) {
1307 $tot += $off += $id3v2->{tag_size};
1308 &$myseek;
1309 }
1310 }
1311
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;
1322 }
1323 }
1324
1325 my $vbr = _get_vbr($fh, $h, \$off);
1326
1327 $h->{headersize}=$off; # data size prepending the actual mp3 data
1328
1329 seek $fh, 0, 2;
1330 $eof = tell $fh;
1331 seek $fh, -128, 2;
1332 $off += 128 if <$fh> =~ /^TAG/ ? 1 : 0;
1333
1334 _close($file, $fh);
1335
1336 $h->{size} = $eof - $off;
1337
1338 return _get_info($h, $vbr);
1339}
1340
1341sub _get_info {
1342 my($h, $vbr) = @_;
1343 my $i;
1344
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;
1349
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};
1354
1355 $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size};
1356
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
1361 );
1362
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;
1369 }
1370 }
1371
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'};
1380
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}];
1385
1386 $i->{headersize} = $h->{headersize};
1387
1388 return $i;
1389}
1390
1391sub _get_head {
1392 my($byte) = @_;
1393 my($bytes, $h);
1394
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
1404 );
1405
1406 $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}];
1407 $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}];
1408
1409 return $h;
1410}
1411
1412sub _is_mp3 {
1413 my $h = $_[0] or return undef;
1414 return ! ( # all below must be false
1415 $h->{bitrate_index} == 0
1416 ||
1417 $h->{version_index} == 1
1418 ||
1419 ($h->{bytes} & 0xFFE00000) != 0xFFE00000
1420 ||
1421 !$h->{fs}
1422 ||
1423 !$h->{bitrate}
1424 ||
1425 $h->{bitrate_index} == 15
1426 ||
1427 !$h->{layer}
1428 ||
1429 $h->{sampling_freq} == 3
1430 ||
1431 $h->{emphasis} == 2
1432 ||
1433 !$h->{bitrate_index}
1434 ||
1435 ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000
1436 ||
1437 ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1)
1438 ||
1439 ($h->{mode_extension} != 0 && $h->{mode} != 1)
1440 );
1441}
1442
1443sub _get_vbr {
1444 my($fh, $h, $roff) = @_;
1445 my($off, $bytes, @bytes, $myseek, %vbr);
1446
1447 $off = $$roff;
1448 @_ = (); # closure confused if we don't do this
1449
1450 $myseek = sub {
1451 my $n = $_[0] || 4;
1452 seek $fh, $off, 0;
1453 read $fh, $bytes, $n;
1454 $off += $n;
1455 };
1456
1457 $off += 4;
1458
1459 if ($h->{ID}) { # MPEG1
1460 $off += $h->{mode} == 3 ? 17 : 32;
1461 } else { # MPEG2
1462 $off += $h->{mode} == 3 ? 9 : 17;
1463 }
1464
1465 &$myseek;
1466 return unless $bytes eq 'Xing';
1467
1468 &$myseek;
1469 $vbr{flags} = _unpack_head($bytes);
1470
1471 if ($vbr{flags} & 1) {
1472 &$myseek;
1473 $vbr{frames} = _unpack_head($bytes);
1474 }
1475
1476 if ($vbr{flags} & 2) {
1477 &$myseek;
1478 $vbr{bytes} = _unpack_head($bytes);
1479 }
1480
1481 if ($vbr{flags} & 4) {
1482 $myseek->(100);
1483# Not used right now ...
1484# $vbr{toc} = _unpack_head($bytes);
1485 }
1486
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;
1492 }
1493
1494 $$roff = $off;
1495 return \%vbr;
1496}
1497
1498sub _get_v2head {
1499 my $fh = $_[0] or return;
1500 my($h, $bytes, @bytes);
1501
1502 # check first three bytes for 'ID3'
1503 seek $fh, 0, 0;
1504 read $fh, $bytes, 3;
1505 return unless $bytes eq 'ID3';
1506
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;
1512
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];
1523 }
1524
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;
1532 }
1533
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;
1542 }
1543 }
1544
1545 return $h;
1546}
1547
1548sub _unpack_head {
1549 unpack('l', pack('L', unpack('N', $_[0])));
1550}
1551
1552sub _close {
1553 my($file, $fh) = @_;
1554 unless (ref $file) { # filehandle not passed
1555 close $fh or warn "Problem closing '$file': $!";
1556 }
1557}
1558
1559BEGIN {
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',
1641 );
1642
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',
1713 );
1714
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]
1719 ],[
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]
1723 ]);
1724
1725 @t_sampling_freq = (
1726 [11025, 12000, 8000],
1727 [undef, undef, undef], # reserved
1728 [22050, 24000, 16000],
1729 [44100, 48000, 32000]
1730 );
1731
1732 @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 }
1733 map { @$_ } @t_sampling_freq;
1734
1735 @mp3_info_fields = qw(
1736 VERSION
1737 LAYER
1738 STEREO
1739 VBR
1740 BITRATE
1741 FREQUENCY
1742 SIZE
1743 SECS
1744 MM
1745 SS
1746 MS
1747 TIME
1748 COPYRIGHT
1749 PADDING
1750 MODE
1751 FRAMES
1752 FRAME_LENGTH
1753 VBR_SCALE
1754 );
1755
1756 %v1_tag_fields =
1757 (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4);
1758
1759 @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
1760
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',
1778 );
1779
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',
1845
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',
1921
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',
1942
1943 # grrrrrrr
1944 'COM ' => 'Broken iTunes comments',
1945 );
1946}
1947
19481;
1949
1950__END__
1951
1952=pod
1953
1954=back
1955
1956=head1 TROUBLESHOOTING
1957
1958If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
1959If you cannot figure out why it does not work for you, please put the MP3 file in
1960a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
1961mail regarding where I can get the file, with a detailed description of the problem.
1962
1963If I download the file, after debugging the problem I will not keep the MP3 file
1964if it is not legal for me to have it. Just let me know if it is legal for me to
1965keep it or not.
1966
1967
1968=head1 TODO
1969
1970=over 4
1971
1972=item ID3v2 Support
1973
1974Still need to do more for reading tags, such as using Compress::Zlib to decompress
1975compressed tags. But until I see this in use more, I won't bother. If something
1976does not work properly with reading, follow the instructions above for
1977troubleshooting.
1978
1979ID3v2 I<writing> is coming soon.
1980
1981=item Get data from scalar
1982
1983Instead of passing a file spec or filehandle, pass the
1984data itself. Would take some work, converting the seeks, etc.
1985
1986=item Padding bit ?
1987
1988Do something with padding bit.
1989
1990=item Test suite
1991
1992Test suite could use a bit of an overhaul and update. Patches very welcome.
1993
1994=over 4
1995
1996=item *
1997
1998Revamp getset.t. Test all the various get_mp3tag args.
1999
2000=item *
2001
2002Test Unicode.
2003
2004=item *
2005
2006Test OOP API.
2007
2008=item *
2009
2010Test error handling, check more for missing files, bad MP3s, etc.
2011
2012=back
2013
2014=item Other VBR
2015
2016Right now, only Xing VBR is supported.
2017
2018=back
2019
2020
2021=head1 THANKS
2022
2023Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>,
2024Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>,
2025Michael Blakeley E<lt>mike@blakeley.comE<gt>,
2026Per Bolmstedt E<lt>tomten@kol14.comE<gt>,
2027Tony Bowden E<lt>tony@tmtm.comE<gt>,
2028Tom Brown E<lt>thecap@usa.netE<gt>,
2029Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>,
2030Chris Dawson E<lt>cdawson@webiphany.comE<gt>,
2031Luke Drumm E<lt>lukedrumm@mypad.comE<gt>,
2032Kyle Farrell E<lt>kyle@cantametrix.comE<gt>,
2033Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>,
2034brian d foy E<lt>comdog@panix.comE<gt>,
2035Ben Gertzfield E<lt>che@debian.orgE<gt>,
2036Brian Goodwin E<lt>brian@fuddmain.comE<gt>,
2037Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>,
2038Todd Harris E<lt>harris@cshl.orgE<gt>,
2039Woodrow Hill E<lt>asim@mindspring.comE<gt>,
2040Kee Hinckley E<lt>nazgul@somewhere.comE<gt>,
2041Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>,
2042Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>,
2043Johann Lindvall,
2044Peter Marschall E<lt>peter.marschall@mayn.deE<gt>,
2045Trond Michelsen E<lt>mike@crusaders.noE<gt>,
2046Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>,
2047Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>,
2048Jake Palmer E<lt>jake.palmer@db.comE<gt>,
2049Andrew Phillips E<lt>asp@wasteland.orgE<gt>,
2050David Reuteler E<lt>reuteler@visi.comE<gt>,
2051John Ruttenberg E<lt>rutt@chezrutt.comE<gt>,
2052Matthew Sachs E<lt>matthewg@zevils.comE<gt>,
2053E<lt>scfc_de@users.sf.netE<gt>,
2054Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>,
2055Chris Sidi E<lt>sidi@angband.orgE<gt>,
2056Roland Steinbach E<lt>roland@support-system.comE<gt>,
2057Stuart E<lt>schneis@users.sourceforge.netE<gt>,
2058Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>,
2059Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>,
2060Bogdan Surdu E<lt>tim@go.roE<gt>,
2061E<lt>tim@tim-landscheidt.deE<gt>,
2062Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>,
2063Tobias Wagener E<lt>tobias@wagener.nuE<gt>,
2064Ronan Waide E<lt>waider@stepstone.ieE<gt>,
2065Andy Waite E<lt>andy@mailroute.comE<gt>,
2066Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>,
2067Meng Weng Wong E<lt>mengwong@pobox.comE<gt>.
2068
2069
2070=head1 AUTHOR AND COPYRIGHT
2071
2072Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
2073
2074Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is
2075free software; you can redistribute it and/or modify it under the terms
2076of the Artistic License, distributed with Perl.
2077
2078
2079=head1 SEE ALSO
2080
2081=over 4
2082
2083=item MP3::Info Project Page
2084
2085 http://projects.pudge.net/
2086
2087=item mp3tools
2088
2089 http://www.zevils.com/linux/mp3tools/
2090
2091=item mpgtools
2092
2093 http://www.dv.co.yu/mpgscript/mpgtools.htm
2094 http://www.dv.co.yu/mpgscript/mpeghdr.htm
2095
2096=item mp3tool
2097
2098 http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
2099
2100=item ID3v2
2101
2102 http://www.id3.org/
2103
2104=item Xing Variable Bitrate
2105
2106 http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
2107
2108=item MP3Ext
2109
2110 http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
2111
2112=item Xmms
2113
2114 http://www.xmms.org/
2115
2116
2117=back
2118
2119=head1 VERSION
2120
2121v1.02, Sunday, March 2, 2003
2122
2123=cut