| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package MP3::Info; | 
| 2 |  |  |  |  |  |  | require 5.006; | 
| 3 | 4 |  |  | 4 |  | 124580 | use overload; | 
|  | 4 |  |  |  |  | 4480 |  | 
|  | 4 |  |  |  |  | 33 |  | 
| 4 | 4 |  |  | 4 |  | 182 | use strict; | 
|  | 4 |  |  |  |  | 8 |  | 
|  | 4 |  |  |  |  | 131 |  | 
| 5 | 4 |  |  | 4 |  | 20 | use Carp; | 
|  | 4 |  |  |  |  | 10 |  | 
|  | 4 |  |  |  |  | 464 |  | 
| 6 |  |  |  |  |  |  |  | 
| 7 | 4 |  |  |  |  | 3374 | use vars qw( | 
| 8 |  |  |  |  |  |  | @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $REVISION | 
| 9 |  |  |  |  |  |  | @mp3_genres %mp3_genres @winamp_genres %winamp_genres $try_harder | 
| 10 |  |  |  |  |  |  | @t_bitrate @t_sampling_freq @frequency_tbl %v1_tag_fields | 
| 11 |  |  |  |  |  |  | @v1_tag_names %v2_tag_names %v2_to_v1_names $AUTOLOAD | 
| 12 |  |  |  |  |  |  | @mp3_info_fields | 
| 13 | 4 |  |  | 4 |  | 22 | ); | 
|  | 4 |  |  |  |  | 6 |  | 
| 14 |  |  |  |  |  |  |  | 
| 15 |  |  |  |  |  |  | @ISA = 'Exporter'; | 
| 16 |  |  |  |  |  |  | @EXPORT = qw( | 
| 17 |  |  |  |  |  |  | set_mp3tag get_mp3tag get_mp3info remove_mp3tag | 
| 18 |  |  |  |  |  |  | use_winamp_genres | 
| 19 |  |  |  |  |  |  | ); | 
| 20 |  |  |  |  |  |  | @EXPORT_OK = qw(@mp3_genres %mp3_genres use_mp3_utf8); | 
| 21 |  |  |  |  |  |  | %EXPORT_TAGS = ( | 
| 22 |  |  |  |  |  |  | genres	=> [qw(@mp3_genres %mp3_genres)], | 
| 23 |  |  |  |  |  |  | utf8	=> [qw(use_mp3_utf8)], | 
| 24 |  |  |  |  |  |  | all	=> [@EXPORT, @EXPORT_OK] | 
| 25 |  |  |  |  |  |  | ); | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  | # $Id: Info.pm,v 1.19 2005/03/11 04:41:29 pudge Exp $ | 
| 28 |  |  |  |  |  |  | ($REVISION) = ' $Revision: 1.19 $ ' =~ /\$Revision:\s+([^\s]+)/; | 
| 29 |  |  |  |  |  |  | $VERSION = '1.13'; | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | =pod | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | =head1 NAME | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | MP3::Info - Manipulate / fetch info from MP3 audio files | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | #!perl -w | 
| 40 |  |  |  |  |  |  | use MP3::Info; | 
| 41 |  |  |  |  |  |  | my $file = 'Pearls_Before_Swine.mp3'; | 
| 42 |  |  |  |  |  |  | set_mp3tag($file, 'Pearls Before Swine', q"77's", | 
| 43 |  |  |  |  |  |  | 'Sticks and Stones', '1990', | 
| 44 |  |  |  |  |  |  | q"(c) 1990 77's LTD.", 'rock & roll'); | 
| 45 |  |  |  |  |  |  |  | 
| 46 |  |  |  |  |  |  | my $tag = get_mp3tag($file) or die "No TAG info"; | 
| 47 |  |  |  |  |  |  | $tag->{GENRE} = 'rock'; | 
| 48 |  |  |  |  |  |  | set_mp3tag($file, $tag); | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | my $info = get_mp3info($file); | 
| 51 |  |  |  |  |  |  | printf "$file length is %d:%d\n", $info->{MM}, $info->{SS}; | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | =cut | 
| 54 |  |  |  |  |  |  |  | 
| 55 |  |  |  |  |  |  | { | 
| 56 |  |  |  |  |  |  | my $c = -1; | 
| 57 |  |  |  |  |  |  | # set all lower-case and regular-cased versions of genres as keys | 
| 58 |  |  |  |  |  |  | # with index as value of each key | 
| 59 |  |  |  |  |  |  | %mp3_genres = map {($_, ++$c, lc, $c)} @mp3_genres; | 
| 60 |  |  |  |  |  |  |  | 
| 61 |  |  |  |  |  |  | # do it again for winamp genres | 
| 62 |  |  |  |  |  |  | $c = -1; | 
| 63 |  |  |  |  |  |  | %winamp_genres = map {($_, ++$c, lc, $c)} @winamp_genres; | 
| 64 |  |  |  |  |  |  | } | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | =pod | 
| 67 |  |  |  |  |  |  |  | 
| 68 |  |  |  |  |  |  | my $mp3 = new MP3::Info $file; | 
| 69 |  |  |  |  |  |  | $mp3->title('Perls Before Swine'); | 
| 70 |  |  |  |  |  |  | printf "$file length is %s, title is %s\n", | 
| 71 |  |  |  |  |  |  | $mp3->time, $mp3->title; | 
| 72 |  |  |  |  |  |  |  | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | =over 4 | 
| 77 |  |  |  |  |  |  |  | 
| 78 |  |  |  |  |  |  | =item $mp3 = MP3::Info-Enew(FILE) | 
| 79 |  |  |  |  |  |  |  | 
| 80 |  |  |  |  |  |  | OOP interface to the rest of the module.  The same keys | 
| 81 |  |  |  |  |  |  | available via get_mp3info and get_mp3tag are available | 
| 82 |  |  |  |  |  |  | via the returned object (using upper case or lower case; | 
| 83 |  |  |  |  |  |  | but note that all-caps "VERSION" will return the module | 
| 84 |  |  |  |  |  |  | version, not the MP3 version). | 
| 85 |  |  |  |  |  |  |  | 
| 86 |  |  |  |  |  |  | Passing a value to one of the methods will set the value | 
| 87 |  |  |  |  |  |  | for that tag in the MP3 file, if applicable. | 
| 88 |  |  |  |  |  |  |  | 
| 89 |  |  |  |  |  |  | =cut | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  | sub new { | 
| 92 | 0 |  |  | 0 | 1 | 0 | my($pack, $file) = @_; | 
| 93 |  |  |  |  |  |  |  | 
| 94 | 0 | 0 |  |  |  | 0 | my $info = get_mp3info($file) or return undef; | 
| 95 | 0 |  | 0 |  |  | 0 | my $tags = get_mp3tag($file) || { map { ($_ => undef) } @v1_tag_names }; | 
| 96 | 0 |  |  |  |  | 0 | my %self = ( | 
| 97 |  |  |  |  |  |  | FILE		=> $file, | 
| 98 |  |  |  |  |  |  | TRY_HARDER	=> 0 | 
| 99 |  |  |  |  |  |  | ); | 
| 100 |  |  |  |  |  |  |  | 
| 101 | 0 |  |  |  |  | 0 | @self{@mp3_info_fields, @v1_tag_names, 'file'} = ( | 
| 102 | 0 |  |  |  |  | 0 | @{$info}{@mp3_info_fields}, | 
| 103 | 0 |  |  |  |  | 0 | @{$tags}{@v1_tag_names}, | 
| 104 |  |  |  |  |  |  | $file | 
| 105 |  |  |  |  |  |  | ); | 
| 106 |  |  |  |  |  |  |  | 
| 107 | 0 |  |  |  |  | 0 | return bless \%self, $pack; | 
| 108 |  |  |  |  |  |  | } | 
| 109 |  |  |  |  |  |  |  | 
| 110 |  |  |  |  |  |  | sub can { | 
| 111 | 0 |  |  | 0 | 0 | 0 | my $self = shift; | 
| 112 | 0 | 0 |  |  |  | 0 | return $self->SUPER::can(@_) unless ref $self; | 
| 113 | 0 |  |  |  |  | 0 | my $name = uc shift; | 
| 114 | 0 | 0 |  | 0 |  | 0 | return sub { $self->$name(@_) } if exists $self->{$name}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 115 | 0 |  |  |  |  | 0 | return undef; | 
| 116 |  |  |  |  |  |  | } | 
| 117 |  |  |  |  |  |  |  | 
| 118 |  |  |  |  |  |  | sub AUTOLOAD { | 
| 119 | 0 |  |  | 0 |  | 0 | my($self) = @_; | 
| 120 | 0 |  |  |  |  | 0 | (my $name = uc $AUTOLOAD) =~ s/^.*://; | 
| 121 |  |  |  |  |  |  |  | 
| 122 | 0 | 0 |  |  |  | 0 | if (exists $self->{$name}) { | 
| 123 |  |  |  |  |  |  | my $sub = exists $v1_tag_fields{$name} | 
| 124 |  |  |  |  |  |  | ? sub { | 
| 125 | 0 | 0 |  | 0 |  | 0 | if (defined $_[1]) { | 
| 126 | 0 |  |  |  |  | 0 | $_[0]->{$name} = $_[1]; | 
| 127 | 0 |  |  |  |  | 0 | set_mp3tag($_[0]->{FILE}, $_[0]); | 
| 128 |  |  |  |  |  |  | } | 
| 129 | 0 |  |  |  |  | 0 | return $_[0]->{$name}; | 
| 130 |  |  |  |  |  |  | } | 
| 131 |  |  |  |  |  |  | : sub { | 
| 132 | 0 |  |  | 0 |  | 0 | return $_[0]->{$name} | 
| 133 | 0 | 0 |  |  |  | 0 | }; | 
| 134 |  |  |  |  |  |  |  | 
| 135 | 4 |  |  | 4 |  | 24 | no strict 'refs'; | 
|  | 4 |  |  |  |  | 6 |  | 
|  | 4 |  |  |  |  | 28262 |  | 
| 136 | 0 |  |  |  |  | 0 | *{$AUTOLOAD} = $sub; | 
|  | 0 |  |  |  |  | 0 |  | 
| 137 | 0 |  |  |  |  | 0 | goto &$AUTOLOAD; | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | } else { | 
| 140 | 0 |  |  |  |  | 0 | carp(sprintf "No method '$name' available in package %s.", | 
| 141 |  |  |  |  |  |  | __PACKAGE__); | 
| 142 |  |  |  |  |  |  | } | 
| 143 |  |  |  |  |  |  | } | 
| 144 |  |  |  |  |  |  |  | 
| 145 | 0 |  |  | 0 |  | 0 | sub DESTROY { | 
| 146 |  |  |  |  |  |  |  | 
| 147 |  |  |  |  |  |  | } | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  |  | 
| 150 |  |  |  |  |  |  | =item use_mp3_utf8([STATUS]) | 
| 151 |  |  |  |  |  |  |  | 
| 152 |  |  |  |  |  |  | Tells MP3::Info to (or not) return TAG info in UTF-8. | 
| 153 |  |  |  |  |  |  | TRUE is 1, FALSE is 0.  Default is TRUE, if available. | 
| 154 |  |  |  |  |  |  |  | 
| 155 |  |  |  |  |  |  | Will only be able to turn it on if Encode is available.  ID3v2 | 
| 156 |  |  |  |  |  |  | tags will be converted to UTF-8 according to the encoding specified | 
| 157 |  |  |  |  |  |  | in each tag; ID3v1 tags will be assumed Latin-1 and converted | 
| 158 |  |  |  |  |  |  | to UTF-8. | 
| 159 |  |  |  |  |  |  |  | 
| 160 |  |  |  |  |  |  | Function returns status (TRUE/FALSE).  If no argument is supplied, | 
| 161 |  |  |  |  |  |  | or an unaccepted argument is supplied, function merely returns status. | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | This function is not exported by default, but may be exported | 
| 164 |  |  |  |  |  |  | with the C<:utf8> or C<:all> export tag. | 
| 165 |  |  |  |  |  |  |  | 
| 166 |  |  |  |  |  |  | =cut | 
| 167 |  |  |  |  |  |  |  | 
| 168 |  |  |  |  |  |  | my $unicode_module = eval { require Encode; require Encode::Guess }; | 
| 169 |  |  |  |  |  |  | my $UNICODE = use_mp3_utf8($unicode_module ? 1 : 0); | 
| 170 |  |  |  |  |  |  |  | 
| 171 |  |  |  |  |  |  | sub use_mp3_utf8 { | 
| 172 | 4 |  |  | 4 | 1 | 14 | my($val) = @_; | 
| 173 | 4 | 50 |  |  |  | 15 | if ($val == 1) { | 
|  |  | 0 |  |  |  |  |  | 
| 174 | 4 | 50 |  |  |  | 16 | if ($unicode_module) { | 
| 175 | 4 |  |  |  |  | 8 | $UNICODE = 1; | 
| 176 | 4 |  |  |  |  | 9 | $Encode::Guess::NoUTFAutoGuess = 1; | 
| 177 |  |  |  |  |  |  | } | 
| 178 |  |  |  |  |  |  | } elsif ($val == 0) { | 
| 179 | 0 |  |  |  |  | 0 | $UNICODE = 0; | 
| 180 |  |  |  |  |  |  | } | 
| 181 | 4 |  |  |  |  | 12 | return $UNICODE; | 
| 182 |  |  |  |  |  |  | } | 
| 183 |  |  |  |  |  |  |  | 
| 184 |  |  |  |  |  |  | =pod | 
| 185 |  |  |  |  |  |  |  | 
| 186 |  |  |  |  |  |  | =item use_winamp_genres() | 
| 187 |  |  |  |  |  |  |  | 
| 188 |  |  |  |  |  |  | Puts WinAmp genres into C<@mp3_genres> and C<%mp3_genres> | 
| 189 |  |  |  |  |  |  | (adds 68 additional genres to the default list of 80). | 
| 190 |  |  |  |  |  |  | This is a separate function because these are non-standard | 
| 191 |  |  |  |  |  |  | genres, but they are included because they are widely used. | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | You can import the data structures with one of: | 
| 194 |  |  |  |  |  |  |  | 
| 195 |  |  |  |  |  |  | use MP3::Info qw(:genres); | 
| 196 |  |  |  |  |  |  | use MP3::Info qw(:DEFAULT :genres); | 
| 197 |  |  |  |  |  |  | use MP3::Info qw(:all); | 
| 198 |  |  |  |  |  |  |  | 
| 199 |  |  |  |  |  |  | =cut | 
| 200 |  |  |  |  |  |  |  | 
| 201 |  |  |  |  |  |  | sub use_winamp_genres { | 
| 202 | 1 |  |  | 1 | 1 | 222 | %mp3_genres = %winamp_genres; | 
| 203 | 1 |  |  |  |  | 39 | @mp3_genres = @winamp_genres; | 
| 204 | 1 |  |  |  |  | 4 | return 1; | 
| 205 |  |  |  |  |  |  | } | 
| 206 |  |  |  |  |  |  |  | 
| 207 |  |  |  |  |  |  | =pod | 
| 208 |  |  |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | =item remove_mp3tag (FILE [, VERSION, BUFFER]) | 
| 210 |  |  |  |  |  |  |  | 
| 211 |  |  |  |  |  |  | Can remove ID3v1 or ID3v2 tags.  VERSION should be C<1> for ID3v1 | 
| 212 |  |  |  |  |  |  | (the default), C<2> for ID3v2, and C for both. | 
| 213 |  |  |  |  |  |  |  | 
| 214 |  |  |  |  |  |  | For ID3v1, removes last 128 bytes from file if those last 128 bytes begin | 
| 215 |  |  |  |  |  |  | with the text 'TAG'.  File will be 128 bytes shorter. | 
| 216 |  |  |  |  |  |  |  | 
| 217 |  |  |  |  |  |  | For ID3v2, removes ID3v2 tag.  Because an ID3v2 tag is at the | 
| 218 |  |  |  |  |  |  | beginning of the file, we rewrite the file after removing the tag data. | 
| 219 |  |  |  |  |  |  | The buffer for rewriting the file is 4MB.  BUFFER (in bytes) ca | 
| 220 |  |  |  |  |  |  | change the buffer size. | 
| 221 |  |  |  |  |  |  |  | 
| 222 |  |  |  |  |  |  | Returns the number of bytes removed, or -1 if no tag removed, | 
| 223 |  |  |  |  |  |  | or undef if there is an error. | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  | =cut | 
| 226 |  |  |  |  |  |  |  | 
| 227 |  |  |  |  |  |  | sub remove_mp3tag { | 
| 228 | 16 |  |  | 16 | 1 | 7430 | my($file, $version, $buf) = @_; | 
| 229 | 16 |  |  |  |  | 21 | my($fh, $return); | 
| 230 |  |  |  |  |  |  |  | 
| 231 | 16 |  | 50 |  |  | 66 | $buf ||= 4096*1024;  # the bigger the faster | 
| 232 | 16 |  | 50 |  |  | 39 | $version ||= 1; | 
| 233 |  |  |  |  |  |  |  | 
| 234 | 16 | 50 | 33 |  |  | 72 | if (not (defined $file && $file ne '')) { | 
| 235 | 0 |  |  |  |  | 0 | $@ = "No file specified"; | 
| 236 | 0 |  |  |  |  | 0 | return undef; | 
| 237 |  |  |  |  |  |  | } | 
| 238 |  |  |  |  |  |  |  | 
| 239 | 16 | 50 |  |  |  | 244 | if (not -s $file) { | 
| 240 | 0 |  |  |  |  | 0 | $@ = "File is empty"; | 
| 241 | 0 |  |  |  |  | 0 | return undef; | 
| 242 |  |  |  |  |  |  | } | 
| 243 |  |  |  |  |  |  |  | 
| 244 | 16 | 50 |  |  |  | 32 | if (ref $file) { # filehandle passed | 
| 245 | 0 |  |  |  |  | 0 | $fh = $file; | 
| 246 |  |  |  |  |  |  | } else { | 
| 247 | 16 | 50 |  |  |  | 527 | if (not open $fh, '+<', $file) { | 
| 248 | 0 |  |  |  |  | 0 | $@ = "Can't open $file: $!"; | 
| 249 | 0 |  |  |  |  | 0 | return undef; | 
| 250 |  |  |  |  |  |  | } | 
| 251 |  |  |  |  |  |  | } | 
| 252 |  |  |  |  |  |  |  | 
| 253 | 16 |  |  |  |  | 28 | binmode $fh; | 
| 254 |  |  |  |  |  |  |  | 
| 255 | 16 | 50 | 33 |  |  | 71 | if ($version eq 1 || $version eq 'ALL') { | 
| 256 | 16 |  |  |  |  | 70 | seek $fh, -128, 2; | 
| 257 | 16 |  |  |  |  | 25 | my $tell = tell $fh; | 
| 258 | 16 | 100 |  |  |  | 325 | if (<$fh> =~ /^TAG/) { | 
| 259 | 5 | 50 |  |  |  | 237 | truncate $fh, $tell or carp "Can't truncate '$file': $!"; | 
| 260 | 5 |  |  |  |  | 10 | $return += 128; | 
| 261 |  |  |  |  |  |  | } | 
| 262 |  |  |  |  |  |  | } | 
| 263 |  |  |  |  |  |  |  | 
| 264 | 16 | 50 | 33 |  |  | 71 | if ($version eq 2 || $version eq 'ALL') { | 
| 265 | 16 |  |  |  |  | 34 | my $v2h = _get_v2head($fh); | 
| 266 | 16 | 100 |  |  |  | 35 | if ($v2h) { | 
| 267 | 6 |  |  |  |  | 17 | local $\; | 
| 268 | 6 |  |  |  |  | 43 | seek $fh, 0, 2; | 
| 269 | 6 |  |  |  |  | 11 | my $eof = tell $fh; | 
| 270 | 6 |  |  |  |  | 7 | my $off = $v2h->{tag_size}; | 
| 271 |  |  |  |  |  |  |  | 
| 272 | 6 |  |  |  |  | 16 | while ($off < $eof) { | 
| 273 | 6 |  |  |  |  | 26 | seek $fh, $off, 0; | 
| 274 | 6 |  |  |  |  | 368 | read $fh, my($bytes), $buf; | 
| 275 | 6 |  |  |  |  | 31 | seek $fh, $off - $v2h->{tag_size}, 0; | 
| 276 | 6 |  |  |  |  | 143 | print $fh $bytes; | 
| 277 | 6 |  |  |  |  | 16 | $off += $buf; | 
| 278 |  |  |  |  |  |  | } | 
| 279 |  |  |  |  |  |  |  | 
| 280 | 6 | 50 |  |  |  | 213 | truncate $fh, $eof - $v2h->{tag_size} | 
| 281 |  |  |  |  |  |  | or carp "Can't truncate '$file': $!"; | 
| 282 | 6 |  |  |  |  | 64 | $return += $v2h->{tag_size}; | 
| 283 |  |  |  |  |  |  | } | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  |  | 
| 286 | 16 |  |  |  |  | 34 | _close($file, $fh); | 
| 287 |  |  |  |  |  |  |  | 
| 288 | 16 |  | 100 |  |  | 142 | return $return || -1; | 
| 289 |  |  |  |  |  |  | } | 
| 290 |  |  |  |  |  |  |  | 
| 291 |  |  |  |  |  |  |  | 
| 292 |  |  |  |  |  |  | =pod | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | =item set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | =item set_mp3tag (FILE, $HASHREF) | 
| 297 |  |  |  |  |  |  |  | 
| 298 |  |  |  |  |  |  | Adds/changes tag information in an MP3 audio file.  Will clobber | 
| 299 |  |  |  |  |  |  | any existing information in file. | 
| 300 |  |  |  |  |  |  |  | 
| 301 |  |  |  |  |  |  | Fields are TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE.  All fields have | 
| 302 |  |  |  |  |  |  | a 30-byte limit, except for YEAR, which has a four-byte limit, and GENRE, | 
| 303 |  |  |  |  |  |  | which is one byte in the file.  The GENRE passed in the function is a | 
| 304 |  |  |  |  |  |  | case-insensitive text string representing a genre found in C<@mp3_genres>. | 
| 305 |  |  |  |  |  |  |  | 
| 306 |  |  |  |  |  |  | Will accept either a list of values, or a hashref of the type | 
| 307 |  |  |  |  |  |  | returned by C. | 
| 308 |  |  |  |  |  |  |  | 
| 309 |  |  |  |  |  |  | If TRACKNUM is present (for ID3v1.1), then the COMMENT field can only be | 
| 310 |  |  |  |  |  |  | 28 bytes. | 
| 311 |  |  |  |  |  |  |  | 
| 312 |  |  |  |  |  |  | ID3v2 support may come eventually.  Note that if you set a tag on a file | 
| 313 |  |  |  |  |  |  | with ID3v2, the set tag will be for ID3v1[.1] only, and if you call | 
| 314 |  |  |  |  |  |  | C on the file, it will show you the (unchanged) ID3v2 tags, | 
| 315 |  |  |  |  |  |  | unless you specify ID3v1. | 
| 316 |  |  |  |  |  |  |  | 
| 317 |  |  |  |  |  |  | =cut | 
| 318 |  |  |  |  |  |  |  | 
| 319 |  |  |  |  |  |  | sub set_mp3tag { | 
| 320 | 5 |  |  | 5 | 1 | 7812 | my($file, $title, $artist, $album, $year, $comment, $genre, $tracknum) = @_; | 
| 321 | 5 |  |  |  |  | 7 | my(%info, $oldfh, $ref, $fh); | 
| 322 | 5 |  |  |  |  | 31 | local %v1_tag_fields = %v1_tag_fields; | 
| 323 |  |  |  |  |  |  |  | 
| 324 |  |  |  |  |  |  | # set each to '' if undef | 
| 325 | 5 | 100 |  |  |  | 29 | for ($title, $artist, $album, $year, $comment, $tracknum, $genre, | 
|  | 70 |  |  |  |  | 109 |  | 
| 326 |  |  |  |  |  |  | (@info{@v1_tag_names})) | 
| 327 |  |  |  |  |  |  | {$_ = defined() ? $_ : ''} | 
| 328 |  |  |  |  |  |  |  | 
| 329 | 5 | 50 |  |  |  | 38 | ($ref) = (overload::StrVal($title) =~ /^(?:.*\=)?([^=]*)\((?:[^\(]*)\)$/) | 
| 330 |  |  |  |  |  |  | if ref $title; | 
| 331 |  |  |  |  |  |  | # populate data to hashref if hashref is not passed | 
| 332 | 5 | 50 |  |  |  | 65 | if (!$ref) { | 
|  |  | 50 |  |  |  |  |  | 
| 333 | 0 |  |  |  |  | 0 | (@info{@v1_tag_names}) = | 
| 334 |  |  |  |  |  |  | ($title, $artist, $album, $year, $comment, $tracknum, $genre); | 
| 335 |  |  |  |  |  |  |  | 
| 336 |  |  |  |  |  |  | # put data from hashref into hashref if hashref is passed | 
| 337 |  |  |  |  |  |  | } elsif ($ref eq 'HASH') { | 
| 338 | 5 |  |  |  |  | 42 | %info = %$title; | 
| 339 |  |  |  |  |  |  |  | 
| 340 |  |  |  |  |  |  | # return otherwise | 
| 341 |  |  |  |  |  |  | } else { | 
| 342 | 0 |  |  |  |  | 0 | carp(<<'EOT'); | 
| 343 |  |  |  |  |  |  | Usage: set_mp3tag (FILE, TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE [, TRACKNUM]) | 
| 344 |  |  |  |  |  |  | set_mp3tag (FILE, $HASHREF) | 
| 345 |  |  |  |  |  |  | EOT | 
| 346 | 0 |  |  |  |  | 0 | return undef; | 
| 347 |  |  |  |  |  |  | } | 
| 348 |  |  |  |  |  |  |  | 
| 349 | 5 | 50 | 33 |  |  | 35 | if (not (defined $file && $file ne '')) { | 
| 350 | 0 |  |  |  |  | 0 | $@ = "No file specified"; | 
| 351 | 0 |  |  |  |  | 0 | return undef; | 
| 352 |  |  |  |  |  |  | } | 
| 353 |  |  |  |  |  |  |  | 
| 354 | 5 | 50 |  |  |  | 109 | if (not -s $file) { | 
| 355 | 0 |  |  |  |  | 0 | $@ = "File is empty"; | 
| 356 | 0 |  |  |  |  | 0 | return undef; | 
| 357 |  |  |  |  |  |  | } | 
| 358 |  |  |  |  |  |  |  | 
| 359 |  |  |  |  |  |  | # comment field length 28 if ID3v1.1 | 
| 360 | 5 | 50 |  |  |  | 21 | $v1_tag_fields{COMMENT} = 28 if $info{TRACKNUM}; | 
| 361 |  |  |  |  |  |  |  | 
| 362 |  |  |  |  |  |  |  | 
| 363 |  |  |  |  |  |  | # only if -w is on | 
| 364 | 5 | 50 |  |  |  | 19 | if ($^W) { | 
| 365 |  |  |  |  |  |  | # warn if fields too long | 
| 366 | 5 |  |  |  |  | 20 | foreach my $field (keys %v1_tag_fields) { | 
| 367 | 25 | 50 |  |  |  | 42 | $info{$field} = '' unless defined $info{$field}; | 
| 368 | 25 | 50 |  |  |  | 64 | if (length($info{$field}) > $v1_tag_fields{$field}) { | 
| 369 | 0 |  |  |  |  | 0 | carp "Data too long for field $field: truncated to " . | 
| 370 |  |  |  |  |  |  | "$v1_tag_fields{$field}"; | 
| 371 |  |  |  |  |  |  | } | 
| 372 |  |  |  |  |  |  | } | 
| 373 |  |  |  |  |  |  |  | 
| 374 | 5 | 50 |  |  |  | 16 | if ($info{GENRE}) { | 
| 375 | 5 | 50 |  |  |  | 30 | carp "Genre `$info{GENRE}' does not exist\n" | 
| 376 |  |  |  |  |  |  | unless exists $mp3_genres{$info{GENRE}}; | 
| 377 |  |  |  |  |  |  | } | 
| 378 |  |  |  |  |  |  | } | 
| 379 |  |  |  |  |  |  |  | 
| 380 | 5 | 50 |  |  |  | 14 | if ($info{TRACKNUM}) { | 
| 381 | 5 |  |  |  |  | 11 | $info{TRACKNUM} =~ s/^(\d+)\/(\d+)$/$1/; | 
| 382 | 5 | 50 | 33 |  |  | 56 | unless ($info{TRACKNUM} =~ /^\d+$/ && | 
|  |  |  | 33 |  |  |  |  | 
| 383 |  |  |  |  |  |  | $info{TRACKNUM} > 0 && $info{TRACKNUM} < 256) { | 
| 384 | 0 | 0 |  |  |  | 0 | carp "Tracknum `$info{TRACKNUM}' must be an integer " . | 
| 385 |  |  |  |  |  |  | "from 1 and 255\n" if $^W; | 
| 386 | 0 |  |  |  |  | 0 | $info{TRACKNUM} = ''; | 
| 387 |  |  |  |  |  |  | } | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  |  | 
| 390 | 5 | 50 |  |  |  | 24 | if (ref $file) { # filehandle passed | 
| 391 | 0 |  |  |  |  | 0 | $fh = $file; | 
| 392 |  |  |  |  |  |  | } else { | 
| 393 | 5 | 50 |  |  |  | 193 | if (not open $fh, '+<', $file) { | 
| 394 | 0 |  |  |  |  | 0 | $@ = "Can't open $file: $!"; | 
| 395 | 0 |  |  |  |  | 0 | return undef; | 
| 396 |  |  |  |  |  |  | } | 
| 397 |  |  |  |  |  |  | } | 
| 398 |  |  |  |  |  |  |  | 
| 399 | 5 |  |  |  |  | 10 | binmode $fh; | 
| 400 | 5 |  |  |  |  | 20 | $oldfh = select $fh; | 
| 401 | 5 |  |  |  |  | 26 | seek $fh, -128, 2; | 
| 402 |  |  |  |  |  |  | # go to end of file if no tag, beginning of file if tag | 
| 403 | 5 | 100 |  |  |  | 129 | seek $fh, (<$fh> =~ /^TAG/ ? -128 : 0), 2; | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | # get genre value | 
| 406 | 5 | 50 | 33 |  |  | 47 | $info{GENRE} = $info{GENRE} && exists $mp3_genres{$info{GENRE}} ? | 
| 407 |  |  |  |  |  |  | $mp3_genres{$info{GENRE}} : 255;  # some default genre | 
| 408 |  |  |  |  |  |  |  | 
| 409 | 5 |  |  |  |  | 13 | local $\; | 
| 410 |  |  |  |  |  |  | # print TAG to file | 
| 411 | 5 | 50 |  |  |  | 11 | if ($info{TRACKNUM}) { | 
| 412 | 5 |  |  |  |  | 50 | print pack 'a3a30a30a30a4a28xCC', 'TAG', @info{@v1_tag_names}; | 
| 413 |  |  |  |  |  |  | } else { | 
| 414 | 0 |  |  |  |  | 0 | print pack 'a3a30a30a30a4a30C', 'TAG', @info{@v1_tag_names[0..4, 6]}; | 
| 415 |  |  |  |  |  |  | } | 
| 416 |  |  |  |  |  |  |  | 
| 417 | 5 |  |  |  |  | 14 | select $oldfh; | 
| 418 |  |  |  |  |  |  |  | 
| 419 | 5 |  |  |  |  | 26 | _close($file, $fh); | 
| 420 |  |  |  |  |  |  |  | 
| 421 | 5 |  |  |  |  | 53 | return 1; | 
| 422 |  |  |  |  |  |  | } | 
| 423 |  |  |  |  |  |  |  | 
| 424 |  |  |  |  |  |  | =pod | 
| 425 |  |  |  |  |  |  |  | 
| 426 |  |  |  |  |  |  | =item get_mp3tag (FILE [, VERSION, RAW_V2]) | 
| 427 |  |  |  |  |  |  |  | 
| 428 |  |  |  |  |  |  | Returns hash reference containing tag information in MP3 file.  The keys | 
| 429 |  |  |  |  |  |  | returned are the same as those supplied for C, except in the | 
| 430 |  |  |  |  |  |  | case of RAW_V2 being set. | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | If VERSION is C<1>, the information is taken from the ID3v1 tag (if present). | 
| 433 |  |  |  |  |  |  | If VERSION is C<2>, the information is taken from the ID3v2 tag (if present). | 
| 434 |  |  |  |  |  |  | If VERSION is not supplied, or is false, the ID3v1 tag is read if present, and | 
| 435 |  |  |  |  |  |  | then, if present, the ID3v2 tag information will override any existing ID3v1 | 
| 436 |  |  |  |  |  |  | tag info. | 
| 437 |  |  |  |  |  |  |  | 
| 438 |  |  |  |  |  |  | If RAW_V2 is C<1>, the raw ID3v2 tag data is returned, without any manipulation | 
| 439 |  |  |  |  |  |  | of text encoding.  The key name is the same as the frame ID (ID to name mappings | 
| 440 |  |  |  |  |  |  | are in the global %v2_tag_names). | 
| 441 |  |  |  |  |  |  |  | 
| 442 |  |  |  |  |  |  | If RAW_V2 is C<2>, the ID3v2 tag data is returned, manipulating for Unicode if | 
| 443 |  |  |  |  |  |  | necessary, etc.  It also takes multiple values for a given key (such as comments) | 
| 444 |  |  |  |  |  |  | and puts them in an arrayref. | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | If the ID3v2 version is older than ID3v2.2.0 or newer than ID3v2.4.0, it will | 
| 447 |  |  |  |  |  |  | not be read. | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  | Strings returned will be in Latin-1, unless UTF-8 is specified (L), | 
| 450 |  |  |  |  |  |  | (unless RAW_V2 is C<1>). | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | Also returns a TAGVERSION key, containing the ID3 version used for the returned | 
| 453 |  |  |  |  |  |  | data (if TAGVERSION argument is C<0>, may contain two versions). | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | =cut | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | sub get_mp3tag { | 
| 458 | 14 |  |  | 14 | 1 | 14329 | my($file, $ver, $raw_v2) = @_; | 
| 459 | 14 |  |  |  |  | 24 | my($tag, $v1, $v2, $v2h, %info, @array, $fh); | 
| 460 | 14 |  | 50 |  |  | 70 | $raw_v2 ||= 0; | 
| 461 | 14 | 0 | 0 |  |  | 37 | $ver = !$ver ? 0 : ($ver == 2 || $ver == 1) ? $ver : 0; | 
|  |  | 50 |  |  |  |  |  | 
| 462 |  |  |  |  |  |  |  | 
| 463 | 14 | 50 | 33 |  |  | 104 | if (not (defined $file && $file ne '')) { | 
| 464 | 0 |  |  |  |  | 0 | $@ = "No file specified"; | 
| 465 | 0 |  |  |  |  | 0 | return undef; | 
| 466 |  |  |  |  |  |  | } | 
| 467 |  |  |  |  |  |  |  | 
| 468 | 14 | 50 |  |  |  | 248 | if (not -s $file) { | 
| 469 | 0 |  |  |  |  | 0 | $@ = "File is empty"; | 
| 470 | 0 |  |  |  |  | 0 | return undef; | 
| 471 |  |  |  |  |  |  | } | 
| 472 |  |  |  |  |  |  |  | 
| 473 | 14 | 50 |  |  |  | 61 | if (ref $file) { # filehandle passed | 
| 474 | 0 |  |  |  |  | 0 | $fh = $file; | 
| 475 |  |  |  |  |  |  | } else { | 
| 476 | 14 | 50 |  |  |  | 518 | if (not open $fh, '<', $file) { | 
| 477 | 0 |  |  |  |  | 0 | $@ = "Can't open $file: $!"; | 
| 478 | 0 |  |  |  |  | 0 | return undef; | 
| 479 |  |  |  |  |  |  | } | 
| 480 |  |  |  |  |  |  | } | 
| 481 |  |  |  |  |  |  |  | 
| 482 | 14 |  |  |  |  | 32 | binmode $fh; | 
| 483 |  |  |  |  |  |  |  | 
| 484 | 14 | 50 |  |  |  | 38 | if ($ver < 2) { | 
| 485 | 14 |  |  |  |  | 77 | seek $fh, -128, 2; | 
| 486 | 14 |  |  |  |  | 349 | while(defined(my $line = <$fh>)) { $tag .= $line } | 
|  | 14 |  |  |  |  | 70 |  | 
| 487 |  |  |  |  |  |  |  | 
| 488 | 14 | 100 | 66 |  |  | 127 | if ($tag && $tag =~ /^TAG/) { | 
|  |  | 50 |  |  |  |  |  | 
| 489 | 8 |  |  |  |  | 15 | $v1 = 1; | 
| 490 | 8 | 100 |  |  |  | 50 | if (substr($tag, -3, 2) =~ /\000[^\000]/) { | 
| 491 | 7 |  |  |  |  | 94 | (undef, @info{@v1_tag_names}) = | 
| 492 |  |  |  |  |  |  | (unpack('a3a30a30a30a4a28', $tag), | 
| 493 |  |  |  |  |  |  | ord(substr($tag, -2, 1)), | 
| 494 |  |  |  |  |  |  | $mp3_genres[ord(substr $tag, -1)]); | 
| 495 | 7 |  |  |  |  | 27 | $info{TAGVERSION} = 'ID3v1.1'; | 
| 496 |  |  |  |  |  |  | } else { | 
| 497 | 1 |  |  |  |  | 17 | (undef, @info{@v1_tag_names[0..4, 6]}) = | 
| 498 |  |  |  |  |  |  | (unpack('a3a30a30a30a4a30', $tag), | 
| 499 |  |  |  |  |  |  | $mp3_genres[ord(substr $tag, -1)]); | 
| 500 | 1 |  |  |  |  | 5 | $info{TAGVERSION} = 'ID3v1'; | 
| 501 |  |  |  |  |  |  | } | 
| 502 | 8 | 50 |  |  |  | 23 | if ($UNICODE) { | 
| 503 | 8 |  |  |  |  | 28 | for my $key (keys %info) { | 
| 504 | 63 | 50 |  |  |  | 350 | next unless $info{$key}; | 
| 505 | 63 |  |  |  |  | 151 | $info{$key} = Encode::encode_utf8($info{$key}); | 
| 506 |  |  |  |  |  |  | } | 
| 507 |  |  |  |  |  |  | } | 
| 508 |  |  |  |  |  |  | } elsif ($ver == 1) { | 
| 509 | 0 |  |  |  |  | 0 | _close($file, $fh); | 
| 510 | 0 |  |  |  |  | 0 | $@ = "No ID3v1 tag found"; | 
| 511 | 0 |  |  |  |  | 0 | return undef; | 
| 512 |  |  |  |  |  |  | } | 
| 513 |  |  |  |  |  |  | } | 
| 514 |  |  |  |  |  |  |  | 
| 515 | 14 |  |  |  |  | 80 | ($v2, $v2h) = _get_v2tag($fh); | 
| 516 |  |  |  |  |  |  |  | 
| 517 | 14 | 50 | 66 |  |  | 79 | unless ($v1 || $v2) { | 
| 518 | 0 |  |  |  |  | 0 | _close($file, $fh); | 
| 519 | 0 |  |  |  |  | 0 | $@ = "No ID3 tag found"; | 
| 520 | 0 |  |  |  |  | 0 | return undef; | 
| 521 |  |  |  |  |  |  | } | 
| 522 |  |  |  |  |  |  |  | 
| 523 | 14 | 100 | 33 |  |  | 104 | if (($ver == 0 || $ver == 2) && $v2) { | 
|  |  |  | 66 |  |  |  |  | 
| 524 | 6 | 50 | 33 |  |  | 36 | if ($raw_v2 == 1 && $ver == 2) { | 
| 525 | 0 |  |  |  |  | 0 | %info = %$v2; | 
| 526 | 0 |  |  |  |  | 0 | $info{TAGVERSION} = $v2h->{version}; | 
| 527 |  |  |  |  |  |  | } else { | 
| 528 | 6 | 50 |  |  |  | 22 | my $hash = $raw_v2 == 2 ? { map { ($_, $_) } keys %v2_tag_names } : \%v2_to_v1_names; | 
|  | 0 |  |  |  |  | 0 |  | 
| 529 | 6 |  |  |  |  | 28 | for my $id (keys %$hash) { | 
| 530 | 84 | 100 |  |  |  | 177 | if (exists $v2->{$id}) { | 
| 531 | 36 |  |  |  |  | 49 | my $data1 = $v2->{$id}; | 
| 532 |  |  |  |  |  |  |  | 
| 533 |  |  |  |  |  |  | # this is tricky ... if this is an arrayref, | 
| 534 |  |  |  |  |  |  | # we want to only return one, so we pick the | 
| 535 |  |  |  |  |  |  | # first one.  but if it is a comment, we pick | 
| 536 |  |  |  |  |  |  | # the first one where the first charcter after | 
| 537 |  |  |  |  |  |  | # the language is NULL and not an additional | 
| 538 |  |  |  |  |  |  | # sub-comment, because that is most likely to be | 
| 539 |  |  |  |  |  |  | # the user-supplied comment | 
| 540 | 36 | 50 | 33 |  |  | 83 | if (ref $data1 && !$raw_v2) { | 
| 541 | 0 | 0 |  |  |  | 0 | if ($id =~ /^COMM?$/) { | 
| 542 | 0 |  |  |  |  | 0 | my($newdata) = grep /^(....\000)/, @{$data1}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 543 | 0 |  | 0 |  |  | 0 | $data1 = $newdata || $data1->[0]; | 
| 544 |  |  |  |  |  |  | } else { | 
| 545 | 0 |  |  |  |  | 0 | $data1 = $data1->[0]; | 
| 546 |  |  |  |  |  |  | } | 
| 547 |  |  |  |  |  |  | } | 
| 548 |  |  |  |  |  |  |  | 
| 549 | 36 | 50 |  |  |  | 104 | $data1 = [ $data1 ] if ! ref $data1; | 
| 550 |  |  |  |  |  |  |  | 
| 551 | 36 |  |  |  |  | 2548 | for my $data (@$data1) { | 
| 552 |  |  |  |  |  |  | # TODO : this should only be done for certain frames; | 
| 553 |  |  |  |  |  |  | # using RAW still gives you access, but we should be smarter | 
| 554 |  |  |  |  |  |  | # about how individual frame types are handled.  it's not | 
| 555 |  |  |  |  |  |  | # like the list is infinitely long. | 
| 556 | 36 |  |  |  |  | 104 | $data =~ s/^(.)//; # strip first char (text encoding) | 
| 557 | 36 |  |  |  |  | 65 | my $encoding = $1; | 
| 558 | 36 |  |  |  |  | 36 | my $desc; | 
| 559 | 36 | 100 |  |  |  | 118 | if ($id =~ /^COM[M ]?$/) { # space for iTunes brokenness | 
| 560 | 6 |  |  |  |  | 18 | $data =~ s/^(?:...)//;		# strip language | 
| 561 |  |  |  |  |  |  | } | 
| 562 |  |  |  |  |  |  |  | 
| 563 | 36 | 50 |  |  |  | 66 | if ($UNICODE) { | 
| 564 | 36 | 50 | 33 |  |  | 198 | if ($encoding eq "\001" || $encoding eq "\002") {  # UTF-16, UTF-16BE | 
|  |  | 50 |  |  |  |  |  | 
|  |  | 50 |  |  |  |  |  | 
| 565 |  |  |  |  |  |  | # text fields can be null-separated lists; | 
| 566 |  |  |  |  |  |  | # UTF-16 therefore needs special care | 
| 567 |  |  |  |  |  |  | # | 
| 568 |  |  |  |  |  |  | # foobar2000 encodes tags in UTF-16LE | 
| 569 |  |  |  |  |  |  | # (which is apparently illegal) | 
| 570 |  |  |  |  |  |  | # Encode dies on a bad BOM, so it is | 
| 571 |  |  |  |  |  |  | # probably wise to wrap it in an eval | 
| 572 |  |  |  |  |  |  | # anyway | 
| 573 | 0 |  | 0 |  |  | 0 | $data = eval { Encode::decode('utf16', $data) } || Encode::decode('utf16le', $data); | 
| 574 |  |  |  |  |  |  | # this split we do doesn't work, because obviously | 
| 575 |  |  |  |  |  |  | # two NULLs can appear where we don't want ... | 
| 576 |  |  |  |  |  |  | #$data = join "\000", map { | 
| 577 |  |  |  |  |  |  | #	eval { Encode::decode('utf16', $_) } || Encode::decode('utf16le', $_) | 
| 578 |  |  |  |  |  |  | #} split /\000\000/, $data; | 
| 579 |  |  |  |  |  |  |  | 
| 580 |  |  |  |  |  |  | } elsif ($encoding eq "\003") { # UTF-8 | 
| 581 |  |  |  |  |  |  | # make sure string is UTF8, and set flag appropriately | 
| 582 | 0 |  |  |  |  | 0 | $data = Encode::decode('utf8', $data); | 
| 583 |  |  |  |  |  |  | } elsif ($encoding eq "\000") { | 
| 584 |  |  |  |  |  |  | # Try and guess the encoding, otherwise just use latin1 | 
| 585 | 36 |  |  |  |  | 148 | my $dec = Encode::Guess->guess($data); | 
| 586 | 36 | 50 |  |  |  | 1825 | if (ref $dec) { | 
| 587 | 36 |  |  |  |  | 117 | $data = $dec->decode($data); | 
| 588 |  |  |  |  |  |  | } else { | 
| 589 |  |  |  |  |  |  | # Best try | 
| 590 | 0 |  |  |  |  | 0 | $data = Encode::decode('iso-8859-1', $data); | 
| 591 |  |  |  |  |  |  | } | 
| 592 |  |  |  |  |  |  | } | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | # do we care about trailing NULL? | 
| 595 |  |  |  |  |  |  | # $data =~ s/\000$//; | 
| 596 |  |  |  |  |  |  |  | 
| 597 |  |  |  |  |  |  | } else { | 
| 598 |  |  |  |  |  |  | # If the string starts with an | 
| 599 |  |  |  |  |  |  | # UTF-16 little endian BOM, use a hack to | 
| 600 |  |  |  |  |  |  | # convert to ASCII per best-effort | 
| 601 | 0 |  |  |  |  | 0 | my $pat; | 
| 602 | 0 | 0 |  |  |  | 0 | if ($data =~ s/^\xFF\xFE//) { | 
|  |  | 0 |  |  |  |  |  | 
| 603 | 0 |  |  |  |  | 0 | $pat = 'v'; | 
| 604 |  |  |  |  |  |  | } elsif ($data =~ s/^\xFE\xFF//) { | 
| 605 | 0 |  |  |  |  | 0 | $pat = 'n'; | 
| 606 |  |  |  |  |  |  | } | 
| 607 | 0 | 0 |  |  |  | 0 | if ($pat) { | 
| 608 | 0 | 0 | 0 |  |  | 0 | $data = pack 'C*', map { | 
| 609 | 0 |  |  |  |  | 0 | (chr =~ /[[:ascii:]]/ && chr =~ /[[:print:]]/) | 
| 610 |  |  |  |  |  |  | ? $_ | 
| 611 |  |  |  |  |  |  | : ord('?') | 
| 612 |  |  |  |  |  |  | } unpack "$pat*", $data; | 
| 613 |  |  |  |  |  |  | } | 
| 614 |  |  |  |  |  |  | } | 
| 615 |  |  |  |  |  |  |  | 
| 616 |  |  |  |  |  |  | # We do this after decoding so we could be certain we're dealing | 
| 617 |  |  |  |  |  |  | # with 8-bit text. | 
| 618 | 36 | 100 |  |  |  | 166 | if ($id =~ /^COM[M ]?$/) { # space for iTunes brokenness | 
|  |  | 100 |  |  |  |  |  | 
| 619 | 6 |  |  |  |  | 49 | $data =~ s/^(.*?)\000//;	# strip up to first NULL(s), | 
| 620 |  |  |  |  |  |  | # for sub-comments (TODO: | 
| 621 |  |  |  |  |  |  | # handle all comment data) | 
| 622 | 6 |  |  |  |  | 14 | $desc = $1; | 
| 623 |  |  |  |  |  |  | } elsif ($id =~ /^TCON?$/) { | 
| 624 | 6 | 50 |  |  |  | 43 | if ($data =~ /^ \(? (\d+) (?:\)|\000)? (.+)?/sx) { | 
| 625 | 6 |  |  |  |  | 18 | my($index, $name) = ($1, $2); | 
| 626 | 6 | 50 | 33 |  |  | 43 | if ($name && $name ne "\000") { | 
| 627 | 0 |  |  |  |  | 0 | $data = $name; | 
| 628 |  |  |  |  |  |  | } else { | 
| 629 | 6 |  |  |  |  | 29 | $data = $mp3_genres[$index]; | 
| 630 |  |  |  |  |  |  | } | 
| 631 |  |  |  |  |  |  | } | 
| 632 |  |  |  |  |  |  | } | 
| 633 |  |  |  |  |  |  |  | 
| 634 | 36 | 50 | 33 |  |  | 200 | if ($raw_v2 == 2 && $desc) { | 
| 635 | 0 |  |  |  |  | 0 | $data = { $desc => $data }; | 
| 636 |  |  |  |  |  |  | } | 
| 637 |  |  |  |  |  |  |  | 
| 638 | 36 | 50 | 33 |  |  | 86 | if ($raw_v2 == 2 && exists $info{$hash->{$id}}) { | 
| 639 | 0 | 0 |  |  |  | 0 | if (ref $info{$hash->{$id}} eq 'ARRAY') { | 
| 640 | 0 |  |  |  |  | 0 | push @{$info{$hash->{$id}}}, $data; | 
|  | 0 |  |  |  |  | 0 |  | 
| 641 |  |  |  |  |  |  | } else { | 
| 642 | 0 |  |  |  |  | 0 | $info{$hash->{$id}} = [ $info{$hash->{$id}}, $data ]; | 
| 643 |  |  |  |  |  |  | } | 
| 644 |  |  |  |  |  |  | } else { | 
| 645 | 36 |  |  |  |  | 155 | $info{$hash->{$id}} = $data; | 
| 646 |  |  |  |  |  |  | } | 
| 647 |  |  |  |  |  |  | } | 
| 648 |  |  |  |  |  |  | } | 
| 649 |  |  |  |  |  |  | } | 
| 650 | 6 | 50 | 33 |  |  | 75 | if ($ver == 0 && $info{TAGVERSION}) { | 
| 651 | 0 |  |  |  |  | 0 | $info{TAGVERSION} .= ' / ' . $v2h->{version}; | 
| 652 |  |  |  |  |  |  | } else { | 
| 653 | 6 |  |  |  |  | 19 | $info{TAGVERSION} = $v2h->{version}; | 
| 654 |  |  |  |  |  |  | } | 
| 655 |  |  |  |  |  |  | } | 
| 656 |  |  |  |  |  |  | } | 
| 657 |  |  |  |  |  |  |  | 
| 658 | 14 | 50 | 33 |  |  | 48 | unless ($raw_v2 && $ver == 2) { | 
| 659 | 14 |  |  |  |  | 44 | foreach my $key (keys %info) { | 
| 660 | 105 | 50 |  |  |  | 221 | if (defined $info{$key}) { | 
| 661 | 105 |  |  |  |  | 248 | $info{$key} =~ s/\000+.*//g; | 
| 662 | 105 |  |  |  |  | 262 | $info{$key} =~ s/\s+$//; | 
| 663 |  |  |  |  |  |  | } | 
| 664 |  |  |  |  |  |  | } | 
| 665 |  |  |  |  |  |  |  | 
| 666 | 14 |  |  |  |  | 44 | for (@v1_tag_names) { | 
| 667 | 98 | 100 |  |  |  | 241 | $info{$_} = '' unless defined $info{$_}; | 
| 668 |  |  |  |  |  |  | } | 
| 669 |  |  |  |  |  |  | } | 
| 670 |  |  |  |  |  |  |  | 
| 671 | 14 | 50 | 33 |  |  | 123 | if (keys %info && exists $info{GENRE} && ! defined $info{GENRE}) { | 
|  |  |  | 33 |  |  |  |  | 
| 672 | 0 |  |  |  |  | 0 | $info{GENRE} = ''; | 
| 673 |  |  |  |  |  |  | } | 
| 674 |  |  |  |  |  |  |  | 
| 675 | 14 |  |  |  |  | 42 | _close($file, $fh); | 
| 676 |  |  |  |  |  |  |  | 
| 677 | 14 | 50 |  |  |  | 226 | return keys %info ? {%info} : undef; | 
| 678 |  |  |  |  |  |  | } | 
| 679 |  |  |  |  |  |  |  | 
| 680 |  |  |  |  |  |  | sub _get_v2tag { | 
| 681 | 14 |  |  | 14 |  | 18 | my($fh) = @_; | 
| 682 | 14 |  |  |  |  | 24 | my($off, $end, $myseek, $v2, $v2h, $hlen, $num, $wholetag); | 
| 683 |  |  |  |  |  |  |  | 
| 684 | 14 |  |  |  |  | 73 | $v2 = {}; | 
| 685 | 14 | 100 |  |  |  | 45 | $v2h = _get_v2head($fh) or return; | 
| 686 |  |  |  |  |  |  |  | 
| 687 | 6 | 50 |  |  |  | 19 | if ($v2h->{major_version} < 2) { | 
| 688 | 0 | 0 |  |  |  | 0 | carp "This is $v2h->{version}; " . | 
| 689 |  |  |  |  |  |  | "ID3v2 versions older than ID3v2.2.0 not supported\n" | 
| 690 |  |  |  |  |  |  | if $^W; | 
| 691 | 0 |  |  |  |  | 0 | return; | 
| 692 |  |  |  |  |  |  | } | 
| 693 |  |  |  |  |  |  |  | 
| 694 |  |  |  |  |  |  | # use syncsafe bytes if using version 2.4 | 
| 695 | 6 | 100 |  |  |  | 18 | my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256; | 
| 696 |  |  |  |  |  |  |  | 
| 697 | 6 | 100 |  |  |  | 24 | if ($v2h->{major_version} == 2) { | 
| 698 | 2 |  |  |  |  | 5 | $hlen = 6; | 
| 699 | 2 |  |  |  |  | 5 | $num = 3; | 
| 700 |  |  |  |  |  |  | } else { | 
| 701 | 4 |  |  |  |  | 8 | $hlen = 10; | 
| 702 | 4 |  |  |  |  | 6 | $num = 4; | 
| 703 |  |  |  |  |  |  | } | 
| 704 |  |  |  |  |  |  |  | 
| 705 | 6 |  |  |  |  | 13 | $off = $v2h->{ext_header_size} + 10; | 
| 706 | 6 |  |  |  |  | 8 | $end = $v2h->{tag_size} + 10; # should we read in the footer too? | 
| 707 |  |  |  |  |  |  |  | 
| 708 | 6 |  |  |  |  | 45 | seek $fh, $v2h->{offset}, 0; | 
| 709 | 6 |  |  |  |  | 87 | read $fh, $wholetag, $end; | 
| 710 |  |  |  |  |  |  |  | 
| 711 | 6 | 50 |  |  |  | 22 | $wholetag =~ s/\xFF\x00/\xFF/gs if $v2h->{unsync}; | 
| 712 |  |  |  |  |  |  |  | 
| 713 |  |  |  |  |  |  | $myseek = sub { | 
| 714 | 42 |  |  | 42 |  | 63 | my $bytes = substr($wholetag, $off, $hlen); | 
| 715 | 42 | 50 | 66 |  |  | 355 | return unless $bytes =~ /^([A-Z0-9]{$num})/ | 
|  |  |  | 66 |  |  |  |  | 
| 716 |  |  |  |  |  |  | || ($num == 4 && $bytes =~ /^(COM )/);  # stupid iTunes | 
| 717 | 36 |  |  |  |  | 70 | my($id, $size) = ($1, $hlen); | 
| 718 | 36 |  |  |  |  | 99 | my @bytes = reverse unpack "C$num", substr($bytes, $num, $num); | 
| 719 |  |  |  |  |  |  |  | 
| 720 | 36 |  |  |  |  | 71 | for my $i (0 .. ($num - 1)) { | 
| 721 | 132 |  |  |  |  | 197 | $size += $bytes[$i] * $bytesize ** $i; | 
| 722 |  |  |  |  |  |  | } | 
| 723 |  |  |  |  |  |  |  | 
| 724 | 36 |  |  |  |  | 98 | my $flags = {}; | 
| 725 | 36 | 100 |  |  |  | 90 | if ($v2h->{major_version} > 3) { | 
| 726 | 12 |  |  |  |  | 94 | my @bits = split //, unpack 'B16', substr($bytes, 8, 2); | 
| 727 | 12 |  |  |  |  | 31 | $flags->{frame_unsync}       = $bits[14]; | 
| 728 | 12 |  |  |  |  | 43 | $flags->{data_len_indicator} = $bits[15]; | 
| 729 |  |  |  |  |  |  | } | 
| 730 |  |  |  |  |  |  |  | 
| 731 | 36 |  |  |  |  | 147 | return($id, $size, $flags); | 
| 732 | 6 |  |  |  |  | 47 | }; | 
| 733 |  |  |  |  |  |  |  | 
| 734 | 6 |  |  |  |  | 23 | while ($off < $end) { | 
| 735 | 42 | 100 |  |  |  | 66 | my($id, $size, $flags) = &$myseek or last; | 
| 736 |  |  |  |  |  |  |  | 
| 737 | 36 |  |  |  |  | 70 | my $bytes = substr($wholetag, $off+$hlen, $size-$hlen); | 
| 738 |  |  |  |  |  |  |  | 
| 739 | 36 |  |  |  |  | 35 | my $data_len; | 
| 740 | 36 | 50 |  |  |  | 70 | if ($flags->{data_len_indicator}) { | 
| 741 | 0 |  |  |  |  | 0 | $data_len = 0; | 
| 742 | 0 |  |  |  |  | 0 | my @data_len_bytes = reverse unpack 'C4', substr($bytes, 0, 4); | 
| 743 | 0 |  |  |  |  | 0 | $bytes = substr($bytes, 4); | 
| 744 | 0 |  |  |  |  | 0 | for my $i (0..3) { | 
| 745 | 0 |  |  |  |  | 0 | $data_len += $data_len_bytes[$i] * 128 ** $i; | 
| 746 |  |  |  |  |  |  | } | 
| 747 |  |  |  |  |  |  | } | 
| 748 |  |  |  |  |  |  |  | 
| 749 |  |  |  |  |  |  | # perform frame-level unsync if needed (skip if already done for whole tag) | 
| 750 | 36 | 50 | 33 |  |  | 79 | $bytes =~ s/\xFF\x00/\xFF/gs if $flags->{frame_unsync} && !$v2h->{unsync}; | 
| 751 |  |  |  |  |  |  |  | 
| 752 |  |  |  |  |  |  | # if we know the data length, sanity check it now. | 
| 753 | 36 | 50 | 33 |  |  | 74 | if ($flags->{data_len_indicator} && defined $data_len) { | 
| 754 | 0 | 0 |  |  |  | 0 | carp "Size mismatch on $id\n" unless $data_len == length($bytes); | 
| 755 |  |  |  |  |  |  | } | 
| 756 |  |  |  |  |  |  |  | 
| 757 | 36 | 50 |  |  |  | 67 | if (exists $v2->{$id}) { | 
| 758 | 0 | 0 |  |  |  | 0 | if (ref $v2->{$id} eq 'ARRAY') { | 
| 759 | 0 |  |  |  |  | 0 | push @{$v2->{$id}}, $bytes; | 
|  | 0 |  |  |  |  | 0 |  | 
| 760 |  |  |  |  |  |  | } else { | 
| 761 | 0 |  |  |  |  | 0 | $v2->{$id} = [$v2->{$id}, $bytes]; | 
| 762 |  |  |  |  |  |  | } | 
| 763 |  |  |  |  |  |  | } else { | 
| 764 | 36 |  |  |  |  | 73 | $v2->{$id} = $bytes; | 
| 765 |  |  |  |  |  |  | } | 
| 766 | 36 |  |  |  |  | 94 | $off += $size; | 
| 767 |  |  |  |  |  |  | } | 
| 768 |  |  |  |  |  |  |  | 
| 769 | 6 |  |  |  |  | 61 | return($v2, $v2h); | 
| 770 |  |  |  |  |  |  | } | 
| 771 |  |  |  |  |  |  |  | 
| 772 |  |  |  |  |  |  |  | 
| 773 |  |  |  |  |  |  | =pod | 
| 774 |  |  |  |  |  |  |  | 
| 775 |  |  |  |  |  |  | =item get_mp3info (FILE) | 
| 776 |  |  |  |  |  |  |  | 
| 777 |  |  |  |  |  |  | Returns hash reference containing file information for MP3 file. | 
| 778 |  |  |  |  |  |  | This data cannot be changed.  Returned data: | 
| 779 |  |  |  |  |  |  |  | 
| 780 |  |  |  |  |  |  | VERSION		MPEG audio version (1, 2, 2.5) | 
| 781 |  |  |  |  |  |  | LAYER		MPEG layer description (1, 2, 3) | 
| 782 |  |  |  |  |  |  | STEREO		boolean for audio is in stereo | 
| 783 |  |  |  |  |  |  |  | 
| 784 |  |  |  |  |  |  | VBR		boolean for variable bitrate | 
| 785 |  |  |  |  |  |  | BITRATE		bitrate in kbps (average for VBR files) | 
| 786 |  |  |  |  |  |  | FREQUENCY	frequency in kHz | 
| 787 |  |  |  |  |  |  | SIZE		bytes in audio stream | 
| 788 |  |  |  |  |  |  | OFFSET		bytes offset that stream begins | 
| 789 |  |  |  |  |  |  |  | 
| 790 |  |  |  |  |  |  | SECS		total seconds | 
| 791 |  |  |  |  |  |  | MM		minutes | 
| 792 |  |  |  |  |  |  | SS		leftover seconds | 
| 793 |  |  |  |  |  |  | MS		leftover milliseconds | 
| 794 |  |  |  |  |  |  | TIME		time in MM:SS | 
| 795 |  |  |  |  |  |  |  | 
| 796 |  |  |  |  |  |  | COPYRIGHT	boolean for audio is copyrighted | 
| 797 |  |  |  |  |  |  | PADDING		boolean for MP3 frames are padded | 
| 798 |  |  |  |  |  |  | MODE		channel mode (0 = stereo, 1 = joint stereo, | 
| 799 |  |  |  |  |  |  | 2 = dual channel, 3 = single channel) | 
| 800 |  |  |  |  |  |  | FRAMES		approximate number of frames | 
| 801 |  |  |  |  |  |  | FRAME_LENGTH	approximate length of a frame | 
| 802 |  |  |  |  |  |  | VBR_SCALE	VBR scale from VBR header | 
| 803 |  |  |  |  |  |  |  | 
| 804 |  |  |  |  |  |  | On error, returns nothing and sets C<$@>. | 
| 805 |  |  |  |  |  |  |  | 
| 806 |  |  |  |  |  |  | =cut | 
| 807 |  |  |  |  |  |  |  | 
| 808 |  |  |  |  |  |  | sub get_mp3info { | 
| 809 | 6 |  |  | 6 | 1 | 14 | my($file) = @_; | 
| 810 | 6 |  |  |  |  | 10 | my($off, $byte, $eof, $h, $tot, $fh); | 
| 811 |  |  |  |  |  |  |  | 
| 812 | 6 | 50 | 33 |  |  | 36 | if (not (defined $file && $file ne '')) { | 
| 813 | 0 |  |  |  |  | 0 | $@ = "No file specified"; | 
| 814 | 0 |  |  |  |  | 0 | return undef; | 
| 815 |  |  |  |  |  |  | } | 
| 816 |  |  |  |  |  |  |  | 
| 817 | 6 | 50 |  |  |  | 118 | if (not -s $file) { | 
| 818 | 0 |  |  |  |  | 0 | $@ = "File is empty"; | 
| 819 | 0 |  |  |  |  | 0 | return undef; | 
| 820 |  |  |  |  |  |  | } | 
| 821 |  |  |  |  |  |  |  | 
| 822 | 6 | 50 |  |  |  | 15 | if (ref $file) { # filehandle passed | 
| 823 | 0 |  |  |  |  | 0 | $fh = $file; | 
| 824 |  |  |  |  |  |  | } else { | 
| 825 | 6 | 50 |  |  |  | 217 | if (not open $fh, '<', $file) { | 
| 826 | 0 |  |  |  |  | 0 | $@ = "Can't open $file: $!"; | 
| 827 | 0 |  |  |  |  | 0 | return undef; | 
| 828 |  |  |  |  |  |  | } | 
| 829 |  |  |  |  |  |  | } | 
| 830 |  |  |  |  |  |  |  | 
| 831 | 6 |  |  |  |  | 9 | $off = 0; | 
| 832 | 6 |  |  |  |  | 7 | $tot = 8192; | 
| 833 |  |  |  |  |  |  |  | 
| 834 | 6 |  |  |  |  | 13 | binmode $fh; | 
| 835 | 6 |  |  |  |  | 35 | seek $fh, $off, 0; | 
| 836 | 6 |  |  |  |  | 91 | read $fh, $byte, 4; | 
| 837 |  |  |  |  |  |  |  | 
| 838 | 6 | 50 |  |  |  | 15 | if ($off == 0) { | 
| 839 | 6 | 50 |  |  |  | 14 | if (my $v2h = _get_v2head($fh)) { | 
| 840 | 0 |  |  |  |  | 0 | $tot += $off += $v2h->{tag_size}; | 
| 841 | 0 |  |  |  |  | 0 | seek $fh, $off, 0; | 
| 842 | 0 |  |  |  |  | 0 | read $fh, $byte, 4; | 
| 843 |  |  |  |  |  |  | } | 
| 844 |  |  |  |  |  |  | } | 
| 845 |  |  |  |  |  |  |  | 
| 846 | 6 |  |  |  |  | 15 | $h = _get_head($byte); | 
| 847 | 6 |  |  |  |  | 17 | my $is_mp3 = _is_mp3($h); | 
| 848 | 6 |  |  |  |  | 15 | until ($is_mp3) { | 
| 849 | 1944 |  |  |  |  | 1640 | $off++; | 
| 850 | 1944 |  |  |  |  | 12799 | seek $fh, $off, 0; | 
| 851 | 1944 |  |  |  |  | 11830 | read $fh, $byte, 4; | 
| 852 | 1944 | 50 | 33 |  |  | 4226 | if ($off > $tot && !$try_harder) { | 
| 853 | 0 |  |  |  |  | 0 | _close($file, $fh); | 
| 854 | 0 |  |  |  |  | 0 | $@ = "Couldn't find MP3 header (perhaps set " . | 
| 855 |  |  |  |  |  |  | '$MP3::Info::try_harder and retry)'; | 
| 856 | 0 |  |  |  |  | 0 | return undef; | 
| 857 |  |  |  |  |  |  | } | 
| 858 | 1944 | 100 |  |  |  | 5146 | next if ord($byte) != 0xFF; | 
| 859 | 66 |  |  |  |  | 115 | $h = _get_head($byte); | 
| 860 | 66 |  |  |  |  | 212 | $is_mp3 = _is_mp3($h); | 
| 861 |  |  |  |  |  |  | } | 
| 862 |  |  |  |  |  |  |  | 
| 863 | 6 |  |  |  |  | 19 | my $vbr = _get_vbr($fh, $h, \$off); | 
| 864 |  |  |  |  |  |  |  | 
| 865 | 6 |  |  |  |  | 40 | seek $fh, 0, 2; | 
| 866 | 6 |  |  |  |  | 9 | $eof = tell $fh; | 
| 867 | 6 |  |  |  |  | 23 | seek $fh, -128, 2; | 
| 868 | 6 | 50 |  |  |  | 96 | $eof -= 128 if <$fh> =~ /^TAG/ ? 1 : 0; | 
|  |  | 50 |  |  |  |  |  | 
| 869 |  |  |  |  |  |  |  | 
| 870 | 6 |  |  |  |  | 17 | _close($file, $fh); | 
| 871 |  |  |  |  |  |  |  | 
| 872 | 6 |  |  |  |  | 16 | $h->{size} = $eof - $off; | 
| 873 | 6 |  |  |  |  | 12 | $h->{offset} = $off; | 
| 874 |  |  |  |  |  |  |  | 
| 875 | 6 |  |  |  |  | 14 | return _get_info($h, $vbr); | 
| 876 |  |  |  |  |  |  | } | 
| 877 |  |  |  |  |  |  |  | 
| 878 |  |  |  |  |  |  | sub _get_info { | 
| 879 | 6 |  |  | 6 |  | 9 | my($h, $vbr) = @_; | 
| 880 | 6 |  |  |  |  | 7 | my $i; | 
| 881 |  |  |  |  |  |  |  | 
| 882 | 6 | 0 |  |  |  | 29 | $i->{VERSION}	= $h->{IDR} == 2 ? 2 : $h->{IDR} == 3 ? 1 : | 
|  |  | 50 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 883 |  |  |  |  |  |  | $h->{IDR} == 0 ? 2.5 : 0; | 
| 884 | 6 |  |  |  |  | 14 | $i->{LAYER}	= 4 - $h->{layer}; | 
| 885 | 6 | 50 |  |  |  | 18 | $i->{VBR}	= defined $vbr ? 1 : 0; | 
| 886 |  |  |  |  |  |  |  | 
| 887 | 6 | 50 |  |  |  | 17 | $i->{COPYRIGHT}	= $h->{copyright} ? 1 : 0; | 
| 888 | 6 | 50 |  |  |  | 16 | $i->{PADDING}	= $h->{padding_bit} ? 1 : 0; | 
| 889 | 6 | 100 |  |  |  | 15 | $i->{STEREO}	= $h->{mode} == 3 ? 0 : 1; | 
| 890 | 6 |  |  |  |  | 11 | $i->{MODE}	= $h->{mode}; | 
| 891 |  |  |  |  |  |  |  | 
| 892 | 6 | 50 | 33 |  |  | 26 | $i->{SIZE}	= $vbr && $vbr->{bytes} ? $vbr->{bytes} : $h->{size}; | 
| 893 | 6 |  |  |  |  | 13 | $i->{OFFSET}	= $h->{offset}; | 
| 894 |  |  |  |  |  |  |  | 
| 895 | 6 | 100 |  |  |  | 17 | my $mfs		= $h->{fs} / ($h->{ID} ? 144000 : 72000); | 
| 896 | 6 | 50 | 33 |  |  | 33 | $i->{FRAMES}	= int($vbr && $vbr->{frames} | 
| 897 |  |  |  |  |  |  | ? $vbr->{frames} | 
| 898 |  |  |  |  |  |  | : $i->{SIZE} / ($h->{bitrate} / $mfs) | 
| 899 |  |  |  |  |  |  | ); | 
| 900 |  |  |  |  |  |  |  | 
| 901 | 6 | 50 |  |  |  | 14 | if ($vbr) { | 
| 902 | 0 | 0 |  |  |  | 0 | $i->{VBR_SCALE}	= $vbr->{scale} if $vbr->{scale}; | 
| 903 | 0 |  |  |  |  | 0 | $h->{bitrate}	= $i->{SIZE} / $i->{FRAMES} * $mfs; | 
| 904 | 0 | 0 |  |  |  | 0 | if (not $h->{bitrate}) { | 
| 905 | 0 |  |  |  |  | 0 | $@ = "Couldn't determine VBR bitrate"; | 
| 906 | 0 |  |  |  |  | 0 | return undef; | 
| 907 |  |  |  |  |  |  | } | 
| 908 |  |  |  |  |  |  | } | 
| 909 |  |  |  |  |  |  |  | 
| 910 | 6 |  |  |  |  | 24 | $h->{'length'}	= ($i->{SIZE} * 8) / $h->{bitrate} / 10; | 
| 911 | 6 |  |  |  |  | 18 | $i->{SECS}	= $h->{'length'} / 100; | 
| 912 | 6 |  |  |  |  | 18 | $i->{MM}	= int $i->{SECS} / 60; | 
| 913 | 6 |  |  |  |  | 16 | $i->{SS}	= int $i->{SECS} % 60; | 
| 914 | 6 |  |  |  |  | 23 | $i->{MS}	= (($i->{SECS} - ($i->{MM} * 60) - $i->{SS}) * 1000); | 
| 915 |  |  |  |  |  |  | #	$i->{LF}	= ($i->{MS} / 1000) * ($i->{FRAMES} / $i->{SECS}); | 
| 916 |  |  |  |  |  |  | #	int($i->{MS} / 100 * 75);  # is this right? | 
| 917 | 6 |  |  |  |  | 10 | $i->{TIME}	= sprintf "%.2d:%.2d", @{$i}{'MM', 'SS'}; | 
|  | 6 |  |  |  |  | 38 |  | 
| 918 |  |  |  |  |  |  |  | 
| 919 | 6 |  |  |  |  | 19 | $i->{BITRATE}		= int $h->{bitrate}; | 
| 920 |  |  |  |  |  |  | # should we just return if ! FRAMES? | 
| 921 | 6 | 50 |  |  |  | 25 | $i->{FRAME_LENGTH}	= int($h->{size} / $i->{FRAMES}) if $i->{FRAMES}; | 
| 922 | 6 |  |  |  |  | 19 | $i->{FREQUENCY}		= $frequency_tbl[3 * $h->{IDR} + $h->{sampling_freq}]; | 
| 923 |  |  |  |  |  |  |  | 
| 924 | 6 |  |  |  |  | 73 | return $i; | 
| 925 |  |  |  |  |  |  | } | 
| 926 |  |  |  |  |  |  |  | 
| 927 |  |  |  |  |  |  | sub _get_head { | 
| 928 | 72 |  |  | 72 |  | 108 | my($byte) = @_; | 
| 929 | 72 |  |  |  |  | 65 | my($bytes, $h); | 
| 930 |  |  |  |  |  |  |  | 
| 931 | 72 |  |  |  |  | 112 | $bytes = _unpack_head($byte); | 
| 932 | 72 |  |  |  |  | 625 | @$h{qw(IDR ID layer protection_bit | 
| 933 |  |  |  |  |  |  | bitrate_index sampling_freq padding_bit private_bit | 
| 934 |  |  |  |  |  |  | mode mode_extension copyright original | 
| 935 |  |  |  |  |  |  | emphasis version_index bytes)} = ( | 
| 936 |  |  |  |  |  |  | ($bytes>>19)&3, ($bytes>>19)&1, ($bytes>>17)&3, ($bytes>>16)&1, | 
| 937 |  |  |  |  |  |  | ($bytes>>12)&15, ($bytes>>10)&3, ($bytes>>9)&1, ($bytes>>8)&1, | 
| 938 |  |  |  |  |  |  | ($bytes>>6)&3, ($bytes>>4)&3, ($bytes>>3)&1, ($bytes>>2)&1, | 
| 939 |  |  |  |  |  |  | $bytes&3, ($bytes>>19)&3, $bytes | 
| 940 |  |  |  |  |  |  | ); | 
| 941 |  |  |  |  |  |  |  | 
| 942 | 72 |  |  |  |  | 292 | $h->{bitrate} = $t_bitrate[$h->{ID}][3 - $h->{layer}][$h->{bitrate_index}]; | 
| 943 | 72 |  |  |  |  | 144 | $h->{fs} = $t_sampling_freq[$h->{IDR}][$h->{sampling_freq}]; | 
| 944 |  |  |  |  |  |  |  | 
| 945 | 72 |  |  |  |  | 139 | return $h; | 
| 946 |  |  |  |  |  |  | } | 
| 947 |  |  |  |  |  |  |  | 
| 948 |  |  |  |  |  |  | sub _is_mp3 { | 
| 949 | 72 | 50 |  | 72 |  | 146 | my $h = $_[0] or return undef; | 
| 950 |  |  |  |  |  |  | return ! (	# all below must be false | 
| 951 | 72 |  | 33 |  |  | 753 | $h->{bitrate_index} == 0 | 
| 952 |  |  |  |  |  |  | || | 
| 953 |  |  |  |  |  |  | $h->{version_index} == 1 | 
| 954 |  |  |  |  |  |  | || | 
| 955 |  |  |  |  |  |  | ($h->{bytes} & 0xFFE00000) != 0xFFE00000 | 
| 956 |  |  |  |  |  |  | || | 
| 957 |  |  |  |  |  |  | !$h->{fs} | 
| 958 |  |  |  |  |  |  | || | 
| 959 |  |  |  |  |  |  | !$h->{bitrate} | 
| 960 |  |  |  |  |  |  | || | 
| 961 |  |  |  |  |  |  | $h->{bitrate_index} == 15 | 
| 962 |  |  |  |  |  |  | || | 
| 963 |  |  |  |  |  |  | !$h->{layer} | 
| 964 |  |  |  |  |  |  | || | 
| 965 |  |  |  |  |  |  | $h->{sampling_freq} == 3 | 
| 966 |  |  |  |  |  |  | || | 
| 967 |  |  |  |  |  |  | $h->{emphasis} == 2 | 
| 968 |  |  |  |  |  |  | || | 
| 969 |  |  |  |  |  |  | !$h->{bitrate_index} | 
| 970 |  |  |  |  |  |  | || | 
| 971 |  |  |  |  |  |  | ($h->{bytes} & 0xFFFF0000) == 0xFFFE0000 | 
| 972 |  |  |  |  |  |  | || | 
| 973 |  |  |  |  |  |  | ($h->{ID} == 1 && $h->{layer} == 3 && $h->{protection_bit} == 1) | 
| 974 |  |  |  |  |  |  | || | 
| 975 |  |  |  |  |  |  | ($h->{mode_extension} != 0 && $h->{mode} != 1) | 
| 976 |  |  |  |  |  |  | ); | 
| 977 |  |  |  |  |  |  | } | 
| 978 |  |  |  |  |  |  |  | 
| 979 |  |  |  |  |  |  | sub _get_vbr { | 
| 980 | 6 |  |  | 6 |  | 13 | my($fh, $h, $roff) = @_; | 
| 981 | 6 |  |  |  |  | 8 | my($off, $bytes, @bytes, $myseek, %vbr); | 
| 982 |  |  |  |  |  |  |  | 
| 983 | 6 |  |  |  |  | 16 | $off = $$roff; | 
| 984 | 6 |  |  |  |  | 15 | @_ = ();	# closure confused if we don't do this | 
| 985 |  |  |  |  |  |  |  | 
| 986 |  |  |  |  |  |  | $myseek = sub { | 
| 987 | 6 |  | 50 | 6 |  | 28 | my $n = $_[0] || 4; | 
| 988 | 6 |  |  |  |  | 41 | seek $fh, $off, 0; | 
| 989 | 6 |  |  |  |  | 43 | read $fh, $bytes, $n; | 
| 990 | 6 |  |  |  |  | 17 | $off += $n; | 
| 991 | 6 |  |  |  |  | 39 | }; | 
| 992 |  |  |  |  |  |  |  | 
| 993 | 6 |  |  |  |  | 11 | $off += 4; | 
| 994 |  |  |  |  |  |  |  | 
| 995 | 6 | 100 |  |  |  | 21 | if ($h->{ID}) {	# MPEG1 | 
| 996 | 3 | 50 |  |  |  | 10 | $off += $h->{mode} == 3 ? 17 : 32; | 
| 997 |  |  |  |  |  |  | } else {	# MPEG2 | 
| 998 | 3 | 50 |  |  |  | 9 | $off += $h->{mode} == 3 ? 9 : 17; | 
| 999 |  |  |  |  |  |  | } | 
| 1000 |  |  |  |  |  |  |  | 
| 1001 | 6 |  |  |  |  | 13 | &$myseek; | 
| 1002 | 6 | 50 |  |  |  | 45 | return unless $bytes eq 'Xing'; | 
| 1003 |  |  |  |  |  |  |  | 
| 1004 | 0 |  |  |  |  | 0 | &$myseek; | 
| 1005 | 0 |  |  |  |  | 0 | $vbr{flags} = _unpack_head($bytes); | 
| 1006 |  |  |  |  |  |  |  | 
| 1007 | 0 | 0 |  |  |  | 0 | if ($vbr{flags} & 1) { | 
| 1008 | 0 |  |  |  |  | 0 | &$myseek; | 
| 1009 | 0 |  |  |  |  | 0 | $vbr{frames} = _unpack_head($bytes); | 
| 1010 |  |  |  |  |  |  | } | 
| 1011 |  |  |  |  |  |  |  | 
| 1012 | 0 | 0 |  |  |  | 0 | if ($vbr{flags} & 2) { | 
| 1013 | 0 |  |  |  |  | 0 | &$myseek; | 
| 1014 | 0 |  |  |  |  | 0 | $vbr{bytes} = _unpack_head($bytes); | 
| 1015 |  |  |  |  |  |  | } | 
| 1016 |  |  |  |  |  |  |  | 
| 1017 | 0 | 0 |  |  |  | 0 | if ($vbr{flags} & 4) { | 
| 1018 | 0 |  |  |  |  | 0 | $myseek->(100); | 
| 1019 |  |  |  |  |  |  | # Not used right now ... | 
| 1020 |  |  |  |  |  |  | #		$vbr{toc} = _unpack_head($bytes); | 
| 1021 |  |  |  |  |  |  | } | 
| 1022 |  |  |  |  |  |  |  | 
| 1023 | 0 | 0 |  |  |  | 0 | if ($vbr{flags} & 8) { # (quality ind., 0=best 100=worst) | 
| 1024 | 0 |  |  |  |  | 0 | &$myseek; | 
| 1025 | 0 |  |  |  |  | 0 | $vbr{scale} = _unpack_head($bytes); | 
| 1026 |  |  |  |  |  |  | } else { | 
| 1027 | 0 |  |  |  |  | 0 | $vbr{scale} = -1; | 
| 1028 |  |  |  |  |  |  | } | 
| 1029 |  |  |  |  |  |  |  | 
| 1030 | 0 |  |  |  |  | 0 | $$roff = $off; | 
| 1031 | 0 |  |  |  |  | 0 | return \%vbr; | 
| 1032 |  |  |  |  |  |  | } | 
| 1033 |  |  |  |  |  |  |  | 
| 1034 |  |  |  |  |  |  | sub _get_v2head { | 
| 1035 | 36 | 50 |  | 36 |  | 90 | my $fh = $_[0] or return; | 
| 1036 | 36 |  |  |  |  | 35 | my($v2h, $bytes, @bytes); | 
| 1037 | 36 |  |  |  |  | 167 | $v2h->{offset} = 0; | 
| 1038 |  |  |  |  |  |  |  | 
| 1039 |  |  |  |  |  |  | # check first three bytes for 'ID3' | 
| 1040 | 36 |  |  |  |  | 191 | seek $fh, 0, 0; | 
| 1041 | 36 |  |  |  |  | 355 | read $fh, $bytes, 3; | 
| 1042 |  |  |  |  |  |  |  | 
| 1043 |  |  |  |  |  |  | # TODO: add support for tags at the end of the file | 
| 1044 | 36 | 50 | 33 |  |  | 194 | if ($bytes eq 'RIF' || $bytes eq 'FOR') { | 
| 1045 | 0 | 0 |  |  |  | 0 | _find_id3_chunk($fh, $bytes) or return; | 
| 1046 | 0 |  |  |  |  | 0 | $v2h->{offset} = tell $fh; | 
| 1047 | 0 |  |  |  |  | 0 | read $fh, $bytes, 3; | 
| 1048 |  |  |  |  |  |  | } | 
| 1049 |  |  |  |  |  |  |  | 
| 1050 | 36 | 100 |  |  |  | 142 | return unless $bytes eq 'ID3'; | 
| 1051 |  |  |  |  |  |  |  | 
| 1052 |  |  |  |  |  |  | # get version | 
| 1053 | 12 |  |  |  |  | 23 | read $fh, $bytes, 2; | 
| 1054 | 12 |  |  |  |  | 109 | $v2h->{version} = sprintf "ID3v2.%d.%d", | 
| 1055 |  |  |  |  |  |  | @$v2h{qw[major_version minor_version]} = | 
| 1056 |  |  |  |  |  |  | unpack 'c2', $bytes; | 
| 1057 |  |  |  |  |  |  |  | 
| 1058 |  |  |  |  |  |  | # get flags | 
| 1059 | 12 |  |  |  |  | 25 | read $fh, $bytes, 1; | 
| 1060 | 12 |  |  |  |  | 71 | my @bits = split //, unpack 'b8', $bytes; | 
| 1061 | 12 | 100 |  |  |  | 37 | if ($v2h->{major_version} == 2) { | 
| 1062 | 4 |  |  |  |  | 11 | $v2h->{unsync}       = $bits[7]; | 
| 1063 | 4 |  |  |  |  | 10 | $v2h->{compression}  = $bits[8]; | 
| 1064 | 4 |  |  |  |  | 8 | $v2h->{ext_header}   = 0; | 
| 1065 | 4 |  |  |  |  | 12 | $v2h->{experimental} = 0; | 
| 1066 |  |  |  |  |  |  | } else { | 
| 1067 | 8 |  |  |  |  | 16 | $v2h->{unsync}       = $bits[7]; | 
| 1068 | 8 |  |  |  |  | 20 | $v2h->{ext_header}   = $bits[6]; | 
| 1069 | 8 |  |  |  |  | 13 | $v2h->{experimental} = $bits[5]; | 
| 1070 | 8 | 100 |  |  |  | 28 | $v2h->{footer}       = $bits[4] if $v2h->{major_version} == 4; | 
| 1071 |  |  |  |  |  |  | } | 
| 1072 |  |  |  |  |  |  |  | 
| 1073 |  |  |  |  |  |  | # get ID3v2 tag length from bytes 7-10 | 
| 1074 | 12 |  |  |  |  | 26 | $v2h->{tag_size} = 10;	# include ID3v2 header size | 
| 1075 | 12 | 50 |  |  |  | 27 | $v2h->{tag_size} += 10 if $v2h->{footer}; | 
| 1076 | 12 |  |  |  |  | 23 | read $fh, $bytes, 4; | 
| 1077 | 12 |  |  |  |  | 40 | @bytes = reverse unpack 'C4', $bytes; | 
| 1078 | 12 |  |  |  |  | 26 | foreach my $i (0 .. 3) { | 
| 1079 |  |  |  |  |  |  | # whoaaaaaa nellllllyyyyyy! | 
| 1080 | 48 |  |  |  |  | 107 | $v2h->{tag_size} += $bytes[$i] * 128 ** $i; | 
| 1081 |  |  |  |  |  |  | } | 
| 1082 |  |  |  |  |  |  |  | 
| 1083 |  |  |  |  |  |  | # get extended header size | 
| 1084 | 12 |  |  |  |  | 24 | $v2h->{ext_header_size} = 0; | 
| 1085 | 12 | 50 |  |  |  | 28 | if ($v2h->{ext_header}) { | 
| 1086 | 0 |  |  |  |  | 0 | read $fh, $bytes, 4; | 
| 1087 | 0 |  |  |  |  | 0 | @bytes = reverse unpack 'C4', $bytes; | 
| 1088 |  |  |  |  |  |  |  | 
| 1089 |  |  |  |  |  |  | # use syncsafe bytes if using version 2.4 | 
| 1090 | 0 | 0 |  |  |  | 0 | my $bytesize = ($v2h->{major_version} > 3) ? 128 : 256; | 
| 1091 | 0 |  |  |  |  | 0 | for my $i (0..3) { | 
| 1092 | 0 |  |  |  |  | 0 | $v2h->{ext_header_size} += $bytes[$i] * $bytesize ** $i; | 
| 1093 |  |  |  |  |  |  | } | 
| 1094 |  |  |  |  |  |  | } | 
| 1095 |  |  |  |  |  |  |  | 
| 1096 | 12 |  |  |  |  | 50 | return $v2h; | 
| 1097 |  |  |  |  |  |  | } | 
| 1098 |  |  |  |  |  |  |  | 
| 1099 |  |  |  |  |  |  | sub _find_id3_chunk { | 
| 1100 | 0 |  |  | 0 |  | 0 | my($fh, $filetype) = @_; | 
| 1101 | 0 |  |  |  |  | 0 | my($bytes, $size, $tag, $pat, $mat); | 
| 1102 |  |  |  |  |  |  |  | 
| 1103 | 0 |  |  |  |  | 0 | read $fh, $bytes, 1; | 
| 1104 | 0 | 0 |  |  |  | 0 | if ($filetype eq 'RIF') {  # WAV | 
|  |  | 0 |  |  |  |  |  | 
| 1105 | 0 | 0 |  |  |  | 0 | return 0 if $bytes ne 'F'; | 
| 1106 | 0 |  |  |  |  | 0 | $pat = 'a4V'; | 
| 1107 | 0 |  |  |  |  | 0 | $mat = 'id3 '; | 
| 1108 |  |  |  |  |  |  | } elsif ($filetype eq 'FOR') { # AIFF | 
| 1109 | 0 | 0 |  |  |  | 0 | return 0 if $bytes ne 'M'; | 
| 1110 | 0 |  |  |  |  | 0 | $pat = 'a4N'; | 
| 1111 | 0 |  |  |  |  | 0 | $mat = 'ID3 '; | 
| 1112 |  |  |  |  |  |  | } | 
| 1113 | 0 |  |  |  |  | 0 | seek $fh, 12, 0;  # skip to the first chunk | 
| 1114 |  |  |  |  |  |  |  | 
| 1115 | 0 |  |  |  |  | 0 | while ((read $fh, $bytes, 8) == 8) { | 
| 1116 | 0 |  |  |  |  | 0 | ($tag, $size)  = unpack $pat, $bytes; | 
| 1117 | 0 | 0 |  |  |  | 0 | return 1 if $tag eq $mat; | 
| 1118 | 0 |  |  |  |  | 0 | seek $fh, $size, 1; | 
| 1119 |  |  |  |  |  |  | } | 
| 1120 |  |  |  |  |  |  |  | 
| 1121 | 0 |  |  |  |  | 0 | return 0; | 
| 1122 |  |  |  |  |  |  | } | 
| 1123 |  |  |  |  |  |  |  | 
| 1124 |  |  |  |  |  |  | sub _unpack_head { | 
| 1125 | 72 |  |  | 72 |  | 444 | unpack('l', pack('L', unpack('N', $_[0]))); | 
| 1126 |  |  |  |  |  |  | } | 
| 1127 |  |  |  |  |  |  |  | 
| 1128 |  |  |  |  |  |  | sub _close { | 
| 1129 | 41 |  |  | 41 |  | 70 | my($file, $fh) = @_; | 
| 1130 | 41 | 50 |  |  |  | 96 | unless (ref $file) { # filehandle not passed | 
| 1131 | 41 | 50 |  |  |  | 1472 | close $fh or carp "Problem closing '$file': $!"; | 
| 1132 |  |  |  |  |  |  | } | 
| 1133 |  |  |  |  |  |  | } | 
| 1134 |  |  |  |  |  |  |  | 
| 1135 |  |  |  |  |  |  | BEGIN { | 
| 1136 | 4 |  |  | 4 |  | 76 | @mp3_genres = ( | 
| 1137 |  |  |  |  |  |  | 'Blues', | 
| 1138 |  |  |  |  |  |  | 'Classic Rock', | 
| 1139 |  |  |  |  |  |  | 'Country', | 
| 1140 |  |  |  |  |  |  | 'Dance', | 
| 1141 |  |  |  |  |  |  | 'Disco', | 
| 1142 |  |  |  |  |  |  | 'Funk', | 
| 1143 |  |  |  |  |  |  | 'Grunge', | 
| 1144 |  |  |  |  |  |  | 'Hip-Hop', | 
| 1145 |  |  |  |  |  |  | 'Jazz', | 
| 1146 |  |  |  |  |  |  | 'Metal', | 
| 1147 |  |  |  |  |  |  | 'New Age', | 
| 1148 |  |  |  |  |  |  | 'Oldies', | 
| 1149 |  |  |  |  |  |  | 'Other', | 
| 1150 |  |  |  |  |  |  | 'Pop', | 
| 1151 |  |  |  |  |  |  | 'R&B', | 
| 1152 |  |  |  |  |  |  | 'Rap', | 
| 1153 |  |  |  |  |  |  | 'Reggae', | 
| 1154 |  |  |  |  |  |  | 'Rock', | 
| 1155 |  |  |  |  |  |  | 'Techno', | 
| 1156 |  |  |  |  |  |  | 'Industrial', | 
| 1157 |  |  |  |  |  |  | 'Alternative', | 
| 1158 |  |  |  |  |  |  | 'Ska', | 
| 1159 |  |  |  |  |  |  | 'Death Metal', | 
| 1160 |  |  |  |  |  |  | 'Pranks', | 
| 1161 |  |  |  |  |  |  | 'Soundtrack', | 
| 1162 |  |  |  |  |  |  | 'Euro-Techno', | 
| 1163 |  |  |  |  |  |  | 'Ambient', | 
| 1164 |  |  |  |  |  |  | 'Trip-Hop', | 
| 1165 |  |  |  |  |  |  | 'Vocal', | 
| 1166 |  |  |  |  |  |  | 'Jazz+Funk', | 
| 1167 |  |  |  |  |  |  | 'Fusion', | 
| 1168 |  |  |  |  |  |  | 'Trance', | 
| 1169 |  |  |  |  |  |  | 'Classical', | 
| 1170 |  |  |  |  |  |  | 'Instrumental', | 
| 1171 |  |  |  |  |  |  | 'Acid', | 
| 1172 |  |  |  |  |  |  | 'House', | 
| 1173 |  |  |  |  |  |  | 'Game', | 
| 1174 |  |  |  |  |  |  | 'Sound Clip', | 
| 1175 |  |  |  |  |  |  | 'Gospel', | 
| 1176 |  |  |  |  |  |  | 'Noise', | 
| 1177 |  |  |  |  |  |  | 'AlternRock', | 
| 1178 |  |  |  |  |  |  | 'Bass', | 
| 1179 |  |  |  |  |  |  | 'Soul', | 
| 1180 |  |  |  |  |  |  | 'Punk', | 
| 1181 |  |  |  |  |  |  | 'Space', | 
| 1182 |  |  |  |  |  |  | 'Meditative', | 
| 1183 |  |  |  |  |  |  | 'Instrumental Pop', | 
| 1184 |  |  |  |  |  |  | 'Instrumental Rock', | 
| 1185 |  |  |  |  |  |  | 'Ethnic', | 
| 1186 |  |  |  |  |  |  | 'Gothic', | 
| 1187 |  |  |  |  |  |  | 'Darkwave', | 
| 1188 |  |  |  |  |  |  | 'Techno-Industrial', | 
| 1189 |  |  |  |  |  |  | 'Electronic', | 
| 1190 |  |  |  |  |  |  | 'Pop-Folk', | 
| 1191 |  |  |  |  |  |  | 'Eurodance', | 
| 1192 |  |  |  |  |  |  | 'Dream', | 
| 1193 |  |  |  |  |  |  | 'Southern Rock', | 
| 1194 |  |  |  |  |  |  | 'Comedy', | 
| 1195 |  |  |  |  |  |  | 'Cult', | 
| 1196 |  |  |  |  |  |  | 'Gangsta', | 
| 1197 |  |  |  |  |  |  | 'Top 40', | 
| 1198 |  |  |  |  |  |  | 'Christian Rap', | 
| 1199 |  |  |  |  |  |  | 'Pop/Funk', | 
| 1200 |  |  |  |  |  |  | 'Jungle', | 
| 1201 |  |  |  |  |  |  | 'Native American', | 
| 1202 |  |  |  |  |  |  | 'Cabaret', | 
| 1203 |  |  |  |  |  |  | 'New Wave', | 
| 1204 |  |  |  |  |  |  | 'Psychadelic', | 
| 1205 |  |  |  |  |  |  | 'Rave', | 
| 1206 |  |  |  |  |  |  | 'Showtunes', | 
| 1207 |  |  |  |  |  |  | 'Trailer', | 
| 1208 |  |  |  |  |  |  | 'Lo-Fi', | 
| 1209 |  |  |  |  |  |  | 'Tribal', | 
| 1210 |  |  |  |  |  |  | 'Acid Punk', | 
| 1211 |  |  |  |  |  |  | 'Acid Jazz', | 
| 1212 |  |  |  |  |  |  | 'Polka', | 
| 1213 |  |  |  |  |  |  | 'Retro', | 
| 1214 |  |  |  |  |  |  | 'Musical', | 
| 1215 |  |  |  |  |  |  | 'Rock & Roll', | 
| 1216 |  |  |  |  |  |  | 'Hard Rock', | 
| 1217 |  |  |  |  |  |  | ); | 
| 1218 |  |  |  |  |  |  |  | 
| 1219 | 4 |  |  |  |  | 113 | @winamp_genres = ( | 
| 1220 |  |  |  |  |  |  | @mp3_genres, | 
| 1221 |  |  |  |  |  |  | 'Folk', | 
| 1222 |  |  |  |  |  |  | 'Folk-Rock', | 
| 1223 |  |  |  |  |  |  | 'National Folk', | 
| 1224 |  |  |  |  |  |  | 'Swing', | 
| 1225 |  |  |  |  |  |  | 'Fast Fusion', | 
| 1226 |  |  |  |  |  |  | 'Bebop', | 
| 1227 |  |  |  |  |  |  | 'Latin', | 
| 1228 |  |  |  |  |  |  | 'Revival', | 
| 1229 |  |  |  |  |  |  | 'Celtic', | 
| 1230 |  |  |  |  |  |  | 'Bluegrass', | 
| 1231 |  |  |  |  |  |  | 'Avantgarde', | 
| 1232 |  |  |  |  |  |  | 'Gothic Rock', | 
| 1233 |  |  |  |  |  |  | 'Progressive Rock', | 
| 1234 |  |  |  |  |  |  | 'Psychedelic Rock', | 
| 1235 |  |  |  |  |  |  | 'Symphonic Rock', | 
| 1236 |  |  |  |  |  |  | 'Slow Rock', | 
| 1237 |  |  |  |  |  |  | 'Big Band', | 
| 1238 |  |  |  |  |  |  | 'Chorus', | 
| 1239 |  |  |  |  |  |  | 'Easy Listening', | 
| 1240 |  |  |  |  |  |  | 'Acoustic', | 
| 1241 |  |  |  |  |  |  | 'Humour', | 
| 1242 |  |  |  |  |  |  | 'Speech', | 
| 1243 |  |  |  |  |  |  | 'Chanson', | 
| 1244 |  |  |  |  |  |  | 'Opera', | 
| 1245 |  |  |  |  |  |  | 'Chamber Music', | 
| 1246 |  |  |  |  |  |  | 'Sonata', | 
| 1247 |  |  |  |  |  |  | 'Symphony', | 
| 1248 |  |  |  |  |  |  | 'Booty Bass', | 
| 1249 |  |  |  |  |  |  | 'Primus', | 
| 1250 |  |  |  |  |  |  | 'Porn Groove', | 
| 1251 |  |  |  |  |  |  | 'Satire', | 
| 1252 |  |  |  |  |  |  | 'Slow Jam', | 
| 1253 |  |  |  |  |  |  | 'Club', | 
| 1254 |  |  |  |  |  |  | 'Tango', | 
| 1255 |  |  |  |  |  |  | 'Samba', | 
| 1256 |  |  |  |  |  |  | 'Folklore', | 
| 1257 |  |  |  |  |  |  | 'Ballad', | 
| 1258 |  |  |  |  |  |  | 'Power Ballad', | 
| 1259 |  |  |  |  |  |  | 'Rhythmic Soul', | 
| 1260 |  |  |  |  |  |  | 'Freestyle', | 
| 1261 |  |  |  |  |  |  | 'Duet', | 
| 1262 |  |  |  |  |  |  | 'Punk Rock', | 
| 1263 |  |  |  |  |  |  | 'Drum Solo', | 
| 1264 |  |  |  |  |  |  | 'Acapella', | 
| 1265 |  |  |  |  |  |  | 'Euro-House', | 
| 1266 |  |  |  |  |  |  | 'Dance Hall', | 
| 1267 |  |  |  |  |  |  | 'Goa', | 
| 1268 |  |  |  |  |  |  | 'Drum & Bass', | 
| 1269 |  |  |  |  |  |  | 'Club-House', | 
| 1270 |  |  |  |  |  |  | 'Hardcore', | 
| 1271 |  |  |  |  |  |  | 'Terror', | 
| 1272 |  |  |  |  |  |  | 'Indie', | 
| 1273 |  |  |  |  |  |  | 'BritPop', | 
| 1274 |  |  |  |  |  |  | 'Negerpunk', | 
| 1275 |  |  |  |  |  |  | 'Polsk Punk', | 
| 1276 |  |  |  |  |  |  | 'Beat', | 
| 1277 |  |  |  |  |  |  | 'Christian Gangsta Rap', | 
| 1278 |  |  |  |  |  |  | 'Heavy Metal', | 
| 1279 |  |  |  |  |  |  | 'Black Metal', | 
| 1280 |  |  |  |  |  |  | 'Crossover', | 
| 1281 |  |  |  |  |  |  | 'Contemporary Christian', | 
| 1282 |  |  |  |  |  |  | 'Christian Rock', | 
| 1283 |  |  |  |  |  |  | 'Merengue', | 
| 1284 |  |  |  |  |  |  | 'Salsa', | 
| 1285 |  |  |  |  |  |  | 'Thrash Metal', | 
| 1286 |  |  |  |  |  |  | 'Anime', | 
| 1287 |  |  |  |  |  |  | 'JPop', | 
| 1288 |  |  |  |  |  |  | 'Synthpop', | 
| 1289 |  |  |  |  |  |  | ); | 
| 1290 |  |  |  |  |  |  |  | 
| 1291 | 4 |  |  |  |  | 79 | @t_bitrate = ([ | 
| 1292 |  |  |  |  |  |  | [0, 32, 48, 56,  64,  80,  96, 112, 128, 144, 160, 176, 192, 224, 256], | 
| 1293 |  |  |  |  |  |  | [0,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160], | 
| 1294 |  |  |  |  |  |  | [0,  8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160] | 
| 1295 |  |  |  |  |  |  | ],[ | 
| 1296 |  |  |  |  |  |  | [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448], | 
| 1297 |  |  |  |  |  |  | [0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384], | 
| 1298 |  |  |  |  |  |  | [0, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320] | 
| 1299 |  |  |  |  |  |  | ]); | 
| 1300 |  |  |  |  |  |  |  | 
| 1301 | 4 |  |  |  |  | 24 | @t_sampling_freq = ( | 
| 1302 |  |  |  |  |  |  | [11025, 12000,  8000], | 
| 1303 |  |  |  |  |  |  | [undef, undef, undef],	# reserved | 
| 1304 |  |  |  |  |  |  | [22050, 24000, 16000], | 
| 1305 |  |  |  |  |  |  | [44100, 48000, 32000] | 
| 1306 |  |  |  |  |  |  | ); | 
| 1307 |  |  |  |  |  |  |  | 
| 1308 | 48 | 100 |  |  |  | 1406 | @frequency_tbl = map { $_ ? eval "${_}e-3" : 0 } | 
|  | 16 |  |  |  |  | 36 |  | 
| 1309 | 4 |  |  |  |  | 12 | map { @$_ } @t_sampling_freq; | 
| 1310 |  |  |  |  |  |  |  | 
| 1311 | 4 |  |  |  |  | 28 | @mp3_info_fields = qw( | 
| 1312 |  |  |  |  |  |  | VERSION | 
| 1313 |  |  |  |  |  |  | LAYER | 
| 1314 |  |  |  |  |  |  | STEREO | 
| 1315 |  |  |  |  |  |  | VBR | 
| 1316 |  |  |  |  |  |  | BITRATE | 
| 1317 |  |  |  |  |  |  | FREQUENCY | 
| 1318 |  |  |  |  |  |  | SIZE | 
| 1319 |  |  |  |  |  |  | OFFSET | 
| 1320 |  |  |  |  |  |  | SECS | 
| 1321 |  |  |  |  |  |  | MM | 
| 1322 |  |  |  |  |  |  | SS | 
| 1323 |  |  |  |  |  |  | MS | 
| 1324 |  |  |  |  |  |  | TIME | 
| 1325 |  |  |  |  |  |  | COPYRIGHT | 
| 1326 |  |  |  |  |  |  | PADDING | 
| 1327 |  |  |  |  |  |  | MODE | 
| 1328 |  |  |  |  |  |  | FRAMES | 
| 1329 |  |  |  |  |  |  | FRAME_LENGTH | 
| 1330 |  |  |  |  |  |  | VBR_SCALE | 
| 1331 |  |  |  |  |  |  | ); | 
| 1332 |  |  |  |  |  |  |  | 
| 1333 | 4 |  |  |  |  | 25 | %v1_tag_fields = | 
| 1334 |  |  |  |  |  |  | (TITLE => 30, ARTIST => 30, ALBUM => 30, COMMENT => 30, YEAR => 4); | 
| 1335 |  |  |  |  |  |  |  | 
| 1336 | 4 |  |  |  |  | 10 | @v1_tag_names = qw(TITLE ARTIST ALBUM YEAR COMMENT TRACKNUM GENRE); | 
| 1337 |  |  |  |  |  |  |  | 
| 1338 | 4 |  |  |  |  | 65 | %v2_to_v1_names = ( | 
| 1339 |  |  |  |  |  |  | # v2.2 tags | 
| 1340 |  |  |  |  |  |  | 'TT2' => 'TITLE', | 
| 1341 |  |  |  |  |  |  | 'TP1' => 'ARTIST', | 
| 1342 |  |  |  |  |  |  | 'TAL' => 'ALBUM', | 
| 1343 |  |  |  |  |  |  | 'TYE' => 'YEAR', | 
| 1344 |  |  |  |  |  |  | 'COM' => 'COMMENT', | 
| 1345 |  |  |  |  |  |  | 'TRK' => 'TRACKNUM', | 
| 1346 |  |  |  |  |  |  | 'TCO' => 'GENRE', # not clean mapping, but ... | 
| 1347 |  |  |  |  |  |  | # v2.3 tags | 
| 1348 |  |  |  |  |  |  | 'TIT2' => 'TITLE', | 
| 1349 |  |  |  |  |  |  | 'TPE1' => 'ARTIST', | 
| 1350 |  |  |  |  |  |  | 'TALB' => 'ALBUM', | 
| 1351 |  |  |  |  |  |  | 'TYER' => 'YEAR', | 
| 1352 |  |  |  |  |  |  | 'COMM' => 'COMMENT', | 
| 1353 |  |  |  |  |  |  | 'TRCK' => 'TRACKNUM', | 
| 1354 |  |  |  |  |  |  | 'TCON' => 'GENRE', | 
| 1355 |  |  |  |  |  |  | ); | 
| 1356 |  |  |  |  |  |  |  | 
| 1357 | 4 |  |  |  |  | 683 | %v2_tag_names = ( | 
| 1358 |  |  |  |  |  |  | # v2.2 tags | 
| 1359 |  |  |  |  |  |  | 'BUF' => 'Recommended buffer size', | 
| 1360 |  |  |  |  |  |  | 'CNT' => 'Play counter', | 
| 1361 |  |  |  |  |  |  | 'COM' => 'Comments', | 
| 1362 |  |  |  |  |  |  | 'CRA' => 'Audio encryption', | 
| 1363 |  |  |  |  |  |  | 'CRM' => 'Encrypted meta frame', | 
| 1364 |  |  |  |  |  |  | 'ETC' => 'Event timing codes', | 
| 1365 |  |  |  |  |  |  | 'EQU' => 'Equalization', | 
| 1366 |  |  |  |  |  |  | 'GEO' => 'General encapsulated object', | 
| 1367 |  |  |  |  |  |  | 'IPL' => 'Involved people list', | 
| 1368 |  |  |  |  |  |  | 'LNK' => 'Linked information', | 
| 1369 |  |  |  |  |  |  | 'MCI' => 'Music CD Identifier', | 
| 1370 |  |  |  |  |  |  | 'MLL' => 'MPEG location lookup table', | 
| 1371 |  |  |  |  |  |  | 'PIC' => 'Attached picture', | 
| 1372 |  |  |  |  |  |  | 'POP' => 'Popularimeter', | 
| 1373 |  |  |  |  |  |  | 'REV' => 'Reverb', | 
| 1374 |  |  |  |  |  |  | 'RVA' => 'Relative volume adjustment', | 
| 1375 |  |  |  |  |  |  | 'SLT' => 'Synchronized lyric/text', | 
| 1376 |  |  |  |  |  |  | 'STC' => 'Synced tempo codes', | 
| 1377 |  |  |  |  |  |  | 'TAL' => 'Album/Movie/Show title', | 
| 1378 |  |  |  |  |  |  | 'TBP' => 'BPM (Beats Per Minute)', | 
| 1379 |  |  |  |  |  |  | 'TCM' => 'Composer', | 
| 1380 |  |  |  |  |  |  | 'TCO' => 'Content type', | 
| 1381 |  |  |  |  |  |  | 'TCR' => 'Copyright message', | 
| 1382 |  |  |  |  |  |  | 'TDA' => 'Date', | 
| 1383 |  |  |  |  |  |  | 'TDY' => 'Playlist delay', | 
| 1384 |  |  |  |  |  |  | 'TEN' => 'Encoded by', | 
| 1385 |  |  |  |  |  |  | 'TFT' => 'File type', | 
| 1386 |  |  |  |  |  |  | 'TIM' => 'Time', | 
| 1387 |  |  |  |  |  |  | 'TKE' => 'Initial key', | 
| 1388 |  |  |  |  |  |  | 'TLA' => 'Language(s)', | 
| 1389 |  |  |  |  |  |  | 'TLE' => 'Length', | 
| 1390 |  |  |  |  |  |  | 'TMT' => 'Media type', | 
| 1391 |  |  |  |  |  |  | 'TOA' => 'Original artist(s)/performer(s)', | 
| 1392 |  |  |  |  |  |  | 'TOF' => 'Original filename', | 
| 1393 |  |  |  |  |  |  | 'TOL' => 'Original Lyricist(s)/text writer(s)', | 
| 1394 |  |  |  |  |  |  | 'TOR' => 'Original release year', | 
| 1395 |  |  |  |  |  |  | 'TOT' => 'Original album/Movie/Show title', | 
| 1396 |  |  |  |  |  |  | 'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group', | 
| 1397 |  |  |  |  |  |  | 'TP2' => 'Band/Orchestra/Accompaniment', | 
| 1398 |  |  |  |  |  |  | 'TP3' => 'Conductor/Performer refinement', | 
| 1399 |  |  |  |  |  |  | 'TP4' => 'Interpreted, remixed, or otherwise modified by', | 
| 1400 |  |  |  |  |  |  | 'TPA' => 'Part of a set', | 
| 1401 |  |  |  |  |  |  | 'TPB' => 'Publisher', | 
| 1402 |  |  |  |  |  |  | 'TRC' => 'ISRC (International Standard Recording Code)', | 
| 1403 |  |  |  |  |  |  | 'TRD' => 'Recording dates', | 
| 1404 |  |  |  |  |  |  | 'TRK' => 'Track number/Position in set', | 
| 1405 |  |  |  |  |  |  | 'TSI' => 'Size', | 
| 1406 |  |  |  |  |  |  | 'TSS' => 'Software/hardware and settings used for encoding', | 
| 1407 |  |  |  |  |  |  | 'TT1' => 'Content group description', | 
| 1408 |  |  |  |  |  |  | 'TT2' => 'Title/Songname/Content description', | 
| 1409 |  |  |  |  |  |  | 'TT3' => 'Subtitle/Description refinement', | 
| 1410 |  |  |  |  |  |  | 'TXT' => 'Lyricist/text writer', | 
| 1411 |  |  |  |  |  |  | 'TXX' => 'User defined text information frame', | 
| 1412 |  |  |  |  |  |  | 'TYE' => 'Year', | 
| 1413 |  |  |  |  |  |  | 'UFI' => 'Unique file identifier', | 
| 1414 |  |  |  |  |  |  | 'ULT' => 'Unsychronized lyric/text transcription', | 
| 1415 |  |  |  |  |  |  | 'WAF' => 'Official audio file webpage', | 
| 1416 |  |  |  |  |  |  | 'WAR' => 'Official artist/performer webpage', | 
| 1417 |  |  |  |  |  |  | 'WAS' => 'Official audio source webpage', | 
| 1418 |  |  |  |  |  |  | 'WCM' => 'Commercial information', | 
| 1419 |  |  |  |  |  |  | 'WCP' => 'Copyright/Legal information', | 
| 1420 |  |  |  |  |  |  | 'WPB' => 'Publishers official webpage', | 
| 1421 |  |  |  |  |  |  | 'WXX' => 'User defined URL link frame', | 
| 1422 |  |  |  |  |  |  |  | 
| 1423 |  |  |  |  |  |  | # v2.3 tags | 
| 1424 |  |  |  |  |  |  | 'AENC' => 'Audio encryption', | 
| 1425 |  |  |  |  |  |  | 'APIC' => 'Attached picture', | 
| 1426 |  |  |  |  |  |  | 'COMM' => 'Comments', | 
| 1427 |  |  |  |  |  |  | 'COMR' => 'Commercial frame', | 
| 1428 |  |  |  |  |  |  | 'ENCR' => 'Encryption method registration', | 
| 1429 |  |  |  |  |  |  | 'EQUA' => 'Equalization', | 
| 1430 |  |  |  |  |  |  | 'ETCO' => 'Event timing codes', | 
| 1431 |  |  |  |  |  |  | 'GEOB' => 'General encapsulated object', | 
| 1432 |  |  |  |  |  |  | 'GRID' => 'Group identification registration', | 
| 1433 |  |  |  |  |  |  | 'IPLS' => 'Involved people list', | 
| 1434 |  |  |  |  |  |  | 'LINK' => 'Linked information', | 
| 1435 |  |  |  |  |  |  | 'MCDI' => 'Music CD identifier', | 
| 1436 |  |  |  |  |  |  | 'MLLT' => 'MPEG location lookup table', | 
| 1437 |  |  |  |  |  |  | 'OWNE' => 'Ownership frame', | 
| 1438 |  |  |  |  |  |  | 'PCNT' => 'Play counter', | 
| 1439 |  |  |  |  |  |  | 'POPM' => 'Popularimeter', | 
| 1440 |  |  |  |  |  |  | 'POSS' => 'Position synchronisation frame', | 
| 1441 |  |  |  |  |  |  | 'PRIV' => 'Private frame', | 
| 1442 |  |  |  |  |  |  | 'RBUF' => 'Recommended buffer size', | 
| 1443 |  |  |  |  |  |  | 'RVAD' => 'Relative volume adjustment', | 
| 1444 |  |  |  |  |  |  | 'RVRB' => 'Reverb', | 
| 1445 |  |  |  |  |  |  | 'SYLT' => 'Synchronized lyric/text', | 
| 1446 |  |  |  |  |  |  | 'SYTC' => 'Synchronized tempo codes', | 
| 1447 |  |  |  |  |  |  | 'TALB' => 'Album/Movie/Show title', | 
| 1448 |  |  |  |  |  |  | 'TBPM' => 'BPM (beats per minute)', | 
| 1449 |  |  |  |  |  |  | 'TCOM' => 'Composer', | 
| 1450 |  |  |  |  |  |  | 'TCON' => 'Content type', | 
| 1451 |  |  |  |  |  |  | 'TCOP' => 'Copyright message', | 
| 1452 |  |  |  |  |  |  | 'TDAT' => 'Date', | 
| 1453 |  |  |  |  |  |  | 'TDLY' => 'Playlist delay', | 
| 1454 |  |  |  |  |  |  | 'TENC' => 'Encoded by', | 
| 1455 |  |  |  |  |  |  | 'TEXT' => 'Lyricist/Text writer', | 
| 1456 |  |  |  |  |  |  | 'TFLT' => 'File type', | 
| 1457 |  |  |  |  |  |  | 'TIME' => 'Time', | 
| 1458 |  |  |  |  |  |  | 'TIT1' => 'Content group description', | 
| 1459 |  |  |  |  |  |  | 'TIT2' => 'Title/songname/content description', | 
| 1460 |  |  |  |  |  |  | 'TIT3' => 'Subtitle/Description refinement', | 
| 1461 |  |  |  |  |  |  | 'TKEY' => 'Initial key', | 
| 1462 |  |  |  |  |  |  | 'TLAN' => 'Language(s)', | 
| 1463 |  |  |  |  |  |  | 'TLEN' => 'Length', | 
| 1464 |  |  |  |  |  |  | 'TMED' => 'Media type', | 
| 1465 |  |  |  |  |  |  | 'TOAL' => 'Original album/movie/show title', | 
| 1466 |  |  |  |  |  |  | 'TOFN' => 'Original filename', | 
| 1467 |  |  |  |  |  |  | 'TOLY' => 'Original lyricist(s)/text writer(s)', | 
| 1468 |  |  |  |  |  |  | 'TOPE' => 'Original artist(s)/performer(s)', | 
| 1469 |  |  |  |  |  |  | 'TORY' => 'Original release year', | 
| 1470 |  |  |  |  |  |  | 'TOWN' => 'File owner/licensee', | 
| 1471 |  |  |  |  |  |  | 'TPE1' => 'Lead performer(s)/Soloist(s)', | 
| 1472 |  |  |  |  |  |  | 'TPE2' => 'Band/orchestra/accompaniment', | 
| 1473 |  |  |  |  |  |  | 'TPE3' => 'Conductor/performer refinement', | 
| 1474 |  |  |  |  |  |  | 'TPE4' => 'Interpreted, remixed, or otherwise modified by', | 
| 1475 |  |  |  |  |  |  | 'TPOS' => 'Part of a set', | 
| 1476 |  |  |  |  |  |  | 'TPUB' => 'Publisher', | 
| 1477 |  |  |  |  |  |  | 'TRCK' => 'Track number/Position in set', | 
| 1478 |  |  |  |  |  |  | 'TRDA' => 'Recording dates', | 
| 1479 |  |  |  |  |  |  | 'TRSN' => 'Internet radio station name', | 
| 1480 |  |  |  |  |  |  | 'TRSO' => 'Internet radio station owner', | 
| 1481 |  |  |  |  |  |  | 'TSIZ' => 'Size', | 
| 1482 |  |  |  |  |  |  | 'TSRC' => 'ISRC (international standard recording code)', | 
| 1483 |  |  |  |  |  |  | 'TSSE' => 'Software/Hardware and settings used for encoding', | 
| 1484 |  |  |  |  |  |  | 'TXXX' => 'User defined text information frame', | 
| 1485 |  |  |  |  |  |  | 'TYER' => 'Year', | 
| 1486 |  |  |  |  |  |  | 'UFID' => 'Unique file identifier', | 
| 1487 |  |  |  |  |  |  | 'USER' => 'Terms of use', | 
| 1488 |  |  |  |  |  |  | 'USLT' => 'Unsychronized lyric/text transcription', | 
| 1489 |  |  |  |  |  |  | 'WCOM' => 'Commercial information', | 
| 1490 |  |  |  |  |  |  | 'WCOP' => 'Copyright/Legal information', | 
| 1491 |  |  |  |  |  |  | 'WOAF' => 'Official audio file webpage', | 
| 1492 |  |  |  |  |  |  | 'WOAR' => 'Official artist/performer webpage', | 
| 1493 |  |  |  |  |  |  | 'WOAS' => 'Official audio source webpage', | 
| 1494 |  |  |  |  |  |  | 'WORS' => 'Official internet radio station homepage', | 
| 1495 |  |  |  |  |  |  | 'WPAY' => 'Payment', | 
| 1496 |  |  |  |  |  |  | 'WPUB' => 'Publishers official webpage', | 
| 1497 |  |  |  |  |  |  | 'WXXX' => 'User defined URL link frame', | 
| 1498 |  |  |  |  |  |  |  | 
| 1499 |  |  |  |  |  |  | # v2.4 additional tags | 
| 1500 |  |  |  |  |  |  | # note that we don't restrict tags from 2.3 or 2.4, | 
| 1501 |  |  |  |  |  |  | 'ASPI' => 'Audio seek point index', | 
| 1502 |  |  |  |  |  |  | 'EQU2' => 'Equalisation (2)', | 
| 1503 |  |  |  |  |  |  | 'RVA2' => 'Relative volume adjustment (2)', | 
| 1504 |  |  |  |  |  |  | 'SEEK' => 'Seek frame', | 
| 1505 |  |  |  |  |  |  | 'SIGN' => 'Signature frame', | 
| 1506 |  |  |  |  |  |  | 'TDEN' => 'Encoding time', | 
| 1507 |  |  |  |  |  |  | 'TDOR' => 'Original release time', | 
| 1508 |  |  |  |  |  |  | 'TDRC' => 'Recording time', | 
| 1509 |  |  |  |  |  |  | 'TDRL' => 'Release time', | 
| 1510 |  |  |  |  |  |  | 'TDTG' => 'Tagging time', | 
| 1511 |  |  |  |  |  |  | 'TIPL' => 'Involved people list', | 
| 1512 |  |  |  |  |  |  | 'TMCL' => 'Musician credits list', | 
| 1513 |  |  |  |  |  |  | 'TMOO' => 'Mood', | 
| 1514 |  |  |  |  |  |  | 'TPRO' => 'Produced notice', | 
| 1515 |  |  |  |  |  |  | 'TSOA' => 'Album sort order', | 
| 1516 |  |  |  |  |  |  | 'TSOP' => 'Performer sort order', | 
| 1517 |  |  |  |  |  |  | 'TSOT' => 'Title sort order', | 
| 1518 |  |  |  |  |  |  | 'TSST' => 'Set subtitle', | 
| 1519 |  |  |  |  |  |  |  | 
| 1520 |  |  |  |  |  |  | # grrrrrrr | 
| 1521 |  |  |  |  |  |  | 'COM ' => 'Broken iTunes comments', | 
| 1522 |  |  |  |  |  |  | ); | 
| 1523 |  |  |  |  |  |  | } | 
| 1524 |  |  |  |  |  |  |  | 
| 1525 |  |  |  |  |  |  | 1; | 
| 1526 |  |  |  |  |  |  |  | 
| 1527 |  |  |  |  |  |  | __END__ |