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