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__ |