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