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 $dir =~ s
|/$|| if ($dir ne "/");
64 if (opendir(DIR, $dir)) {
65 # my @mp3 = grep { /\.mp3$/ && -f "$dir/$_" } readdir(DIR);
66 my @all = readdir(DIR);
71 warn "can
't opendir $dir: $!\n";
76 my ($dir, @files) = @_;
79 if( /\.mp[23]$/ && -f "$dir/$_" ) {
87 my ($dir, @files) = @_;
91 if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
101 # print "Check $file\n";
103 my $hash = get_mp3tag($file);
104 # my $hash = get_mp3info($file);
107 # print "Info: $_ ".$hash->{$_}."\n";
109 return $hash; # a hash reference
119 # getdir() returns all entries in the given dir
120 my @a = getdir($dir);
122 # extractmp3 filters out only the mp3 files from all given entries
123 my @m = extractmp3($dir, @a);
129 my $id3 = singlefile("$dir/$f");
139 # don't
index songs without tags
140 if (not defined $$id3{'ARTIST'} and
141 not defined $$id3{'ALBUM'} and
142 not defined $$id3{'TITLE'})
147 #printf "Artist: %s\n", $id3->{'ARTIST'};
148 my $path = "$dir/$f";
149 if ($strip ne "" and $path =~ /^$strip(.*)/) {
153 $entries{$path}= $id3;
154 $artists{$id3->{'ARTIST'}}++ if($id3->{'ARTIST'});
155 $genres{$id3->{'GENRE'}}++ if($id3->{'GENRE'});
156 $years{$id3->{'YEAR'}}++ if($id3->{'YEAR'});
159 $$id3{'ARTIST'} = "<no artist tag>" if ($$id3{'ARTIST'} eq "");
160 $$id3{'ALBUM'} = "<no album tag>" if ($$id3{'ALBUM'} eq "");
161 $$id3{'TITLE'} = "<no title tag>" if ($$id3{'TITLE'} eq "");
163 # prepend Artist name to handle duplicate album names from other
165 my $albumid = $id3->{'ALBUM'}."___".$id3->{'ARTIST'};
166 if($albumid ne "<no album tag>___<no artist tag>") {
167 my $num = ++$albums{$albumid};
168 if($num > $maxsongperalbum) {
169 $maxsongperalbum = $num;
170 $longestalbum = $albumid;
172 $album2songs{$albumid}{$$id3{TITLE
}} = $id3;
173 $artist2albums{$$id3{ARTIST
}}{$$id3{ALBUM
}} = $id3;
177 # extractdirs filters out only subdirectories from all given entries
178 my @d = extractdirs
($dir, @a);
190 print "File name table\n" if ($verbose);
192 for(sort keys %entries) {
193 printf(" %s\n", $_) if ($verbose);
199 print "\nSong title table\n" if ($verbose);
201 for(sort {$entries{$a}->{'TITLE'} cmp $entries{$b}->{'TITLE'}} keys %entries) {
202 printf(" %s\n", $entries{$_}->{'TITLE'} ) if ($verbose);
203 my $l = length($entries{$_}->{'TITLE'});
204 if($l > $maxsonglen) {
206 $longestsong = $entries{$_}->{'TITLE'};
209 $maxsonglen++; # include zero termination byte
210 while($maxsonglen&3) {
214 my $maxartistlen = 0;
215 print "\nArtist table\n" if ($verbose);
218 for(sort keys %artists) {
219 printf(" %s\n", $_) if ($verbose);
220 $artistcount{$_}=$i++;
222 if($l > $maxartistlen) {
227 $l = scalar keys %{$artist2albums{$_}};
228 if ($l > $maxalbumsperartist) {
229 $maxalbumsperartist = $l;
232 $maxartistlen++; # include zero termination byte
233 while($maxartistlen&3) {
238 print "\nGenre table\n";
239 for(sort keys %genres) {
243 print "\nYear table\n";
244 for(sort keys %years) {
249 print "\nAlbum table\n" if ($verbose);
253 for(sort keys %albums) {
254 my @moo=split(/___/, $_);
255 printf(" %s\n", $moo[0]) if ($verbose);
256 $albumcount{$_} = $i++;
257 my $l = length($moo[0]);
258 if($l > $maxalbumlen) {
260 $longestalbumname = $moo[0];
263 $maxalbumlen++; # include zero termination byte
264 while($maxalbumlen&3) {
273 # print "int: $num\n";
275 printf DB
("%c%c%c%c",
282 if (!scalar keys %entries) {
283 print "No songs found. Did you specify the right --path ?\n";
284 print "Use the --help parameter to see all options.\n";
289 my $songentrysize = $maxsonglen + 12;
290 my $albumentrysize = $maxalbumlen + 4 + $maxsongperalbum*4;
291 my $artistentrysize = $maxartistlen + $maxalbumsperartist*4;
293 printf "Number of artists : %d\n", scalar keys %artists;
294 printf "Number of albums : %d\n", scalar keys %albums;
295 printf "Number of songs : %d\n", scalar keys %entries;
296 print "Max artist length : $maxartistlen ($longestartist)\n";
297 print "Max album length : $maxalbumlen ($longestalbumname)\n";
298 print "Max song length : $maxsonglen ($longestsong)\n";
299 print "Max songs per album: $maxsongperalbum ($longestalbum)\n";
300 print "Database version: $dbver\n" if ($verbose);
302 open(DB
, ">$db") || die "couldn't make $db";
304 printf DB
"RDB%c", $dbver;
306 $pathindex = 48; # paths always start at index 48
308 $songindex = $pathindex + $fc; # fc is size of all paths
309 $songindex++ while ($songindex & 3); # align to 32 bits
311 dumpint
($songindex); # file position index of song table
312 dumpint
(scalar(keys %entries)); # number of songs
313 dumpint
($maxsonglen); # length of song name field
315 # set total size of song title table
316 $sc = scalar(keys %entries) * $songentrysize;
318 $albumindex = $songindex + $sc; # sc is size of all songs
319 dumpint
($albumindex); # file position index of album table
320 dumpint
(scalar(keys %albums)); # number of albums
321 dumpint
($maxalbumlen); # length of album name field
322 dumpint
($maxsongperalbum); # number of entries in songs-per-album array
324 my $ac = scalar(keys %albums) * $albumentrysize;
326 $artistindex = $albumindex + $ac; # ac is size of all albums
327 dumpint
($artistindex); # file position index of artist table
328 dumpint
(scalar(keys %artists)); # number of artists
329 dumpint
($maxartistlen); # length of artist name field
330 dumpint
($maxalbumsperartist); # number of entries in albums-per-artist array
334 #### TABLE of file names ###
338 for $f (sort keys %entries) {
339 printf DB
("%s\x00", $f);
340 $filenamepos{$f}= $l;
348 #### TABLE of songs ###
350 # pointer to artist of song1
351 # pointer to album of song1
352 # pointer to filename of song1
354 my $offset = $songindex;
355 for(sort {$entries{$a}->{'TITLE'} cmp $entries{$b}->{'TITLE'}} keys %entries) {
357 my $id3 = $entries{$f};
358 my $t = $id3->{'TITLE'};
359 my $str = $t."\x00" x
($maxsonglen- length($t));
361 print DB
$str; # title
363 my $a = $artistcount{$id3->{'ARTIST'}} * $artistentrysize;
364 dumpint
($a + $artistindex); # pointer to artist of this song
366 $a = $albumcount{"$$id3{ALBUM}___$$id3{ARTIST}"} * $albumentrysize;
367 dumpint
($a + $albumindex); # pointer to album of this song
369 # pointer to filename of this song
370 dumpint
($filenamepos{$f} + $pathindex);
372 $$id3{'songoffset'} = $offset;
373 $offset += $songentrysize;
376 #### TABLE of albums ###
378 # pointers to artists of album1
379 # pointers to songs on album1
381 for(sort keys %albums) {
383 my @moo=split(/___/, $_);
385 my $str = $t."\x00" x
($maxalbumlen - length($t));
388 my $aoffset = $artistcount{$moo[0]} * $artistentrysize;
389 dumpint
($aoffset + $artistindex); # pointer to artist of this album
391 my @songlist = keys %{$album2songs{$albumid}};
392 my $id3 = $album2songs{$albumid}{$songlist[0]};
393 if (defined $id3->{'TRACKNUM'}) {
395 $album2songs{$albumid}{$a}->{'TRACKNUM'} <=>
396 $album2songs{$albumid}{$b}->{'TRACKNUM'}
400 @songlist = sort @songlist;
404 my $id3 = $album2songs{$albumid}{$_};
405 dumpint
($$id3{'songoffset'});
408 for (scalar keys %{$album2songs{$albumid}} .. $maxsongperalbum-1) {
409 print DB
"\x00\x00\x00\x00";
413 #### TABLE of artists ###
415 # pointers to albums of artist1
417 for (sort keys %artists) {
419 my $str = $_."\x00" x
($maxartistlen - length($_));
422 for (sort keys %{$artist2albums{$artist}}) {
423 my $id3 = $artist2albums{$artist}{$_};
424 my $a = $albumcount{"$$id3{'ALBUM'}___$$id3{'ARTIST'}"} * $albumentrysize;
425 dumpint
($a + $albumindex);
428 for (scalar keys %{$artist2albums{$artist}} .. $maxalbumsperartist-1) {
429 print DB
"\x00\x00\x00\x00";
438 ### Here follows module MP3::Info Copyright (c) 1998-2004 Chris Nandor
439 ### Modified by Björn Stenberg to remove use of external libraries
443 @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS, $VERSION, $REVISION,
444 @mp3_genres, %mp3_genres, @winamp_genres, %winamp_genres, $try_harder,
445 @t_bitrate, @t_sampling_freq, @frequency_tbl, %v1_tag_fields,
446 @v1_tag_names, %v2_tag_names, %v2_to_v1_names, $AUTOLOAD,
452 set_mp3tag get_mp3tag get_mp3info remove_mp3tag
455 @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8);
457 genres => [qw(@mp3_genres %mp3_genres)],
458 utf8 => [qw(use_mp3_utf8)],
459 all
=> [@EXPORT, @EXPORT_OK]
463 ($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
470 MP3::Info - Manipulate / fetch info from MP3 audio files
476 my $file = 'Pearls_Before_Swine.mp3';
477 set_mp3tag($file, 'Pearls Before Swine', q"77's",
478 'Sticks and Stones', '1990',
479 q"(c) 1990 77's LTD.", 'rock & roll');
481 my $tag = get_mp3tag($file) or die "No TAG info";
482 $tag->{GENRE} = 'rock';
483 set_mp3tag($file, $tag);
485 my $info = get_mp3info($file);
486 printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
492 # set all lower-case and regular-cased versions of genres as keys
493 # with index as value of each key
494 %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres;
496 # do it again for winamp genres
498 %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
503 my $mp3 = new MP3::Info $file;
504 $mp3->title('Perls Before Swine');
505 printf "$file length is %s, title is %s\n",
506 $mp3->time, $mp3->title;
513 =item $mp3 = MP3::Info-E<gt>new(FILE)
515 OOP interface to the rest of the module. The same keys
516 available via get_mp3info and get_mp3tag are available
517 via the returned object (using upper case or lower case;
518 but note that all-caps "VERSION" will return the module
519 version, not the MP3 version).
521 Passing a value to one of the methods will set the value
522 for that tag in the MP3 file, if applicable.
527 my($pack, $file) = @_;
529 my $info = get_mp3info
($file) or return undef;
530 my $tags = get_mp3tag
($file) || { map { ($_ => undef) } @v1_tag_names };
536 @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
537 @
{$info}{@mp3_info_fields},
538 @
{$tags}{@v1_tag_names},
542 return bless \
%self, $pack;
547 return $self->SUPER::can
(@_) unless ref $self;
549 return sub { $self->$name(@_) } if exists $self->{$name};
555 (my $name = uc $AUTOLOAD) =~ s/^.*://;
557 if (exists $self->{$name}) {
558 my $sub = exists $v1_tag_fields{$name}
561 $_[0]->{$name} = $_[1];
562 set_mp3tag
($_[0]->{FILE
}, $_[0]);
564 return $_[0]->{$name};
567 return $_[0]->{$name}
574 warn(sprintf "No method '$name' available in package %s.",
584 =item use_mp3_utf8([STATUS])
586 Tells MP3::Info to (or not) return TAG info in UTF-8.
587 TRUE is 1, FALSE is 0. Default is FALSE.
589 Will only be able to it on if Unicode::String is available. ID3v2
590 tags will be converted to UTF-8 according to the encoding specified
591 in each tag; ID3v1 tags will be assumed Latin-1 and converted
594 Function returns status (TRUE/FALSE). If no argument is supplied,
595 or an unaccepted argument is supplied, function merely returns status.
597 This function is not exported by default, but may be exported
598 with the C<:utf8> or C<:all> export tag.
602 my $unicode_module = eval { require Unicode
::String
};
608 $UNICODE = 1 if $unicode_module;
609 } elsif ($val == 0) {
617 =item use_winamp_genres()
619 Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
620 (adds 68 additional genres to the default list of 80).
621 This is a separate function because these are non-standard
622 genres, but they are included because they are widely used.
624 You can import the data structures with one of:
626 use MP3::Info qw(:genres);
627 use MP3::Info qw(:DEFAULT :genres);
628 use MP3::Info qw(:all);
632 sub use_winamp_genres
{
633 %mp3_genres = %winamp_genres;
634 @mp3_genres = @winamp_genres;
640 =item remove_mp3tag (FILE [, VERSION, BUFFER])
642 Can remove ID3v1 or ID3v2 tags. VERSION should be C<1> for ID3v1,
643 C<2> for ID3v2, and C<ALL> for both.
645 For ID3v1, removes last 128 bytes from file if those last 128 bytes begin
646 with the text 'TAG'. File will be 128 bytes shorter.
648 For ID3v2, removes ID3v2 tag. Because an ID3v2 tag is at the
649 beginning of the file, we rewrite the file after removing the tag data.
650 The buffer for rewriting the file is 4MB. BUFFER (in bytes) ca
651 change the buffer size.
653 Returns the number of bytes removed, or -1 if no tag removed,
654 or undef if there is an error.
659 my($file, $version, $buf) = @_;
662 $buf ||= 4096*1024; # the bigger the faster
665 if (not (defined $file && $file ne '')) {
666 $@
= "No file specified";
671 $@
= "File is empty";
675 if (ref $file) { # filehandle passed
679 if (not open $fh, "+< $file\0") {
680 $@
= "Can't open $file: $!";
687 if ($version eq 1 || $version eq 'ALL') {
690 if (<$fh> =~ /^TAG/) {
691 truncate $fh, $tell or warn "Can't truncate '$file': $!";
696 if ($version eq 2 || $version eq 'ALL') {
697 my $h = _get_v2head
($fh);
702 my $off = $h->{tag_size
};
704 while ($off < $eof) {
706 read $fh, my($bytes), $buf;
707 seek $fh, $off - $h->{tag_size
}, 0;
712 truncate $fh, $eof - $h->{tag_size
}
713 or warn "Can't truncate '$file': $!";
714 $return += $h->{tag_size
};
720 return $return || -1;
726 =item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
728 =item set_mp3tag (FILE, $HASHREF)
730 Adds/changes tag information in an MP3 audio file. Will clobber
731 any existing information in file.
733 Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE. All fields have
734 a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE,
735 which is one byte in the file. The GENRE passed in the function is a
736 case-insensitive text string representing a genre found in C<@mp3_genres>.
738 Will accept either a list of values, or a hashref of the type
739 returned by C<get_mp3tag>.
741 If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be
744 ID3v2 support may come eventually. Note that if you set a tag on a file
745 with ID3v2, the set tag will be for ID3v1[.1] only, and if you call
746 C<get_mp3_tag> on the file, it will show you the (unchanged) ID3v2 tags,
747 unless you specify ID3v1.
752 my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_;
753 my(%info, $oldfh, $ref, $fh);
754 local %v1_tag_fields = %v1_tag_fields;
756 # set each to '' if undef
757 for ($title, $artist, $album, $year, $comment, $tracknum, $genre,
758 (@info{@v1_tag_names}))
759 {$_ = defined() ?
$_ : ''}
761 ($ref) = (overload
::StrVal
($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/)
763 # populate data to hashref if hashref is not passed
765 (@info{@v1_tag_names}) =
766 ($title, $artist, $album, $year, $comment, $tracknum, $genre);
768 # put data from hashref into hashref if hashref is passed
769 } elsif ($ref eq 'HASH') {
775 Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
776 set_mp3tag (FILE, $HASHREF)
781 if (not (defined $file && $file ne '')) {
782 $@
= "No file specified";
787 $@
= "File is empty";
791 # comment field length 28 if ID3v1.1
792 $v1_tag_fields{COMMENT
} = 28 if $info{TRACKNUM
};
797 # warn if fields too long
798 foreach my $field (keys %v1_tag_fields) {
799 $info{$field} = '' unless defined $info{$field};
800 if (length($info{$field}) > $v1_tag_fields{$field}) {
801 warn "Data too long for field $field: truncated to " .
802 "$v1_tag_fields{$field}";
807 warn "Genre `$info{GENRE}' does not exist\n"
808 unless exists $mp3_genres{$info{GENRE
}};
812 if ($info{TRACKNUM
}) {
813 $info{TRACKNUM
} =~ s/^(\d+)\/(\d+)$/$1/;
814 unless ($info{TRACKNUM
} =~ /^\d+$/ &&
815 $info{TRACKNUM
} > 0 && $info{TRACKNUM
} < 256) {
816 warn "Tracknum `$info{TRACKNUM}' must be an integer " .
817 "from 1 and 255\n" if $^W
;
818 $info{TRACKNUM
} = '';
822 if (ref $file) { # filehandle passed
826 if (not open $fh, "+< $file\0") {
827 $@
= "Can't open $file: $!";
835 # go to end of file if no tag, beginning of file if tag
836 seek $fh, (<$fh> =~ /^TAG/ ?
-128 : 0), 2;
839 $info{GENRE
} = $info{GENRE
} && exists $mp3_genres{$info{GENRE
}} ?
840 $mp3_genres{$info{GENRE
}} : 255; # some default genre
844 if ($info{TRACKNUM
}) {
845 print pack "a3a30a30a30a4a28xCC", 'TAG', @info{@v1_tag_names};
847 print pack "a3a30a30a30a4a30C", 'TAG', @info{@v1_tag_names[0..4, 6]};
859 =item get_mp3tag (FILE [, VERSION, RAW_V2])
861 Returns hash reference containing tag information in MP3 file. The keys
862 returned are the same as those supplied for C<set_mp3tag>, except in the
863 case of RAW_V2 being set.
865 If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
866 If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
867 If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
868 then, if present, the ID3v2 tag information will override any existing ID3v1
871 If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
872 of text encoding. The key name is the same as the frame ID (ID to name mappings
873 are in the global %v2_tag_names).
875 If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
876 necessary, etc. It also takes multiple values for a given key (such as comments)
877 and puts them in an arrayref.
879 If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
882 Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
883 (unless RAW_V2 is C<1>).
885 Also returns a TAGVERSION key, containing the ID3 version used for the returned
886 data (if TAGVERSION argument is C<0>, may contain two versions).
891 my($file, $ver, $raw_v2) = @_;
892 my($tag, $v1, $v2, $v2h, %info, @array, $fh);
894 $ver = !$ver ?
0 : ($ver == 2 || $ver == 1) ?
$ver : 0;
896 if (not (defined $file && $file ne '')) {
897 $@
= "No file specified";
902 $@
= "File is empty";
906 if (ref $file) { # filehandle passed
910 if (not open $fh, "< $file\0") {
911 $@
= "Can't open $file: $!";
920 while(defined(my $line = <$fh>)) { $tag .= $line }
922 if ($tag =~ /^TAG/) {
924 if (substr($tag, -3, 2) =~ /\000[^\000]/) {
925 (undef, @info{@v1_tag_names}) =
926 (unpack('a3a30a30a30a4a28', $tag),
927 ord(substr($tag, -2, 1)),
928 $mp3_genres[ord(substr $tag, -1)]);
929 $info{TAGVERSION
} = 'ID3v1.1';
931 (undef, @info{@v1_tag_names[0..4, 6]}) =
932 (unpack('a3a30a30a30a4a30', $tag),
933 $mp3_genres[ord(substr $tag, -1)]);
934 $info{TAGVERSION
} = 'ID3v1';
937 for my $key (keys %info) {
938 next unless $info{$key};
939 my $u = Unicode
::String
::latin1
($info{$key});
940 $info{$key} = $u->utf8;
943 } elsif ($ver == 1) {
945 $@
= "No ID3v1 tag found";
950 ($v2, $v2h) = _get_v2tag
($fh);
952 unless ($v1 || $v2) {
954 $@
= "No ID3 tag found";
958 if (($ver == 0 || $ver == 2) && $v2) {
959 if ($raw_v2 == 1 && $ver == 2) {
961 $info{TAGVERSION
} = $v2h->{version
};
963 my $hash = $raw_v2 == 2 ?
{ map { ($_, $_) } keys %v2_tag_names } : \
%v2_to_v1_names;
964 for my $id (keys %$hash) {
965 if (exists $v2->{$id}) {
966 if ($id =~ /^TCON?$/ && $v2->{$id} =~ /^.?\((\d+)\)/) {
967 $info{$hash->{$id}} = $mp3_genres[$1];
969 my $data1 = $v2->{$id};
971 # this is tricky ... if this is an arrayref, we want
972 # to only return one, so we pick the first one. but
973 # if it is a comment, we pick the first one where the
974 # first charcter after the language is NULL and not an
975 # additional sub-comment, because that is most likely
976 # to be the user-supplied comment
978 if (ref $data1 && !$raw_v2) {
979 if ($id =~ /^COMM?$/) {
980 my($newdata) = grep /^(....\000)/, @
{$data1};
981 $data1 = $newdata || $data1->[0];
983 $data1 = $data1->[0];
987 $data1 = [ $data1 ] if ! ref $data1;
989 for my $data (@
$data1) {
990 $data =~ s/^(.)//; # strip first char (text encoding)
993 if ($id =~ /^COM[M ]?$/) {
994 $data =~ s/^(?:...)//; # strip language
995 $data =~ s/^(.*?)\000+//; # strip up to first NULL(s),
1001 if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE
1002 my $u = Unicode
::String
::utf16
($data);
1004 $data =~ s/^\xEF\xBB\xBF//; # strip BOM
1005 } elsif ($encoding eq "\000") {
1006 my $u = Unicode
::String
::latin1
($data);
1011 if ($raw_v2 == 2 && $desc) {
1012 $data = { $desc => $data };
1015 if ($raw_v2 == 2 && exists $info{$hash->{$id}}) {
1016 if (ref $info{$hash->{$id}} eq 'ARRAY') {
1017 push @
{$info{$hash->{$id}}}, $data;
1019 $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ];
1022 $info{$hash->{$id}} = $data;
1028 if ($ver == 0 && $info{TAGVERSION
}) {
1029 $info{TAGVERSION
} .= ' / ' . $v2h->{version
};
1031 $info{TAGVERSION
} = $v2h->{version
};
1036 unless ($raw_v2 && $ver == 2) {
1037 foreach my $key (keys %info) {
1038 if (defined $info{$key}) {
1039 $info{$key} =~ s/\000+.*//g;
1040 $info{$key} =~ s/\s+$//;
1044 for (@v1_tag_names) {
1045 $info{$_} = '' unless defined $info{$_};
1049 if (keys %info && exists $info{GENRE
} && ! defined $info{GENRE
}) {
1055 return keys %info ?
{%info} : undef;
1060 my($off, $myseek, $myseek_22, $myseek_23, $v2, $h, $hlen, $num);
1063 $v2 = _get_v2head
($fh) or return;
1064 if ($v2->{major_version
} < 2) {
1065 warn "This is $v2->{version}; " .
1066 "ID3v2 versions older than ID3v2.2.0 not supported\n"
1071 if ($v2->{major_version
} == 2) {
1081 read $fh, my($bytes), $hlen;
1082 return unless $bytes =~ /^([A-Z0-9]{$num})/
1083 || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes
1084 my($id, $size) = ($1, $hlen);
1085 my @bytes = reverse unpack "C$num", substr($bytes, $num, $num);
1086 for my $i (0 .. ($num - 1)) {
1087 $size += $bytes[$i] * 256 ** $i;
1092 $off = $v2->{ext_header_size
} + 10;
1094 while ($off < $v2->{tag_size
}) {
1095 my($id, $size) = &$myseek or last;
1096 seek $fh, $off + $hlen, 0;
1097 read $fh, my($bytes), $size - $hlen;
1098 if (exists $h->{$id}) {
1099 if (ref $h->{$id} eq 'ARRAY') {
1100 push @
{$h->{$id}}, $bytes;
1102 $h->{$id} = [$h->{$id}, $bytes];
1116 =item get_mp3info (FILE)
1118 Returns hash reference containing file information for MP3 file.
1119 This data cannot be changed. Returned data:
1121 VERSION MPEG audio version (1, 2, 2.5)
1122 LAYER MPEG layer description (1, 2, 3)
1123 STEREO boolean for audio is in stereo
1125 VBR boolean for variable bitrate
1126 BITRATE bitrate in kbps (average for VBR files)
1127 FREQUENCY frequency in kHz
1128 SIZE bytes in audio stream
1133 MS leftover milliseconds
1136 COPYRIGHT boolean for audio is copyrighted
1137 PADDING boolean for MP3 frames are padded
1138 MODE channel mode (0 = stereo, 1 = joint stereo,
1139 2 = dual channel, 3 = single channel)
1140 FRAMES approximate number of frames
1141 FRAME_LENGTH approximate length of a frame
1142 VBR_SCALE VBR scale from VBR header
1144 On error, returns nothing and sets C<$@>.
1150 my($off, $myseek, $byte, $eof, $h, $tot, $fh);
1152 if (not (defined $file && $file ne '')) {
1153 $@
= "No file specified";
1158 $@
= "File is empty";
1162 if (ref $file) { # filehandle passed
1166 if (not open $fh, "< $file\0") {
1167 $@
= "Can't open $file: $!";
1184 if (my $id3v2 = _get_v2head
($fh)) {
1185 $tot += $off += $id3v2->{tag_size
};
1190 $h = _get_head
($byte);
1191 until (_is_mp3
($h)) {
1194 $h = _get_head
($byte);
1195 if ($off > $tot && !$try_harder) {
1197 $@
= "Couldn't find MP3 header (perhaps set " .
1198 '$MP3::Info::try_harder and retry)';
1203 my $vbr = _get_vbr
($fh, $h, \
$off);
1208 $off += 128 if <$fh> =~ /^TAG/ ?
1 : 0;
1212 $h->{size
} = $eof - $off;
1214 return _get_info
($h, $vbr);
1221 $i->{VERSION
} = $h->{IDR
} == 2 ?
2 : $h->{IDR
} == 3 ?
1 :
1222 $h->{IDR
} == 0 ?
2.5 : 0;
1223 $i->{LAYER
} = 4 - $h->{layer
};
1224 $i->{VBR
} = defined $vbr ?
1 : 0;
1226 $i->{COPYRIGHT
} = $h->{copyright
} ?
1 : 0;
1227 $i->{PADDING
} = $h->{padding_bit
} ?
1 : 0;
1228 $i->{STEREO
} = $h->{mode
} == 3 ?
0 : 1;
1229 $i->{MODE
} = $h->{mode
};
1231 $i->{SIZE
} = $vbr && $vbr->{bytes
} ?
$vbr->{bytes
} : $h->{size
};
1233 my $mfs = $h->{fs
} / ($h->{ID
} ?
144000 : 72000);
1234 $i->{FRAMES
} = int($vbr && $vbr->{frames
}
1236 : $i->{SIZE
} / $h->{bitrate} / $mfs
1240 $i->{VBR_SCALE
} = $vbr->{scale
} if $vbr->{scale
};
1241 $h->{bitrate
} = $i->{SIZE
} / $i->{FRAMES
} * $mfs;
1242 if (not $h->{bitrate
}) {
1243 $@
= "Couldn't determine VBR bitrate";
1248 $h->{'length'} = ($i->{SIZE
} * 8) / $h->{bitrate} / 10;
1249 $i->{SECS
} = $h->{'length'} / 100;
1250 $i->{MM
} = int $i->{SECS
} / 60;
1251 $i->{SS
} = int $i->{SECS
} % 60;
1252 $i->{MS
} = (($i->{SECS
} - ($i->{MM
} * 60) - $i->{SS
}) * 1000);
1253 # $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS});
1254 # int($i->{MS} / 100 * 75); # is this right?
1255 $i->{TIME
} = sprintf "%.2d:%.2d", @
{$i}{'MM', 'SS'};
1257 $i->{BITRATE
} = int $h->{bitrate
};
1258 # should we just return if ! FRAMES?
1259 $i->{FRAME_LENGTH
} = int($h->{size
} / $i->{FRAMES
}) if $i->{FRAMES
};
1260 $i->{FREQUENCY
} = $frequency_tbl[3 * $h->{IDR
} + $h->{sampling_freq
}];
1269 $bytes = _unpack_head
($byte);
1270 @
$h{qw(IDR ID layer protection_bit
1271 bitrate_index sampling_freq padding_bit private_bit
1272 mode mode_extension copyright original
1273 emphasis version_index bytes)} = (
1274 ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1,
1275 ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1,
1276 ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1,
1277 $bytes&3, ($bytes>>19)&3, $bytes
1280 $h->{bitrate
} = $t_bitrate[$h->{ID
}][3 - $h->{layer
}][$h->{bitrate_index
}];
1281 $h->{fs
} = $t_sampling_freq[$h->{IDR
}][$h->{sampling_freq
}];
1287 my $h = $_[0] or return undef;
1288 return ! ( # all below must be false
1289 $h->{bitrate_index
} == 0
1291 $h->{version_index
} == 1
1293 ($h->{bytes
} & 0xFFE00000) != 0xFFE00000
1299 $h->{bitrate_index
} == 15
1303 $h->{sampling_freq
} == 3
1307 !$h->{bitrate_index
}
1309 ($h->{bytes
} & 0xFFFF0000) == 0xFFFE0000
1311 ($h->{ID
} == 1 && $h->{layer
} == 3 && $h->{protection_bit
} == 1)
1313 ($h->{mode_extension
} != 0 && $h->{mode
} != 1)
1318 my($fh, $h, $roff) = @_;
1319 my($off, $bytes, @bytes, $myseek, %vbr);
1322 @_ = (); # closure confused if we don't do this
1327 read $fh, $bytes, $n;
1333 if ($h->{ID
}) { # MPEG1
1334 $off += $h->{mode
} == 3 ?
17 : 32;
1336 $off += $h->{mode
} == 3 ?
9 : 17;
1340 return unless $bytes eq 'Xing';
1343 $vbr{flags
} = _unpack_head
($bytes);
1345 if ($vbr{flags
} & 1) {
1347 $vbr{frames
} = _unpack_head
($bytes);
1350 if ($vbr{flags
} & 2) {
1352 $vbr{bytes
} = _unpack_head
($bytes);
1355 if ($vbr{flags
} & 4) {
1357 # Not used right now ...
1358 # $vbr{toc} = _unpack_head($bytes);
1361 if ($vbr{flags
} & 8) { # (quality ind., 0=best 100=worst)
1363 $vbr{scale
} = _unpack_head
($bytes);
1373 my $fh = $_[0] or return;
1374 my($h, $bytes, @bytes);
1376 # check first three bytes for 'ID3'
1378 read $fh, $bytes, 3;
1379 return unless $bytes eq 'ID3';
1382 read $fh, $bytes, 2;
1383 $h->{version
} = sprintf "ID3v2.%d.%d",
1384 @
$h{qw
[major_version minor_version
]} =
1385 unpack 'c2', $bytes;
1388 read $fh, $bytes, 1;
1389 if ($h->{major_version
} == 2) {
1390 @
$h{qw
[unsync compression
]} =
1391 (unpack 'b8', $bytes)[7, 6];
1392 $h->{ext_header
} = 0;
1393 $h->{experimental
} = 0;
1395 @
$h{qw
[unsync ext_header experimental
]} =
1396 (unpack 'b8', $bytes)[7, 6, 5];
1399 # get ID3v2 tag length from bytes 7-10
1400 $h->{tag_size
} = 10; # include ID3v2 header size
1401 read $fh, $bytes, 4;
1402 @bytes = reverse unpack 'C4', $bytes;
1403 foreach my $i (0 .. 3) {
1404 # whoaaaaaa nellllllyyyyyy!
1405 $h->{tag_size
} += $bytes[$i] * 128 ** $i;
1408 # get extended header size
1409 $h->{ext_header_size
} = 0;
1410 if ($h->{ext_header
}) {
1411 $h->{ext_header_size
} += 10;
1412 read $fh, $bytes, 4;
1413 @bytes = reverse unpack 'C4', $bytes;
1415 $h->{ext_header_size
} += $bytes[$i] * 256 ** $i;
1423 unpack('l', pack('L', unpack('N', $_[0])));
1427 my($file, $fh) = @_;
1428 unless (ref $file) { # filehandle not passed
1429 close $fh or warn "Problem closing '$file': $!";
1482 'Instrumental Rock',
1486 'Techno-Industrial',
1575 'Christian Gangsta Rap',
1579 'Contemporary Christian',
1590 [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
1591 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
1592 [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
1594 [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
1595 [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
1596 [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
1599 @t_sampling_freq = (
1600 [11025, 12000, 8000],
1601 [undef, undef, undef], # reserved
1602 [22050, 24000, 16000],
1603 [44100, 48000, 32000]
1606 @frequency_tbl = map { $_ ?
eval "${_}e-3" : 0 }
1607 map { @
$_ } @t_sampling_freq;
1609 @mp3_info_fields = qw(
1631 (TITLE
=> 30, ARTIST
=> 30, ALBUM
=> 30, COMMENT
=> 30, YEAR
=> 4);
1633 @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
1642 'TRK' => 'TRACKNUM',
1643 'TCO' => 'GENRE', # not clean mapping, but ...
1649 'COMM' => 'COMMENT',
1650 'TRCK' => 'TRACKNUM',
1656 'BUF' => 'Recommended buffer size',
1657 'CNT' => 'Play counter',
1658 'COM' => 'Comments',
1659 'CRA' => 'Audio encryption',
1660 'CRM' => 'Encrypted meta frame',
1661 'ETC' => 'Event timing codes',
1662 'EQU' => 'Equalization',
1663 'GEO' => 'General encapsulated object',
1664 'IPL' => 'Involved people list',
1665 'LNK' => 'Linked information',
1666 'MCI' => 'Music CD Identifier',
1667 'MLL' => 'MPEG location lookup table',
1668 'PIC' => 'Attached picture',
1669 'POP' => 'Popularimeter',
1671 'RVA' => 'Relative volume adjustment',
1672 'SLT' => 'Synchronized lyric/text',
1673 'STC' => 'Synced tempo codes',
1674 'TAL' => 'Album/Movie/Show title',
1675 'TBP' => 'BPM (Beats Per Minute)',
1676 'TCM' => 'Composer',
1677 'TCO' => 'Content type',
1678 'TCR' => 'Copyright message',
1680 'TDY' => 'Playlist delay',
1681 'TEN' => 'Encoded by',
1682 'TFT' => 'File type',
1684 'TKE' => 'Initial key',
1685 'TLA' => 'Language(s)',
1687 'TMT' => 'Media type',
1688 'TOA' => 'Original artist(s)/performer(s)',
1689 'TOF' => 'Original filename',
1690 'TOL' => 'Original Lyricist(s)/text writer(s)',
1691 'TOR' => 'Original release year',
1692 'TOT' => 'Original album/Movie/Show title',
1693 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
1694 'TP2' => 'Band/Orchestra/Accompaniment',
1695 'TP3' => 'Conductor/Performer refinement',
1696 'TP4' => 'Interpreted, remixed, or otherwise modified by',
1697 'TPA' => 'Part of a set',
1698 'TPB' => 'Publisher',
1699 'TRC' => 'ISRC (International Standard Recording Code)',
1700 'TRD' => 'Recording dates',
1701 'TRK' => 'Track number/Position in set',
1703 'TSS' => 'Software/hardware and settings used for encoding',
1704 'TT1' => 'Content group description',
1705 'TT2' => 'Title/Songname/Content description',
1706 'TT3' => 'Subtitle/Description refinement',
1707 'TXT' => 'Lyricist/text writer',
1708 'TXX' => 'User defined text information frame',
1710 'UFI' => 'Unique file identifier',
1711 'ULT' => 'Unsychronized lyric/text transcription',
1712 'WAF' => 'Official audio file webpage',
1713 'WAR' => 'Official artist/performer webpage',
1714 'WAS' => 'Official audio source webpage',
1715 'WCM' => 'Commercial information',
1716 'WCP' => 'Copyright/Legal information',
1717 'WPB' => 'Publishers official webpage',
1718 'WXX' => 'User defined URL link frame',
1721 'AENC' => 'Audio encryption',
1722 'APIC' => 'Attached picture',
1723 'COMM' => 'Comments',
1724 'COMR' => 'Commercial frame',
1725 'ENCR' => 'Encryption method registration',
1726 'EQUA' => 'Equalization',
1727 'ETCO' => 'Event timing codes',
1728 'GEOB' => 'General encapsulated object',
1729 'GRID' => 'Group identification registration',
1730 'IPLS' => 'Involved people list',
1731 'LINK' => 'Linked information',
1732 'MCDI' => 'Music CD identifier',
1733 'MLLT' => 'MPEG location lookup table',
1734 'OWNE' => 'Ownership frame',
1735 'PCNT' => 'Play counter',
1736 'POPM' => 'Popularimeter',
1737 'POSS' => 'Position synchronisation frame',
1738 'PRIV' => 'Private frame',
1739 'RBUF' => 'Recommended buffer size',
1740 'RVAD' => 'Relative volume adjustment',
1742 'SYLT' => 'Synchronized lyric/text',
1743 'SYTC' => 'Synchronized tempo codes',
1744 'TALB' => 'Album/Movie/Show title',
1745 'TBPM' => 'BPM (beats per minute)',
1746 'TCOM' => 'Composer',
1747 'TCON' => 'Content type',
1748 'TCOP' => 'Copyright message',
1750 'TDLY' => 'Playlist delay',
1751 'TENC' => 'Encoded by',
1752 'TEXT' => 'Lyricist/Text writer',
1753 'TFLT' => 'File type',
1755 'TIT1' => 'Content group description',
1756 'TIT2' => 'Title/songname/content description',
1757 'TIT3' => 'Subtitle/Description refinement',
1758 'TKEY' => 'Initial key',
1759 'TLAN' => 'Language(s)',
1761 'TMED' => 'Media type',
1762 'TOAL' => 'Original album/movie/show title',
1763 'TOFN' => 'Original filename',
1764 'TOLY' => 'Original lyricist(s)/text writer(s)',
1765 'TOPE' => 'Original artist(s)/performer(s)',
1766 'TORY' => 'Original release year',
1767 'TOWN' => 'File owner/licensee',
1768 'TPE1' => 'Lead performer(s)/Soloist(s)',
1769 'TPE2' => 'Band/orchestra/accompaniment',
1770 'TPE3' => 'Conductor/performer refinement',
1771 'TPE4' => 'Interpreted, remixed, or otherwise modified by',
1772 'TPOS' => 'Part of a set',
1773 'TPUB' => 'Publisher',
1774 'TRCK' => 'Track number/Position in set',
1775 'TRDA' => 'Recording dates',
1776 'TRSN' => 'Internet radio station name',
1777 'TRSO' => 'Internet radio station owner',
1779 'TSRC' => 'ISRC (international standard recording code)',
1780 'TSSE' => 'Software/Hardware and settings used for encoding',
1781 'TXXX' => 'User defined text information frame',
1783 'UFID' => 'Unique file identifier',
1784 'USER' => 'Terms of use',
1785 'USLT' => 'Unsychronized lyric/text transcription',
1786 'WCOM' => 'Commercial information',
1787 'WCOP' => 'Copyright/Legal information',
1788 'WOAF' => 'Official audio file webpage',
1789 'WOAR' => 'Official artist/performer webpage',
1790 'WOAS' => 'Official audio source webpage',
1791 'WORS' => 'Official internet radio station homepage',
1792 'WPAY' => 'Payment',
1793 'WPUB' => 'Publishers official webpage',
1794 'WXXX' => 'User defined URL link frame',
1796 # v2.4 additional tags
1797 # note that we don't restrict tags from 2.3 or 2.4,
1798 'ASPI' => 'Audio seek point index',
1799 'EQU2' => 'Equalisation (2)',
1800 'RVA2' => 'Relative volume adjustment (2)',
1801 'SEEK' => 'Seek frame',
1802 'SIGN' => 'Signature frame',
1803 'TDEN' => 'Encoding time',
1804 'TDOR' => 'Original release time',
1805 'TDRC' => 'Recording time',
1806 'TDRL' => 'Release time',
1807 'TDTG' => 'Tagging time',
1808 'TIPL' => 'Involved people list',
1809 'TMCL' => 'Musician credits list',
1811 'TPRO' => 'Produced notice',
1812 'TSOA' => 'Album sort order',
1813 'TSOP' => 'Performer sort order',
1814 'TSOT' => 'Title sort order',
1815 'TSST' => 'Set subtitle',
1818 'COM ' => 'Broken iTunes comments',
1830 =head1 TROUBLESHOOTING
1832 If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
1833 If you cannot figure out why it does not work for you, please put the MP3 file in
1834 a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
1835 mail regarding where I can get the file, with a detailed description of the problem.
1837 If I download the file, after debugging the problem I will not keep the MP3 file
1838 if it is not legal for me to have it. Just let me know if it is legal for me to
1848 Still need to do more for reading tags, such as using Compress::Zlib to decompress
1849 compressed tags. But until I see this in use more, I won't bother. If something
1850 does not work properly with reading, follow the instructions above for
1853 ID3v2 I<writing> is coming soon.
1855 =item Get data from scalar
1857 Instead of passing a file spec or filehandle, pass the
1858 data itself. Would take some work, converting the seeks, etc.
1862 Do something with padding bit.
1866 Test suite could use a bit of an overhaul and update. Patches very welcome.
1872 Revamp getset.t. Test all the various get_mp3tag args.
1884 Test error handling, check more for missing files, bad MP3s, etc.
1890 Right now, only Xing VBR is supported.
1897 Edward Allen E<lt>allenej@c51844-a.spokn1.wa.home.comE<gt>,
1898 Vittorio Bertola E<lt>v.bertola@vitaminic.comE<gt>,
1899 Michael Blakeley E<lt>mike@blakeley.comE<gt>,
1900 Per Bolmstedt E<lt>tomten@kol14.comE<gt>,
1901 Tony Bowden E<lt>tony@tmtm.comE<gt>,
1902 Tom Brown E<lt>thecap@usa.netE<gt>,
1903 Sergio Camarena E<lt>scamarena@users.sourceforge.netE<gt>,
1904 Chris Dawson E<lt>cdawson@webiphany.comE<gt>,
1905 Luke Drumm E<lt>lukedrumm@mypad.comE<gt>,
1906 Kyle Farrell E<lt>kyle@cantametrix.comE<gt>,
1907 Jeffrey Friedl E<lt>jfriedl@yahoo.comE<gt>,
1908 brian d foy E<lt>comdog@panix.comE<gt>,
1909 Ben Gertzfield E<lt>che@debian.orgE<gt>,
1910 Brian Goodwin E<lt>brian@fuddmain.comE<gt>,
1911 Todd Hanneken E<lt>thanneken@hds.harvard.eduE<gt>,
1912 Todd Harris E<lt>harris@cshl.orgE<gt>,
1913 Woodrow Hill E<lt>asim@mindspring.comE<gt>,
1914 Kee Hinckley E<lt>nazgul@somewhere.comE<gt>,
1915 Roman Hodek E<lt>Roman.Hodek@informatik.uni-erlangen.deE<gt>,
1916 Peter Kovacs E<lt>kovacsp@egr.uri.eduE<gt>,
1918 Peter Marschall E<lt>peter.marschall@mayn.deE<gt>,
1919 Trond Michelsen E<lt>mike@crusaders.noE<gt>,
1920 Dave O'Neill E<lt>dave@nexus.carleton.caE<gt>,
1921 Christoph Oberauer E<lt>christoph.oberauer@sbg.ac.atE<gt>,
1922 Jake Palmer E<lt>jake.palmer@db.comE<gt>,
1923 Andrew Phillips E<lt>asp@wasteland.orgE<gt>,
1924 David Reuteler E<lt>reuteler@visi.comE<gt>,
1925 John Ruttenberg E<lt>rutt@chezrutt.comE<gt>,
1926 Matthew Sachs E<lt>matthewg@zevils.comE<gt>,
1927 E<lt>scfc_de@users.sf.netE<gt>,
1928 Hermann Schwaerzler E<lt>Hermann.Schwaerzler@uibk.ac.atE<gt>,
1929 Chris Sidi E<lt>sidi@angband.orgE<gt>,
1930 Roland Steinbach E<lt>roland@support-system.comE<gt>,
1931 Stuart E<lt>schneis@users.sourceforge.netE<gt>,
1932 Jeffery Sumler E<lt>jsumler@mediaone.netE<gt>,
1933 Predrag Supurovic E<lt>mpgtools@dv.co.yuE<gt>,
1934 Bogdan Surdu E<lt>tim@go.roE<gt>,
1935 E<lt>tim@tim-landscheidt.deE<gt>,
1936 Pass F. B. Travis E<lt>pftravis@bellsouth.netE<gt>,
1937 Tobias Wagener E<lt>tobias@wagener.nuE<gt>,
1938 Ronan Waide E<lt>waider@stepstone.ieE<gt>,
1939 Andy Waite E<lt>andy@mailroute.comE<gt>,
1940 Ken Williams E<lt>ken@forum.swarthmore.eduE<gt>,
1941 Meng Weng Wong E<lt>mengwong@pobox.comE<gt>.
1944 =head1 AUTHOR AND COPYRIGHT
1946 Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
1948 Copyright (c) 1998-2003 Chris Nandor. All rights reserved. This program is
1949 free software; you can redistribute it and/or modify it under the terms
1950 of the Artistic License, distributed with Perl.
1957 =item MP3::Info Project Page
1959 http://projects.pudge.net/
1963 http://www.zevils.com/linux/mp3tools/
1967 http://www.dv.co.yu/mpgscript/mpgtools.htm
1968 http://www.dv.co.yu/mpgscript/mpeghdr.htm
1972 http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
1978 =item Xing Variable Bitrate
1980 http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
1984 http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
1988 http://www.xmms.org/
1995 v1.02, Sunday, March 2, 2003