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 my $db = "rockbox.id3db";
17 if($ARGV[0] eq "--db") {
22 elsif($ARGV[0] eq "--path") {
27 elsif($ARGV[0] eq "--strip") {
32 elsif($ARGV[0] eq "--verbose") {
36 elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
52 if(! -d
$dir or $help) {
53 print "'$dir' is not a directory\n" if ($dir ne "" and ! -d
$dir);
54 print "songdb --path <dir> [--db <file>] [--strip <path>] [--verbose] [--help]\n";
58 # return ALL directory entries in the given dir
62 opendir(DIR
, $dir) || die "can't opendir $dir: $!";
63 # my @mp3 = grep { /\.mp3$/ && -f "$dir/$_" } readdir(DIR);
64 my @all = readdir(DIR
);
70 my ($dir, @files) = @_;
73 if( /\.mp3$/ && -f
"$dir/$_" ) {
81 my ($dir, @files) = @_;
84 if( -d
"$dir/$_" && ($_ !~ /^\
.(|\
.)$/)) {
94 # print "Check $file\n";
96 my $hash = get_mp3tag
($file);
97 # my $hash = get_mp3info($file);
100 # print "Info: $_ ".$hash->{$_}."\n";
102 return $hash; # a hash reference
112 # getdir() returns all entries in the given dir
113 my @a = getdir
($dir);
115 # extractmp3 filters out only the mp3 files from all given entries
116 my @m = extractmp3
($dir, @a);
122 my $id3 = singlefile
("$dir/$f");
132 # don't index songs without tags
133 if (not defined $$id3{'ARTIST'} and
134 not defined $$id3{'ALBUM'} and
135 not defined $$id3{'TITLE'})
140 #printf "Artist: %s\n", $id3->{'ARTIST'};
141 my $path = "$dir/$f";
142 if ($strip ne "" and $path =~ /^$strip(.*)/) {
146 $entries{$path}= $id3;
147 $artists{$id3->{'ARTIST'}}++ if($id3->{'ARTIST'});
148 $genres{$id3->{'GENRE'}}++ if($id3->{'GENRE'});
149 $years{$id3->{'YEAR'}}++ if($id3->{'YEAR'});
152 $$id3{'ARTIST'} = "<no artist tag>" if ($$id3{'ARTIST'} eq "");
153 $$id3{'ALBUM'} = "<no album tag>" if ($$id3{'ALBUM'} eq "");
154 $$id3{'TITLE'} = "<no title tag>" if ($$id3{'TITLE'} eq "");
156 # prepend Artist name to handle duplicate album names from other
158 my $albumid = $id3->{'ALBUM'}."___".$id3->{'ARTIST'};
159 if($albumid ne "<no album tag>___<no artist tag>") {
160 my $num = ++$albums{$albumid};
161 if($num > $maxsongperalbum) {
162 $maxsongperalbum = $num;
163 $longestalbum = $albumid;
165 $album2songs{$albumid}{$$id3{TITLE
}} = $id3;
166 $artist2albums{$$id3{ARTIST
}}{$$id3{ALBUM
}} = $id3;
170 # extractdirs filters out only subdirectories from all given entries
171 my @d = extractdirs
($dir, @a);
174 #print "Subdir: $d\n";
183 print "File name table\n" if ($verbose);
185 for(sort keys %entries) {
186 printf(" %s\n", $_) if ($verbose);
192 print "\nSong title table\n" if ($verbose);
194 for(sort {$entries{$a}->{'TITLE'} cmp $entries{$b}->{'TITLE'}} keys %entries) {
195 printf(" %s\n", $entries{$_}->{'TITLE'} ) if ($verbose);
196 my $l = length($entries{$_}->{'TITLE'});
197 if($l > $maxsonglen) {
199 $longestsong = $entries{$_}->{'TITLE'};
202 $maxsonglen++; # include zero termination byte
203 while($maxsonglen&3) {
207 my $maxartistlen = 0;
208 print "\nArtist table\n" if ($verbose);
211 for(sort keys %artists) {
212 printf(" %s\n", $_) if ($verbose);
213 $artistcount{$_}=$i++;
215 if($l > $maxartistlen) {
220 $l = scalar keys %{$artist2albums{$_}};
221 if ($l > $maxalbumsperartist) {
222 $maxalbumsperartist = $l;
225 $maxartistlen++; # include zero termination byte
226 while($maxartistlen&3) {
231 print "\nGenre table\n";
232 for(sort keys %genres) {
236 print "\nYear table\n";
237 for(sort keys %years) {
242 print "\nAlbum table\n" if ($verbose);
246 for(sort keys %albums) {
247 my @moo=split(/___/, $_);
248 printf(" %s\n", $moo[0]) if ($verbose);
249 $albumcount{$_} = $i++;
250 my $l = length($moo[0]);
251 if($l > $maxalbumlen) {
253 $longestalbumname = $moo[0];
256 $maxalbumlen++; # include zero termination byte
257 while($maxalbumlen&3) {
266 # print "int: $num\n";
268 printf DB
("%c%c%c%c",
275 if (!scalar keys %entries) {
276 print "No songs found. Did you specify the right --path ?\n";
277 print "Use the --help parameter to see all options.\n";
282 my $songentrysize = $maxsonglen + 12;
283 my $albumentrysize = $maxalbumlen + 4 + $maxsongperalbum*4;
284 my $artistentrysize = $maxartistlen + $maxalbumsperartist*4;
286 printf "Number of artists : %d\n", scalar keys %artists;
287 printf "Number of albums : %d\n", scalar keys %albums;
288 printf "Number of songs : %d\n", scalar keys %entries;
289 print "Max artist length : $maxartistlen ($longestartist)\n";
290 print "Max album length : $maxalbumlen ($longestalbumname)\n";
291 print "Max song length : $maxsonglen ($longestsong)\n";
292 print "Max songs per album: $maxsongperalbum ($longestalbum)\n";
293 print "Database version: $dbver\n" if ($verbose);
295 open(DB
, ">$db") || die "couldn't make $db";
296 printf DB
"RDB%c", $dbver;
298 $pathindex = 48; # paths always start at index 48
300 $songindex = $pathindex + $fc; # fc is size of all paths
301 $songindex++ while ($songindex & 3); # align to 32 bits
303 dumpint
($songindex); # file position index of song table
304 dumpint
(scalar(keys %entries)); # number of songs
305 dumpint
($maxsonglen); # length of song name field
307 # set total size of song title table
308 $sc = scalar(keys %entries) * $songentrysize;
310 $albumindex = $songindex + $sc; # sc is size of all songs
311 dumpint
($albumindex); # file position index of album table
312 dumpint
(scalar(keys %albums)); # number of albums
313 dumpint
($maxalbumlen); # length of album name field
314 dumpint
($maxsongperalbum); # number of entries in songs-per-album array
316 my $ac = scalar(keys %albums) * $albumentrysize;
318 $artistindex = $albumindex + $ac; # ac is size of all albums
319 dumpint
($artistindex); # file position index of artist table
320 dumpint
(scalar(keys %artists)); # number of artists
321 dumpint
($maxartistlen); # length of artist name field
322 dumpint
($maxalbumsperartist); # number of entries in albums-per-artist array
326 #### TABLE of file names ###
330 for $f (sort keys %entries) {
331 printf DB
("%s\x00", $f);
332 $filenamepos{$f}= $l;
340 #### TABLE of songs ###
342 # pointer to artist of song1
343 # pointer to album of song1
344 # pointer to filename of song1
346 my $offset = $songindex;
347 for(sort {$entries{$a}->{'TITLE'} cmp $entries{$b}->{'TITLE'}} keys %entries) {
349 my $id3 = $entries{$f};
350 my $t = $id3->{'TITLE'};
351 my $str = $t."\x00" x
($maxsonglen- length($t));
353 print DB
$str; # title
355 my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
356 dumpint
($a + $artistindex); # pointer to artist of this song
358 $a = $albumcount{"$$id3{ALBUM}___$$id3{ARTIST}"} * $albumentrysize;
359 dumpint
($a + $albumindex); # pointer to album of this song
361 # pointer to filename of this song
362 dumpint
($filenamepos{$f} + $pathindex);
364 $$id3{'songoffset'} = $offset;
365 $offset += $songentrysize;
368 #### TABLE of albums ###
370 # pointers to artists of album1
371 # pointers to songs on album1
373 for(sort keys %albums) {
375 my @moo=split(/___/, $_);
377 my $str = $t."\x00" x
($maxalbumlen - length($t));
380 my $aoffset = $artistcount{$moo[0]} * $artistentrysize;
381 dumpint
($aoffset + $artistindex); # pointer to artist of this album
383 my @songlist = keys %{$album2songs{$albumid}};
384 my $id3 = $album2songs{$albumid}{$songlist[0]};
385 if (defined $id3->{'TRACKNUM'}) {
387 $album2songs{$albumid}{$a}->{'TRACKNUM'} <=>
388 $album2songs{$albumid}{$b}->{'TRACKNUM'}
392 @songlist = sort @songlist;
396 my $id3 = $album2songs{$albumid}{$_};
397 dumpint
($$id3{'songoffset'});
400 for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) {
401 print DB
"\x00\x00\x00\x00";
405 #### TABLE of artists ###
407 # pointers to albums of artist1
409 for (sort keys %artists) {
411 my $str = $_."\x00" x
($maxartistlen - length($_));
414 for (sort keys %{$artist2albums{$artist}}) {
415 my $id3 = $artist2albums{$artist}{$_};
416 my $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'ARTIST'}"} * $albumentrysize;
417 dumpint
($a + $albumindex);
420 for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) {
421 print DB
"\x00\x00\x00\x00";
430 ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor
431 ### Modified by Björn Stenberg to remove use of external libraries
435 @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION,
436 @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder,
437 @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields,
438 @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD,
444 set_mp3tag get_mp3tag get_mp3info remove_mp3tag
447 @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8);
449 genres => [qw(@mp3_genres %mp3_genres)],
450 utf8 => [qw(use_mp3_utf8)],
451 all
=> [@EXPORT, @EXPORT_OK]
455 ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
462 MP3::Info - Manipulate / fetch info from MP3 audio files
468 my $file = 'Pearls_Before_Swine.mp3';
469 set_mp3tag($file, 'Pearls Before Swine', q"77's",
470 'Sticks and Stones', '1990',
471 q"(c) 1990 77's LTD.", 'rock & roll');
473 my $tag = get_mp3tag($file) or die "No TAG info";
474 $tag->{GENRE} = 'rock';
475 set_mp3tag($file, $tag);
477 my $info = get_mp3info($file);
478 printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
484 # set all lower-case and regular-cased versions of genres as keys
485 # with index as value of each key
486 %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres;
488 # do it again for winamp genres
490 %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
495 my $mp3 = new MP3::Info $file;
496 $mp3->title('Perls Before Swine');
497 printf "$file length is %s, title is %s\n",
498 $mp3->time, $mp3->title;
505 =item $mp3 = MP3::Info-E<gt>new(FILE)
507 OOP interface to the rest of the module. The same keys
508 available via get_mp3info and get_mp3tag are available
509 via the returned object (using upper case or lower case;
510 but note that all-caps "VERSION" will return the module
511 version, not the MP3 version).
513 Passing a value to one of the methods will set the value
514 for that tag in the MP3 file, if applicable.
519 my($pack, $file) = @_;
521 my $info = get_mp3info
($file) or return undef;
522 my $tags = get_mp3tag
($file) || { map { ($_ => undef) } @v1_tag_names };
528 @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
529 @
{$info}{@mp3_info_fields},
530 @
{$tags}{@v1_tag_names},
534 return bless \
%self, $pack;
539 return $self->SUPER::can
(@_) unless ref $self;
541 return sub { $self->$name(@_) } if exists $self->{$name};
547 (my $name = uc $AUTOLOAD) =~ s/^.*://;
549 if (exists $self->{$name}) {
550 my $sub = exists $v1_tag_fields{$name}
553 $_[0]->{$name} = $_[1];
554 set_mp3tag
($_[0]->{FILE
}, $_[0]);
556 return $_[0]->{$name};
559 return $_[0]->{$name}
567 warn(sprintf "No method '$name' available in package %s.",
577 =item use_mp3_utf8([STATUS])
579 Tells MP3::Info to (or not) return TAG info in UTF-8.
580 TRUE is 1, FALSE is 0. Default is FALSE.
582 Will only be able to it on if Unicode::String is available. ID3v2
583 tags will be converted to UTF-8 according to the encoding specified
584 in each tag; ID3v1 tags will be assumed Latin-1 and converted
587 Function returns status (TRUE/FALSE). If no argument is supplied,
588 or an unaccepted argument is supplied, function merely returns status.
590 This function is not exported by default, but may be exported
591 with the C<:utf8> or C<:all> export tag.
595 my $unicode_module = eval { require Unicode
::String
};
601 $UNICODE = 1 if $unicode_module;
602 } elsif ($val == 0) {
610 =item use_winamp_genres()
612 Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
613 (adds 68 additional genres to the default list of 80).
614 This is a separate function because these are non-standard
615 genres, but they are included because they are widely used.
617 You can import the data structures with one of:
619 use MP3::Info qw(:genres);
620 use MP3::Info qw(:DEFAULT :genres);
621 use MP3::Info qw(:all);
625 sub use_winamp_genres
{
626 %mp3_genres = %winamp_genres;
627 @mp3_genres = @winamp_genres;
633 =item remove_mp3tag (FILE [, VERSION, BUFFER])
635 Can remove ID3v1 or ID3v2 tags. VERSION should be C<1> for ID3v1,
636 C<2> for ID3v2, and C<ALL> for both.
638 For ID3v1, removes last 128 bytes from file if those last 128 bytes begin
639 with the text 'TAG'. File will be 128 bytes shorter.
641 For ID3v2, removes ID3v2 tag. Because an ID3v2 tag is at the
642 beginning of the file, we rewrite the file after removing the tag data.
643 The buffer for rewriting the file is 4MB. BUFFER (in bytes) ca
644 change the buffer size.
646 Returns the number of bytes removed, or -1 if no tag removed,
647 or undef if there is an error.
652 my($file, $version, $buf) = @_;
655 $buf ||= 4096*1024; # the bigger the faster
658 if (not (defined $file && $file ne '')) {
659 $@
= "No file specified";
664 $@
= "File is empty";
668 if (ref $file) { # filehandle passed
672 if (not open $fh, "+< $file\0") {
673 $@
= "Can't open $file: $!";
680 if ($version eq 1 || $version eq 'ALL') {
683 if (<$fh> =~ /^TAG/) {
684 truncate $fh, $tell or warn "Can't truncate '$file': $!";
689 if ($version eq 2 || $version eq 'ALL') {
690 my $h = _get_v2head
($fh);
695 my $off = $h->{tag_size
};
697 while ($off < $eof) {
699 read $fh, my($bytes), $buf;
700 seek $fh, $off - $h->{tag_size
}, 0;
705 truncate $fh, $eof - $h->{tag_size
}
706 or warn "Can't truncate '$file': $!";
707 $return += $h->{tag_size
};
713 return $return || -1;
719 =item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
721 =item set_mp3tag (FILE, $HASHREF)
723 Adds/changes tag information in an MP3 audio file. Will clobber
724 any existing information in file.
726 Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE. All fields have
727 a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE,
728 which is one byte in the file. The GENRE passed in the function is a
729 case-insensitive text string representing a genre found in C<@mp3_genres>.
731 Will accept either a list of values, or a hashref of the type
732 returned by C<get_mp3tag>.
734 If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be
737 ID3v2 support may come eventually. Note that if you set a tag on a file
738 with ID3v2, the set tag will be for ID3v1[.1] only, and if you call
739 C<get_mp3_tag> on the file, it will show you the (unchanged) ID3v2 tags,
740 unless you specify ID3v1.
745 my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_;
746 my(%info, $oldfh, $ref, $fh);
747 local %v1_tag_fields = %v1_tag_fields;
749 # set each to '' if undef
750 for ($title, $artist, $album, $year, $comment, $tracknum, $genre,
751 (@info{@v1_tag_names}))
752 {$_ = defined() ?
$_ : ''}
754 ($ref) = (overload
::StrVal
($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/)
756 # populate data to hashref if hashref is not passed
758 (@info{@v1_tag_names}) =
759 ($title, $artist, $album, $year, $comment, $tracknum, $genre);
761 # put data from hashref into hashref if hashref is passed
762 } elsif ($ref eq 'HASH') {
768 Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
769 set_mp3tag (FILE, $HASHREF)
774 if (not (defined $file && $file ne '')) {
775 $@
= "No file specified";
780 $@
= "File is empty";
784 # comment field length 28 if ID3v1.1
785 $v1_tag_fields{COMMENT
} = 28 if $info{TRACKNUM
};
790 # warn if fields too long
791 foreach my $field (keys %v1_tag_fields) {
792 $info{$field} = '' unless defined $info{$field};
793 if (length($info{$field}) > $v1_tag_fields{$field}) {
794 warn "Data too long for field $field: truncated to " .
795 "$v1_tag_fields{$field}";
800 warn "Genre `$info{GENRE}' does not exist\n"
801 unless exists $mp3_genres{$info{GENRE
}};
805 if ($info{TRACKNUM
}) {
806 $info{TRACKNUM
} =~ s/^(\d+)\/(\d+)$/$1/;
807 unless ($info{TRACKNUM
} =~ /^\d+$/ &&
808 $info{TRACKNUM
} > 0 && $info{TRACKNUM
} < 256) {
809 warn "Tracknum `$info{TRACKNUM}' must be an integer " .
810 "from 1 and 255\n" if $^W
;
811 $info{TRACKNUM
} = '';
815 if (ref $file) { # filehandle passed
819 if (not open $fh, "+< $file\0") {
820 $@
= "Can't open $file: $!";
828 # go to end of file if no tag, beginning of file if tag
829 seek $fh, (<$fh> =~ /^TAG/ ?
-128 : 0), 2;
832 $info{GENRE
} = $info{GENRE
} && exists $mp3_genres{$info{GENRE
}} ?
833 $mp3_genres{$info{GENRE
}} : 255; # some default genre
837 if ($info{TRACKNUM
}) {
838 print pack "a3a30a30a30a4a28xCC", 'TAG', @info{@v1_tag_names};
840 print pack "a3a30a30a30a4a30C", 'TAG', @info{@v1_tag_names[0..4, 6]};
852 =item get_mp3tag (FILE [, VERSION, RAW_V2])
854 Returns hash reference containing tag information in MP3 file. The keys
855 returned are the same as those supplied for C<set_mp3tag>, except in the
856 case of RAW_V2 being set.
858 If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
859 If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
860 If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
861 then, if present, the ID3v2 tag information will override any existing ID3v1
864 If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
865 of text encoding. The key name is the same as the frame ID (ID to name mappings
866 are in the global %v2_tag_names).
868 If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
869 necessary, etc. It also takes multiple values for a given key (such as comments)
870 and puts them in an arrayref.
872 If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
875 Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
876 (unless RAW_V2 is C<1>).
878 Also returns a TAGVERSION key, containing the ID3 version used for the returned
879 data (if TAGVERSION argument is C<0>, may contain two versions).
884 my($file, $ver, $raw_v2) = @_;
885 my($tag, $v1, $v2, $v2h, %info, @array, $fh);
887 $ver = !$ver ?
0 : ($ver == 2 || $ver == 1) ?
$ver : 0;
889 if (not (defined $file && $file ne '')) {
890 $@
= "No file specified";
895 $@
= "File is empty";
899 if (ref $file) { # filehandle passed
903 if (not open $fh, "< $file\0") {
904 $@
= "Can't open $file: $!";
913 while(defined(my $line = <$fh>)) { $tag .= $line }
915 if ($tag =~ /^TAG/) {
917 if (substr($tag, -3, 2) =~ /\000[^\000]/) {
918 (undef, @info{@v1_tag_names}) =
919 (unpack('a3a30a30a30a4a28', $tag),
920 ord(substr($tag, -2, 1)),
921 $mp3_genres[ord(substr $tag, -1)]);
922 $info{TAGVERSION
} = 'ID3v1.1';
924 (undef, @info{@v1_tag_names[0..4, 6]}) =
925 (unpack('a3a30a30a30a4a30', $tag),
926 $mp3_genres[ord(substr $tag, -1)]);
927 $info{TAGVERSION
} = 'ID3v1';
930 for my $key (keys %info) {
931 next unless $info{$key};
932 my $u = Unicode
::String
::latin1
($info{$key});
933 $info{$key} = $u->utf8;
936 } elsif ($ver == 1) {
938 $@
= "No ID3v1 tag found";
943 ($v2, $v2h) = _get_v2tag
($fh);
945 unless ($v1 || $v2) {
947 $@
= "No ID3 tag found";
951 if (($ver == 0 || $ver == 2) && $v2) {
952 if ($raw_v2 == 1 && $ver == 2) {
954 $info{TAGVERSION
} = $v2h->{version
};
956 my $hash = $raw_v2 == 2 ?
{ map { ($_, $_) } keys %v2_tag_names } : \
%v2_to_v1_names;
957 for my $id (keys %$hash) {
958 if (exists $v2->{$id}) {
959 if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) {
960 $info{$hash->{$id}} = $mp3_genres[$1];
962 my $data1 = $v2->{$id};
964 # this is tricky ... if this is an arrayref,
965 # we want to only return one, so we pick the
966 # first one. but if it is a comment, we pick
967 # the first one where the first charcter after
968 # the language is NULL and not an additional
969 # sub-comment, because that is most likely to be
970 # the user-supplied comment
971 if (ref $data1 && !$raw_v2) {
972 if ($id =~ /^COMM?$/) {
973 my($newdata) = grep /^(....\000)/, @
{$data1};
974 $data1 = $newdata || $data1->[0];
976 $data1 = $data1->[0];
980 $data1 = [ $data1 ] if ! ref $data1;
982 for my $data (@
$data1) {
983 $data =~ s/^(.)//; # strip first char (text encoding)
986 if ($id =~ /^COM[M ]?$/) {
987 $data =~ s/^(?:...)//; # strip language
988 $data =~ s/^(.*?)\000+//; # strip up to first NULL(s),
994 if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE
995 my $u = Unicode
::String
::utf16
($data);
997 $data =~ s/^\xEF\xBB\xBF//; # strip BOM
998 } elsif ($encoding eq "\000") {
999 my $u = Unicode
::String
::latin1
($data);
1004 if ($raw_v2 == 2 && $desc) {
1005 $data = { $desc => $data };
1008 if ($raw_v2 == 2 && exists $info{$hash->{$id}}) {
1009 if (ref $info{$hash->{$id}} eq 'ARRAY') {
1010 push @
{$info{$hash->{$id}}}, $data;
1012 $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ];
1015 $info{$hash->{$id}} = $data;
1021 if ($ver == 0 && $info{TAGVERSION
}) {
1022 $info{TAGVERSION
} .= ' / ' . $v2h->{version
};
1024 $info{TAGVERSION
} = $v2h->{version
};
1029 unless ($raw_v2 && $ver == 2) {
1030 foreach my $key (keys %info) {
1031 if (defined $info{$key}) {
1032 $info{$key} =~ s/\000+.*//g;
1033 $info{$key} =~ s/\s+$//;
1037 for (@v1_tag_names) {
1038 $info{$_} = '' unless defined $info{$_};
1042 if (keys %info && exists $info{GENRE
} && ! defined $info{GENRE
}) {
1048 return keys %info ?
{%info} : undef;
1053 my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num);
1056 $v2 = _get_v2head
($fh) or return;
1057 if ($v2->{major_version
} < 2) {
1058 warn "This is $v2->{version}; " .
1059 "ID3v2 versions older than ID3v2.2.0 not supported\n"
1064 if ($v2->{major_version
} == 2) {
1074 read $fh, my($bytes), $hlen;
1075 return unless $bytes =~ /^([A-Z0-9]{$num})/
1076 || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes
1077 my($id, $size) = ($1, $hlen);
1078 my @bytes = reverse unpack "C$num", substr($bytes, $num, $num);
1079 for my $i (0 .. ($num - 1)) {
1080 $size += $bytes[$i] * 256 ** $i;
1085 $off = $v2->{ext_header_size
} + 10;
1087 while ($off < $v2->{tag_size
}) {
1088 my($id, $size) = &$myseek or last;
1089 seek $fh, $off + $hlen, 0;
1090 read $fh, my($bytes), $size - $hlen;
1091 if (exists $h->{$id}) {
1092 if (ref $h->{$id} eq 'ARRAY') {
1093 push @
{$h->{$id}}, $bytes;
1095 $h->{$id} = [$h->{$id}, $bytes];
1109 =item get_mp3info (FILE)
1111 Returns hash reference containing file information for MP3 file.
1112 This data cannot be changed. Returned data:
1114 VERSION MPEG audio version (1, 2, 2.5)
1115 LAYER MPEG layer description (1, 2, 3)
1116 STEREO boolean for audio is in stereo
1118 VBR boolean for variable bitrate
1119 BITRATE bitrate in kbps (average for VBR files)
1120 FREQUENCY frequency in kHz
1121 SIZE bytes in audio stream
1126 MS leftover milliseconds
1129 COPYRIGHT boolean for audio is copyrighted
1130 PADDING boolean for MP3 frames are padded
1131 MODE channel mode (0 = stereo, 1 = joint stereo,
1132 2 = dual channel, 3 = single channel)
1133 FRAMES approximate number of frames
1134 FRAME_LENGTH approximate length of a frame
1135 VBR_SCALE VBR scale from VBR header
1137 On error, returns nothing and sets C<$@>.
1143 my($off, $myseek, $byte, $eof, $h, $tot, $fh);
1145 if (not (defined $file && $file ne '')) {
1146 $@
= "No file specified";
1151 $@
= "File is empty";
1155 if (ref $file) { # filehandle passed
1159 if (not open $fh, "< $file\0") {
1160 $@
= "Can't open $file: $!";
1177 if (my $id3v2 = _get_v2head
($fh)) {
1178 $tot += $off += $id3v2->{tag_size
};
1183 $h = _get_head
($byte);
1184 until (_is_mp3
($h)) {
1187 $h = _get_head
($byte);
1188 if ($off > $tot && !$try_harder) {
1190 $@
= "Couldn't find MP3 header (perhaps set " .
1191 '$MP3::Info::try_harder and retry)';
1196 my $vbr = _get_vbr
($fh, $h, \
$off);
1201 $off += 128 if <$fh> =~ /^TAG/ ?
1 : 0;
1205 $h->{size
} = $eof - $off;
1207 return _get_info
($h, $vbr);
1214 $i->{VERSION
} = $h->{IDR
} == 2 ?
2 : $h->{IDR
} == 3 ?
1 :
1215 $h->{IDR
} == 0 ?
2.5 : 0;
1216 $i->{LAYER
} = 4 - $h->{layer
};
1217 $i->{VBR
} = defined $vbr ?
1 : 0;
1219 $i->{COPYRIGHT
} = $h->{copyright
} ?
1 : 0;
1220 $i->{PADDING
} = $h->{padding_bit
} ?
1 : 0;
1221 $i->{STEREO
} = $h->{mode
} == 3 ?
0 : 1;
1222 $i->{MODE
} = $h->{mode
};
1224 $i->{SIZE
} = $vbr && $vbr->{bytes
} ?
$vbr->{bytes
} : $h->{size
};
1226 my $mfs = $h->{fs
} / ($h->{ID
} ?
144000 : 72000);
1227 $i->{FRAMES
} = int($vbr && $vbr->{frames
}
1229 : $i->{SIZE
} / $h->{bitrate} / $mfs
1233 $i->{VBR_SCALE
} = $vbr->{scale
} if $vbr->{scale
};
1234 $h->{bitrate
} = $i->{SIZE
} / $i->{FRAMES
} * $mfs;
1235 if (not $h->{bitrate
}) {
1236 $@
= "Couldn't determine VBR bitrate";
1241 $h->{'length'} = ($i->{SIZE
} * 8) / $h->{bitrate} / 10;
1242 $i->{SECS
} = $h->{'length'} / 100;
1243 $i->{MM
} = int $i->{SECS
} / 60;
1244 $i->{SS
} = int $i->{SECS
} % 60;
1245 $i->{MS
} = (($i->{SECS
} - ($i->{MM
} * 60) - $i->{SS
}) * 1000);
1246 # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS});
1247 # int($i->{MS} / 100 * 75); # is this right?
1248 $i->{TIME
} = sprintf "%.2d:%.2d", @
{$i}{'MM', 'SS'};
1250 $i->{BITRATE
} = int $h->{bitrate
};
1251 # should we just return if ! FRAMES?
1252 $i->{FRAME_LENGTH
} = int($h->{size
} / $i->{FRAMES
}) if $i->{FRAMES
};
1253 $i->{FREQUENCY
} = $frequency_tbl[3 * $h->{IDR
} + $h->{sampling_freq
}];
1262 $bytes = _unpack_head
($byte);
1263 @
$h{qw(IDR ID layer protection_bit
1264 bitrate_index sampling_freq padding_bit private_bit
1265 mode mode_extension copyright original
1266 emphasis version_index bytes)} = (
1267 ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1,
1268 ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1,
1269 ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1,
1270 $bytes&3, ($bytes>>19)&3, $bytes
1273 $h->{bitrate
} = $t_bitrate[$h->{ID
}][3 - $h->{layer
}][$h->{bitrate_index
}];
1274 $h->{fs
} = $t_sampling_freq[$h->{IDR
}][$h->{sampling_freq
}];
1280 my $h = $_[0] or return undef;
1281 return ! ( # all below must be false
1282 $h->{bitrate_index
} == 0
1284 $h->{version_index
} == 1
1286 ($h->{bytes
} & 0xFFE00000) != 0xFFE00000
1292 $h->{bitrate_index
} == 15
1296 $h->{sampling_freq
} == 3
1300 !$h->{bitrate_index
}
1302 ($h->{bytes
} & 0xFFFF0000) == 0xFFFE0000
1304 ($h->{ID
} == 1 && $h->{layer
} == 3 && $h->{protection_bit
} == 1)
1306 ($h->{mode_extension
} != 0 && $h->{mode
} != 1)
1311 my($fh, $h, $roff) = @_;
1312 my($off, $bytes, @bytes, $myseek, %vbr);
1315 @_ = (); # closure confused if we don't do this
1320 read $fh, $bytes, $n;
1326 if ($h->{ID
}) { # MPEG1
1327 $off += $h->{mode
} == 3 ?
17 : 32;
1329 $off += $h->{mode
} == 3 ?
9 : 17;
1333 return unless $bytes eq 'Xing';
1336 $vbr{flags
} = _unpack_head
($bytes);
1338 if ($vbr{flags
} & 1) {
1340 $vbr{frames
} = _unpack_head
($bytes);
1343 if ($vbr{flags
} & 2) {
1345 $vbr{bytes
} = _unpack_head
($bytes);
1348 if ($vbr{flags
} & 4) {
1350 # Not used right now ...
1351 # $vbr{toc} = _unpack_head($bytes);
1354 if ($vbr{flags
} & 8) { # (quality ind., 0=best 100=worst)
1356 $vbr{scale
} = _unpack_head
($bytes);
1366 my $fh = $_[0] or return;
1367 my($h, $bytes, @bytes);
1369 # check first three bytes for 'ID3'
1371 read $fh, $bytes, 3;
1372 return unless $bytes eq 'ID3';
1375 read $fh, $bytes, 2;
1376 $h->{version
} = sprintf "ID3v2.%d.%d",
1377 @
$h{qw
[major_version minor_version
]} =
1378 unpack 'c2', $bytes;
1381 read $fh, $bytes, 1;
1382 if ($h->{major_version
} == 2) {
1383 @
$h{qw
[unsync compression
]} =
1384 (unpack 'b8', $bytes)[7, 6];
1385 $h->{ext_header
} = 0;
1386 $h->{experimental
} = 0;
1388 @
$h{qw
[unsync ext_header experimental
]} =
1389 (unpack 'b8', $bytes)[7, 6, 5];
1392 # get ID3v2 tag length from bytes 7-10
1393 $h->{tag_size
} = 10; # include ID3v2 header size
1394 read $fh, $bytes, 4;
1395 @bytes = reverse unpack 'C4', $bytes;
1396 foreach my $i (0 .. 3) {
1397 # whoaaaaaa nellllllyyyyyy!
1398 $h->{tag_size
} += $bytes[$i] * 128 ** $i;
1401 # get extended header size
1402 $h->{ext_header_size
} = 0;
1403 if ($h->{ext_header
}) {
1404 $h->{ext_header_size
} += 10;
1405 read $fh, $bytes, 4;
1406 @bytes = reverse unpack 'C4', $bytes;
1408 $h->{ext_header_size
} += $bytes[$i] * 256 ** $i;
1416 unpack('l', pack('L', unpack('N', $_[0])));
1420 my($file, $fh) = @_;
1421 unless (ref $file) { # filehandle not passed
1422 close $fh or warn "Problem closing '$file': $!";
1475 'Instrumental Rock',
1479 'Techno-Industrial',
1568 'Christian Gangsta Rap',
1572 'Contemporary Christian',
1583 [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
1584 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
1585 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
1587 [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
1588 [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
1589 [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
1592 @t_sampling_freq = (
1593 [11025, 12000, 8000],
1594 [undef, undef, undef], # reserved
1595 [22050, 24000, 16000],
1596 [44100, 48000, 32000]
1599 @frequency_tbl = map { $_ ?
eval "${_}e-3" : 0 }
1600 map { @
$_ } @t_sampling_freq;
1602 @mp3_info_fields = qw(
1624 (TITLE
=> 30, ARTIST
=> 30, ALBUM
=> 30, COMMENT
=> 30, YEAR
=> 4);
1626 @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
1635 'TRK' => 'TRACKNUM',
1636 'TCO' => 'GENRE', # not clean mapping, but ...
1642 'COMM' => 'COMMENT',
1643 'TRCK' => 'TRACKNUM',
1649 'BUF' => 'Recommended buffer size',
1650 'CNT' => 'Play counter',
1651 'COM' => 'Comments',
1652 'CRA' => 'Audio encryption',
1653 'CRM' => 'Encrypted meta frame',
1654 'ETC' => 'Event timing codes',
1655 'EQU' => 'Equalization',
1656 'GEO' => 'General encapsulated object',
1657 'IPL' => 'Involved people list',
1658 'LNK' => 'Linked information',
1659 'MCI' => 'Music CD Identifier',
1660 'MLL' => 'MPEG location lookup table',
1661 'PIC' => 'Attached picture',
1662 'POP' => 'Popularimeter',
1664 'RVA' => 'Relative volume adjustment',
1665 'SLT' => 'Synchronized lyric/text',
1666 'STC' => 'Synced tempo codes',
1667 'TAL' => 'Album/Movie/Show title',
1668 'TBP' => 'BPM (Beats Per Minute)',
1669 'TCM' => 'Composer',
1670 'TCO' => 'Content type',
1671 'TCR' => 'Copyright message',
1673 'TDY' => 'Playlist delay',
1674 'TEN' => 'Encoded by',
1675 'TFT' => 'File type',
1677 'TKE' => 'Initial key',
1678 'TLA' => 'Language(s)',
1680 'TMT' => 'Media type',
1681 'TOA' => 'Original artist(s)/performer(s)',
1682 'TOF' => 'Original filename',
1683 'TOL' => 'Original Lyricist(s)/text writer(s)',
1684 'TOR' => 'Original release year',
1685 'TOT' => 'Original album/Movie/Show title',
1686 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
1687 'TP2' => 'Band/Orchestra/Accompaniment',
1688 'TP3' => 'Conductor/Performer refinement',
1689 'TP4' => 'Interpreted, remixed, or otherwise modified by',
1690 'TPA' => 'Part of a set',
1691 'TPB' => 'Publisher',
1692 'TRC' => 'ISRC (International Standard Recording Code)',
1693 'TRD' => 'Recording dates',
1694 'TRK' => 'Track number/Position in set',
1696 'TSS' => 'Software/hardware and settings used for encoding',
1697 'TT1' => 'Content group description',
1698 'TT2' => 'Title/Songname/Content description',
1699 'TT3' => 'Subtitle/Description refinement',
1700 'TXT' => 'Lyricist/text writer',
1701 'TXX' => 'User defined text information frame',
1703 'UFI' => 'Unique file identifier',
1704 'ULT' => 'Unsychronized lyric/text transcription',
1705 'WAF' => 'Official audio file webpage',
1706 'WAR' => 'Official artist/performer webpage',
1707 'WAS' => 'Official audio source webpage',
1708 'WCM' => 'Commercial information',
1709 'WCP' => 'Copyright/Legal information',
1710 'WPB' => 'Publishers official webpage',
1711 'WXX' => 'User defined URL link frame',
1714 'AENC' => 'Audio encryption',
1715 'APIC' => 'Attached picture',
1716 'COMM' => 'Comments',
1717 'COMR' => 'Commercial frame',
1718 'ENCR' => 'Encryption method registration',
1719 'EQUA' => 'Equalization',
1720 'ETCO' => 'Event timing codes',
1721 'GEOB' => 'General encapsulated object',
1722 'GRID' => 'Group identification registration',
1723 'IPLS' => 'Involved people list',
1724 'LINK' => 'Linked information',
1725 'MCDI' => 'Music CD identifier',
1726 'MLLT' => 'MPEG location lookup table',
1727 'OWNE' => 'Ownership frame',
1728 'PCNT' => 'Play counter',
1729 'POPM' => 'Popularimeter',
1730 'POSS' => 'Position synchronisation frame',
1731 'PRIV' => 'Private frame',
1732 'RBUF' => 'Recommended buffer size',
1733 'RVAD' => 'Relative volume adjustment',
1735 'SYLT' => 'Synchronized lyric/text',
1736 'SYTC' => 'Synchronized tempo codes',
1737 'TALB' => 'Album/Movie/Show title',
1738 'TBPM' => 'BPM (beats per minute)',
1739 'TCOM' => 'Composer',
1740 'TCON' => 'Content type',
1741 'TCOP' => 'Copyright message',
1743 'TDLY' => 'Playlist delay',
1744 'TENC' => 'Encoded by',
1745 'TEXT' => 'Lyricist/Text writer',
1746 'TFLT' => 'File type',
1748 'TIT1' => 'Content group description',
1749 'TIT2' => 'Title/songname/content description',
1750 'TIT3' => 'Subtitle/Description refinement',
1751 'TKEY' => 'Initial key',
1752 'TLAN' => 'Language(s)',
1754 'TMED' => 'Media type',
1755 'TOAL' => 'Original album/movie/show title',
1756 'TOFN' => 'Original filename',
1757 'TOLY' => 'Original lyricist(s)/text writer(s)',
1758 'TOPE' => 'Original artist(s)/performer(s)',
1759 'TORY' => 'Original release year',
1760 'TOWN' => 'File owner/licensee',
1761 'TPE1' => 'Lead performer(s)/Soloist(s)',
1762 'TPE2' => 'Band/orchestra/accompaniment',
1763 'TPE3' => 'Conductor/performer refinement',
1764 'TPE4' => 'Interpreted, remixed, or otherwise modified by',
1765 'TPOS' => 'Part of a set',
1766 'TPUB' => 'Publisher',
1767 'TRCK' => 'Track number/Position in set',
1768 'TRDA' => 'Recording dates',
1769 'TRSN' => 'Internet radio station name',
1770 'TRSO' => 'Internet radio station owner',
1772 'TSRC' => 'ISRC (international standard recording code)',
1773 'TSSE' => 'Software/Hardware and settings used for encoding',
1774 'TXXX' => 'User defined text information frame',
1776 'UFID' => 'Unique file identifier',
1777 'USER' => 'Terms of use',
1778 'USLT' => 'Unsychronized lyric/text transcription',
1779 'WCOM' => 'Commercial information',
1780 'WCOP' => 'Copyright/Legal information',
1781 'WOAF' => 'Official audio file webpage',
1782 'WOAR' => 'Official artist/performer webpage',
1783 'WOAS' => 'Official audio source webpage',
1784 'WORS' => 'Official internet radio station homepage',
1785 'WPAY' => 'Payment',
1786 'WPUB' => 'Publishers official webpage',
1787 'WXXX' => 'User defined URL link frame',
1789 # v2.4 additional tags
1790 # note that we don't restrict tags from 2.3 or 2.4,
1791 'ASPI' => 'Audio seek point index',
1792 'EQU2' => 'Equalisation (2)',
1793 'RVA2' => 'Relative volume adjustment (2)',
1794 'SEEK' => 'Seek frame',
1795 'SIGN' => 'Signature frame',
1796 'TDEN' => 'Encoding time',
1797 'TDOR' => 'Original release time',
1798 'TDRC' => 'Recording time',
1799 'TDRL' => 'Release time',
1800 'TDTG' => 'Tagging time',
1801 'TIPL' => 'Involved people list',
1802 'TMCL' => 'Musician credits list',
1804 'TPRO' => 'Produced notice',
1805 'TSOA' => 'Album sort order',
1806 'TSOP' => 'Performer sort order',
1807 'TSOT' => 'Title sort order',
1808 'TSST' => 'Set subtitle',
1811 'COM ' => 'Broken iTunes comments',
1823 =head1 TROUBLESHOOTING
1825 If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
1826 If you cannot figure out why it does not work for you, please put the MP3 file in
1827 a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
1828 mail regarding where I can get the file, with a detailed description of the problem.
1830 If I download the file, after debugging the problem I will not keep the MP3 file
1831 if it is not legal for me to have it. Just let me know if it is legal for me to
1841 Still need to do more for reading tags, such as using Compress::Zlib to decompress
1842 compressed tags. But until I see this in use more, I won't bother. If something
1843 does not work properly with reading, follow the instructions above for
1846 ID3v2 I<writing> is coming soon.
1848 =item Get data from scalar
1850 Instead of passing a file spec or filehandle, pass the
1851 data itself. Would take some work, converting the seeks, etc.
1855 Do something with padding bit.
1859 Test suite could use a bit of an overhaul and update. Patches very welcome.
1865 Revamp getset.t. Test all the various get_mp3tag args.
1877 Test error handling, check more for missing files, bad MP3s, etc.
1883 Right now, only Xing VBR is supported.
1890 Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>,
1891 Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>,
1892 Michael Blakeley E<lt>mike@blakeley.comE<gt>,
1893 Per Bolmstedt E<lt>tomten@kol14.comE<gt>,
1894 Tony Bowden E<lt>tony@tmtm.comE<gt>,
1895 Tom Brown E<lt>thecap@usa.netE<gt>,
1896 Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>,
1897 Chris Dawson E<lt>cdawson@webiphany.comE<gt>,
1898 Luke Drumm E<lt>lukedrumm@mypad.comE<gt>,
1899 Kyle Farrell E<lt>kyle@cantametrix.comE<gt>,
1900 Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>,
1901 brian d foy E<lt>comdog@panix.comE<gt>,
1902 Ben Gertzfield E<lt>che@debian.orgE<gt>,
1903 Brian Goodwin E<lt>brian@fuddmain.comE<gt>,
1904 Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>,
1905 Todd Harris E<lt>harris@cshl.orgE<gt>,
1906 Woodrow Hill E<lt>asim@mindspring.comE<gt>,
1907 Kee Hinckley E<lt>nazgul@somewhere.comE<gt>,
1908 Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>,
1909 Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>,
1911 Peter Marschall E<lt>peter.marschall@mayn.deE<gt>,
1912 Trond Michelsen E<lt>mike@crusaders.noE<gt>,
1913 Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>,
1914 Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>,
1915 Jake Palmer E<lt>jake.palmer@db.comE<gt>,
1916 Andrew Phillips E<lt>asp@wasteland.orgE<gt>,
1917 David Reuteler E<lt>reuteler@visi.comE<gt>,
1918 John Ruttenberg E<lt>rutt@chezrutt.comE<gt>,
1919 Matthew Sachs E<lt>matthewg@zevils.comE<gt>,
1920 E<lt>scfc_de@users.sf.netE<gt>,
1921 Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>,
1922 Chris Sidi E<lt>sidi@angband.orgE<gt>,
1923 Roland Steinbach E<lt>roland@support-system.comE<gt>,
1924 Stuart E<lt>schneis@users.sourceforge.netE<gt>,
1925 Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>,
1926 Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>,
1927 Bogdan Surdu E<lt>tim@go.roE<gt>,
1928 E<lt>tim@tim-landscheidt.deE<gt>,
1929 Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>,
1930 Tobias Wagener E<lt>tobias@wagener.nuE<gt>,
1931 Ronan Waide E<lt>waider@stepstone.ieE<gt>,
1932 Andy Waite E<lt>andy@mailroute.comE<gt>,
1933 Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>,
1934 Meng Weng Wong E<lt>mengwong@pobox.comE<gt>.
1937 =head1 AUTHOR AND COPYRIGHT
1939 Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
1941 Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is
1942 free software; you can redistribute it and/or modify it under the terms
1943 of the Artistic License, distributed with Perl.
1950 =item MP3::Info Project Page
1952 http://projects.pudge.net/
1956 http://www.zevils.com/linux/mp3tools/
1960 http://www.dv.co.yu/mpgscript/mpgtools.htm
1961 http://www.dv.co.yu/mpgscript/mpeghdr.htm
1965 http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
1971 =item Xing Variable Bitrate
1973 http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
1977 http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
1981 http://www.xmms.org/
1988 v1.02, Sunday, March 2, 2003