| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# |
|
2
|
|
|
|
|
|
|
# Copyright (c) 2004-2010 Jonathan Harris |
|
3
|
|
|
|
|
|
|
# |
|
4
|
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify it |
|
5
|
|
|
|
|
|
|
# under the the same terms as Perl itself. |
|
6
|
|
|
|
|
|
|
# |
|
7
|
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
package MP4::Info; |
|
9
|
|
|
|
|
|
|
|
|
10
|
1
|
|
|
1
|
|
31271
|
use overload; |
|
|
1
|
|
|
|
|
1518
|
|
|
|
1
|
|
|
|
|
7
|
|
|
11
|
1
|
|
|
1
|
|
54
|
use strict; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
44
|
|
|
12
|
1
|
|
|
1
|
|
7
|
use Carp; |
|
|
1
|
|
|
|
|
8
|
|
|
|
1
|
|
|
|
|
77
|
|
|
13
|
1
|
|
|
1
|
|
1246
|
use Symbol; |
|
|
1
|
|
|
|
|
1480
|
|
|
|
1
|
|
|
|
|
101
|
|
|
14
|
1
|
|
|
1
|
|
13116
|
use Encode; |
|
|
1
|
|
|
|
|
29690
|
|
|
|
1
|
|
|
|
|
115
|
|
|
15
|
1
|
|
|
1
|
|
6242
|
use Encode::Guess qw(latin1); |
|
|
1
|
|
|
|
|
30797
|
|
|
|
1
|
|
|
|
|
9
|
|
|
16
|
1
|
|
|
1
|
|
5741
|
use IO::String; |
|
|
1
|
|
|
|
|
6212
|
|
|
|
1
|
|
|
|
|
65
|
|
|
17
|
|
|
|
|
|
|
|
|
18
|
1
|
|
|
|
|
21408
|
use vars qw( |
|
19
|
|
|
|
|
|
|
$VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $AUTOLOAD |
|
20
|
|
|
|
|
|
|
%data_atoms %other_atoms %container_atoms @mp4_genres |
|
21
|
1
|
|
|
1
|
|
13
|
); |
|
|
1
|
|
|
|
|
3
|
|
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
@ISA = 'Exporter'; |
|
24
|
|
|
|
|
|
|
@EXPORT = qw(get_mp4tag get_mp4info); |
|
25
|
|
|
|
|
|
|
@EXPORT_OK = qw(use_mp4_utf8); |
|
26
|
|
|
|
|
|
|
%EXPORT_TAGS = ( |
|
27
|
|
|
|
|
|
|
utf8 => [qw(use_mp4_utf8)], |
|
28
|
|
|
|
|
|
|
all => [@EXPORT, @EXPORT_OK] |
|
29
|
|
|
|
|
|
|
); |
|
30
|
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
$VERSION = '1.13'; |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
my $debug = 0; |
|
34
|
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
=head1 NAME |
|
37
|
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
MP4::Info - Fetch info from MPEG-4 files (.mp4, .m4a, .m4p, .3gp) |
|
39
|
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
41
|
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
#!perl -w |
|
43
|
|
|
|
|
|
|
use MP4::Info; |
|
44
|
|
|
|
|
|
|
my $file = 'Pearls_Before_Swine.m4a'; |
|
45
|
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
my $tag = get_mp4tag($file) or die "No TAG info"; |
|
47
|
|
|
|
|
|
|
printf "$file is a %s track\n", $tag->{GENRE}; |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
my $info = get_mp4info($file); |
|
50
|
|
|
|
|
|
|
printf "$file length is %d:%d\n", $info->{MM}, $info->{SS}; |
|
51
|
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
my $mp4 = new MP4::Info $file; |
|
53
|
|
|
|
|
|
|
printf "$file length is %s, title is %s\n", |
|
54
|
|
|
|
|
|
|
$mp4->time, $mp4->title; |
|
55
|
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
57
|
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
The MP4::Info module can be used to extract tag and meta information from |
|
59
|
|
|
|
|
|
|
MPEG-4 audio (AAC) and video files. It is designed as a drop-in replacement |
|
60
|
|
|
|
|
|
|
for L. |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
Note that this module does not allow you to update the information in MPEG-4 |
|
63
|
|
|
|
|
|
|
files. |
|
64
|
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=over 4 |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=item $mp4 = MP4::Info-Enew(FILE) |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
OOP interface to the rest of the module. The same keys available via |
|
70
|
|
|
|
|
|
|
C and C are available via the returned object |
|
71
|
|
|
|
|
|
|
(using upper case or lower case; but note that all-caps 'VERSION' will |
|
72
|
|
|
|
|
|
|
return the module version, not the MPEG-4 version). |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
Passing a value to one of the methods will B set the value for that tag |
|
75
|
|
|
|
|
|
|
in the MPEG-4 file. |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
=cut |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
sub new |
|
80
|
|
|
|
|
|
|
{ |
|
81
|
5
|
|
|
5
|
1
|
7854
|
my ($class, $file) = @_; |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
# Supported tags |
|
84
|
5
|
|
|
|
|
125
|
my %tag_names = |
|
85
|
|
|
|
|
|
|
( |
|
86
|
|
|
|
|
|
|
ALB => 1, APID => 1, ART => 1, CMT => 1, COVR => 1, CPIL => 1, CPRT => 1, DAY => 1, DISK => 1, GNRE => 1, GRP => 1, NAM => 1, RTNG => 1, TMPO => 1, TOO => 1, TRKN => 1, WRT => 1, |
|
87
|
|
|
|
|
|
|
TITLE => 1, ARTIST => 1, ALBUM => 1, YEAR => 1, COMMENT => 1, GENRE => 1, TRACKNUM => 1, |
|
88
|
|
|
|
|
|
|
VERSION => 1, LAYER => 1, |
|
89
|
|
|
|
|
|
|
BITRATE => 1, FREQUENCY => 1, SIZE => 1, |
|
90
|
|
|
|
|
|
|
SECS => 1, MM => 1, SS => 1, MS => 1, TIME => 1, |
|
91
|
|
|
|
|
|
|
COPYRIGHT => 1, ENCODING => 1, ENCRYPTED => 1, |
|
92
|
|
|
|
|
|
|
); |
|
93
|
|
|
|
|
|
|
|
|
94
|
5
|
50
|
|
|
|
20
|
my $tags = get_mp4tag ($file) or return undef; |
|
95
|
5
|
|
|
|
|
119
|
my $self = { |
|
96
|
|
|
|
|
|
|
_permitted => \%tag_names, |
|
97
|
|
|
|
|
|
|
%$tags |
|
98
|
|
|
|
|
|
|
}; |
|
99
|
5
|
|
|
|
|
74
|
return bless $self, $class; |
|
100
|
|
|
|
|
|
|
} |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
# Create accessor functions - see perltoot manpage |
|
104
|
|
|
|
|
|
|
sub AUTOLOAD |
|
105
|
|
|
|
|
|
|
{ |
|
106
|
313
|
|
|
313
|
|
54682
|
my $self = shift; |
|
107
|
313
|
50
|
|
|
|
960
|
my $type = ref($self) or croak "$self is not an object"; |
|
108
|
313
|
|
|
|
|
476
|
my $name = $AUTOLOAD; |
|
109
|
313
|
|
|
|
|
2424
|
$name =~ s/.*://; # strip fully-qualified portion |
|
110
|
|
|
|
|
|
|
|
|
111
|
313
|
50
|
|
|
|
1050
|
unless (exists $self->{_permitted}->{uc $name} ) |
|
112
|
|
|
|
|
|
|
{ |
|
113
|
0
|
|
|
|
|
0
|
croak "No method '$name' available in class $type"; |
|
114
|
|
|
|
|
|
|
} |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
# Ignore any parameter |
|
117
|
313
|
|
|
|
|
1715
|
return $self->{uc $name}; |
|
118
|
|
|
|
|
|
|
} |
|
119
|
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
sub DESTROY |
|
122
|
0
|
|
|
0
|
|
0
|
{ |
|
123
|
|
|
|
|
|
|
} |
|
124
|
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
############################################################################ |
|
127
|
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
=item use_mp4_utf8([STATUS]) |
|
129
|
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
Tells MP4::Info whether to assume that ambiguously encoded TAG info is UTF-8 |
|
131
|
|
|
|
|
|
|
or Latin-1. 1 is UTF-8, 0 is Latin-1. Default is UTF-8. |
|
132
|
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
Function returns new status (1/0). If no argument is supplied, or an |
|
134
|
|
|
|
|
|
|
unaccepted argument is supplied, function merely returns existing status. |
|
135
|
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
This function is not exported by default, but may be exported |
|
137
|
|
|
|
|
|
|
with the C<:utf8> or C<:all> export tag. |
|
138
|
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
=cut |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
my $utf8 = 1; |
|
142
|
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub use_mp4_utf8 |
|
144
|
|
|
|
|
|
|
{ |
|
145
|
1
|
|
|
1
|
1
|
1798
|
my ($val) = @_; |
|
146
|
1
|
50
|
33
|
|
|
8
|
$utf8 = $val if (($val == 0) || ($val == 1)); |
|
147
|
1
|
|
|
|
|
3
|
return $utf8; |
|
148
|
|
|
|
|
|
|
} |
|
149
|
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
=item get_mp4tag (FILE) |
|
152
|
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
Returns hash reference containing the tag information from the MP4 file. |
|
154
|
|
|
|
|
|
|
The following keys may be defined: |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
ALB Album |
|
157
|
|
|
|
|
|
|
APID Apple Store ID |
|
158
|
|
|
|
|
|
|
ART Artist |
|
159
|
|
|
|
|
|
|
CMT Comment |
|
160
|
|
|
|
|
|
|
COVR Album art (typically JPEG or PNG data) |
|
161
|
|
|
|
|
|
|
CPIL Compilation (boolean) |
|
162
|
|
|
|
|
|
|
CPRT Copyright statement |
|
163
|
|
|
|
|
|
|
DAY Year |
|
164
|
|
|
|
|
|
|
DISK Disk number & total (2 integers) |
|
165
|
|
|
|
|
|
|
GNRE Genre |
|
166
|
|
|
|
|
|
|
GRP Grouping |
|
167
|
|
|
|
|
|
|
NAM Title |
|
168
|
|
|
|
|
|
|
RTNG Rating (integer) |
|
169
|
|
|
|
|
|
|
TMPO Tempo (integer) |
|
170
|
|
|
|
|
|
|
TOO Encoder |
|
171
|
|
|
|
|
|
|
TRKN Track number & total (2 integers) |
|
172
|
|
|
|
|
|
|
WRT Author or composer |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
For compatibility with L, the MP3 ID3v1-style keys |
|
175
|
|
|
|
|
|
|
TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE and TRACKNUM are defined as |
|
176
|
|
|
|
|
|
|
synonyms for NAM, ART, ALB, DAY, CMT, GNRE and TRKN[0]. |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
Any and all of these keys may be undefined if the corresponding information |
|
179
|
|
|
|
|
|
|
is missing from the MPEG-4 file. |
|
180
|
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
On error, returns nothing and sets C<$@>. |
|
182
|
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=cut |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
sub get_mp4tag |
|
186
|
|
|
|
|
|
|
{ |
|
187
|
12
|
|
|
12
|
1
|
540
|
my ($file) = @_; |
|
188
|
12
|
|
|
|
|
29
|
my (%tags); |
|
189
|
|
|
|
|
|
|
|
|
190
|
12
|
50
|
|
|
|
45
|
return parse_file ($file, \%tags) ? undef : {%tags}; |
|
191
|
|
|
|
|
|
|
} |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
=item get_mp4info (FILE) |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
Returns hash reference containing file information from the MPEG-4 file. |
|
197
|
|
|
|
|
|
|
The following keys may be defined: |
|
198
|
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
VERSION MPEG version (=4) |
|
200
|
|
|
|
|
|
|
LAYER MPEG layer description (=1 for compatibility with MP3::Info) |
|
201
|
|
|
|
|
|
|
BITRATE bitrate in kbps (average for VBR files) |
|
202
|
|
|
|
|
|
|
FREQUENCY frequency in kHz |
|
203
|
|
|
|
|
|
|
SIZE bytes in audio stream |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
SECS total seconds, rounded to nearest second |
|
206
|
|
|
|
|
|
|
MM minutes |
|
207
|
|
|
|
|
|
|
SS leftover seconds |
|
208
|
|
|
|
|
|
|
MS leftover milliseconds, rounded to nearest millisecond |
|
209
|
|
|
|
|
|
|
TIME time in MM:SS, rounded to nearest second |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
COPYRIGHT boolean for audio is copyrighted |
|
212
|
|
|
|
|
|
|
ENCODING audio codec name. Possible values include: |
|
213
|
|
|
|
|
|
|
'mp4a' - AAC, aacPlus |
|
214
|
|
|
|
|
|
|
'alac' - Apple lossless |
|
215
|
|
|
|
|
|
|
'drms' - Apple encrypted AAC |
|
216
|
|
|
|
|
|
|
'samr' - 3GPP narrow-band AMR |
|
217
|
|
|
|
|
|
|
'sawb' - 3GPP wide-band AMR |
|
218
|
|
|
|
|
|
|
'enca' - Unspecified encrypted audio |
|
219
|
|
|
|
|
|
|
ENCRYPTED boolean for audio data is encrypted |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
Any and all of these keys may be undefined if the corresponding information |
|
222
|
|
|
|
|
|
|
is missing from the MPEG-4 file. |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
On error, returns nothing and sets C<$@>. |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=cut |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
sub get_mp4info |
|
229
|
|
|
|
|
|
|
{ |
|
230
|
5
|
|
|
5
|
1
|
15636
|
my ($file) = @_; |
|
231
|
5
|
|
|
|
|
13
|
my (%tags); |
|
232
|
|
|
|
|
|
|
|
|
233
|
5
|
50
|
|
|
|
24
|
return parse_file ($file, \%tags) ? undef : {%tags}; |
|
234
|
|
|
|
|
|
|
} |
|
235
|
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
############################################################################ |
|
238
|
|
|
|
|
|
|
# No user-servicable parts below |
|
239
|
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
# Interesting atoms that contain data in standard format. |
|
242
|
|
|
|
|
|
|
# The items marked ??? contain integers - I don't know what these are for |
|
243
|
|
|
|
|
|
|
# but return them anyway because the user might know. |
|
244
|
|
|
|
|
|
|
my %data_atoms = |
|
245
|
|
|
|
|
|
|
( |
|
246
|
|
|
|
|
|
|
AART => 1, # Album artist - returned in ART field no ART found |
|
247
|
|
|
|
|
|
|
ALB => 1, |
|
248
|
|
|
|
|
|
|
ART => 1, |
|
249
|
|
|
|
|
|
|
CMT => 1, |
|
250
|
|
|
|
|
|
|
COVR => 1, # Cover art |
|
251
|
|
|
|
|
|
|
CPIL => 1, |
|
252
|
|
|
|
|
|
|
CPRT => 1, |
|
253
|
|
|
|
|
|
|
DAY => 1, |
|
254
|
|
|
|
|
|
|
DISK => 1, |
|
255
|
|
|
|
|
|
|
GEN => 1, # Custom genre - returned in GNRE field no GNRE found |
|
256
|
|
|
|
|
|
|
GNRE => 1, # Standard ID3/WinAmp genre |
|
257
|
|
|
|
|
|
|
GRP => 1, |
|
258
|
|
|
|
|
|
|
NAM => 1, |
|
259
|
|
|
|
|
|
|
RTNG => 1, |
|
260
|
|
|
|
|
|
|
TMPO => 1, |
|
261
|
|
|
|
|
|
|
TOO => 1, |
|
262
|
|
|
|
|
|
|
TRKN => 1, |
|
263
|
|
|
|
|
|
|
WRT => 1, |
|
264
|
|
|
|
|
|
|
# Apple store |
|
265
|
|
|
|
|
|
|
APID => 1, |
|
266
|
|
|
|
|
|
|
AKID => 1, # ??? |
|
267
|
|
|
|
|
|
|
ATID => 1, # ??? |
|
268
|
|
|
|
|
|
|
CNID => 1, # ??? |
|
269
|
|
|
|
|
|
|
GEID => 1, # Some kind of watermarking ??? |
|
270
|
|
|
|
|
|
|
PLID => 1, # ??? |
|
271
|
|
|
|
|
|
|
# 3GPP |
|
272
|
|
|
|
|
|
|
TITL => 1, # title - returned in NAM field no NAM found |
|
273
|
|
|
|
|
|
|
DSCP => 1, # description - returned in CMT field no CMT found |
|
274
|
|
|
|
|
|
|
#CPRT=> 1, |
|
275
|
|
|
|
|
|
|
PERF => 1, # performer - returned in ART field no ART found |
|
276
|
|
|
|
|
|
|
AUTH => 1, # author - returned in WRT field no WRT found |
|
277
|
|
|
|
|
|
|
#GNRE=> 1, |
|
278
|
|
|
|
|
|
|
MEAN => 1, |
|
279
|
|
|
|
|
|
|
NAME => 1, |
|
280
|
|
|
|
|
|
|
DATA => 1, |
|
281
|
|
|
|
|
|
|
); |
|
282
|
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
# More interesting atoms, but with non-standard data layouts |
|
284
|
|
|
|
|
|
|
my %other_atoms = |
|
285
|
|
|
|
|
|
|
( |
|
286
|
|
|
|
|
|
|
MOOV => \&parse_moov, |
|
287
|
|
|
|
|
|
|
MDAT => \&parse_mdat, |
|
288
|
|
|
|
|
|
|
META => \&parse_meta, |
|
289
|
|
|
|
|
|
|
MVHD => \&parse_mvhd, |
|
290
|
|
|
|
|
|
|
STSD => \&parse_stsd, |
|
291
|
|
|
|
|
|
|
UUID => \&parse_uuid, |
|
292
|
|
|
|
|
|
|
); |
|
293
|
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
# Standard container atoms that contain either kind of above atoms |
|
295
|
|
|
|
|
|
|
my %container_atoms = |
|
296
|
|
|
|
|
|
|
( |
|
297
|
|
|
|
|
|
|
ILST => 1, |
|
298
|
|
|
|
|
|
|
MDIA => 1, |
|
299
|
|
|
|
|
|
|
MINF => 1, |
|
300
|
|
|
|
|
|
|
STBL => 1, |
|
301
|
|
|
|
|
|
|
TRAK => 1, |
|
302
|
|
|
|
|
|
|
UDTA => 1, |
|
303
|
|
|
|
|
|
|
'----' => 1, # iTunes and aacgain info |
|
304
|
|
|
|
|
|
|
); |
|
305
|
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
# Standard ID3 plus non-standard WinAmp genres |
|
308
|
|
|
|
|
|
|
my @mp4_genres = |
|
309
|
|
|
|
|
|
|
( |
|
310
|
|
|
|
|
|
|
'N/A', 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', |
|
311
|
|
|
|
|
|
|
'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', |
|
312
|
|
|
|
|
|
|
'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', |
|
313
|
|
|
|
|
|
|
'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', |
|
314
|
|
|
|
|
|
|
'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', |
|
315
|
|
|
|
|
|
|
'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', |
|
316
|
|
|
|
|
|
|
'Acid', 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', |
|
317
|
|
|
|
|
|
|
'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', |
|
318
|
|
|
|
|
|
|
'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic', |
|
319
|
|
|
|
|
|
|
'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', |
|
320
|
|
|
|
|
|
|
'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta', |
|
321
|
|
|
|
|
|
|
'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', |
|
322
|
|
|
|
|
|
|
'Cabaret', 'New Wave', 'Psychadelic', 'Rave', 'Showtunes', |
|
323
|
|
|
|
|
|
|
'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', |
|
324
|
|
|
|
|
|
|
'Retro', 'Musical', 'Rock & Roll', 'Hard Rock', 'Folk', |
|
325
|
|
|
|
|
|
|
'Folk/Rock', 'National Folk', 'Swing', 'Fast-Fusion', 'Bebob', |
|
326
|
|
|
|
|
|
|
'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', |
|
327
|
|
|
|
|
|
|
'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock', |
|
328
|
|
|
|
|
|
|
'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', |
|
329
|
|
|
|
|
|
|
'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', |
|
330
|
|
|
|
|
|
|
'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', |
|
331
|
|
|
|
|
|
|
'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', |
|
332
|
|
|
|
|
|
|
'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', |
|
333
|
|
|
|
|
|
|
'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'A capella', |
|
334
|
|
|
|
|
|
|
'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club House', |
|
335
|
|
|
|
|
|
|
'Hardcore', 'Terror', 'Indie', 'BritPop', 'NegerPunk', |
|
336
|
|
|
|
|
|
|
'Polsk Punk', 'Beat', 'Christian Gangsta', 'Heavy Metal', |
|
337
|
|
|
|
|
|
|
'Black Metal', 'Crossover', 'Contemporary C', 'Christian Rock', |
|
338
|
|
|
|
|
|
|
'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'JPop', 'SynthPop' |
|
339
|
|
|
|
|
|
|
); |
|
340
|
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
sub parse_file |
|
343
|
|
|
|
|
|
|
{ |
|
344
|
17
|
|
|
17
|
0
|
41
|
my ($file, $tags) = @_; |
|
345
|
17
|
|
|
|
|
28
|
my ($fh, $err, $header, $size); |
|
346
|
|
|
|
|
|
|
|
|
347
|
17
|
50
|
33
|
|
|
140
|
if (not (defined $file && $file ne '')) |
|
348
|
|
|
|
|
|
|
{ |
|
349
|
0
|
|
|
|
|
0
|
$@ = 'No file specified'; |
|
350
|
0
|
|
|
|
|
0
|
return -1; |
|
351
|
|
|
|
|
|
|
} |
|
352
|
|
|
|
|
|
|
|
|
353
|
17
|
50
|
|
|
|
60
|
if (ref $file) # filehandle passed |
|
354
|
|
|
|
|
|
|
{ |
|
355
|
0
|
|
|
|
|
0
|
$fh = $file; |
|
356
|
|
|
|
|
|
|
} |
|
357
|
|
|
|
|
|
|
else |
|
358
|
|
|
|
|
|
|
{ |
|
359
|
17
|
|
|
|
|
86
|
$fh = gensym; |
|
360
|
17
|
50
|
|
|
|
2008
|
if (not open $fh, "< $file\0") |
|
361
|
|
|
|
|
|
|
{ |
|
362
|
0
|
|
|
|
|
0
|
$@ = "Can't open $file: $!"; |
|
363
|
0
|
|
|
|
|
0
|
return -1; |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
} |
|
366
|
|
|
|
|
|
|
|
|
367
|
17
|
|
|
|
|
65
|
binmode $fh; |
|
368
|
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
# Sanity check that this looks vaguely like an MP4 file |
|
370
|
17
|
50
|
33
|
|
|
16074
|
if ((read ($fh, $header, 8) != 8) || (lc substr ($header, 4) ne 'ftyp')) |
|
371
|
|
|
|
|
|
|
{ |
|
372
|
0
|
|
|
|
|
0
|
close ($fh); |
|
373
|
0
|
|
|
|
|
0
|
$@ = 'Not an MPEG-4 file'; |
|
374
|
0
|
|
|
|
|
0
|
return -1; |
|
375
|
|
|
|
|
|
|
} |
|
376
|
17
|
|
|
|
|
17459
|
seek $fh, 0, 2; |
|
377
|
17
|
|
|
|
|
46
|
$size = tell $fh; |
|
378
|
17
|
|
|
|
|
96
|
seek $fh, 0, 0; |
|
379
|
|
|
|
|
|
|
|
|
380
|
17
|
|
|
|
|
80
|
$err = parse_container($fh, 0, $size, $tags); |
|
381
|
17
|
|
|
|
|
414
|
close ($fh); |
|
382
|
17
|
50
|
|
|
|
59
|
return $err if $err; |
|
383
|
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
# remaining get_mp4tag() stuff |
|
385
|
17
|
100
|
|
|
|
69
|
$tags->{CPIL} = 0 unless defined ($tags->{CPIL}); |
|
386
|
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
# MP3::Info compatibility |
|
388
|
17
|
50
|
|
|
|
99
|
$tags->{TITLE} = $tags->{NAM} if defined ($tags->{NAM}); |
|
389
|
17
|
50
|
|
|
|
78
|
$tags->{ARTIST} = $tags->{ART} if defined ($tags->{ART}); |
|
390
|
17
|
100
|
|
|
|
87
|
$tags->{ALBUM} = $tags->{ALB} if defined ($tags->{ALB}); |
|
391
|
17
|
100
|
|
|
|
78
|
$tags->{YEAR} = $tags->{DAY} if defined ($tags->{DAY}); |
|
392
|
17
|
100
|
|
|
|
63
|
$tags->{COMMENT} = $tags->{CMT} if defined ($tags->{CMT}); |
|
393
|
17
|
100
|
|
|
|
62
|
$tags->{GENRE} = $tags->{GNRE} if defined ($tags->{GNRE}); |
|
394
|
17
|
100
|
|
|
|
77
|
$tags->{TRACKNUM} = $tags->{TRKN}[0] if defined ($tags->{TRKN}); |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
# remaining get_mp4info() stuff |
|
397
|
17
|
|
|
|
|
88
|
$tags->{VERSION} = 4; |
|
398
|
17
|
50
|
|
|
|
67
|
$tags->{LAYER} = 1 if defined ($tags->{FREQUENCY}); |
|
399
|
17
|
50
|
|
|
|
62
|
$tags->{COPYRIGHT}= (defined ($tags->{CPRT}) ? 1 : 0); |
|
400
|
17
|
50
|
|
|
|
59
|
$tags->{ENCRYPTED}= 0 unless defined ($tags->{ENCRYPTED}); |
|
401
|
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
# Returns actual (not requested) bitrate |
|
403
|
17
|
50
|
33
|
|
|
357
|
if (defined($tags->{SIZE}) && $tags->{SIZE} && defined($tags->{SECS}) && ($tags->{MM}+$tags->{SS}+$tags->{MS})) |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
404
|
|
|
|
|
|
|
{ |
|
405
|
17
|
|
|
|
|
137
|
$tags->{BITRATE} = int (0.5 + $tags->{SIZE} * 0.008 / ($tags->{MM}*60+$tags->{SS}+$tags->{MS}/1000)) |
|
406
|
|
|
|
|
|
|
} |
|
407
|
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
# Post process '---' container |
|
409
|
17
|
100
|
66
|
|
|
2351
|
if ($tags->{MEAN} && ref($tags->{MEAN}) eq 'ARRAY') |
|
410
|
|
|
|
|
|
|
{ |
|
411
|
8
|
|
|
|
|
21
|
for (my $i = 0; $i < scalar @{$tags->{MEAN}}; $i++) |
|
|
16
|
|
|
|
|
53
|
|
|
412
|
|
|
|
|
|
|
{ |
|
413
|
8
|
|
|
|
|
14
|
push @{$tags->{META}}, { |
|
|
8
|
|
|
|
|
91
|
|
|
414
|
|
|
|
|
|
|
MEAN => $tags->{MEAN}->[$i], |
|
415
|
|
|
|
|
|
|
NAME => $tags->{NAME}->[$i], |
|
416
|
|
|
|
|
|
|
DATA => $tags->{DATA}->[$i], |
|
417
|
|
|
|
|
|
|
}; |
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
8
|
|
|
|
|
32
|
delete $tags->{MEAN}; |
|
421
|
8
|
|
|
|
|
19
|
delete $tags->{NAME}; |
|
422
|
8
|
|
|
|
|
27
|
delete $tags->{DATA}; |
|
423
|
|
|
|
|
|
|
} |
|
424
|
|
|
|
|
|
|
|
|
425
|
17
|
|
|
|
|
529
|
return 0; |
|
426
|
|
|
|
|
|
|
} |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
# Pre: $size=size of container contents |
|
430
|
|
|
|
|
|
|
# $fh points to start of container contents |
|
431
|
|
|
|
|
|
|
# Post: $fh points past end of container contents |
|
432
|
|
|
|
|
|
|
sub parse_container |
|
433
|
|
|
|
|
|
|
{ |
|
434
|
185
|
|
|
185
|
0
|
311
|
my ($fh, $level, $size, $tags) = @_; |
|
435
|
185
|
|
|
|
|
235
|
my ($end, $err); |
|
436
|
|
|
|
|
|
|
|
|
437
|
185
|
|
|
|
|
228
|
$level++; |
|
438
|
185
|
|
|
|
|
537
|
$end = (tell $fh) + $size; |
|
439
|
185
|
|
|
|
|
2163
|
while (tell $fh < $end) |
|
440
|
|
|
|
|
|
|
{ |
|
441
|
703
|
|
|
|
|
12433
|
$err = parse_atom($fh, $level, $end-(tell $fh), $tags); |
|
442
|
703
|
50
|
|
|
|
4568
|
return $err if $err; |
|
443
|
|
|
|
|
|
|
} |
|
444
|
185
|
50
|
|
|
|
1513
|
if (tell $fh != $end) |
|
445
|
|
|
|
|
|
|
{ |
|
446
|
0
|
|
|
|
|
0
|
$@ = 'Parse error'; |
|
447
|
0
|
|
|
|
|
0
|
return -1; |
|
448
|
|
|
|
|
|
|
} |
|
449
|
185
|
|
|
|
|
1633
|
return 0; |
|
450
|
|
|
|
|
|
|
} |
|
451
|
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# Pre: $fh points to start of atom |
|
454
|
|
|
|
|
|
|
# $parentsize is remaining size of parent container |
|
455
|
|
|
|
|
|
|
# Post: $fh points past end of atom |
|
456
|
|
|
|
|
|
|
sub parse_atom |
|
457
|
|
|
|
|
|
|
{ |
|
458
|
703
|
|
|
703
|
0
|
6329
|
my ($fh, $level, $parentsize, $tags) = @_; |
|
459
|
703
|
|
|
|
|
862
|
my ($header, $size, $id, $err, $pos); |
|
460
|
703
|
50
|
|
|
|
22219
|
if (read ($fh, $header, 8) != 8) |
|
461
|
|
|
|
|
|
|
{ |
|
462
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
463
|
0
|
|
|
|
|
0
|
return -1; |
|
464
|
|
|
|
|
|
|
} |
|
465
|
|
|
|
|
|
|
|
|
466
|
703
|
|
|
|
|
15762
|
($size,$id) = unpack 'Na4', $header; |
|
467
|
703
|
50
|
|
|
|
2782
|
if ($size==0) |
|
|
|
50
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
{ |
|
469
|
|
|
|
|
|
|
# Zero-sized atom extends to eof (14496-12:2004 S4.2) |
|
470
|
0
|
|
|
|
|
0
|
$pos=tell($fh); |
|
471
|
0
|
|
|
|
|
0
|
seek $fh, 0, 2; |
|
472
|
0
|
|
|
|
|
0
|
$size = tell($fh) - $pos; # Error if parent size doesn't match |
|
473
|
0
|
|
|
|
|
0
|
seek $fh, $pos, 0; |
|
474
|
|
|
|
|
|
|
} |
|
475
|
|
|
|
|
|
|
elsif ($size == 1) |
|
476
|
|
|
|
|
|
|
{ |
|
477
|
|
|
|
|
|
|
# extended size |
|
478
|
0
|
|
|
|
|
0
|
my ($hi, $lo); |
|
479
|
0
|
0
|
|
|
|
0
|
if (read ($fh, $header, 8) != 8) |
|
480
|
|
|
|
|
|
|
{ |
|
481
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
482
|
0
|
|
|
|
|
0
|
return -1; |
|
483
|
|
|
|
|
|
|
} |
|
484
|
0
|
|
|
|
|
0
|
($hi,$lo) = unpack 'NN', $header; |
|
485
|
0
|
|
|
|
|
0
|
$size=$hi*(2**32) + $lo; |
|
486
|
0
|
0
|
|
|
|
0
|
if ($size>$parentsize) |
|
487
|
|
|
|
|
|
|
{ |
|
488
|
|
|
|
|
|
|
# atom extends outside of parent container - skip to end of parent |
|
489
|
0
|
|
|
|
|
0
|
seek $fh, $parentsize-16, 1; |
|
490
|
0
|
|
|
|
|
0
|
return 0; |
|
491
|
|
|
|
|
|
|
} |
|
492
|
0
|
|
|
|
|
0
|
$size -= 16; |
|
493
|
|
|
|
|
|
|
} |
|
494
|
|
|
|
|
|
|
else |
|
495
|
|
|
|
|
|
|
{ |
|
496
|
703
|
50
|
|
|
|
1347
|
if ($size>$parentsize) |
|
497
|
|
|
|
|
|
|
{ |
|
498
|
|
|
|
|
|
|
# atom extends outside of parent container - skip to end of parent |
|
499
|
0
|
|
|
|
|
0
|
seek $fh, $parentsize-8, 1; |
|
500
|
0
|
|
|
|
|
0
|
return 0; |
|
501
|
|
|
|
|
|
|
} |
|
502
|
703
|
|
|
|
|
1050
|
$size -= 8; |
|
503
|
|
|
|
|
|
|
} |
|
504
|
703
|
50
|
|
|
|
1331
|
if ($size<0) |
|
505
|
|
|
|
|
|
|
{ |
|
506
|
0
|
|
|
|
|
0
|
$@ = 'Parse error'; |
|
507
|
0
|
|
|
|
|
0
|
return -1; |
|
508
|
|
|
|
|
|
|
} |
|
509
|
703
|
|
|
|
|
1817
|
$id =~ s/[^\w\-]//; |
|
510
|
703
|
|
|
|
|
10438
|
$id = uc $id; |
|
511
|
|
|
|
|
|
|
|
|
512
|
703
|
50
|
|
|
|
6879
|
printf "%s%s: %d bytes\n", ' 'x(2*$level), $id, $size if $debug; |
|
513
|
|
|
|
|
|
|
|
|
514
|
703
|
100
|
|
|
|
4352
|
if (defined($data_atoms{$id})) |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
{ |
|
516
|
194
|
|
|
|
|
425
|
return parse_data ($fh, $level, $size, $id, $tags); |
|
517
|
|
|
|
|
|
|
} |
|
518
|
|
|
|
|
|
|
elsif (defined($other_atoms{$id})) |
|
519
|
|
|
|
|
|
|
{ |
|
520
|
94
|
|
|
|
|
224
|
return &{$other_atoms{$id}}($fh, $level, $size, $tags); |
|
|
94
|
|
|
|
|
29536
|
|
|
521
|
|
|
|
|
|
|
} |
|
522
|
|
|
|
|
|
|
elsif ($container_atoms{$id}) |
|
523
|
|
|
|
|
|
|
{ |
|
524
|
134
|
|
|
|
|
322
|
return parse_container ($fh, $level, $size, $tags); |
|
525
|
|
|
|
|
|
|
} |
|
526
|
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
# Unkown atom - skip past it |
|
528
|
281
|
|
|
|
|
1108
|
seek $fh, $size, 1; |
|
529
|
281
|
|
|
|
|
11999
|
return 0; |
|
530
|
|
|
|
|
|
|
} |
|
531
|
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
534
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
535
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
536
|
|
|
|
|
|
|
sub parse_moov |
|
537
|
|
|
|
|
|
|
{ |
|
538
|
17
|
|
|
17
|
0
|
54
|
my ($fh, $level, $size, $tags) = @_; |
|
539
|
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
# MOOV is a normal container. |
|
541
|
|
|
|
|
|
|
# Read ahead to improve performance on high-latency filesystems. |
|
542
|
17
|
|
|
|
|
25
|
my $data; |
|
543
|
17
|
50
|
|
|
|
119
|
if (read ($fh, $data, $size) != $size) |
|
544
|
|
|
|
|
|
|
{ |
|
545
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
546
|
0
|
|
|
|
|
0
|
return -1; |
|
547
|
|
|
|
|
|
|
} |
|
548
|
17
|
|
|
|
|
176
|
my $cache=IO::String->new($data); |
|
549
|
17
|
|
|
|
|
2235
|
return parse_container ($cache, $level, $size, $tags); |
|
550
|
|
|
|
|
|
|
} |
|
551
|
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
554
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
555
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
556
|
|
|
|
|
|
|
sub parse_mdat |
|
557
|
|
|
|
|
|
|
{ |
|
558
|
20
|
|
|
20
|
0
|
36
|
my ($fh, $level, $size, $tags) = @_; |
|
559
|
|
|
|
|
|
|
|
|
560
|
20
|
100
|
|
|
|
106
|
$tags->{SIZE} = 0 unless defined($tags->{SIZE}); |
|
561
|
20
|
|
|
|
|
43
|
$tags->{SIZE} += $size; |
|
562
|
20
|
|
|
|
|
190
|
seek $fh, $size, 1; |
|
563
|
|
|
|
|
|
|
|
|
564
|
20
|
|
|
|
|
72
|
return 0; |
|
565
|
|
|
|
|
|
|
} |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
569
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
570
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
571
|
|
|
|
|
|
|
sub parse_meta |
|
572
|
|
|
|
|
|
|
{ |
|
573
|
17
|
|
|
17
|
0
|
38
|
my ($fh, $level, $size, $tags) = @_; |
|
574
|
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
# META is just a container preceded by a version field |
|
576
|
17
|
|
|
|
|
61
|
seek $fh, 4, 1; |
|
577
|
17
|
|
|
|
|
240
|
return parse_container ($fh, $level, $size-4, $tags); |
|
578
|
|
|
|
|
|
|
} |
|
579
|
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
582
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
583
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
584
|
|
|
|
|
|
|
sub parse_mvhd |
|
585
|
|
|
|
|
|
|
{ |
|
586
|
17
|
|
|
17
|
0
|
44
|
my ($fh, $level, $size, $tags) = @_; |
|
587
|
17
|
|
|
|
|
53
|
my ($data, $version, $scale, $duration, $secs); |
|
588
|
|
|
|
|
|
|
|
|
589
|
17
|
50
|
|
|
|
55
|
if ($size < 32) |
|
590
|
|
|
|
|
|
|
{ |
|
591
|
0
|
|
|
|
|
0
|
$@ = 'Parse error'; |
|
592
|
0
|
|
|
|
|
0
|
return -1; |
|
593
|
|
|
|
|
|
|
} |
|
594
|
17
|
50
|
|
|
|
67
|
if (read ($fh, $data, $size) != $size) |
|
595
|
|
|
|
|
|
|
{ |
|
596
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
597
|
0
|
|
|
|
|
0
|
return -1; |
|
598
|
|
|
|
|
|
|
} |
|
599
|
|
|
|
|
|
|
|
|
600
|
17
|
|
|
|
|
303
|
$version = unpack('C', $data) & 255; |
|
601
|
17
|
50
|
|
|
|
54
|
if ($version==0) |
|
|
|
0
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
{ |
|
603
|
17
|
|
|
|
|
61
|
($scale,$duration) = unpack 'NN', substr ($data, 12, 8); |
|
604
|
|
|
|
|
|
|
} |
|
605
|
|
|
|
|
|
|
elsif ($version==1) |
|
606
|
|
|
|
|
|
|
{ |
|
607
|
0
|
|
|
|
|
0
|
my ($hi,$lo); |
|
608
|
0
|
0
|
|
|
|
0
|
print "Long version\n" if $debug; |
|
609
|
0
|
|
|
|
|
0
|
($scale,$hi,$lo) = unpack 'NNN', substr ($data, 20, 12); |
|
610
|
0
|
|
|
|
|
0
|
$duration=$hi*(2**32) + $lo; |
|
611
|
|
|
|
|
|
|
} |
|
612
|
|
|
|
|
|
|
else |
|
613
|
|
|
|
|
|
|
{ |
|
614
|
0
|
|
|
|
|
0
|
return 0; |
|
615
|
|
|
|
|
|
|
} |
|
616
|
|
|
|
|
|
|
|
|
617
|
17
|
50
|
|
|
|
47
|
printf " %sDur/Scl=$duration/$scale\n", ' 'x(2*$level) if $debug; |
|
618
|
17
|
|
|
|
|
44
|
$secs=$duration/$scale; |
|
619
|
17
|
|
|
|
|
74
|
$tags->{SECS} = int (0.5+$secs); |
|
620
|
17
|
|
|
|
|
39
|
$tags->{MM} = int ($secs/60); |
|
621
|
17
|
|
|
|
|
64
|
$tags->{SS} = int ($secs - $tags->{MM}*60); |
|
622
|
17
|
|
|
|
|
51
|
$tags->{MS} = int (0.5 + 1000*($secs - int ($secs))); |
|
623
|
17
|
|
|
|
|
167
|
$tags->{TIME} = sprintf "%02d:%02d", |
|
624
|
|
|
|
|
|
|
$tags->{MM}, $tags->{SECS} - $tags->{MM}*60; |
|
625
|
|
|
|
|
|
|
|
|
626
|
17
|
|
|
|
|
61
|
return 0; |
|
627
|
|
|
|
|
|
|
} |
|
628
|
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
631
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
632
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
633
|
|
|
|
|
|
|
sub parse_stsd |
|
634
|
|
|
|
|
|
|
{ |
|
635
|
23
|
|
|
23
|
0
|
59
|
my ($fh, $level, $size, $tags) = @_; |
|
636
|
23
|
|
|
|
|
32
|
my ($data, $data_format); |
|
637
|
|
|
|
|
|
|
|
|
638
|
23
|
50
|
|
|
|
67
|
if ($size < 44) |
|
639
|
|
|
|
|
|
|
{ |
|
640
|
0
|
|
|
|
|
0
|
$@ = 'Parse error'; |
|
641
|
0
|
|
|
|
|
0
|
return -1; |
|
642
|
|
|
|
|
|
|
} |
|
643
|
23
|
50
|
|
|
|
256
|
if (read ($fh, $data, $size) != $size) |
|
644
|
|
|
|
|
|
|
{ |
|
645
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
646
|
0
|
|
|
|
|
0
|
return -1; |
|
647
|
|
|
|
|
|
|
} |
|
648
|
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
# Assumes first entry in table contains the data |
|
650
|
23
|
50
|
|
|
|
343
|
printf " %sSample=%s\n", ' 'x(2*$level), substr ($data, 12, 4) if $debug; |
|
651
|
23
|
|
|
|
|
58
|
$data_format = lc substr ($data, 12, 4); |
|
652
|
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
# Is this an audio track? (Ought to look for presence of an SMHD uncle |
|
654
|
|
|
|
|
|
|
# atom instead to allow for other audio data formats). |
|
655
|
23
|
50
|
100
|
|
|
220
|
if (($data_format eq 'mp4a') || # AAC, aacPlus |
|
|
|
|
66
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
656
|
|
|
|
|
|
|
($data_format eq 'alac') || # Apple lossless |
|
657
|
|
|
|
|
|
|
($data_format eq 'drms') || # Apple encrypted AAC |
|
658
|
|
|
|
|
|
|
($data_format eq 'samr') || # Narrow-band AMR |
|
659
|
|
|
|
|
|
|
($data_format eq 'sawb') || # AMR wide-band |
|
660
|
|
|
|
|
|
|
($data_format eq 'sawp') || # AMR wide-band + |
|
661
|
|
|
|
|
|
|
($data_format eq 'enca')) # Generic encrypted audio |
|
662
|
|
|
|
|
|
|
{ |
|
663
|
17
|
|
|
|
|
51
|
$tags->{ENCODING} = $data_format; |
|
664
|
|
|
|
|
|
|
# $version = unpack "n", substr ($data, 24, 2); |
|
665
|
|
|
|
|
|
|
# s8.16 is inconsistent. In practice, channels always appears == 2. |
|
666
|
|
|
|
|
|
|
# $tags->{STEREO} = (unpack ("n", substr ($data, 32, 2)) > 1) ? 1 : 0; |
|
667
|
|
|
|
|
|
|
# Old Quicktime field. No longer used. |
|
668
|
|
|
|
|
|
|
# $tags->{VBR} = (unpack ("n", substr ($data, 36, 2)) == -2) ? 1 : 0; |
|
669
|
17
|
|
|
|
|
77
|
$tags->{FREQUENCY} = unpack ('N', substr ($data, 40, 4)) / 65536000; |
|
670
|
17
|
50
|
|
|
|
54
|
printf " %sFreq=%s\n", ' 'x(2*$level), $tags->{FREQUENCY} if $debug; |
|
671
|
|
|
|
|
|
|
} |
|
672
|
|
|
|
|
|
|
|
|
673
|
23
|
50
|
33
|
|
|
165
|
$tags->{ENCRYPTED}=1 if (($data_format eq 'drms') || |
|
674
|
|
|
|
|
|
|
(substr($data_format, 0, 3) eq 'enc')); |
|
675
|
|
|
|
|
|
|
|
|
676
|
23
|
|
|
|
|
78
|
return 0; |
|
677
|
|
|
|
|
|
|
} |
|
678
|
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
# User-defined box. Used by PSP - See ffmpeg libavformat/movenc.c |
|
681
|
|
|
|
|
|
|
# |
|
682
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
683
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
684
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
685
|
|
|
|
|
|
|
sub parse_uuid |
|
686
|
|
|
|
|
|
|
{ |
|
687
|
0
|
|
|
0
|
0
|
0
|
my ($fh, $level, $size, $tags) = @_; |
|
688
|
0
|
|
|
|
|
0
|
my $data; |
|
689
|
|
|
|
|
|
|
|
|
690
|
0
|
0
|
|
|
|
0
|
if (read ($fh, $data, $size) != $size) |
|
691
|
|
|
|
|
|
|
{ |
|
692
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
693
|
0
|
|
|
|
|
0
|
return -1; |
|
694
|
|
|
|
|
|
|
} |
|
695
|
0
|
0
|
|
|
|
0
|
($size > 26) || return 0; # 16byte uuid, 10byte psp-specific |
|
696
|
|
|
|
|
|
|
|
|
697
|
0
|
|
|
|
|
0
|
my ($u1,$u2,$u3,$u4)=unpack 'a4NNN', $data; |
|
698
|
0
|
0
|
|
|
|
0
|
if ($u1 eq 'USMT') # PSP also uses a uuid starting with 'PROF' |
|
699
|
|
|
|
|
|
|
{ |
|
700
|
0
|
|
|
|
|
0
|
my ($pspsize,$pspid) = unpack 'Na4', substr ($data, 16, 8); |
|
701
|
0
|
0
|
|
|
|
0
|
printf " %s$pspid: $pspsize bytes\n", ' 'x(2*$level) if $debug; |
|
702
|
0
|
0
|
|
|
|
0
|
($pspsize==$size-16) || return 0; # sanity check |
|
703
|
0
|
0
|
|
|
|
0
|
if ($pspid eq 'MTDT') |
|
704
|
|
|
|
|
|
|
{ |
|
705
|
0
|
|
|
|
|
0
|
my $nblocks = unpack 'n', substr ($data, 24, 2); |
|
706
|
0
|
|
|
|
|
0
|
$data = substr($data, 26); |
|
707
|
0
|
|
|
|
|
0
|
while ($nblocks) |
|
708
|
|
|
|
|
|
|
{ |
|
709
|
0
|
|
|
|
|
0
|
my ($bsize, $btype, $flags, $ptype) = unpack 'nNnn', $data; |
|
710
|
0
|
0
|
|
|
|
0
|
printf " %s0x%x: $bsize bytes, Type=$ptype\n", ' 'x(2*$level), $btype if $debug; |
|
711
|
0
|
0
|
0
|
|
|
0
|
if ($btype==1 && $bsize>12 && $ptype==1 && !defined($tags->{NAM})) |
|
|
|
0
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
712
|
|
|
|
|
|
|
{ |
|
713
|
|
|
|
|
|
|
# Could have titles in different langauges - use first |
|
714
|
0
|
|
|
|
|
0
|
$tags->{NAM} = decode("UTF-16BE", substr($data, 10, $bsize-12)); |
|
715
|
|
|
|
|
|
|
} |
|
716
|
|
|
|
|
|
|
elsif ($btype==4 && $bsize>12 && $ptype==1) |
|
717
|
|
|
|
|
|
|
{ |
|
718
|
0
|
|
|
|
|
0
|
$tags->{TOO} = decode("UTF-16BE", substr($data, 10, $bsize-12)); |
|
719
|
|
|
|
|
|
|
} |
|
720
|
0
|
|
|
|
|
0
|
$data = substr($data, $bsize); |
|
721
|
0
|
|
|
|
|
0
|
$nblocks-=1; |
|
722
|
|
|
|
|
|
|
} |
|
723
|
|
|
|
|
|
|
} |
|
724
|
|
|
|
|
|
|
} |
|
725
|
0
|
|
|
|
|
0
|
return 0; |
|
726
|
|
|
|
|
|
|
} |
|
727
|
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
|
|
729
|
|
|
|
|
|
|
# Pre: $size=size of atom contents |
|
730
|
|
|
|
|
|
|
# $fh points to start of atom contents |
|
731
|
|
|
|
|
|
|
# Post: $fh points past end of atom contents |
|
732
|
|
|
|
|
|
|
sub parse_data |
|
733
|
|
|
|
|
|
|
{ |
|
734
|
194
|
|
|
194
|
0
|
1153
|
my ($fh, $level, $size, $id, $tags) = @_; |
|
735
|
194
|
|
|
|
|
1943
|
my ($data, $atom, $type); |
|
736
|
|
|
|
|
|
|
|
|
737
|
194
|
50
|
|
|
|
532
|
if (read ($fh, $data, $size) != $size) |
|
738
|
|
|
|
|
|
|
{ |
|
739
|
0
|
|
|
|
|
0
|
$@ = 'Premature eof'; |
|
740
|
0
|
|
|
|
|
0
|
return -1; |
|
741
|
|
|
|
|
|
|
} |
|
742
|
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
# 3GPP - different format when child of 'udta'. |
|
744
|
|
|
|
|
|
|
# Let existing tags (if any) override these. |
|
745
|
194
|
100
|
33
|
|
|
4794
|
if (($id eq 'TITL') || |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
746
|
|
|
|
|
|
|
($id eq 'DSCP') || |
|
747
|
|
|
|
|
|
|
($id eq 'CPRT') || |
|
748
|
|
|
|
|
|
|
($id eq 'PERF') || |
|
749
|
|
|
|
|
|
|
($id eq 'AUTH') || |
|
750
|
|
|
|
|
|
|
($id eq 'GNRE')) |
|
751
|
|
|
|
|
|
|
{ |
|
752
|
9
|
|
|
|
|
23
|
my ($ver) = unpack 'N', $data; |
|
753
|
9
|
50
|
|
|
|
27
|
if ($ver == 0) |
|
754
|
|
|
|
|
|
|
{ |
|
755
|
0
|
0
|
|
|
|
0
|
($size > 7) || return 0; |
|
756
|
0
|
|
|
|
|
0
|
$size -= 7; |
|
757
|
0
|
|
|
|
|
0
|
$type = 1; |
|
758
|
0
|
|
|
|
|
0
|
$data = substr ($data, 6, $size); |
|
759
|
|
|
|
|
|
|
|
|
760
|
0
|
0
|
|
|
|
0
|
if ($id eq 'TITL') |
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
{ |
|
762
|
0
|
0
|
|
|
|
0
|
return 0 if defined ($tags->{NAM}); |
|
763
|
0
|
|
|
|
|
0
|
$id = 'NAM'; |
|
764
|
|
|
|
|
|
|
} |
|
765
|
|
|
|
|
|
|
elsif ($id eq 'DSCP') |
|
766
|
|
|
|
|
|
|
{ |
|
767
|
0
|
0
|
|
|
|
0
|
return 0 if defined ($tags->{CMT}); |
|
768
|
0
|
|
|
|
|
0
|
$id = 'CMT'; |
|
769
|
|
|
|
|
|
|
} |
|
770
|
|
|
|
|
|
|
elsif ($id eq 'PERF') |
|
771
|
|
|
|
|
|
|
{ |
|
772
|
0
|
0
|
|
|
|
0
|
return 0 if defined ($tags->{ART}); |
|
773
|
0
|
|
|
|
|
0
|
$id = 'ART'; |
|
774
|
|
|
|
|
|
|
} |
|
775
|
|
|
|
|
|
|
elsif ($id eq 'AUTH') |
|
776
|
|
|
|
|
|
|
{ |
|
777
|
0
|
0
|
|
|
|
0
|
return 0 if defined ($tags->{WRT}); |
|
778
|
0
|
|
|
|
|
0
|
$id = 'WRT'; |
|
779
|
|
|
|
|
|
|
} |
|
780
|
|
|
|
|
|
|
} |
|
781
|
|
|
|
|
|
|
} |
|
782
|
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
# Parse out the tuple that contains aacgain data, etc. |
|
784
|
194
|
100
|
100
|
|
|
1232
|
if (($id eq 'MEAN') || |
|
|
|
|
100
|
|
|
|
|
|
785
|
|
|
|
|
|
|
($id eq 'NAME') || |
|
786
|
|
|
|
|
|
|
($id eq 'DATA')) |
|
787
|
|
|
|
|
|
|
{ |
|
788
|
|
|
|
|
|
|
# The first 4 or 8 bytes are nulls. |
|
789
|
24
|
100
|
|
|
|
66
|
if ($id eq 'DATA') |
|
790
|
|
|
|
|
|
|
{ |
|
791
|
8
|
|
|
|
|
22
|
$data = substr ($data, 8); |
|
792
|
|
|
|
|
|
|
} |
|
793
|
|
|
|
|
|
|
else |
|
794
|
|
|
|
|
|
|
{ |
|
795
|
16
|
|
|
|
|
35
|
$data = substr ($data, 4); |
|
796
|
|
|
|
|
|
|
} |
|
797
|
|
|
|
|
|
|
|
|
798
|
24
|
|
|
|
|
30
|
push @{$tags->{$id}}, $data; |
|
|
24
|
|
|
|
|
92
|
|
|
799
|
24
|
|
|
|
|
83
|
return 0; |
|
800
|
|
|
|
|
|
|
} |
|
801
|
|
|
|
|
|
|
|
|
802
|
170
|
50
|
|
|
|
475
|
if (!defined($type)) |
|
803
|
|
|
|
|
|
|
{ |
|
804
|
170
|
50
|
|
|
|
344
|
($size > 16) || return 0; |
|
805
|
|
|
|
|
|
|
|
|
806
|
|
|
|
|
|
|
# Assumes first atom is the data atom we're after |
|
807
|
170
|
|
|
|
|
2253
|
($size,$atom,$type) = unpack 'Na4N', $data; |
|
808
|
170
|
50
|
|
|
|
584
|
(lc $atom eq 'data') || return 0; |
|
809
|
170
|
50
|
|
|
|
316
|
($size > 16) || return 0; |
|
810
|
170
|
|
|
|
|
234
|
$size -= 16; |
|
811
|
170
|
|
|
|
|
203
|
$type &= 255; |
|
812
|
170
|
|
|
|
|
1030
|
$data = substr ($data, 16, $size); |
|
813
|
|
|
|
|
|
|
} |
|
814
|
170
|
50
|
|
|
|
331
|
printf " %sType=$type, Size=$size\n", ' 'x(2*$level) if $debug; |
|
815
|
|
|
|
|
|
|
|
|
816
|
170
|
50
|
|
|
|
525
|
if ($id eq 'COVR') |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
817
|
|
|
|
|
|
|
{ |
|
818
|
|
|
|
|
|
|
# iTunes appears to use random data types for cover art |
|
819
|
0
|
|
|
|
|
0
|
$tags->{$id} = $data; |
|
820
|
|
|
|
|
|
|
} |
|
821
|
|
|
|
|
|
|
elsif ($type==0) # 16bit int data array |
|
822
|
|
|
|
|
|
|
{ |
|
823
|
34
|
|
|
|
|
150
|
my @ints = unpack 'n' x ($size / 2), $data; |
|
824
|
34
|
100
|
66
|
|
|
158
|
if ($id eq 'GNRE') |
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
{ |
|
826
|
9
|
|
|
|
|
39
|
$tags->{$id} = $mp4_genres[$ints[0]]; |
|
827
|
|
|
|
|
|
|
} |
|
828
|
|
|
|
|
|
|
elsif ($id eq 'DISK' or $id eq 'TRKN') |
|
829
|
|
|
|
|
|
|
{ |
|
830
|
|
|
|
|
|
|
# Real 10.0 sometimes omits the second integer, but we require it |
|
831
|
25
|
50
|
|
|
|
155
|
$tags->{$id} = [$ints[1], ($size>=6 ? $ints[2] : 0)] if ($size>=4); |
|
|
|
50
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
} |
|
833
|
|
|
|
|
|
|
elsif ($size>=4) |
|
834
|
|
|
|
|
|
|
{ |
|
835
|
0
|
|
|
|
|
0
|
$tags->{$id} = $ints[1]; |
|
836
|
|
|
|
|
|
|
} |
|
837
|
|
|
|
|
|
|
} |
|
838
|
|
|
|
|
|
|
elsif ($type==1) # Char data |
|
839
|
|
|
|
|
|
|
{ |
|
840
|
|
|
|
|
|
|
# faac 1.24 and Real 10.0 encode data as unspecified 8 bit, which |
|
841
|
|
|
|
|
|
|
# goes against s8.28 of ISO/IEC 14496-12:2004. How tedious. |
|
842
|
|
|
|
|
|
|
# Assume data is utf8 if it could be utf8, otherwise assume latin1. |
|
843
|
117
|
|
|
|
|
536
|
my $decoder = Encode::Guess->guess ($data); |
|
844
|
117
|
100
|
|
|
|
37649
|
$data = (ref ($decoder)) ? |
|
|
|
100
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
$decoder->decode($data) : # found one of utf8, utf16, latin1 |
|
846
|
|
|
|
|
|
|
decode($utf8 ? 'utf8' : 'latin1', $data); # ambiguous so force |
|
847
|
|
|
|
|
|
|
|
|
848
|
117
|
100
|
|
|
|
1151
|
if ($id eq 'GEN') |
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
849
|
|
|
|
|
|
|
{ |
|
850
|
5
|
50
|
|
|
|
24
|
return 0 if defined ($tags->{GNRE}); |
|
851
|
5
|
|
|
|
|
74
|
$id='GNRE'; |
|
852
|
|
|
|
|
|
|
} |
|
853
|
|
|
|
|
|
|
elsif ($id eq 'AART') |
|
854
|
|
|
|
|
|
|
{ |
|
855
|
0
|
0
|
|
|
|
0
|
return 0 if defined ($tags->{ART}); |
|
856
|
0
|
|
|
|
|
0
|
$id = 'ART'; |
|
857
|
|
|
|
|
|
|
} |
|
858
|
|
|
|
|
|
|
elsif ($id eq 'DAY') |
|
859
|
|
|
|
|
|
|
{ |
|
860
|
14
|
|
|
|
|
67
|
$data = substr ($data, 0, 4); |
|
861
|
|
|
|
|
|
|
# Real 10.0 supplies DAY=0 instead of deleting the atom if the |
|
862
|
|
|
|
|
|
|
# year is not known. What's wrong with these people? |
|
863
|
14
|
50
|
|
|
|
58
|
return 0 if $data==0; |
|
864
|
|
|
|
|
|
|
} |
|
865
|
117
|
|
|
|
|
363
|
$tags->{$id} = $data; |
|
866
|
|
|
|
|
|
|
} |
|
867
|
|
|
|
|
|
|
elsif ($type==21) # Integer data |
|
868
|
|
|
|
|
|
|
{ |
|
869
|
|
|
|
|
|
|
# Convert to an integer if of an appropriate size |
|
870
|
19
|
100
|
|
|
|
49
|
if ($size==1) |
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
{ |
|
872
|
11
|
|
|
|
|
41
|
$tags->{$id} = unpack 'C', $data; |
|
873
|
|
|
|
|
|
|
} |
|
874
|
|
|
|
|
|
|
elsif ($size==2) |
|
875
|
|
|
|
|
|
|
{ |
|
876
|
8
|
|
|
|
|
28
|
$tags->{$id} = unpack 'n', $data; |
|
877
|
|
|
|
|
|
|
} |
|
878
|
|
|
|
|
|
|
elsif ($size==4) |
|
879
|
|
|
|
|
|
|
{ |
|
880
|
0
|
|
|
|
|
0
|
$tags->{$id} = unpack 'N', $data; |
|
881
|
|
|
|
|
|
|
} |
|
882
|
|
|
|
|
|
|
elsif ($size==8) |
|
883
|
|
|
|
|
|
|
{ |
|
884
|
0
|
|
|
|
|
0
|
my ($hi,$lo); |
|
885
|
0
|
|
|
|
|
0
|
($hi,$lo) = unpack 'NN', $data; |
|
886
|
0
|
|
|
|
|
0
|
$tags->{$id} = $hi*(2**32) + $lo; |
|
887
|
|
|
|
|
|
|
} |
|
888
|
|
|
|
|
|
|
else |
|
889
|
|
|
|
|
|
|
{ |
|
890
|
|
|
|
|
|
|
# Non-standard size - just return the raw data |
|
891
|
0
|
|
|
|
|
0
|
$tags->{$id} = $data; |
|
892
|
|
|
|
|
|
|
} |
|
893
|
|
|
|
|
|
|
} |
|
894
|
|
|
|
|
|
|
|
|
895
|
|
|
|
|
|
|
# Silently ignore other data types |
|
896
|
170
|
|
|
|
|
510
|
return 0; |
|
897
|
|
|
|
|
|
|
} |
|
898
|
|
|
|
|
|
|
|
|
899
|
|
|
|
|
|
|
1; |
|
900
|
|
|
|
|
|
|
|
|
901
|
|
|
|
|
|
|
__END__ |