···11+package mp3info;
22+33+require 5.006;
44+55+use overload;
66+use strict;
77+use Carp;
88+99+use vars qw(
1010+ @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $REVISION
1111+ @mp3_genres %mp3_genres @winamp_genres %winamp_genres $try_harder
1212+ @t_bitrate @t_sampling_freq @frequency_tbl %v1_tag_fields
1313+ @v1_tag_names %v2_tag_names %v2_to_v1_names $AUTOLOAD
1414+ @mp3_info_fields %rva2_channel_types
1515+);
1616+1717+@ISA = 'Exporter';
1818+@EXPORT = qw(
1919+ set_mp3tag get_mp3tag get_mp3info remove_mp3tag
2020+ use_winamp_genres, use_mp3_utf8
2121+);
2222+@EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8);
2323+%EXPORT_TAGS = (
2424+ genres => [qw(@mp3_genres %mp3_genres)],
2525+ utf8 => [qw(use_mp3_utf8)],
2626+ all => [@EXPORT, @EXPORT_OK]
2727+);
2828+2929+# $Id$
3030+($REVISION) = ' $Revision$ ' =~ /\$Revision:\s+([^\s]+)/;
3131+$VERSION = '1.20';
3232+3333+=pod
3434+3535+=head1 NAME
3636+3737+MP3::Info - Manipulate / fetch info from MP3 audio files
3838+3939+=head1 SYNOPSIS
4040+4141+ #!perl -w
4242+ use MP3::Info;
4343+ my $file = 'Pearls_Before_Swine.mp3';
4444+ set_mp3tag($file, 'Pearls Before Swine', q"77's",
4545+ 'Sticks and Stones', '1990',
4646+ q"(c) 1990 77's LTD.", 'rock & roll');
4747+4848+ my $tag = get_mp3tag($file) or die "No TAG info";
4949+ $tag->{GENRE} = 'rock';
5050+ set_mp3tag($file, $tag);
5151+5252+ my $info = get_mp3info($file);
5353+ printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
5454+5555+=cut
5656+5757+{
5858+ my $c = -1;
5959+ # set all lower-case and regular-cased versions of genres as keys
6060+ # with index as value of each key
6161+ %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres;
6262+6363+ # do it again for winamp genres
6464+ $c = -1;
6565+ %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres;
6666+}
6767+6868+=pod
6969+7070+ my $mp3 = new MP3::Info $file;
7171+ $mp3->title('Perls Before Swine');
7272+ printf "$file length is %s, title is %s\n",
7373+ $mp3->time, $mp3->title;
7474+7575+7676+=head1 DESCRIPTION
7777+7878+=over 4
7979+8080+=item $mp3 = MP3::Info-E<gt>new(FILE)
8181+8282+OOP interface to the rest of the module. The same keys
8383+available via get_mp3info and get_mp3tag are available
8484+via the returned object (using upper case or lower case;
8585+but note that all-caps "VERSION" will return the module
8686+version, not the MP3 version).
8787+8888+Passing a value to one of the methods will set the value
8989+for that tag in the MP3 file, if applicable.
9090+9191+=cut
9292+9393+sub new {
9494+ my($pack, $file) = @_;
9595+9696+ my $info = get_mp3info($file) or return undef;
9797+ my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names };
9898+ my %self = (
9999+ FILE => $file,
100100+ TRY_HARDER => 0
101101+ );
102102+103103+ @self{@mp3_info_fields, @v1_tag_names, 'file'} = (
104104+ @{$info}{@mp3_info_fields},
105105+ @{$tags}{@v1_tag_names},
106106+ $file
107107+ );
108108+109109+ return bless \%self, $pack;
110110+}
111111+112112+sub can {
113113+ my $self = shift;
114114+ return $self->SUPER::can(@_) unless ref $self;
115115+ my $name = uc shift;
116116+ return sub { $self->$name(@_) } if exists $self->{$name};
117117+ return undef;
118118+}
119119+120120+sub AUTOLOAD {
121121+ my($self) = @_;
122122+ (my $name = uc $AUTOLOAD) =~ s/^.*://;
123123+124124+ if (exists $self->{$name}) {
125125+ my $sub = exists $v1_tag_fields{$name}
126126+ ? sub {
127127+ if (defined $_[1]) {
128128+ $_[0]->{$name} = $_[1];
129129+ set_mp3tag($_[0]->{FILE}, $_[0]);
130130+ }
131131+ return $_[0]->{$name};
132132+ }
133133+ : sub {
134134+ return $_[0]->{$name}
135135+ };
136136+137137+ no strict 'refs';
138138+ *{$AUTOLOAD} = $sub;
139139+ goto &$AUTOLOAD;
140140+141141+ } else {
142142+ carp(sprintf "No method '$name' available in package %s.",
143143+ __PACKAGE__);
144144+ }
145145+}
146146+147147+sub DESTROY {
148148+149149+}
150150+151151+152152+=item use_mp3_utf8([STATUS])
153153+154154+Tells MP3::Info to (or not) return TAG info in UTF-8.
155155+TRUE is 1, FALSE is 0. Default is TRUE, if available.
156156+157157+Will only be able to turn it on if Encode is available. ID3v2
158158+tags will be converted to UTF-8 according to the encoding specified
159159+in each tag; ID3v1 tags will be assumed Latin-1 and converted
160160+to UTF-8.
161161+162162+Function returns status (TRUE/FALSE). If no argument is supplied,
163163+or an unaccepted argument is supplied, function merely returns status.
164164+165165+This function is not exported by default, but may be exported
166166+with the C<:utf8> or C<:all> export tag.
167167+168168+=cut
169169+170170+my $unicode_module = eval { require Encode; require Encode::Guess };
171171+my $UNICODE = use_mp3_utf8($unicode_module ? 1 : 0);
172172+173173+sub use_mp3_utf8 {
174174+ my($val) = @_;
175175+ if ($val == 1) {
176176+ if ($unicode_module) {
177177+ $UNICODE = 1;
178178+ $Encode::Guess::NoUTFAutoGuess = 1;
179179+ }
180180+ } elsif ($val == 0) {
181181+ $UNICODE = 0;
182182+ }
183183+ return $UNICODE;
184184+}
185185+186186+=pod
187187+188188+=item use_winamp_genres()
189189+190190+Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres>
191191+(adds 68 additional genres to the default list of 80).
192192+This is a separate function because these are non-standard
193193+genres, but they are included because they are widely used.
194194+195195+You can import the data structures with one of:
196196+197197+ use MP3::Info qw(:genres);
198198+ use MP3::Info qw(:DEFAULT :genres);
199199+ use MP3::Info qw(:all);
200200+201201+=cut
202202+203203+sub use_winamp_genres {
204204+ %mp3_genres = %winamp_genres;
205205+ @mp3_genres = @winamp_genres;
206206+ return 1;
207207+}
208208+209209+=pod
210210+211211+=item remove_mp3tag (FILE [, VERSION, BUFFER])
212212+213213+Can remove ID3v1 or ID3v2 tags. VERSION should be C<1> for ID3v1
214214+(the default), C<2> for ID3v2, and C<ALL> for both.
215215+216216+For ID3v1, removes last 128 bytes from file if those last 128 bytes begin
217217+with the text 'TAG'. File will be 128 bytes shorter.
218218+219219+For ID3v2, removes ID3v2 tag. Because an ID3v2 tag is at the
220220+beginning of the file, we rewrite the file after removing the tag data.
221221+The buffer for rewriting the file is 4MB. BUFFER (in bytes) ca
222222+change the buffer size.
223223+224224+Returns the number of bytes removed, or -1 if no tag removed,
225225+or undef if there is an error.
226226+227227+=cut
228228+229229+sub remove_mp3tag {
230230+ my($file, $version, $buf) = @_;
231231+ my($fh, $return);
232232+233233+ $buf ||= 4096*1024; # the bigger the faster
234234+ $version ||= 1;
235235+236236+ if (not (defined $file && $file ne '')) {
237237+ $@ = "No file specified";
238238+ return undef;
239239+ }
240240+241241+ if (not -s $file) {
242242+ $@ = "File is empty";
243243+ return undef;
244244+ }
245245+246246+ if (ref $file) { # filehandle passed
247247+ $fh = $file;
248248+ } else {
249249+ if (not open $fh, '+<', $file) {
250250+ $@ = "Can't open $file: $!";
251251+ return undef;
252252+ }
253253+ }
254254+255255+ binmode $fh;
256256+257257+ if ($version eq 1 || $version eq 'ALL') {
258258+ seek $fh, -128, 2;
259259+ my $tell = tell $fh;
260260+ if (<$fh> =~ /^TAG/) {
261261+ truncate $fh, $tell or carp "Can't truncate '$file': $!";
262262+ $return += 128;
263263+ }
264264+ }
265265+266266+ if ($version eq 2 || $version eq 'ALL') {
267267+ my $v2h = _get_v2head($fh);
268268+ if ($v2h) {
269269+ local $\;
270270+ seek $fh, 0, 2;
271271+ my $eof = tell $fh;
272272+ my $off = $v2h->{tag_size};
273273+274274+ while ($off < $eof) {
275275+ seek $fh, $off, 0;
276276+ read $fh, my($bytes), $buf;
277277+ seek $fh, $off - $v2h->{tag_size}, 0;
278278+ print $fh $bytes;
279279+ $off += $buf;
280280+ }
281281+282282+ truncate $fh, $eof - $v2h->{tag_size}
283283+ or carp "Can't truncate '$file': $!";
284284+ $return += $v2h->{tag_size};
285285+ }
286286+ }
287287+288288+ _close($file, $fh);
289289+290290+ return $return || -1;
291291+}
292292+293293+294294+=pod
295295+296296+=item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
297297+298298+=item set_mp3tag (FILE, $HASHREF)
299299+300300+Adds/changes tag information in an MP3 audio file. Will clobber
301301+any existing information in file.
302302+303303+Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE. All fields have
304304+a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE,
305305+which is one byte in the file. The GENRE passed in the function is a
306306+case-insensitive text string representing a genre found in C<@mp3_genres>.
307307+308308+Will accept either a list of values, or a hashref of the type
309309+returned by C<get_mp3tag>.
310310+311311+If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be
312312+28 bytes.
313313+314314+ID3v2 support may come eventually. Note that if you set a tag on a file
315315+with ID3v2, the set tag will be for ID3v1[.1] only, and if you call
316316+C<get_mp3tag> on the file, it will show you the (unchanged) ID3v2 tags,
317317+unless you specify ID3v1.
318318+319319+=cut
320320+321321+sub set_mp3tag {
322322+ my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_;
323323+ my(%info, $oldfh, $ref, $fh);
324324+ local %v1_tag_fields = %v1_tag_fields;
325325+326326+ # set each to '' if undef
327327+ for ($title, $artist, $album, $year, $comment, $tracknum, $genre,
328328+ (@info{@v1_tag_names}))
329329+ {$_ = defined() ? $_ : ''}
330330+331331+ ($ref) = (overload::StrVal($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/)
332332+ if ref $title;
333333+ # populate data to hashref if hashref is not passed
334334+ if (!$ref) {
335335+ (@info{@v1_tag_names}) =
336336+ ($title, $artist, $album, $year, $comment, $tracknum, $genre);
337337+338338+ # put data from hashref into hashref if hashref is passed
339339+ } elsif ($ref eq 'HASH') {
340340+ %info = %$title;
341341+342342+ # return otherwise
343343+ } else {
344344+ carp(<<'EOT');
345345+Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM])
346346+ set_mp3tag (FILE, $HASHREF)
347347+EOT
348348+ return undef;
349349+ }
350350+351351+ if (not (defined $file && $file ne '')) {
352352+ $@ = "No file specified";
353353+ return undef;
354354+ }
355355+356356+ if (not -s $file) {
357357+ $@ = "File is empty";
358358+ return undef;
359359+ }
360360+361361+ # comment field length 28 if ID3v1.1
362362+ $v1_tag_fields{COMMENT} = 28 if $info{TRACKNUM};
363363+364364+365365+ # only if -w is on
366366+ if ($^W) {
367367+ # warn if fields too long
368368+ foreach my $field (keys %v1_tag_fields) {
369369+ $info{$field} = '' unless defined $info{$field};
370370+ if (length($info{$field}) > $v1_tag_fields{$field}) {
371371+ carp "Data too long for field $field: truncated to " .
372372+ "$v1_tag_fields{$field}";
373373+ }
374374+ }
375375+376376+ if ($info{GENRE}) {
377377+ carp "Genre `$info{GENRE}' does not exist\n"
378378+ unless exists $mp3_genres{$info{GENRE}};
379379+ }
380380+ }
381381+382382+ if ($info{TRACKNUM}) {
383383+ $info{TRACKNUM} =~ s/^(\d+)\/(\d+)$/$1/;
384384+ unless ($info{TRACKNUM} =~ /^\d+$/ &&
385385+ $info{TRACKNUM} > 0 && $info{TRACKNUM} < 256) {
386386+ carp "Tracknum `$info{TRACKNUM}' must be an integer " .
387387+ "from 1 and 255\n" if $^W;
388388+ $info{TRACKNUM} = '';
389389+ }
390390+ }
391391+392392+ if (ref $file) { # filehandle passed
393393+ $fh = $file;
394394+ } else {
395395+ if (not open $fh, '+<', $file) {
396396+ $@ = "Can't open $file: $!";
397397+ return undef;
398398+ }
399399+ }
400400+401401+ binmode $fh;
402402+ $oldfh = select $fh;
403403+ seek $fh, -128, 2;
404404+ # go to end of file if no tag, beginning of file if tag
405405+ seek $fh, (<$fh> =~ /^TAG/ ? -128 : 0), 2;
406406+407407+ # get genre value
408408+ $info{GENRE} = $info{GENRE} && exists $mp3_genres{$info{GENRE}} ?
409409+ $mp3_genres{$info{GENRE}} : 255; # some default genre
410410+411411+ local $\;
412412+ # print TAG to file
413413+ if ($info{TRACKNUM}) {
414414+ print pack 'a3a30a30a30a4a28xCC', 'TAG', @info{@v1_tag_names};
415415+ } else {
416416+ print pack 'a3a30a30a30a4a30C', 'TAG', @info{@v1_tag_names[0..4, 6]};
417417+ }
418418+419419+ select $oldfh;
420420+421421+ _close($file, $fh);
422422+423423+ return 1;
424424+}
425425+426426+=pod
427427+428428+=item get_mp3tag (FILE [, VERSION, RAW_V2])
429429+430430+Returns hash reference containing tag information in MP3 file. The keys
431431+returned are the same as those supplied for C<set_mp3tag>, except in the
432432+case of RAW_V2 being set.
433433+434434+If VERSION is C<1>, the information is taken from the ID3v1 tag (if present).
435435+If VERSION is C<2>, the information is taken from the ID3v2 tag (if present).
436436+If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and
437437+then, if present, the ID3v2 tag information will override any existing ID3v1
438438+tag info.
439439+440440+If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation
441441+of text encoding. The key name is the same as the frame ID (ID to name mappings
442442+are in the global %v2_tag_names).
443443+444444+If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if
445445+necessary, etc. It also takes multiple values for a given key (such as comments)
446446+and puts them in an arrayref.
447447+448448+If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will
449449+not be read.
450450+451451+Strings returned will be in Latin-1, unless UTF-8 is specified (L<use_mp3_utf8>),
452452+(unless RAW_V2 is C<1>).
453453+454454+Also returns a TAGVERSION key, containing the ID3 version used for the returned
455455+data (if TAGVERSION argument is C<0>, may contain two versions).
456456+457457+=cut
458458+459459+sub get_mp3tag {
460460+ my ($file, $ver, $raw_v2, $find_ape) = @_;
461461+ my ($tag, $v2h, $fh);
462462+463463+ my $v1 = {};
464464+ my $v2 = {};
465465+ my $ape = {};
466466+ my %info = ();
467467+ my @array = ();
468468+469469+ $raw_v2 ||= 0;
470470+ $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0;
471471+472472+ if (not (defined $file && $file ne '')) {
473473+ $@ = "No file specified";
474474+ return undef;
475475+ }
476476+477477+ my $filesize = -s $file;
478478+479479+ if (!$filesize) {
480480+ $@ = "File is empty";
481481+ return undef;
482482+ }
483483+484484+ if (ref $file) { # filehandle passed
485485+ $fh = $file;
486486+ } else {
487487+ if (not open $fh, '<', $file) {
488488+ $@ = "Can't open $file: $!";
489489+ return undef;
490490+ }
491491+ }
492492+493493+ binmode $fh;
494494+495495+ # Try and find an APE Tag - this is where FooBar2k & others
496496+ # store ReplayGain information
497497+ if ($find_ape) {
498498+499499+ $ape = _parse_ape_tag($fh, $filesize, \%info);
500500+ }
501501+502502+ if ($ver < 2) {
503503+504504+ $v1 = _get_v1tag($fh, \%info);
505505+506506+ if ($ver == 1 && !$v1) {
507507+ _close($file, $fh);
508508+ $@ = "No ID3v1 tag found";
509509+ return undef;
510510+ }
511511+ }
512512+513513+ if ($ver == 2 || $ver == 0) {
514514+ ($v2, $v2h) = _get_v2tag($fh);
515515+ }
516516+517517+ if (!$v1 && !$v2 && !$ape) {
518518+ _close($file, $fh);
519519+ $@ = "No ID3 tag found";
520520+ return undef;
521521+ }
522522+523523+ if (($ver == 0 || $ver == 2) && $v2) {
524524+525525+ if ($raw_v2 == 1 && $ver == 2) {
526526+527527+ %info = %$v2;
528528+529529+ $info{'TAGVERSION'} = $v2h->{'version'};
530530+531531+ } else {
532532+533533+ _parse_v2tag($raw_v2, $v2, \%info);
534534+535535+ if ($ver == 0 && $info{'TAGVERSION'}) {
536536+ $info{'TAGVERSION'} .= ' / ' . $v2h->{'version'};
537537+ } else {
538538+ $info{'TAGVERSION'} = $v2h->{'version'};
539539+ }
540540+ }
541541+ }
542542+543543+ unless ($raw_v2 && $ver == 2) {
544544+ foreach my $key (keys %info) {
545545+ if (defined $info{$key}) {
546546+ $info{$key} =~ s/\000+.*//g;
547547+ $info{$key} =~ s/\s+$//;
548548+ }
549549+ }
550550+551551+ for (@v1_tag_names) {
552552+ $info{$_} = '' unless defined $info{$_};
553553+ }
554554+ }
555555+556556+ if (keys %info && exists $info{'GENRE'} && ! defined $info{'GENRE'}) {
557557+ $info{'GENRE'} = '';
558558+ }
559559+560560+ _close($file, $fh);
561561+562562+ return keys %info ? {%info} : undef;
563563+}
564564+565565+sub _get_v1tag {
566566+ my ($fh, $info) = @_;
567567+568568+ seek $fh, -128, 2;
569569+ read($fh, my $tag, 128);
570570+571571+ if (!defined($tag) || $tag !~ /^TAG/) {
572572+573573+ return 0;
574574+ }
575575+576576+ if (substr($tag, -3, 2) =~ /\000[^\000]/) {
577577+578578+ (undef, @{$info}{@v1_tag_names}) =
579579+ (unpack('a3a30a30a30a4a28', $tag),
580580+ ord(substr($tag, -2, 1)),
581581+ $mp3_genres[ord(substr $tag, -1)]);
582582+583583+ $info->{'TAGVERSION'} = 'ID3v1.1';
584584+585585+ } else {
586586+587587+ (undef, @{$info}{@v1_tag_names[0..4, 6]}) =
588588+ (unpack('a3a30a30a30a4a30', $tag),
589589+ $mp3_genres[ord(substr $tag, -1)]);
590590+591591+ $info->{'TAGVERSION'} = 'ID3v1';
592592+ }
593593+594594+ if ($UNICODE) {
595595+596596+ # Save off the old suspects list, since we add
597597+ # iso-8859-1 below, but don't want that there
598598+ # for possible ID3 v2.x parsing below.
599599+ my $oldSuspects = $Encode::Encoding{'Guess'}->{'Suspects'};
600600+601601+ for my $key (keys %{$info}) {
602602+603603+ next unless $info->{$key};
604604+605605+ # Try and guess the encoding.
606606+ my $value = $info->{$key};
607607+ my $icode = Encode::Guess->guess($value);
608608+609609+ unless (ref($icode)) {
610610+611611+ # Often Latin1 bytes are
612612+ # stuffed into a 1.1 tag.
613613+ Encode::Guess->add_suspects('iso-8859-1');
614614+615615+ while (length($value)) {
616616+617617+ $icode = Encode::Guess->guess($value);
618618+619619+ last if ref($icode);
620620+621621+ # Remove garbage and retry
622622+ # (string is truncated in the
623623+ # middle of a multibyte char?)
624624+ $value =~ s/(.)$//;
625625+ }
626626+ }
627627+628628+ $info->{$key} = Encode::decode(ref($icode) ? $icode->name : 'iso-8859-1', $info->{$key});
629629+ }
630630+631631+ Encode::Guess->set_suspects(keys %{$oldSuspects});
632632+ }
633633+634634+ return 1;
635635+}
636636+637637+sub _parse_v2tag {
638638+ my ($raw_v2, $v2, $info) = @_;
639639+640640+ # Make sure any existing TXXX flags are an array.
641641+ # As we might need to append comments to it below.
642642+ if ($v2->{'TXXX'} && ref($v2->{'TXXX'}) ne 'ARRAY') {
643643+644644+ $v2->{'TXXX'} = [ $v2->{'TXXX'} ];
645645+ }
646646+647647+ # J.River Media Center sticks RG tags in comments.
648648+ # Ugh. Make them look like TXXX tags, which is really what they are.
649649+ if (ref($v2->{'COMM'}) eq 'ARRAY' && grep { /Media Jukebox/ } @{$v2->{'COMM'}}) {
650650+651651+ for my $comment (@{$v2->{'COMM'}}) {
652652+653653+ if ($comment =~ /Media Jukebox/) {
654654+655655+ # we only want one null to lead.
656656+ $comment =~ s/^\000+//g;
657657+658658+ push @{$v2->{'TXXX'}}, "\000$comment";
659659+ }
660660+ }
661661+ }
662662+663663+ my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names;
664664+665665+ for my $id (keys %$hash) {
666666+667667+ next if !exists $v2->{$id};
668668+669669+ if ($id =~ /^UFID?$/) {
670670+671671+ my @ufid_list = split(/\0/, $v2->{$id});
672672+673673+ $info->{$hash->{$id}} = $ufid_list[1] if ($#ufid_list > 0);
674674+675675+ } elsif ($id =~ /^RVA[D2]?$/) {
676676+677677+ # Expand these binary fields. See the ID3 spec for Relative Volume Adjustment.
678678+ if ($id eq 'RVA2') {
679679+680680+ # ID is a text string
681681+ ($info->{$hash->{$id}}->{'ID'}, my $rvad) = split /\0/, $v2->{$id};
682682+683683+ my $channel = $rva2_channel_types{ ord(substr($rvad, 0, 1, '')) };
684684+685685+ $info->{$hash->{$id}}->{$channel}->{'REPLAYGAIN_TRACK_GAIN'} =
686686+ sprintf('%f', _grab_int_16(\$rvad) / 512);
687687+688688+ my $peakBytes = ord(substr($rvad, 0, 1, ''));
689689+690690+ if (int($peakBytes / 8)) {
691691+692692+ $info->{$hash->{$id}}->{$channel}->{'REPLAYGAIN_TRACK_PEAK'} =
693693+ sprintf('%f', _grab_int_16(\$rvad) / 512);
694694+ }
695695+696696+ } elsif ($id eq 'RVAD' || $id eq 'RVA') {
697697+698698+ my $rvad = $v2->{$id};
699699+ my $flags = ord(substr($rvad, 0, 1, ''));
700700+ my $desc = ord(substr($rvad, 0, 1, ''));
701701+702702+ # iTunes appears to be the only program that actually writes
703703+ # out a RVA/RVAD tag. Everyone else punts.
704704+ for my $type (qw(REPLAYGAIN_TRACK_GAIN REPLAYGAIN_TRACK_PEAK)) {
705705+706706+ for my $channel (qw(RIGHT LEFT)) {
707707+708708+ my $val = _grab_uint_16(\$rvad) / 256;
709709+710710+ # iTunes uses a range of -255 to 255
711711+ # to be -100% (silent) to 100% (+6dB)
712712+ if ($val == -255) {
713713+ $val = -96.0;
714714+ } else {
715715+ $val = 20.0 * log(($val+255)/255)/log(10);
716716+ }
717717+718718+ $info->{$hash->{$id}}->{$channel}->{$type} = $flags & 0x01 ? $val : -$val;
719719+ }
720720+ }
721721+ }
722722+723723+ } elsif ($id =~ /^A?PIC$/) {
724724+725725+ my $pic = $v2->{$id};
726726+727727+ # if there is more than one picture, just grab the first one.
728728+ if (ref($pic) eq 'ARRAY') {
729729+ $pic = (@$pic)[0];
730730+ }
731731+732732+ use bytes;
733733+734734+ my $valid_pic = 0;
735735+ my $pic_len = 0;
736736+ my $pic_format = '';
737737+738738+ # look for ID3 v2.2 picture
739739+ if ($pic && $id eq 'PIC') {
740740+741741+ # look for ID3 v2.2 picture
742742+ my ($encoding, $format, $picture_type, $description) = unpack 'Ca3CZ*', $pic;
743743+ $pic_len = length($description) + 1 + 5;
744744+745745+ # skip extra terminating null if unicode
746746+ if ($encoding) { $pic_len++; }
747747+748748+ if ($pic_len < length($pic)) {
749749+ $valid_pic = 1;
750750+ $pic_format = $format;
751751+ }
752752+753753+ } elsif ($pic && $id eq 'APIC') {
754754+755755+ # look for ID3 v2.3 picture
756756+ my ($encoding, $format) = unpack 'C Z*', $pic;
757757+758758+ $pic_len = length($format) + 2;
759759+760760+ if ($pic_len < length($pic)) {
761761+762762+ my ($picture_type, $description) = unpack "x$pic_len C Z*", $pic;
763763+764764+ $pic_len += 1 + length($description) + 1;
765765+766766+ # skip extra terminating null if unicode
767767+ if ($encoding) { $pic_len++; }
768768+769769+ $valid_pic = 1;
770770+ $pic_format = $format;
771771+ }
772772+ }
773773+774774+ # Proceed if we have a valid picture.
775775+ if ($valid_pic && $pic_format) {
776776+777777+ my ($data) = unpack("x$pic_len A*", $pic);
778778+779779+ if (length($data) && $pic_format) {
780780+781781+ $info->{$hash->{$id}} = {
782782+ 'DATA' => $data,
783783+ 'FORMAT' => $pic_format,
784784+ }
785785+ }
786786+ }
787787+788788+ } else {
789789+ my $data1 = $v2->{$id};
790790+791791+ # this is tricky ... if this is an arrayref,
792792+ # we want to only return one, so we pick the
793793+ # first one. but if it is a comment, we pick
794794+ # the first one where the first charcter after
795795+ # the language is NULL and not an additional
796796+ # sub-comment, because that is most likely to be
797797+ # the user-supplied comment
798798+ if (ref $data1 && !$raw_v2) {
799799+ if ($id =~ /^COMM?$/) {
800800+ my($newdata) = grep /^(....\000)/, @{$data1};
801801+ $data1 = $newdata || $data1->[0];
802802+ } elsif ($id !~ /^(?:TXXX?|PRIV)$/) {
803803+ # We can get multiple User Defined Text frames in a mp3 file
804804+ $data1 = $data1->[0];
805805+ }
806806+ }
807807+808808+ $data1 = [ $data1 ] if ! ref $data1;
809809+810810+ for my $data (@$data1) {
811811+ # TODO : this should only be done for certain frames;
812812+ # using RAW still gives you access, but we should be smarter
813813+ # about how individual frame types are handled. it's not
814814+ # like the list is infinitely long.
815815+ $data =~ s/^(.)//; # strip first char (text encoding)
816816+ my $encoding = $1;
817817+ my $desc;
818818+819819+ # Comments & Unsyncronized Lyrics have the same format.
820820+ if ($id =~ /^(COM[M ]?|USLT)$/) { # space for iTunes brokenness
821821+822822+ $data =~ s/^(?:...)//; # strip language
823823+ }
824824+825825+ if ($UNICODE) {
826826+827827+ if ($encoding eq "\001" || $encoding eq "\002") { # UTF-16, UTF-16BE
828828+ # text fields can be null-separated lists;
829829+ # UTF-16 therefore needs special care
830830+ #
831831+ # foobar2000 encodes tags in UTF-16LE
832832+ # (which is apparently illegal)
833833+ # Encode dies on a bad BOM, so it is
834834+ # probably wise to wrap it in an eval
835835+ # anyway
836836+ $data = eval { Encode::decode('utf16', $data) } || Encode::decode('utf16le', $data);
837837+838838+ } elsif ($encoding eq "\003") { # UTF-8
839839+840840+ # make sure string is UTF8, and set flag appropriately
841841+ $data = Encode::decode('utf8', $data);
842842+843843+ } elsif ($encoding eq "\000") {
844844+845845+ # Only guess if it's not ascii.
846846+ if ($data && $data !~ /^[\x00-\x7F]+$/) {
847847+848848+ # Try and guess the encoding, otherwise just use latin1
849849+ my $dec = Encode::Guess->guess($data);
850850+851851+ if (ref $dec) {
852852+ $data = $dec->decode($data);
853853+ } else {
854854+ # Best try
855855+ $data = Encode::decode('iso-8859-1', $data);
856856+ }
857857+ }
858858+ }
859859+860860+ } else {
861861+862862+ # If the string starts with an
863863+ # UTF-16 little endian BOM, use a hack to
864864+ # convert to ASCII per best-effort
865865+ my $pat;
866866+ if ($data =~ s/^\xFF\xFE//) {
867867+ $pat = 'v';
868868+ } elsif ($data =~ s/^\xFE\xFF//) {
869869+ $pat = 'n';
870870+ }
871871+872872+ if ($pat) {
873873+ $data = pack 'C*', map {
874874+ (chr =~ /[[:ascii:]]/ && chr =~ /[[:print:]]/)
875875+ ? $_
876876+ : ord('?')
877877+ } unpack "$pat*", $data;
878878+ }
879879+ }
880880+881881+ # We do this after decoding so we could be certain we're dealing
882882+ # with 8-bit text.
883883+ if ($id =~ /^(COM[M ]?|USLT)$/) { # space for iTunes brokenness
884884+885885+ $data =~ s/^(.*?)\000//; # strip up to first NULL(s),
886886+ # for sub-comments (TODO:
887887+ # handle all comment data)
888888+ $desc = $1;
889889+890890+ } elsif ($id =~ /^TCON?$/) {
891891+892892+ my ($index, $name);
893893+894894+ # Turn multiple nulls into a single.
895895+ $data =~ s/\000+/\000/g;
896896+897897+ # Handle the ID3v2.x spec -
898898+ #
899899+ # just an index number, possibly
900900+ # paren enclosed - referer to the v1 genres.
901901+ if ($data =~ /^ \(? (\d+) \)?\000?$/sx) {
902902+903903+ $index = $1;
904904+905905+ # Paren enclosed index with refinement.
906906+ # (4)Eurodisco
907907+ } elsif ($data =~ /^ \( (\d+) \)\000? ([^\(].+)$/x) {
908908+909909+ ($index, $name) = ($1, $2);
910910+911911+ # List of indexes: (37)(38)
912912+ } elsif ($data =~ /^ \( (\d+) \)\000?/x) {
913913+914914+ my @genres = ();
915915+916916+ while ($data =~ s/^ \( (\d+) \)\000?//x) {
917917+918918+ push @genres, $mp3_genres[$1];
919919+ }
920920+921921+ $data = \@genres;
922922+ }
923923+924924+ # Text based genres will fall through.
925925+ if ($name && $name ne "\000") {
926926+ $data = $name;
927927+ } elsif (defined $index) {
928928+ $data = $mp3_genres[$index];
929929+ }
930930+ }
931931+932932+ if ($raw_v2 == 2 && $desc) {
933933+ $data = { $desc => $data };
934934+ }
935935+936936+ if ($raw_v2 == 2 && exists $info->{$hash->{$id}}) {
937937+938938+ if (ref $info->{$hash->{$id}} eq 'ARRAY') {
939939+ push @{$info->{$hash->{$id}}}, $data;
940940+ } else {
941941+ $info->{$hash->{$id}} = [ $info->{$hash->{$id}}, $data ];
942942+ }
943943+944944+ } else {
945945+946946+ # User defined frame
947947+ if ($id eq 'TXXX') {
948948+949949+ my ($key, $val) = split(/\0/, $data);
950950+ $info->{uc($key)} = $val;
951951+952952+ } elsif ($id eq 'PRIV') {
953953+954954+ my ($key, $val) = split(/\0/, $data);
955955+ $info->{uc($key)} = unpack('v', $val);
956956+957957+ } else {
958958+959959+ $info->{$hash->{$id}} = $data;
960960+ }
961961+ }
962962+ }
963963+ }
964964+ }
965965+}
966966+967967+sub _get_v2tag {
968968+ my($fh) = @_;
969969+ my($off, $end, $myseek, $v2, $v2h, $hlen, $num, $wholetag);
970970+971971+ $v2 = {};
972972+ $v2h = _get_v2head($fh) or return;
973973+974974+ if ($v2h->{major_version} < 2) {
975975+ carp "This is $v2h->{version}; " .
976976+ "ID3v2 versions older than ID3v2.2.0 not supported\n"
977977+ if $^W;
978978+ return;
979979+ }
980980+981981+ # use syncsafe bytes if using version 2.4
982982+ # my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256;
983983+984984+ # alas, that's what the spec says, but iTunes and others don't syncsafe
985985+ # the length, which breaks MP3 files with v2.4 tags longer than 128 bytes,
986986+ # like every image file.
987987+ my $bytesize = 256;
988988+989989+ if ($v2h->{major_version} == 2) {
990990+ $hlen = 6;
991991+ $num = 3;
992992+ } else {
993993+ $hlen = 10;
994994+ $num = 4;
995995+ }
996996+997997+ $off = $v2h->{ext_header_size} + 10;
998998+ $end = $v2h->{tag_size} + 10; # should we read in the footer too?
999999+10001000+ seek $fh, $v2h->{offset}, 0;
10011001+ read $fh, $wholetag, $end;
10021002+10031003+ $wholetag =~ s/\xFF\x00/\xFF/gs if $v2h->{unsync};
10041004+10051005+ $myseek = sub {
10061006+ my $bytes = substr($wholetag, $off, $hlen);
10071007+ return unless $bytes =~ /^([A-Z0-9]{$num})/
10081008+ || ($num == 4 && $bytes =~ /^(COM )/); # stupid iTunes
10091009+ my($id, $size) = ($1, $hlen);
10101010+ my @bytes = reverse unpack "C$num", substr($bytes, $num, $num);
10111011+10121012+ for my $i (0 .. ($num - 1)) {
10131013+ $size += $bytes[$i] * $bytesize ** $i;
10141014+ }
10151015+10161016+ my $flags = {};
10171017+ if ($v2h->{major_version} > 3) {
10181018+ my @bits = split //, unpack 'B16', substr($bytes, 8, 2);
10191019+ $flags->{frame_unsync} = $bits[14];
10201020+ $flags->{data_len_indicator} = $bits[15];
10211021+ }
10221022+10231023+ return($id, $size, $flags);
10241024+ };
10251025+10261026+ while ($off < $end) {
10271027+ my($id, $size, $flags) = &$myseek or last;
10281028+10291029+ my $bytes = substr($wholetag, $off+$hlen, $size-$hlen);
10301030+10311031+ my $data_len;
10321032+ if ($flags->{data_len_indicator}) {
10331033+ $data_len = 0;
10341034+ my @data_len_bytes = reverse unpack 'C4', substr($bytes, 0, 4);
10351035+ $bytes = substr($bytes, 4);
10361036+ for my $i (0..3) {
10371037+ $data_len += $data_len_bytes[$i] * 128 ** $i;
10381038+ }
10391039+ }
10401040+10411041+ # perform frame-level unsync if needed (skip if already done for whole tag)
10421042+ $bytes =~ s/\xFF\x00/\xFF/gs if $flags->{frame_unsync} && !$v2h->{unsync};
10431043+10441044+ # if we know the data length, sanity check it now.
10451045+ if ($flags->{data_len_indicator} && defined $data_len) {
10461046+ carp "Size mismatch on $id\n" unless $data_len == length($bytes);
10471047+ }
10481048+10491049+ if (exists $v2->{$id}) {
10501050+ if (ref $v2->{$id} eq 'ARRAY') {
10511051+ push @{$v2->{$id}}, $bytes;
10521052+ } else {
10531053+ $v2->{$id} = [$v2->{$id}, $bytes];
10541054+ }
10551055+ } else {
10561056+ $v2->{$id} = $bytes;
10571057+ }
10581058+ $off += $size;
10591059+ }
10601060+10611061+ return($v2, $v2h);
10621062+}
10631063+10641064+10651065+=pod
10661066+10671067+=item get_mp3info (FILE)
10681068+10691069+Returns hash reference containing file information for MP3 file.
10701070+This data cannot be changed. Returned data:
10711071+10721072+ VERSION MPEG audio version (1, 2, 2.5)
10731073+ LAYER MPEG layer description (1, 2, 3)
10741074+ STEREO boolean for audio is in stereo
10751075+10761076+ VBR boolean for variable bitrate
10771077+ BITRATE bitrate in kbps (average for VBR files)
10781078+ FREQUENCY frequency in kHz
10791079+ SIZE bytes in audio stream
10801080+ OFFSET bytes offset that stream begins
10811081+10821082+ SECS total seconds
10831083+ MM minutes
10841084+ SS leftover seconds
10851085+ MS leftover milliseconds
10861086+ TIME time in MM:SS
10871087+10881088+ COPYRIGHT boolean for audio is copyrighted
10891089+ PADDING boolean for MP3 frames are padded
10901090+ MODE channel mode (0 = stereo, 1 = joint stereo,
10911091+ 2 = dual channel, 3 = single channel)
10921092+ FRAMES approximate number of frames
10931093+ FRAME_LENGTH approximate length of a frame
10941094+ VBR_SCALE VBR scale from VBR header
10951095+10961096+On error, returns nothing and sets C<$@>.
10971097+10981098+=cut
10991099+11001100+sub get_mp3info {
11011101+ my($file) = @_;
11021102+ my($off, $byte, $eof, $h, $tot, $fh);
11031103+11041104+ if (not (defined $file && $file ne '')) {
11051105+ $@ = "No file specified";
11061106+ return undef;
11071107+ }
11081108+11091109+ if (not -s $file) {
11101110+ $@ = "File is empty";
11111111+ return undef;
11121112+ }
11131113+11141114+ if (ref $file) { # filehandle passed
11151115+ $fh = $file;
11161116+ } else {
11171117+ if (not open $fh, '<', $file) {
11181118+ $@ = "Can't open $file: $!";
11191119+ return undef;
11201120+ }
11211121+ }
11221122+11231123+ $off = 0;
11241124+ $tot = 8192;
11251125+11261126+ # Let the caller change how far we seek in looking for a header.
11271127+ if ($try_harder) {
11281128+ $tot *= $try_harder;
11291129+ }
11301130+11311131+ binmode $fh;
11321132+ seek $fh, $off, 0;
11331133+ read $fh, $byte, 4;
11341134+11351135+ if ($off == 0) {
11361136+ if (my $v2h = _get_v2head($fh)) {
11371137+ $tot += $off += $v2h->{tag_size};
11381138+ seek $fh, $off, 0;
11391139+ read $fh, $byte, 4;
11401140+ }
11411141+ }
11421142+11431143+ $h = _get_head($byte);
11441144+ my $is_mp3 = _is_mp3($h);
11451145+11461146+ # the head wasn't where we were expecting it.. dig deeper.
11471147+ unless ($is_mp3) {
11481148+11491149+ # do only one read - it's _much_ faster
11501150+ $off++;
11511151+ seek $fh, $off, 0;
11521152+ read $fh, $byte, $tot;
11531153+11541154+ my $i;
11551155+11561156+ # now walk the bytes looking for the head
11571157+ for ($i = 0; $i < $tot; $i++) {
11581158+11591159+ last if ($tot - $i) < 4;
11601160+11611161+ my $head = substr($byte, $i, 4) || last;
11621162+11631163+ next if (ord($head) != 0xff);
11641164+11651165+ $h = _get_head($head);
11661166+ $is_mp3 = _is_mp3($h);
11671167+ last if $is_mp3;
11681168+ }
11691169+11701170+ # adjust where we are for _get_vbr()
11711171+ $off += $i;
11721172+11731173+ if ($off > $tot && !$try_harder) {
11741174+ _close($file, $fh);
11751175+ $@ = "Couldn't find MP3 header (perhaps set " .
11761176+ '$MP3::Info::try_harder and retry)';
11771177+ return undef;
11781178+ }
11791179+ }
11801180+11811181+ my $vbr = _get_vbr($fh, $h, \$off);
11821182+11831183+ seek $fh, 0, 2;
11841184+ $eof = tell $fh;
11851185+ seek $fh, -128, 2;
11861186+ $eof -= 128 if <$fh> =~ /^TAG/ ? 1 : 0;
11871187+11881188+ _close($file, $fh);
11891189+11901190+ $h->{size} = $eof - $off;
11911191+ $h->{offset} = $off;
11921192+11931193+ return _get_info($h, $vbr);
11941194+}
11951195+11961196+sub _get_info {
11971197+ my($h, $vbr) = @_;
11981198+ my $i;
11991199+12001200+ # No bitrate or sample rate? Something's wrong.
12011201+ unless ($h->{bitrate} && $h->{fs}) {
12021202+ return {};
12031203+ }
12041204+12051205+ $i->{VERSION} = $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 :
12061206+ $h->{IDR} == 0 ? 2.5 : 0;
12071207+ $i->{LAYER} = 4 - $h->{layer};
12081208+ $i->{VBR} = defined $vbr ? 1 : 0;
12091209+12101210+ $i->{COPYRIGHT} = $h->{copyright} ? 1 : 0;
12111211+ $i->{PADDING} = $h->{padding_bit} ? 1 : 0;
12121212+ $i->{STEREO} = $h->{mode} == 3 ? 0 : 1;
12131213+ $i->{MODE} = $h->{mode};
12141214+12151215+ $i->{SIZE} = $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size};
12161216+ $i->{OFFSET} = $h->{offset};
12171217+12181218+ my $mfs = $h->{fs} / ($h->{ID} ? 144000 : 72000);
12191219+ $i->{FRAMES} = int($vbr && $vbr->{frames}
12201220+ ? $vbr->{frames}
12211221+ : $i->{SIZE} / ($h->{bitrate} / $mfs)
12221222+ );
12231223+12241224+ if ($vbr) {
12251225+ $i->{VBR_SCALE} = $vbr->{scale} if $vbr->{scale};
12261226+ $h->{bitrate} = $i->{SIZE} / $i->{FRAMES} * $mfs;
12271227+ if (not $h->{bitrate}) {
12281228+ $@ = "Couldn't determine VBR bitrate";
12291229+ return undef;
12301230+ }
12311231+ }
12321232+12331233+ $h->{'length'} = ($i->{SIZE} * 8) / $h->{bitrate} / 10;
12341234+ $i->{SECS} = $h->{'length'} / 100;
12351235+ $i->{MM} = int $i->{SECS} / 60;
12361236+ $i->{SS} = int $i->{SECS} % 60;
12371237+ $i->{MS} = (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000);
12381238+# $i->{LF} = ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS});
12391239+# int($i->{MS} / 100 * 75); # is this right?
12401240+ $i->{TIME} = sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'};
12411241+12421242+ $i->{BITRATE} = int $h->{bitrate};
12431243+ # should we just return if ! FRAMES?
12441244+ $i->{FRAME_LENGTH} = int($h->{size} / $i->{FRAMES}) if $i->{FRAMES};
12451245+ $i->{FREQUENCY} = $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}];
12461246+12471247+ return $i;
12481248+}
12491249+12501250+sub _get_head {
12511251+ my($byte) = @_;
12521252+ my($bytes, $h);
12531253+12541254+ $bytes = _unpack_head($byte);
12551255+ @$h{qw(IDR ID layer protection_bit
12561256+ bitrate_index sampling_freq padding_bit private_bit
12571257+ mode mode_extension copyright original
12581258+ emphasis version_index bytes)} = (
12591259+ ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1,
12601260+ ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1,
12611261+ ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1,
12621262+ $bytes&3, ($bytes>>19)&3, $bytes
12631263+ );
12641264+12651265+ $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}];
12661266+ $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}];
12671267+12681268+ return $h;
12691269+}
12701270+12711271+sub _is_mp3 {
12721272+ my $h = $_[0] or return undef;
12731273+ return ! ( # all below must be false
12741274+ $h->{bitrate_index} == 0
12751275+ ||
12761276+ $h->{version_index} == 1
12771277+ ||
12781278+ ($h->{bytes} & 0xFFE00000) != 0xFFE00000
12791279+ ||
12801280+ !$h->{fs}
12811281+ ||
12821282+ !$h->{bitrate}
12831283+ ||
12841284+ $h->{bitrate_index} == 15
12851285+ ||
12861286+ !$h->{layer}
12871287+ ||
12881288+ $h->{sampling_freq} == 3
12891289+ ||
12901290+ $h->{emphasis} == 2
12911291+ ||
12921292+ !$h->{bitrate_index}
12931293+ ||
12941294+ ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000
12951295+ ||
12961296+ ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1)
12971297+ # mode extension should only be applicable when mode = 1
12981298+ # however, failing just becuase mode extension is used when unneeded is a bit strict
12991299+ # ||
13001300+ #($h->{mode_extension} != 0 && $h->{mode} != 1)
13011301+ );
13021302+}
13031303+13041304+sub _vbr_seek {
13051305+ my $fh = shift;
13061306+ my $off = shift;
13071307+ my $bytes = shift;
13081308+ my $n = shift || 4;
13091309+13101310+ seek $fh, $$off, 0;
13111311+ read $fh, $$bytes, $n;
13121312+13131313+ $$off += $n;
13141314+}
13151315+13161316+sub _get_vbr {
13171317+ my($fh, $h, $roff) = @_;
13181318+ my($off, $bytes, @bytes, %vbr);
13191319+13201320+ $off = $$roff;
13211321+13221322+ $off += 4;
13231323+13241324+ if ($h->{ID}) { # MPEG1
13251325+ $off += $h->{mode} == 3 ? 17 : 32;
13261326+ } else { # MPEG2
13271327+ $off += $h->{mode} == 3 ? 9 : 17;
13281328+ }
13291329+13301330+ _vbr_seek($fh, \$off, \$bytes);
13311331+ return unless $bytes eq 'Xing';
13321332+13331333+ _vbr_seek($fh, \$off, \$bytes);
13341334+ $vbr{flags} = _unpack_head($bytes);
13351335+13361336+ if ($vbr{flags} & 1) {
13371337+ _vbr_seek($fh, \$off, \$bytes);
13381338+ $vbr{frames} = _unpack_head($bytes);
13391339+ }
13401340+13411341+ if ($vbr{flags} & 2) {
13421342+ _vbr_seek($fh, \$off, \$bytes);
13431343+ $vbr{bytes} = _unpack_head($bytes);
13441344+ }
13451345+13461346+ if ($vbr{flags} & 4) {
13471347+ _vbr_seek($fh, \$off, \$bytes, 100);
13481348+# Not used right now ...
13491349+# $vbr{toc} = _unpack_head($bytes);
13501350+ }
13511351+13521352+ if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst)
13531353+ _vbr_seek($fh, \$off, \$bytes);
13541354+ $vbr{scale} = _unpack_head($bytes);
13551355+ } else {
13561356+ $vbr{scale} = -1;
13571357+ }
13581358+13591359+ $$roff = $off;
13601360+ return \%vbr;
13611361+}
13621362+13631363+sub _get_v2head {
13641364+ my $fh = $_[0] or return;
13651365+ my($v2h, $bytes, @bytes);
13661366+ $v2h->{offset} = 0;
13671367+13681368+ # check first three bytes for 'ID3'
13691369+ seek $fh, 0, 0;
13701370+ read $fh, $bytes, 3;
13711371+13721372+ # TODO: add support for tags at the end of the file
13731373+ if ($bytes eq 'RIF' || $bytes eq 'FOR') {
13741374+ _find_id3_chunk($fh, $bytes) or return;
13751375+ $v2h->{offset} = tell $fh;
13761376+ read $fh, $bytes, 3;
13771377+ }
13781378+13791379+ return unless $bytes eq 'ID3';
13801380+13811381+ # get version
13821382+ read $fh, $bytes, 2;
13831383+ $v2h->{version} = sprintf "ID3v2.%d.%d",
13841384+ @$v2h{qw[major_version minor_version]} =
13851385+ unpack 'c2', $bytes;
13861386+13871387+ # get flags
13881388+ read $fh, $bytes, 1;
13891389+ my @bits = split //, unpack 'b8', $bytes;
13901390+ if ($v2h->{major_version} == 2) {
13911391+ $v2h->{unsync} = $bits[7];
13921392+ $v2h->{compression} = $bits[8];
13931393+ $v2h->{ext_header} = 0;
13941394+ $v2h->{experimental} = 0;
13951395+ } else {
13961396+ $v2h->{unsync} = $bits[7];
13971397+ $v2h->{ext_header} = $bits[6];
13981398+ $v2h->{experimental} = $bits[5];
13991399+ $v2h->{footer} = $bits[4] if $v2h->{major_version} == 4;
14001400+ }
14011401+14021402+ # get ID3v2 tag length from bytes 7-10
14031403+ $v2h->{tag_size} = 10; # include ID3v2 header size
14041404+ $v2h->{tag_size} += 10 if $v2h->{footer};
14051405+ read $fh, $bytes, 4;
14061406+ @bytes = reverse unpack 'C4', $bytes;
14071407+ foreach my $i (0 .. 3) {
14081408+ # whoaaaaaa nellllllyyyyyy!
14091409+ $v2h->{tag_size} += $bytes[$i] * 128 ** $i;
14101410+ }
14111411+14121412+ # get extended header size
14131413+ $v2h->{ext_header_size} = 0;
14141414+ if ($v2h->{ext_header}) {
14151415+ read $fh, $bytes, 4;
14161416+ @bytes = reverse unpack 'C4', $bytes;
14171417+14181418+ # use syncsafe bytes if using version 2.4
14191419+ my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256;
14201420+ for my $i (0..3) {
14211421+ $v2h->{ext_header_size} += $bytes[$i] * $bytesize ** $i;
14221422+ }
14231423+ }
14241424+14251425+ return $v2h;
14261426+}
14271427+14281428+sub _find_id3_chunk {
14291429+ my($fh, $filetype) = @_;
14301430+ my($bytes, $size, $tag, $pat, $mat);
14311431+14321432+ read $fh, $bytes, 1;
14331433+ if ($filetype eq 'RIF') { # WAV
14341434+ return 0 if $bytes ne 'F';
14351435+ $pat = 'a4V';
14361436+ $mat = 'id3 ';
14371437+ } elsif ($filetype eq 'FOR') { # AIFF
14381438+ return 0 if $bytes ne 'M';
14391439+ $pat = 'a4N';
14401440+ $mat = 'ID3 ';
14411441+ }
14421442+ seek $fh, 12, 0; # skip to the first chunk
14431443+14441444+ while ((read $fh, $bytes, 8) == 8) {
14451445+ ($tag, $size) = unpack $pat, $bytes;
14461446+ return 1 if $tag eq $mat;
14471447+ seek $fh, $size, 1;
14481448+ }
14491449+14501450+ return 0;
14511451+}
14521452+14531453+sub _unpack_head {
14541454+ unpack('l', pack('L', unpack('N', $_[0])));
14551455+}
14561456+14571457+sub _grab_int_16 {
14581458+ my $data = shift;
14591459+ my $value = unpack('s',substr($$data,0,2));
14601460+ $$data = substr($$data,2);
14611461+ return $value;
14621462+}
14631463+14641464+sub _grab_uint_16 {
14651465+ my $data = shift;
14661466+ my $value = unpack('S',substr($$data,0,2));
14671467+ $$data = substr($$data,2);
14681468+ return $value;
14691469+}
14701470+14711471+sub _grab_int_32 {
14721472+ my $data = shift;
14731473+ my $value = unpack('V',substr($$data,0,4));
14741474+ $$data = substr($$data,4);
14751475+ return $value;
14761476+}
14771477+14781478+sub _parse_ape_tag {
14791479+ my ($fh, $filesize, $info) = @_;
14801480+14811481+ my $ape_tag_id = 'APETAGEX';
14821482+14831483+ seek $fh, -256, 2;
14841484+ read($fh, my $tag, 256);
14851485+ my $pre_tag = substr($tag, 0, 128, '');
14861486+14871487+ # Try and bail early if there's no ape tag.
14881488+ if (substr($pre_tag, 96, 8) ne $ape_tag_id && substr($tag, 96, 8) ne $ape_tag_id) {
14891489+14901490+ seek($fh, 0, 0);
14911491+ return 0;
14921492+ }
14931493+14941494+ my $id3v1_tag_size = 128;
14951495+ my $ape_tag_header_size = 32;
14961496+ my $lyrics3_tag_size = 10;
14971497+ my $tag_offset_start = 0;
14981498+ my $tag_offset_end = 0;
14991499+15001500+ seek($fh, (0 - $id3v1_tag_size - $ape_tag_header_size - $lyrics3_tag_size), 2);
15011501+15021502+ read($fh, my $ape_footer_id3v1, $id3v1_tag_size + $ape_tag_header_size + $lyrics3_tag_size);
15031503+15041504+ if (substr($ape_footer_id3v1, (length($ape_footer_id3v1) - $id3v1_tag_size - $ape_tag_header_size), 8) eq $ape_tag_id) {
15051505+15061506+ $tag_offset_end = $filesize - $id3v1_tag_size;
15071507+15081508+ } elsif (substr($ape_footer_id3v1, (length($ape_footer_id3v1) - $ape_tag_header_size), 8) eq $ape_tag_id) {
15091509+15101510+ $tag_offset_end = $filesize;
15111511+ }
15121512+15131513+ seek($fh, $tag_offset_end - $ape_tag_header_size, 0);
15141514+15151515+ read($fh, my $ape_footer_data, 32);
15161516+15171517+ my $ape_footer = _parse_ape_header_or_footer($ape_footer_data);
15181518+15191519+ if (keys %{$ape_footer}) {
15201520+15211521+ my $ape_tag_data = '';
15221522+15231523+ if ($ape_footer->{'flags'}->{'header'}) {
15241524+15251525+ seek($fh, ($tag_offset_end - $ape_footer->{'tag_size'} - $ape_tag_header_size), 0);
15261526+15271527+ $tag_offset_start = tell($fh);
15281528+15291529+ read($fh, $ape_tag_data, $ape_footer->{'tag_size'} + $ape_tag_header_size);
15301530+15311531+ } else {
15321532+15331533+ $tag_offset_start = $tag_offset_end - $ape_footer->{'tag_size'};
15341534+15351535+ seek($fh, $tag_offset_start, 0);
15361536+15371537+ read($fh, $ape_tag_data, $ape_footer->{'tag_size'});
15381538+ }
15391539+15401540+ my $ape_header_data = substr($ape_tag_data, 0, $ape_tag_header_size, '');
15411541+ my $ape_header = _parse_ape_header_or_footer($ape_header_data);
15421542+15431543+ for (my $c = 0; $c < $ape_header->{'tag_items'}; $c++) {
15441544+15451545+ # Loop through the tag items
15461546+ my $tag_len = _grab_int_32(\$ape_tag_data);
15471547+ my $tag_flags = _grab_int_32(\$ape_tag_data);
15481548+15491549+ $ape_tag_data =~ s/^(.*?)\0//;
15501550+15511551+ my $tag_item_key = uc($1 || 'UNKNOWN');
15521552+15531553+ $info->{$tag_item_key} = substr($ape_tag_data, 0, $tag_len, '');
15541554+ }
15551555+ }
15561556+15571557+ seek($fh, 0, 0);
15581558+15591559+ return 1;
15601560+}
15611561+15621562+sub _parse_ape_header_or_footer {
15631563+ my $bytes = shift;
15641564+ my %data = ();
15651565+15661566+ if (substr($bytes, 0, 8, '') eq 'APETAGEX') {
15671567+15681568+ $data{'version'} = _grab_int_32(\$bytes);
15691569+ $data{'tag_size'} = _grab_int_32(\$bytes);
15701570+ $data{'tag_items'} = _grab_int_32(\$bytes);
15711571+ $data{'global_flags'} = _grab_int_32(\$bytes);
15721572+15731573+ # trim the reseved bytes
15741574+ _grab_int_32(\$bytes);
15751575+ _grab_int_32(\$bytes);
15761576+15771577+ $data{'flags'}->{'header'} = ($data{'global_flags'} & 0x80000000) ? 1 : 0;
15781578+ $data{'flags'}->{'footer'} = ($data{'global_flags'} & 0x40000000) ? 1 : 0;
15791579+ $data{'flags'}->{'is_header'} = ($data{'global_flags'} & 0x20000000) ? 1 : 0;
15801580+ }
15811581+15821582+ return \%data;
15831583+}
15841584+15851585+sub _close {
15861586+ my($file, $fh) = @_;
15871587+ unless (ref $file) { # filehandle not passed
15881588+ close $fh or carp "Problem closing '$file': $!";
15891589+ }
15901590+}
15911591+15921592+BEGIN {
15931593+ @mp3_genres = (
15941594+ 'Blues',
15951595+ 'Classic Rock',
15961596+ 'Country',
15971597+ 'Dance',
15981598+ 'Disco',
15991599+ 'Funk',
16001600+ 'Grunge',
16011601+ 'Hip-Hop',
16021602+ 'Jazz',
16031603+ 'Metal',
16041604+ 'New Age',
16051605+ 'Oldies',
16061606+ 'Other',
16071607+ 'Pop',
16081608+ 'R&B',
16091609+ 'Rap',
16101610+ 'Reggae',
16111611+ 'Rock',
16121612+ 'Techno',
16131613+ 'Industrial',
16141614+ 'Alternative',
16151615+ 'Ska',
16161616+ 'Death Metal',
16171617+ 'Pranks',
16181618+ 'Soundtrack',
16191619+ 'Euro-Techno',
16201620+ 'Ambient',
16211621+ 'Trip-Hop',
16221622+ 'Vocal',
16231623+ 'Jazz+Funk',
16241624+ 'Fusion',
16251625+ 'Trance',
16261626+ 'Classical',
16271627+ 'Instrumental',
16281628+ 'Acid',
16291629+ 'House',
16301630+ 'Game',
16311631+ 'Sound Clip',
16321632+ 'Gospel',
16331633+ 'Noise',
16341634+ 'AlternRock',
16351635+ 'Bass',
16361636+ 'Soul',
16371637+ 'Punk',
16381638+ 'Space',
16391639+ 'Meditative',
16401640+ 'Instrumental Pop',
16411641+ 'Instrumental Rock',
16421642+ 'Ethnic',
16431643+ 'Gothic',
16441644+ 'Darkwave',
16451645+ 'Techno-Industrial',
16461646+ 'Electronic',
16471647+ 'Pop-Folk',
16481648+ 'Eurodance',
16491649+ 'Dream',
16501650+ 'Southern Rock',
16511651+ 'Comedy',
16521652+ 'Cult',
16531653+ 'Gangsta',
16541654+ 'Top 40',
16551655+ 'Christian Rap',
16561656+ 'Pop/Funk',
16571657+ 'Jungle',
16581658+ 'Native American',
16591659+ 'Cabaret',
16601660+ 'New Wave',
16611661+ 'Psychadelic',
16621662+ 'Rave',
16631663+ 'Showtunes',
16641664+ 'Trailer',
16651665+ 'Lo-Fi',
16661666+ 'Tribal',
16671667+ 'Acid Punk',
16681668+ 'Acid Jazz',
16691669+ 'Polka',
16701670+ 'Retro',
16711671+ 'Musical',
16721672+ 'Rock & Roll',
16731673+ 'Hard Rock',
16741674+ );
16751675+16761676+ @winamp_genres = (
16771677+ @mp3_genres,
16781678+ 'Folk',
16791679+ 'Folk-Rock',
16801680+ 'National Folk',
16811681+ 'Swing',
16821682+ 'Fast Fusion',
16831683+ 'Bebop',
16841684+ 'Latin',
16851685+ 'Revival',
16861686+ 'Celtic',
16871687+ 'Bluegrass',
16881688+ 'Avantgarde',
16891689+ 'Gothic Rock',
16901690+ 'Progressive Rock',
16911691+ 'Psychedelic Rock',
16921692+ 'Symphonic Rock',
16931693+ 'Slow Rock',
16941694+ 'Big Band',
16951695+ 'Chorus',
16961696+ 'Easy Listening',
16971697+ 'Acoustic',
16981698+ 'Humour',
16991699+ 'Speech',
17001700+ 'Chanson',
17011701+ 'Opera',
17021702+ 'Chamber Music',
17031703+ 'Sonata',
17041704+ 'Symphony',
17051705+ 'Booty Bass',
17061706+ 'Primus',
17071707+ 'Porn Groove',
17081708+ 'Satire',
17091709+ 'Slow Jam',
17101710+ 'Club',
17111711+ 'Tango',
17121712+ 'Samba',
17131713+ 'Folklore',
17141714+ 'Ballad',
17151715+ 'Power Ballad',
17161716+ 'Rhythmic Soul',
17171717+ 'Freestyle',
17181718+ 'Duet',
17191719+ 'Punk Rock',
17201720+ 'Drum Solo',
17211721+ 'Acapella',
17221722+ 'Euro-House',
17231723+ 'Dance Hall',
17241724+ 'Goa',
17251725+ 'Drum & Bass',
17261726+ 'Club-House',
17271727+ 'Hardcore',
17281728+ 'Terror',
17291729+ 'Indie',
17301730+ 'BritPop',
17311731+ 'Negerpunk',
17321732+ 'Polsk Punk',
17331733+ 'Beat',
17341734+ 'Christian Gangsta Rap',
17351735+ 'Heavy Metal',
17361736+ 'Black Metal',
17371737+ 'Crossover',
17381738+ 'Contemporary Christian',
17391739+ 'Christian Rock',
17401740+ 'Merengue',
17411741+ 'Salsa',
17421742+ 'Thrash Metal',
17431743+ 'Anime',
17441744+ 'JPop',
17451745+ 'Synthpop',
17461746+ );
17471747+17481748+ @t_bitrate = ([
17491749+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
17501750+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
17511751+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
17521752+ ],[
17531753+ [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
17541754+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
17551755+ [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
17561756+ ]);
17571757+17581758+ @t_sampling_freq = (
17591759+ [11025, 12000, 8000],
17601760+ [undef, undef, undef], # reserved
17611761+ [22050, 24000, 16000],
17621762+ [44100, 48000, 32000]
17631763+ );
17641764+17651765+ @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 }
17661766+ map { @$_ } @t_sampling_freq;
17671767+17681768+ @mp3_info_fields = qw(
17691769+ VERSION
17701770+ LAYER
17711771+ STEREO
17721772+ VBR
17731773+ BITRATE
17741774+ FREQUENCY
17751775+ SIZE
17761776+ OFFSET
17771777+ SECS
17781778+ MM
17791779+ SS
17801780+ MS
17811781+ TIME
17821782+ COPYRIGHT
17831783+ PADDING
17841784+ MODE
17851785+ FRAMES
17861786+ FRAME_LENGTH
17871787+ VBR_SCALE
17881788+ );
17891789+17901790+ %rva2_channel_types = (
17911791+ 0x00 => 'OTHER',
17921792+ 0x01 => 'MASTER',
17931793+ 0x02 => 'FRONT_RIGHT',
17941794+ 0x03 => 'FRONT_LEFT',
17951795+ 0x04 => 'BACK_RIGHT',
17961796+ 0x05 => 'BACK_LEFT',
17971797+ 0x06 => 'FRONT_CENTER',
17981798+ 0x07 => 'BACK_CENTER',
17991799+ 0x08 => 'SUBWOOFER',
18001800+ );
18011801+18021802+ %v1_tag_fields =
18031803+ (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4);
18041804+18051805+ @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE);
18061806+18071807+ %v2_to_v1_names = (
18081808+ # v2.2 tags
18091809+ 'TT2' => 'TITLE',
18101810+ 'TP1' => 'ARTIST',
18111811+ 'TAL' => 'ALBUM',
18121812+ 'TYE' => 'YEAR',
18131813+ 'COM' => 'COMMENT',
18141814+ 'TRK' => 'TRACKNUM',
18151815+ 'TCO' => 'GENRE', # not clean mapping, but ...
18161816+ # v2.3 tags
18171817+ 'TIT2' => 'TITLE',
18181818+ 'TPE1' => 'ARTIST',
18191819+ 'TALB' => 'ALBUM',
18201820+ 'TYER' => 'YEAR',
18211821+ 'COMM' => 'COMMENT',
18221822+ 'TRCK' => 'TRACKNUM',
18231823+ 'TCON' => 'GENRE',
18241824+ # v2.3 tags - needed for MusicBrainz
18251825+ 'UFID' => 'Unique file identifier',
18261826+ 'TXXX' => 'User defined text information frame',
18271827+ );
18281828+18291829+ %v2_tag_names = (
18301830+ # v2.2 tags
18311831+ 'BUF' => 'Recommended buffer size',
18321832+ 'CNT' => 'Play counter',
18331833+ 'COM' => 'Comments',
18341834+ 'CRA' => 'Audio encryption',
18351835+ 'CRM' => 'Encrypted meta frame',
18361836+ 'ETC' => 'Event timing codes',
18371837+ 'EQU' => 'Equalization',
18381838+ 'GEO' => 'General encapsulated object',
18391839+ 'IPL' => 'Involved people list',
18401840+ 'LNK' => 'Linked information',
18411841+ 'MCI' => 'Music CD Identifier',
18421842+ 'MLL' => 'MPEG location lookup table',
18431843+ 'PIC' => 'Attached picture',
18441844+ 'POP' => 'Popularimeter',
18451845+ 'REV' => 'Reverb',
18461846+ 'RVA' => 'Relative volume adjustment',
18471847+ 'SLT' => 'Synchronized lyric/text',
18481848+ 'STC' => 'Synced tempo codes',
18491849+ 'TAL' => 'Album/Movie/Show title',
18501850+ 'TBP' => 'BPM (Beats Per Minute)',
18511851+ 'TCM' => 'Composer',
18521852+ 'TCO' => 'Content type',
18531853+ 'TCR' => 'Copyright message',
18541854+ 'TDA' => 'Date',
18551855+ 'TDY' => 'Playlist delay',
18561856+ 'TEN' => 'Encoded by',
18571857+ 'TFT' => 'File type',
18581858+ 'TIM' => 'Time',
18591859+ 'TKE' => 'Initial key',
18601860+ 'TLA' => 'Language(s)',
18611861+ 'TLE' => 'Length',
18621862+ 'TMT' => 'Media type',
18631863+ 'TOA' => 'Original artist(s)/performer(s)',
18641864+ 'TOF' => 'Original filename',
18651865+ 'TOL' => 'Original Lyricist(s)/text writer(s)',
18661866+ 'TOR' => 'Original release year',
18671867+ 'TOT' => 'Original album/Movie/Show title',
18681868+ 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
18691869+ 'TP2' => 'Band/Orchestra/Accompaniment',
18701870+ 'TP3' => 'Conductor/Performer refinement',
18711871+ 'TP4' => 'Interpreted, remixed, or otherwise modified by',
18721872+ 'TPA' => 'Part of a set',
18731873+ 'TPB' => 'Publisher',
18741874+ 'TRC' => 'ISRC (International Standard Recording Code)',
18751875+ 'TRD' => 'Recording dates',
18761876+ 'TRK' => 'Track number/Position in set',
18771877+ 'TSI' => 'Size',
18781878+ 'TSS' => 'Software/hardware and settings used for encoding',
18791879+ 'TT1' => 'Content group description',
18801880+ 'TT2' => 'Title/Songname/Content description',
18811881+ 'TT3' => 'Subtitle/Description refinement',
18821882+ 'TXT' => 'Lyricist/text writer',
18831883+ 'TXX' => 'User defined text information frame',
18841884+ 'TYE' => 'Year',
18851885+ 'UFI' => 'Unique file identifier',
18861886+ 'ULT' => 'Unsychronized lyric/text transcription',
18871887+ 'WAF' => 'Official audio file webpage',
18881888+ 'WAR' => 'Official artist/performer webpage',
18891889+ 'WAS' => 'Official audio source webpage',
18901890+ 'WCM' => 'Commercial information',
18911891+ 'WCP' => 'Copyright/Legal information',
18921892+ 'WPB' => 'Publishers official webpage',
18931893+ 'WXX' => 'User defined URL link frame',
18941894+18951895+ # v2.3 tags
18961896+ 'AENC' => 'Audio encryption',
18971897+ 'APIC' => 'Attached picture',
18981898+ 'COMM' => 'Comments',
18991899+ 'COMR' => 'Commercial frame',
19001900+ 'ENCR' => 'Encryption method registration',
19011901+ 'EQUA' => 'Equalization',
19021902+ 'ETCO' => 'Event timing codes',
19031903+ 'GEOB' => 'General encapsulated object',
19041904+ 'GRID' => 'Group identification registration',
19051905+ 'IPLS' => 'Involved people list',
19061906+ 'LINK' => 'Linked information',
19071907+ 'MCDI' => 'Music CD identifier',
19081908+ 'MLLT' => 'MPEG location lookup table',
19091909+ 'OWNE' => 'Ownership frame',
19101910+ 'PCNT' => 'Play counter',
19111911+ 'POPM' => 'Popularimeter',
19121912+ 'POSS' => 'Position synchronisation frame',
19131913+ 'PRIV' => 'Private frame',
19141914+ 'RBUF' => 'Recommended buffer size',
19151915+ 'RVAD' => 'Relative volume adjustment',
19161916+ 'RVRB' => 'Reverb',
19171917+ 'SYLT' => 'Synchronized lyric/text',
19181918+ 'SYTC' => 'Synchronized tempo codes',
19191919+ 'TALB' => 'Album/Movie/Show title',
19201920+ 'TBPM' => 'BPM (beats per minute)',
19211921+ 'TCOM' => 'Composer',
19221922+ 'TCON' => 'Content type',
19231923+ 'TCOP' => 'Copyright message',
19241924+ 'TDAT' => 'Date',
19251925+ 'TDLY' => 'Playlist delay',
19261926+ 'TENC' => 'Encoded by',
19271927+ 'TEXT' => 'Lyricist/Text writer',
19281928+ 'TFLT' => 'File type',
19291929+ 'TIME' => 'Time',
19301930+ 'TIT1' => 'Content group description',
19311931+ 'TIT2' => 'Title/songname/content description',
19321932+ 'TIT3' => 'Subtitle/Description refinement',
19331933+ 'TKEY' => 'Initial key',
19341934+ 'TLAN' => 'Language(s)',
19351935+ 'TLEN' => 'Length',
19361936+ 'TMED' => 'Media type',
19371937+ 'TOAL' => 'Original album/movie/show title',
19381938+ 'TOFN' => 'Original filename',
19391939+ 'TOLY' => 'Original lyricist(s)/text writer(s)',
19401940+ 'TOPE' => 'Original artist(s)/performer(s)',
19411941+ 'TORY' => 'Original release year',
19421942+ 'TOWN' => 'File owner/licensee',
19431943+ 'TPE1' => 'Lead performer(s)/Soloist(s)',
19441944+ 'TPE2' => 'Band/orchestra/accompaniment',
19451945+ 'TPE3' => 'Conductor/performer refinement',
19461946+ 'TPE4' => 'Interpreted, remixed, or otherwise modified by',
19471947+ 'TPOS' => 'Part of a set',
19481948+ 'TPUB' => 'Publisher',
19491949+ 'TRCK' => 'Track number/Position in set',
19501950+ 'TRDA' => 'Recording dates',
19511951+ 'TRSN' => 'Internet radio station name',
19521952+ 'TRSO' => 'Internet radio station owner',
19531953+ 'TSIZ' => 'Size',
19541954+ 'TSRC' => 'ISRC (international standard recording code)',
19551955+ 'TSSE' => 'Software/Hardware and settings used for encoding',
19561956+ 'TXXX' => 'User defined text information frame',
19571957+ 'TYER' => 'Year',
19581958+ 'UFID' => 'Unique file identifier',
19591959+ 'USER' => 'Terms of use',
19601960+ 'USLT' => 'Unsychronized lyric/text transcription',
19611961+ 'WCOM' => 'Commercial information',
19621962+ 'WCOP' => 'Copyright/Legal information',
19631963+ 'WOAF' => 'Official audio file webpage',
19641964+ 'WOAR' => 'Official artist/performer webpage',
19651965+ 'WOAS' => 'Official audio source webpage',
19661966+ 'WORS' => 'Official internet radio station homepage',
19671967+ 'WPAY' => 'Payment',
19681968+ 'WPUB' => 'Publishers official webpage',
19691969+ 'WXXX' => 'User defined URL link frame',
19701970+19711971+ # v2.4 additional tags
19721972+ # note that we don't restrict tags from 2.3 or 2.4,
19731973+ 'ASPI' => 'Audio seek point index',
19741974+ 'EQU2' => 'Equalisation (2)',
19751975+ 'RVA2' => 'Relative volume adjustment (2)',
19761976+ 'SEEK' => 'Seek frame',
19771977+ 'SIGN' => 'Signature frame',
19781978+ 'TDEN' => 'Encoding time',
19791979+ 'TDOR' => 'Original release time',
19801980+ 'TDRC' => 'Recording time',
19811981+ 'TDRL' => 'Release time',
19821982+ 'TDTG' => 'Tagging time',
19831983+ 'TIPL' => 'Involved people list',
19841984+ 'TMCL' => 'Musician credits list',
19851985+ 'TMOO' => 'Mood',
19861986+ 'TPRO' => 'Produced notice',
19871987+ 'TSOA' => 'Album sort order',
19881988+ 'TSOP' => 'Performer sort order',
19891989+ 'TSOT' => 'Title sort order',
19901990+ 'TSST' => 'Set subtitle',
19911991+19921992+ # grrrrrrr
19931993+ 'COM ' => 'Broken iTunes comments',
19941994+ );
19951995+}
19961996+19971997+1;
19981998+19991999+__END__
20002000+20012001+=pod
20022002+20032003+=back
20042004+20052005+=head1 TROUBLESHOOTING
20062006+20072007+If you find a bug, please send me a patch (see the project page in L<"SEE ALSO">).
20082008+If you cannot figure out why it does not work for you, please put the MP3 file in
20092009+a place where I can get it (preferably via FTP, or HTTP, or .Mac iDisk) and send me
20102010+mail regarding where I can get the file, with a detailed description of the problem.
20112011+20122012+If I download the file, after debugging the problem I will not keep the MP3 file
20132013+if it is not legal for me to have it. Just let me know if it is legal for me to
20142014+keep it or not.
20152015+20162016+20172017+=head1 TODO
20182018+20192019+=over 4
20202020+20212021+=item ID3v2 Support
20222022+20232023+Still need to do more for reading tags, such as using Compress::Zlib to decompress
20242024+compressed tags. But until I see this in use more, I won't bother. If something
20252025+does not work properly with reading, follow the instructions above for
20262026+troubleshooting.
20272027+20282028+ID3v2 I<writing> is coming soon.
20292029+20302030+=item Get data from scalar
20312031+20322032+Instead of passing a file spec or filehandle, pass the
20332033+data itself. Would take some work, converting the seeks, etc.
20342034+20352035+=item Padding bit ?
20362036+20372037+Do something with padding bit.
20382038+20392039+=item Test suite
20402040+20412041+Test suite could use a bit of an overhaul and update. Patches very welcome.
20422042+20432043+=over 4
20442044+20452045+=item *
20462046+20472047+Revamp getset.t. Test all the various get_mp3tag args.
20482048+20492049+=item *
20502050+20512051+Test Unicode.
20522052+20532053+=item *
20542054+20552055+Test OOP API.
20562056+20572057+=item *
20582058+20592059+Test error handling, check more for missing files, bad MP3s, etc.
20602060+20612061+=back
20622062+20632063+=item Other VBR
20642064+20652065+Right now, only Xing VBR is supported.
20662066+20672067+=back
20682068+20692069+20702070+=head1 THANKS
20712071+20722072+Edward Allen,
20732073+Vittorio Bertola,
20742074+Michael Blakeley,
20752075+Per Bolmstedt,
20762076+Tony Bowden,
20772077+Tom Brown,
20782078+Sergio Camarena,
20792079+Chris Dawson,
20802080+Anthony DiSante,
20812081+Luke Drumm,
20822082+Kyle Farrell,
20832083+Jeffrey Friedl,
20842084+brian d foy,
20852085+Ben Gertzfield,
20862086+Brian Goodwin,
20872087+Todd Hanneken,
20882088+Todd Harris,
20892089+Woodrow Hill,
20902090+Kee Hinckley,
20912091+Roman Hodek,
20922092+Ilya Konstantinov,
20932093+Peter Kovacs,
20942094+Johann Lindvall,
20952095+Alex Marandon,
20962096+Peter Marschall,
20972097+michael,
20982098+Trond Michelsen,
20992099+Dave O'Neill,
21002100+Christoph Oberauer,
21012101+Jake Palmer,
21022102+Andrew Phillips,
21032103+David Reuteler,
21042104+John Ruttenberg,
21052105+Matthew Sachs,
21062106+scfc_de,
21072107+Hermann Schwaerzler,
21082108+Chris Sidi,
21092109+Roland Steinbach,
21102110+Brian S. Stephan,
21112111+Stuart,
21122112+Dan Sully,
21132113+Jeffery Sumler,
21142114+Predrag Supurovic,
21152115+Bogdan Surdu,
21162116+Pierre-Yves Thoulon,
21172117+tim,
21182118+Pass F. B. Travis,
21192119+Tobias Wagener,
21202120+Ronan Waide,
21212121+Andy Waite,
21222122+Ken Williams,
21232123+Ben Winslow,
21242124+Meng Weng Wong.
21252125+21262126+21272127+=head1 CURRENT AUTHOR
21282128+21292129+Dan Sully E<lt>dan | at | slimdevices.comE<gt> & Slim Devices, Inc.
21302130+21312131+=head1 AUTHOR EMERITUS
21322132+21332133+Chris Nandor E<lt>pudge@pobox.comE<gt>, http://pudge.net/
21342134+21352135+=head1 COPYRIGHT AND LICENSE
21362136+21372137+Copyright (c) 2006 Dan Sully & Slim Devices, Inc. All rights reserved.
21382138+21392139+Copyright (c) 1998-2005 Chris Nandor. All rights reserved.
21402140+21412141+This program is free software; you can redistribute it and/or modify it under
21422142+the same terms as Perl itself.
21432143+21442144+=head1 SEE ALSO
21452145+21462146+=over 4
21472147+21482148+=item Slim Devices
21492149+21502150+ http://www.slimdevices.com/
21512151+21522152+=item mp3tools
21532153+21542154+ http://www.zevils.com/linux/mp3tools/
21552155+21562156+=item mpgtools
21572157+21582158+ http://www.dv.co.yu/mpgscript/mpgtools.htm
21592159+ http://www.dv.co.yu/mpgscript/mpeghdr.htm
21602160+21612161+=item mp3tool
21622162+21632163+ http://www.dtek.chalmers.se/~d2linjo/mp3/mp3tool.html
21642164+21652165+=item ID3v2
21662166+21672167+ http://www.id3.org/
21682168+21692169+=item Xing Variable Bitrate
21702170+21712171+ http://www.xingtech.com/support/partner_developer/mp3/vbr_sdk/
21722172+21732173+=item MP3Ext
21742174+21752175+ http://rupert.informatik.uni-stuttgart.de/~mutschml/MP3ext/
21762176+21772177+=item Xmms
21782178+21792179+ http://www.xmms.org/
21802180+21812181+21822182+=back
21832183+21842184+=cut
+448
tools/songdb.pl
···11+#!/usr/bin/perl
22+#
33+# Rockbox song database docs:
44+# http://www.rockbox.org/twiki/bin/view/Main/TagCache
55+#
66+77+use mp3info;
88+use vorbiscomm;
99+1010+# configuration settings
1111+my $db = "tagcache";
1212+my $dir;
1313+my $strip;
1414+my $add;
1515+my $verbose;
1616+my $help;
1717+my $dirisalbum;
1818+my $littleendian = 0;
1919+my $dbver = 0x54434804;
2020+2121+# file data
2222+my %entries;
2323+2424+while($ARGV[0]) {
2525+ if($ARGV[0] eq "--path") {
2626+ $dir = $ARGV[1];
2727+ shift @ARGV;
2828+ shift @ARGV;
2929+ }
3030+ elsif($ARGV[0] eq "--db") {
3131+ $db = $ARGV[1];
3232+ shift @ARGV;
3333+ shift @ARGV;
3434+ }
3535+ elsif($ARGV[0] eq "--strip") {
3636+ $strip = $ARGV[1];
3737+ shift @ARGV;
3838+ shift @ARGV;
3939+ }
4040+ elsif($ARGV[0] eq "--add") {
4141+ $add = $ARGV[1];
4242+ shift @ARGV;
4343+ shift @ARGV;
4444+ }
4545+ elsif($ARGV[0] eq "--dirisalbum") {
4646+ $dirisalbum = 1;
4747+ shift @ARGV;
4848+ }
4949+ elsif($ARGV[0] eq "--littleendian") {
5050+ $littleendian = 1;
5151+ shift @ARGV;
5252+ }
5353+ elsif($ARGV[0] eq "--verbose") {
5454+ $verbose = 1;
5555+ shift @ARGV;
5656+ }
5757+ elsif($ARGV[0] eq "--help" or ($ARGV[0] eq "-h")) {
5858+ $help = 1;
5959+ shift @ARGV;
6060+ }
6161+ else {
6262+ shift @ARGV;
6363+ }
6464+}
6565+6666+if(! -d $dir or $help) {
6767+ print "'$dir' is not a directory\n" if ($dir ne "" and ! -d $dir);
6868+ print <<MOO
6969+7070+songdb --path <dir> [--db <file>] [--strip <path>] [--add <path>] [--dirisalbum] [--littleendian] [--verbose] [--help]
7171+7272+Options:
7373+7474+ --path <dir> Where your music collection is found
7575+ --db <file> Prefix for output files. Defaults to tagcache.
7676+ --strip <path> Removes this string from the left of all file names
7777+ --add <path> Adds this string to the left of all file names
7878+ --dirisalbum Use dir name as album name if the album name is missing in the
7979+ tags
8080+ --littleendian Write out data as little endian (for simulator)
8181+ --verbose Shows more details while working
8282+ --help This text
8383+MOO
8484+;
8585+ exit;
8686+}
8787+8888+sub get_oggtag {
8989+ my $fn = shift;
9090+ my %hash;
9191+9292+ my $ogg = vorbiscomm->new($fn);
9393+9494+ my $h= $ogg->load;
9595+9696+ # Convert this format into the same format used by the id3 parser hash
9797+9898+ foreach my $k ($ogg->comment_tags())
9999+ {
100100+ foreach my $cmmt ($ogg->comment($k))
101101+ {
102102+ my $n;
103103+ if($k =~ /^artist$/i) {
104104+ $n = 'ARTIST';
105105+ }
106106+ elsif($k =~ /^album$/i) {
107107+ $n = 'ALBUM';
108108+ }
109109+ elsif($k =~ /^title$/i) {
110110+ $n = 'TITLE';
111111+ }
112112+ $hash{$n}=$cmmt if($n);
113113+ }
114114+ }
115115+116116+ return \%hash;
117117+}
118118+119119+sub get_ogginfo {
120120+ my $fn = shift;
121121+ my %hash;
122122+123123+ my $ogg = vorbiscomm->new($fn);
124124+125125+ my $h= $ogg->load;
126126+127127+ return $ogg->{'INFO'};
128128+}
129129+130130+# return ALL directory entries in the given dir
131131+sub getdir {
132132+ my ($dir) = @_;
133133+134134+ $dir =~ s|/$|| if ($dir ne "/");
135135+136136+ if (opendir(DIR, $dir)) {
137137+ my @all = readdir(DIR);
138138+ closedir DIR;
139139+ return @all;
140140+ }
141141+ else {
142142+ warn "can't opendir $dir: $!\n";
143143+ }
144144+}
145145+146146+sub extractmp3 {
147147+ my ($dir, @files) = @_;
148148+ my @mp3;
149149+ for(@files) {
150150+ if( (/\.mp[23]$/i || /\.ogg$/i) && -f "$dir/$_" ) {
151151+ push @mp3, $_;
152152+ }
153153+ }
154154+ return @mp3;
155155+}
156156+157157+sub extractdirs {
158158+ my ($dir, @files) = @_;
159159+ $dir =~ s|/$||;
160160+ my @dirs;
161161+ for(@files) {
162162+ if( -d "$dir/$_" && ($_ !~ /^\.(|\.)$/)) {
163163+ push @dirs, $_;
164164+ }
165165+ }
166166+ return @dirs;
167167+}
168168+169169+sub singlefile {
170170+ my ($file) = @_;
171171+ my $hash;
172172+ my $info;
173173+174174+ if($file =~ /\.ogg$/i) {
175175+ $hash = get_oggtag($file);
176176+ $info = get_ogginfo($file);
177177+ }
178178+ else {
179179+ $hash = get_mp3tag($file);
180180+ $info = get_mp3info($file);
181181+ if (defined $$info{'BITRATE'}) {
182182+ $$hash{'BITRATE'} = $$info{'BITRATE'};
183183+ }
184184+185185+ if (defined $$info{'SECS'}) {
186186+ $$hash{'SECS'} = $$info{'SECS'};
187187+ }
188188+ }
189189+190190+ return $hash;
191191+}
192192+193193+sub dodir {
194194+ my ($dir)=@_;
195195+196196+ my %lcartists;
197197+ my %lcalbums;
198198+199199+ print "$dir\n";
200200+201201+ # getdir() returns all entries in the given dir
202202+ my @a = getdir($dir);
203203+204204+ # extractmp3 filters out only the mp3 files from all given entries
205205+ my @m = extractmp3($dir, @a);
206206+207207+ my $f;
208208+209209+ for $f (sort @m) {
210210+211211+ my $id3 = singlefile("$dir/$f");
212212+213213+ if (not defined $$id3{'ARTIST'} or $$id3{'ARTIST'} eq "") {
214214+ $$id3{'ARTIST'} = "<Untagged>";
215215+ }
216216+217217+ # Only use one case-variation of each artist
218218+ if (exists($lcartists{lc($$id3{'ARTIST'})})) {
219219+ $$id3{'ARTIST'} = $lcartists{lc($$id3{'ARTIST'})};
220220+ }
221221+ else {
222222+ $lcartists{lc($$id3{'ARTIST'})} = $$id3{'ARTIST'};
223223+ }
224224+ #printf "Artist: %s\n", $$id3{'ARTIST'};
225225+226226+ if (not defined $$id3{'ALBUM'} or $$id3{'ALBUM'} eq "") {
227227+ $$id3{'ALBUM'} = "<Untagged>";
228228+ if ($dirisalbum) {
229229+ $$id3{'ALBUM'} = $dir;
230230+ }
231231+ }
232232+233233+ # Only use one case-variation of each album
234234+ if (exists($lcalbums{lc($$id3{'ALBUM'})})) {
235235+ $$id3{'ALBUM'} = $lcalbums{lc($$id3{'ALBUM'})};
236236+ }
237237+ else {
238238+ $lcalbums{lc($$id3{'ALBUM'})} = $$id3{'ALBUM'};
239239+ }
240240+ #printf "Album: %s\n", $$id3{'ALBUM'};
241241+242242+ if (not defined $$id3{'GENRE'} or $$id3{'GENRE'} eq "") {
243243+ $$id3{'GENRE'} = "<Untagged>";
244244+ }
245245+ #printf "Genre: %s\n", $$id3{'GENRE'};
246246+247247+ if (not defined $$id3{'TITLE'} or $$id3{'TITLE'} eq "") {
248248+ # fall back on basename of the file if no title tag.
249249+ ($$id3{'TITLE'} = $f) =~ s/\.\w+$//;
250250+ }
251251+ #printf "Title: %s\n", $$id3{'TITLE'};
252252+253253+ my $path = "$dir/$f";
254254+ if ($strip ne "" and $path =~ /^$strip(.*)/) {
255255+ $path = $1;
256256+ }
257257+258258+ if ($add ne "") {
259259+ $path = $add . $path;
260260+ }
261261+ #printf "Path: %s\n", $path;
262262+263263+ if (not defined $$id3{'COMPOSER'} or $$id3{'COMPOSER'} eq "") {
264264+ $$id3{'COMPOSER'} = "<Untagged>";
265265+ }
266266+ #printf "Composer: %s\n", $$id3{'COMPOSER'};
267267+268268+ if (not defined $$id3{'YEAR'} or $$id3{'YEAR'} eq "") {
269269+ $$id3{'YEAR'} = "-1";
270270+ }
271271+ #printf "Year: %s\n", $$id3{'YEAR'};
272272+273273+ if (not defined $$id3{'TRACKNUM'} or $$id3{'TRACKNUM'} eq "") {
274274+ $$id3{'TRACKNUM'} = "-1";
275275+ }
276276+ #printf "Track num: %s\n", $$id3{'TRACKNUM'};
277277+278278+ if (not defined $$id3{'BITRATE'} or $$id3{'BITRATE'} eq "") {
279279+ $$id3{'BITRATE'} = "-1";
280280+ }
281281+ #printf "Bitrate: %s\n", $$id3{'BITRATE'};
282282+283283+ if (not defined $$id3{'SECS'} or $$id3{'SECS'} eq "") {
284284+ $$id3{'SECS'} = "-1";
285285+ }
286286+ #printf "Length: %s\n", $$id3{'SECS'};
287287+288288+ $$id3{'PATH'} = $path;
289289+ $entries{$path} = $id3;
290290+ }
291291+292292+ # extractdirs filters out only subdirectories from all given entries
293293+ my @d = extractdirs($dir, @a);
294294+ my $d;
295295+296296+ for $d (sort @d) {
297297+ $dir =~ s|/$||;
298298+ dodir("$dir/$d");
299299+ }
300300+}
301301+302302+use_mp3_utf8(1);
303303+dodir($dir);
304304+print "\n";
305305+306306+sub dumpshort {
307307+ my ($num)=@_;
308308+309309+ # print "int: $num\n";
310310+311311+ if ($littleendian) {
312312+ print DB pack "v", $num;
313313+ }
314314+ else {
315315+ print DB pack "n", $num;
316316+ }
317317+}
318318+319319+sub dumpint {
320320+ my ($num)=@_;
321321+322322+# print "int: $num\n";
323323+324324+ if ($littleendian) {
325325+ print DB pack "V", $num;
326326+ }
327327+ else {
328328+ print DB pack "N", $num;
329329+ }
330330+}
331331+332332+sub dump_tag_string {
333333+ my ($s, $index) = @_;
334334+335335+ my $strlen = length($s)+1;
336336+ my $padding = $strlen%4;
337337+ if ($padding > 0) {
338338+ $padding = 4 - $padding;
339339+ $strlen += $padding;
340340+ }
341341+342342+ dumpshort($strlen);
343343+ dumpshort($index);
344344+ print DB $s."\0";
345345+346346+ for (my $i = 0; $i < $padding; $i++) {
347347+ print DB "X";
348348+ }
349349+}
350350+351351+sub dump_tag_header {
352352+ my ($entry_count) = @_;
353353+354354+ my $size = tell(DB) - 12;
355355+ seek(DB, 0, 0);
356356+357357+ dumpint($dbver);
358358+ dumpint($size);
359359+ dumpint($entry_count);
360360+}
361361+362362+sub openfile {
363363+ my ($f) = @_;
364364+ open(DB, "> $f") || die "couldn't open $f";
365365+ binmode(DB);
366366+}
367367+368368+sub create_tagcache_index_file {
369369+ my ($index, $key, $unique) = @_;
370370+371371+ my $num = 0;
372372+ my $prev = "";
373373+ my $offset = 12;
374374+375375+ openfile $db ."_".$index.".tcd";
376376+ dump_tag_header(0);
377377+378378+ for(sort {uc($entries{$a}->{$key}) cmp uc($entries{$b}->{$key})} keys %entries) {
379379+ if (!$unique || !($entries{$_}->{$key} eq $prev)) {
380380+ my $index;
381381+382382+ $num++;
383383+ $prev = $entries{$_}->{$key};
384384+ $offset = tell(DB);
385385+ printf(" %s\n", $prev) if ($verbose);
386386+387387+ if ($unique) {
388388+ $index = 0xFFFF;
389389+ }
390390+ else {
391391+ $index = $entries{$_}->{'INDEX'};
392392+ }
393393+ dump_tag_string($prev, $index);
394394+ }
395395+ $entries{$_}->{$key."_OFFSET"} = $offset;
396396+ }
397397+398398+ dump_tag_header($num);
399399+ close(DB);
400400+}
401401+402402+if (!scalar keys %entries) {
403403+ print "No songs found. Did you specify the right --path ?\n";
404404+ print "Use the --help parameter to see all options.\n";
405405+ exit;
406406+}
407407+408408+my $i = 0;
409409+for (sort keys %entries) {
410410+ $entries{$_}->{'INDEX'} = $i;
411411+ $i++;
412412+}
413413+414414+if ($db) {
415415+ # Artists
416416+ create_tagcache_index_file(0, 'ARTIST', 1);
417417+ # Albums
418418+ create_tagcache_index_file(1, 'ALBUM', 1);
419419+ # Genres
420420+ create_tagcache_index_file(2, 'GENRE', 1);
421421+ # Titles
422422+ create_tagcache_index_file(3, 'TITLE', 0);
423423+ # Filenames
424424+ create_tagcache_index_file(4, 'PATH', 0);
425425+ # Composers
426426+ create_tagcache_index_file(5, 'COMPOSER', 1);
427427+428428+ # Master index file
429429+ openfile $db ."_idx.tcd";
430430+ dump_tag_header(0);
431431+432432+ for (sort keys %entries) {
433433+ dumpint($entries{$_}->{'ARTIST_OFFSET'});
434434+ dumpint($entries{$_}->{'ALBUM_OFFSET'});
435435+ dumpint($entries{$_}->{'GENRE_OFFSET'});
436436+ dumpint($entries{$_}->{'TITLE_OFFSET'});
437437+ dumpint($entries{$_}->{'PATH_OFFSET'});
438438+ dumpint($entries{$_}->{'COMPOSER_OFFSET'});
439439+ dumpint($entries{$_}->{'YEAR'});
440440+ dumpint($entries{$_}->{'TRACKNUM'});
441441+ dumpint($entries{$_}->{'BITRATE'});
442442+ dumpint($entries{$_}->{'SECS'});
443443+ dumpint(0);
444444+ }
445445+446446+ dump_tag_header(scalar keys %entries);
447447+ close(DB);
448448+}
+732
tools/vorbiscomm.pm
···11+#############################################################################
22+# This is
33+# http://search.cpan.org/~amolloy/Ogg-Vorbis-Header-PurePerl-0.07/PurePerl.pm
44+# written by Andrew Molloy
55+# Code under GNU GENERAL PUBLIC LICENCE v2
66+# $Id$
77+#############################################################################
88+99+package vorbiscomm;
1010+1111+use 5.005;
1212+use strict;
1313+use warnings;
1414+1515+use Fcntl qw/SEEK_END/;
1616+1717+our $VERSION = '0.07';
1818+1919+sub new
2020+{
2121+ my $class = shift;
2222+ my $file = shift;
2323+2424+ return load($class, $file);
2525+}
2626+2727+sub load
2828+{
2929+ my $class = shift;
3030+ my $file = shift;
3131+ my $from_new = shift;
3232+ my %data;
3333+ my $self;
3434+3535+ # there must be a better way...
3636+ if ($class eq 'vorbiscomm')
3737+ {
3838+ $self = bless \%data, $class;
3939+ }
4040+ else
4141+ {
4242+ $self = $class;
4343+ }
4444+4545+ if ($self->{'FILE_LOADED'})
4646+ {
4747+ return $self;
4848+ }
4949+5050+ $self->{'FILE_LOADED'} = 1;
5151+5252+ # check that the file exists and is readable
5353+ unless ( -e $file && -r _ )
5454+ {
5555+ warn "File does not exist or cannot be read.";
5656+ # file does not exist, can't do anything
5757+ return undef;
5858+ }
5959+ # open up the file
6060+ open FILE, $file;
6161+ # make sure dos-type systems can handle it...
6262+ binmode FILE;
6363+6464+ $data{'filename'} = $file;
6565+ $data{'fileHandle'} = \*FILE;
6666+6767+ if (_init(\%data)) {
6868+ _loadInfo(\%data);
6969+ _loadComments(\%data);
7070+ _calculateTrackLength(\%data);
7171+ }
7272+7373+ close FILE;
7474+7575+ return $self;
7676+}
7777+7878+sub info
7979+{
8080+ my $self = shift;
8181+ my $key = shift;
8282+8383+ # if the user did not supply a key, return the entire hash
8484+ unless ($key)
8585+ {
8686+ return $self->{'INFO'};
8787+ }
8888+8989+ # otherwise, return the value for the given key
9090+ return $self->{'INFO'}{lc $key};
9191+}
9292+9393+sub comment_tags
9494+{
9595+ my $self = shift;
9696+9797+ if ( $self && $self->{'COMMENT_KEYS'} ) {
9898+ return @{$self->{'COMMENT_KEYS'}};
9999+ }
100100+101101+ return undef;
102102+}
103103+104104+sub comment
105105+{
106106+ my $self = shift;
107107+ my $key = shift;
108108+109109+ # if the user supplied key does not exist, return undef
110110+ unless($self->{'COMMENTS'}{lc $key})
111111+ {
112112+ return undef;
113113+ }
114114+115115+ return @{$self->{'COMMENTS'}{lc $key}};
116116+}
117117+118118+sub add_comments
119119+{
120120+ warn "Ogg::Vorbis::Header::PurePerl add_comments() unimplemented.";
121121+}
122122+123123+sub edit_comment
124124+{
125125+ warn "Ogg::Vorbis::Header::PurePerl edit_comment() unimplemented.";
126126+}
127127+128128+sub delete_comment
129129+{
130130+ warn "Ogg::Vorbis::Header::PurePerl delete_comment() unimplemented.";
131131+}
132132+133133+sub clear_comments
134134+{
135135+ warn "Ogg::Vorbis::Header::PurePerl clear_comments() unimplemented.";
136136+}
137137+138138+sub path
139139+{
140140+ my $self = shift;
141141+142142+ return $self->{'fileName'};
143143+}
144144+145145+sub write_vorbis
146146+{
147147+ warn "Ogg::Vorbis::Header::PurePerl write_vorbis unimplemented.";
148148+}
149149+150150+# "private" methods
151151+152152+sub _init
153153+{
154154+ my $data = shift;
155155+ my $fh = $data->{'fileHandle'};
156156+ my $byteCount = 0;
157157+158158+ # check the header to make sure this is actually an Ogg-Vorbis file
159159+ $byteCount = _checkHeader($data);
160160+161161+ unless($byteCount)
162162+ {
163163+ # if it's not, we can't do anything
164164+ return undef;
165165+ }
166166+167167+ $data->{'startInfoHeader'} = $byteCount;
168168+ return 1; # Success
169169+}
170170+171171+sub _checkHeader
172172+{
173173+ my $data = shift;
174174+ my $fh = $data->{'fileHandle'};
175175+ my $buffer;
176176+ my $pageSegCount;
177177+ my $byteCount = 0; # stores how far into the file we've read,
178178+ # so later reads into the file can skip right
179179+ # past all of the header stuff
180180+181181+ # check that the first four bytes are 'OggS'
182182+ read($fh, $buffer, 4);
183183+ if ($buffer ne 'OggS')
184184+ {
185185+ warn "This is not an Ogg bitstream (no OggS header).";
186186+ return undef;
187187+ }
188188+ $byteCount += 4;
189189+190190+ # check the stream structure version (1 byte, should be 0x00)
191191+ read($fh, $buffer, 1);
192192+ if (ord($buffer) != 0x00)
193193+ {
194194+ warn "This is not an Ogg bitstream (invalid structure version).";
195195+ return undef;
196196+ }
197197+ $byteCount += 1;
198198+199199+ # check the header type flag
200200+ # This is a bitfield, so technically we should check all of the bits
201201+ # that could potentially be set. However, the only value this should
202202+ # possibly have at the beginning of a proper Ogg-Vorbis file is 0x02,
203203+ # so we just check for that. If it's not that, we go on anyway, but
204204+ # give a warning (this behavior may (should?) be modified in the future.
205205+ read($fh, $buffer, 1);
206206+ if (ord($buffer) != 0x02)
207207+ {
208208+ warn "Invalid header type flag (trying to go ahead anyway).";
209209+ }
210210+ $byteCount += 1;
211211+212212+ # skip to the page_segments count
213213+ read($fh, $buffer, 20);
214214+ $byteCount += 20;
215215+ # we do nothing with this data
216216+217217+ # read the number of page segments
218218+ read($fh, $buffer, 1);
219219+ $pageSegCount = ord($buffer);
220220+ $byteCount += 1;
221221+222222+ # read $pageSegCount bytes, then throw 'em out
223223+ read($fh, $buffer, $pageSegCount);
224224+ $byteCount += $pageSegCount;
225225+226226+ # check packet type. Should be 0x01 (for indentification header)
227227+ read($fh, $buffer, 1);
228228+ if (ord($buffer) != 0x01)
229229+ {
230230+ warn "Wrong vorbis header type, giving up.";
231231+ return undef;
232232+ }
233233+ $byteCount += 1;
234234+235235+ # check that the packet identifies itself as 'vorbis'
236236+ read($fh, $buffer, 6);
237237+ if ($buffer ne 'vorbis')
238238+ {
239239+ warn "This does not appear to be a vorbis stream, giving up.";
240240+ return undef;
241241+ }
242242+ $byteCount += 6;
243243+244244+ # at this point, we assume the bitstream is valid
245245+ return $byteCount;
246246+}
247247+248248+sub _loadInfo
249249+{
250250+ my $data = shift;
251251+ my $start = $data->{'startInfoHeader'};
252252+ my $fh = $data->{'fileHandle'};
253253+ my $buffer;
254254+ my $byteCount = $start;
255255+ my %info;
256256+257257+ seek $fh, $start, 0;
258258+259259+ # read the vorbis version
260260+ read($fh, $buffer, 4);
261261+ $info{'version'} = _decodeInt($buffer);
262262+ $byteCount += 4;
263263+264264+ # read the number of audio channels
265265+ read($fh, $buffer, 1);
266266+ $info{'channels'} = ord($buffer);
267267+ $byteCount += 1;
268268+269269+ # read the sample rate
270270+ read($fh, $buffer, 4);
271271+ $info{'rate'} = _decodeInt($buffer);
272272+ $byteCount += 4;
273273+274274+ # read the bitrate maximum
275275+ read($fh, $buffer, 4);
276276+ $info{'bitrate_upper'} = _decodeInt($buffer);
277277+ $byteCount += 4;
278278+279279+ # read the bitrate nominal
280280+ read($fh, $buffer, 4);
281281+ $info{'bitrate_nominal'} = _decodeInt($buffer);
282282+ $byteCount += 4;
283283+284284+ # read the bitrate minimal
285285+ read($fh, $buffer, 4);
286286+ $info{'bitrate_lower'} = _decodeInt($buffer);
287287+ $byteCount += 4;
288288+289289+ # read the blocksize_0 and blocksize_1
290290+ read($fh, $buffer, 1);
291291+ # these are each 4 bit fields, whose actual value is 2 to the power
292292+ # of the value of the field
293293+ $info{'blocksize_0'} = 2 << ((ord($buffer) & 0xF0) >> 4);
294294+ $info{'blocksize_1'} = 2 << (ord($buffer) & 0x0F);
295295+ $byteCount += 1;
296296+297297+ # read the framing_flag
298298+ read($fh, $buffer, 1);
299299+ $info{'framing_flag'} = ord($buffer);
300300+ $byteCount += 1;
301301+302302+ # bitrate_window is -1 in the current version of vorbisfile
303303+ $info{'bitrate_window'} = -1;
304304+305305+ $data->{'startCommentHeader'} = $byteCount;
306306+307307+ $data->{'INFO'} = \%info;
308308+}
309309+310310+sub _loadComments
311311+{
312312+ my $data = shift;
313313+ my $fh = $data->{'fileHandle'};
314314+ my $start = $data->{'startCommentHeader'};
315315+ my $buffer;
316316+ my $page_segments;
317317+ my $vendor_length;
318318+ my $user_comment_count;
319319+ my $byteCount = $start;
320320+ my %comments;
321321+322322+ seek $fh, $start, 0;
323323+324324+ # check that the first four bytes are 'OggS'
325325+ read($fh, $buffer, 4);
326326+ if ($buffer ne 'OggS')
327327+ {
328328+ warn "No comment header?";
329329+ return undef;
330330+ }
331331+ $byteCount += 4;
332332+333333+ # skip over next ten bytes
334334+ read($fh, $buffer, 10);
335335+ $byteCount += 10;
336336+337337+ # read the stream serial number
338338+ read($fh, $buffer, 4);
339339+ push @{$data->{'commentSerialNumber'}}, _decodeInt($buffer);
340340+ $byteCount += 4;
341341+342342+ # read the page sequence number (should be 0x01)
343343+ read($fh, $buffer, 4);
344344+ if (_decodeInt($buffer) != 0x01)
345345+ {
346346+ warn "Comment header page sequence number is not 0x01: " +
347347+ _decodeInt($buffer);
348348+ warn "Going to keep going anyway.";
349349+ }
350350+ $byteCount += 4;
351351+352352+ # and ignore the page checksum for now
353353+ read($fh, $buffer, 4);
354354+ $byteCount += 4;
355355+356356+ # get the number of entries in the segment_table...
357357+ read($fh, $buffer, 1);
358358+ $page_segments = _decodeInt($buffer);
359359+ $byteCount += 1;
360360+ # then skip on past it
361361+ read($fh, $buffer, $page_segments);
362362+ $byteCount += $page_segments;
363363+364364+ # check the header type (should be 0x03)
365365+ read($fh, $buffer, 1);
366366+ if (ord($buffer) != 0x03)
367367+ {
368368+ warn "Wrong header type: " . ord($buffer);
369369+ }
370370+ $byteCount += 1;
371371+372372+ # now we should see 'vorbis'
373373+ read($fh, $buffer, 6);
374374+ if ($buffer ne 'vorbis')
375375+ {
376376+ warn "Missing comment header. Should have found 'vorbis', found " .
377377+ $buffer;
378378+ }
379379+ $byteCount += 6;
380380+381381+ # get the vendor length
382382+ read($fh, $buffer, 4);
383383+ $vendor_length = _decodeInt($buffer);
384384+ $byteCount += 4;
385385+386386+ # read in the vendor
387387+ read($fh, $buffer, $vendor_length);
388388+ $comments{'vendor'} = $buffer;
389389+ $byteCount += $vendor_length;
390390+391391+ # read in the number of user comments
392392+ read($fh, $buffer, 4);
393393+ $user_comment_count = _decodeInt($buffer);
394394+ $byteCount += 4;
395395+396396+ $data->{'COMMENT_KEYS'} = [];
397397+398398+ # finally, read the comments
399399+ for (my $i = 0; $i < $user_comment_count; $i++)
400400+ {
401401+ # first read the length
402402+ read($fh, $buffer, 4);
403403+ my $comment_length = _decodeInt($buffer);
404404+ $byteCount += 4;
405405+406406+ # then the comment itself
407407+ read($fh, $buffer, $comment_length);
408408+ $byteCount += $comment_length;
409409+410410+ my ($key) = $buffer =~ /^([^=]+)/;
411411+ my ($value) = $buffer =~ /=(.*)$/;
412412+413413+ push @{$comments{lc $key}}, $value;
414414+ push @{$data->{'COMMENT_KEYS'}}, lc $key;
415415+ }
416416+417417+ # read past the framing_bit
418418+ read($fh, $buffer, 1);
419419+ $byteCount += 1;
420420+421421+ $data->{'INFO'}{'offset'} = $byteCount;
422422+423423+ $data->{'COMMENTS'} = \%comments;
424424+425425+ # Now find the offset of the first page
426426+ # with audio data.
427427+ while(_findPage($fh))
428428+ {
429429+ $byteCount = tell($fh) - 4;
430430+431431+ # version flag
432432+ read($fh, $buffer, 1);
433433+ if (ord($buffer) != 0x00)
434434+ {
435435+ warn "Invalid stream structure version: " .
436436+ sprintf("%x", ord($buffer));
437437+ return;
438438+ }
439439+440440+ # header type flag
441441+ read($fh, $buffer, 1);
442442+ # Audio data starts as a fresh packet on a new page, so
443443+ # if header_type is odd it's not a fresh packet
444444+ next if ( ord($buffer) % 2 );
445445+446446+ # skip past granule position, stream_serial_number,
447447+ # page_sequence_number, and crc
448448+ read($fh, $buffer, 20);
449449+450450+ # page_segments
451451+ read($fh, $buffer, 1);
452452+ my $page_segments = ord($buffer);
453453+454454+ # skip past the segment table
455455+ read($fh, $buffer, $page_segments);
456456+457457+ # read packet_type byte
458458+ read($fh, $buffer, 1);
459459+460460+ # Not an audio packet. All audio packet numbers are even
461461+ next if ( ord($buffer) % 2 );
462462+463463+ # Found the first audio packet
464464+ last;
465465+ }
466466+467467+ $data->{'INFO'}{'audio_offset'} = $byteCount;
468468+}
469469+470470+sub _calculateTrackLength
471471+{
472472+ my $data = shift;
473473+ my $fh = $data->{'fileHandle'};
474474+ my $buffer;
475475+ my $pageSize;
476476+ my $granule_position;
477477+478478+ seek($fh,-8500,SEEK_END); # that magic number is from vorbisfile.c
479479+ # in the constant CHUNKSIZE, which comes
480480+ # with the comment /* a shade over 8k;
481481+ # anyone using pages well over 8k gets
482482+ # what they deserve */
483483+484484+ # we just keep looking through the headers until we get to the last one
485485+ # (there might be a couple of blocks here)
486486+ while(_findPage($fh))
487487+ {
488488+ # stream structure version - must be 0x00
489489+ read($fh, $buffer, 1);
490490+ if (ord($buffer) != 0x00)
491491+ {
492492+ warn "Invalid stream structure version: " .
493493+ sprintf("%x", ord($buffer));
494494+ return;
495495+ }
496496+497497+ # header type flag
498498+ read($fh, $buffer, 1);
499499+ # we should check this, but for now we'll just ignore it
500500+501501+ # absolute granule position - this is what we need!
502502+ read($fh, $buffer, 8);
503503+ $granule_position = _decodeInt($buffer);
504504+505505+ # skip past stream_serial_number, page_sequence_number, and crc
506506+ read($fh, $buffer, 12);
507507+508508+ # page_segments
509509+ read($fh, $buffer, 1);
510510+ my $page_segments = ord($buffer);
511511+512512+ # reset pageSize
513513+ $pageSize = 0;
514514+515515+ # calculate approx. page size
516516+ for (my $i = 0; $i < $page_segments; $i++)
517517+ {
518518+ read($fh, $buffer, 1);
519519+ $pageSize += ord($buffer);
520520+ }
521521+522522+ seek $fh, $pageSize, 1;
523523+ }
524524+525525+ $data->{'INFO'}{'length'} =
526526+ int($granule_position / $data->{'INFO'}{'rate'});
527527+}
528528+529529+sub _findPage
530530+{
531531+ # search forward in the file for the 'OggS' page header
532532+ my $fh = shift;
533533+ my $char;
534534+ my $curStr = '';
535535+536536+ while (read($fh, $char, 1))
537537+ {
538538+ $curStr = $char . $curStr;
539539+ $curStr = substr($curStr, 0, 4);
540540+541541+ # we are actually looking for the string 'SggO' because we
542542+ # tack character on to our test string backwards, to make
543543+ # trimming it to 4 characters easier.
544544+ if ($curStr eq 'SggO')
545545+ {
546546+ return 1;
547547+ }
548548+ }
549549+550550+ return undef;
551551+}
552552+553553+sub _decodeInt
554554+{
555555+ my $bytes = shift;
556556+ my $num = 0;
557557+ my @byteList = split //, $bytes;
558558+ my $numBytes = @byteList;
559559+ my $mult = 1;
560560+561561+ for (my $i = 0; $i < $numBytes; $i ++)
562562+ {
563563+ $num += ord($byteList[$i]) * $mult;
564564+ $mult *= 256;
565565+ }
566566+567567+ return $num;
568568+}
569569+570570+sub _decodeInt5Bit
571571+{
572572+ my $byte = ord(shift);
573573+574574+ $byte = $byte & 0xF8; # clear out the bottm 3 bits
575575+ $byte = $byte >> 3; # and shifted down to where it belongs
576576+577577+ return $byte;
578578+}
579579+580580+sub _decodeInt4Bit
581581+{
582582+ my $byte = ord(shift);
583583+584584+ $byte = $byte & 0xFC; # clear out the bottm 4 bits
585585+ $byte = $byte >> 4; # and shifted down to where it belongs
586586+587587+ return $byte;
588588+}
589589+590590+sub _ilog
591591+{
592592+ my $x = shift;
593593+ my $ret = 0;
594594+595595+ unless ($x > 0)
596596+ {
597597+ return 0;
598598+ }
599599+600600+ while ($x > 0)
601601+ {
602602+ $ret++;
603603+ $x = $x >> 1;
604604+ }
605605+606606+ return $ret;
607607+}
608608+609609+1;
610610+__DATA__
611611+612612+=head1 NAME
613613+614614+Ogg::Vorbis::Header::PurePerl - An object-oriented interface to Ogg Vorbis
615615+information and comment fields, implemented entirely in Perl. Intended to be
616616+a drop in replacement for Ogg::Vobis::Header.
617617+618618+Unlike Ogg::Vorbis::Header, this module will go ahead and fill in all of the
619619+information fields as soon as you construct the object. In other words,
620620+the C<new> and C<load> constructors have identical behavior.
621621+622622+=head1 SYNOPSIS
623623+624624+ use Ogg::Vorbis::Header::PurePerl;
625625+ my $ogg = Ogg::Vorbis::Header::PurePerl->new("song.ogg");
626626+ while (my ($k, $v) = each %{$ogg->info}) {
627627+ print "$k: $v\n";
628628+ }
629629+ foreach my $com ($ogg->comment_tags) {
630630+ print "$com: $_\n" foreach $ogg->comment($com);
631631+ }
632632+633633+=head1 DESCRIPTION
634634+635635+This module is intended to be a drop in replacement for Ogg::Vorbis::Header,
636636+implemented entirely in Perl. It provides an object-oriented interface to
637637+Ogg Vorbis information and comment fields. (NOTE: This module currently
638638+supports only read operations).
639639+640640+=head1 CONSTRUCTORS
641641+642642+=head2 C<new ($filename)>
643643+644644+Opens an Ogg Vorbis file, ensuring that it exists and is actually an
645645+Ogg Vorbis stream. This method does not actually read any of the
646646+information or comment fields, and closes the file immediately.
647647+648648+=head2 C<load ([$filename])>
649649+650650+Opens an Ogg Vorbis file, ensuring that it exists and is actually an
651651+Ogg Vorbis stream, then loads the information and comment fields. This
652652+method can also be used without a filename to load the information
653653+and fields of an already constructed instance.
654654+655655+=head1 INSTANCE METHODS
656656+657657+=head2 C<info ([$key])>
658658+659659+Returns a hashref containing information about the Ogg Vorbis file from
660660+the file's information header. Hash fields are: version, channels, rate,
661661+bitrate_upper, bitrate_nominal, bitrate_lower, bitrate_window, and length.
662662+The bitrate_window value is not currently used by the vorbis codec, and
663663+will always be -1.
664664+665665+The optional parameter, key, allows you to retrieve a single value from
666666+the object's hash. Returns C<undef> if the key is not found.
667667+668668+=head2 C<comment_tags ()>
669669+670670+Returns an array containing the key values for the comment fields.
671671+These values can then be passed to C<comment> to retrieve their values.
672672+673673+=head2 C<comment ($key)>
674674+675675+Returns an array of comment values associated with the given key.
676676+677677+=head2 C<add_comments ($key, $value, [$key, $value, ...])>
678678+679679+Unimplemented.
680680+681681+=head2 C<edit_comment ($key, $value, [$num])>
682682+683683+Unimplemented.
684684+685685+=head2 C<delete_comment ($key, [$num])>
686686+687687+Unimplemented.
688688+689689+=head2 C<clear_comments ([@keys])>
690690+691691+Unimplemented.
692692+693693+=head2 C<write_vorbis ()>
694694+695695+Unimplemented.
696696+697697+=head2 C<path ()>
698698+699699+Returns the path/filename of the file the object represents.
700700+701701+=head1 NOTE
702702+703703+This is ALPHA SOFTWARE. It may very well be very broken. Do not use it in
704704+a production environment. You have been warned.
705705+706706+=head1 ACKNOWLEDGEMENTS
707707+708708+Dave Brown <cpan@dagbrown.com> made this module significantly faster
709709+at calculating the length of ogg files.
710710+711711+Robert Moser II <rlmoser@earthlink.net> fixed a problem with files that
712712+have no comments.
713713+714714+=head1 AUTHOR
715715+716716+Andrew Molloy E<lt>amolloy@kaizolabs.comE<gt>
717717+718718+=head1 COPYRIGHT
719719+720720+Copyright (c) 2003, Andrew Molloy. All Rights Reserved.
721721+722722+This program is free software; you can redistribute it and/or modify it
723723+under the terms of the GNU General Public License as published by the
724724+Free Software Foundation; either version 2 of the License, or (at
725725+your option) any later version. A copy of this license is included
726726+with this module (LICENSE.GPL).
727727+728728+=head1 SEE ALSO
729729+730730+L<Ogg::Vorbis::Header>, L<Ogg::Vorbis::Decoder>
731731+732732+=cut