1 | #------------------------------------------------------------------------------
|
---|
2 | # File: ID3.pm
|
---|
3 | #
|
---|
4 | # Description: Read ID3 meta information
|
---|
5 | #
|
---|
6 | # Revisions: 09/12/2005 - P. Harvey Created
|
---|
7 | #
|
---|
8 | # References: 1) http://www.id3.org/
|
---|
9 | # 2) http://www.mp3-tech.org/
|
---|
10 | # 3) http://www.fortunecity.com/underworld/sonic/3/id3tag.html
|
---|
11 | #------------------------------------------------------------------------------
|
---|
12 |
|
---|
13 | package Image::ExifTool::ID3;
|
---|
14 |
|
---|
15 | use strict;
|
---|
16 | use vars qw($VERSION);
|
---|
17 | use Image::ExifTool qw(:DataAccess :Utils);
|
---|
18 |
|
---|
19 | $VERSION = '1.28';
|
---|
20 |
|
---|
21 | sub ProcessID3v2($$$);
|
---|
22 | sub ProcessPrivate($$$);
|
---|
23 | sub ConvertID3v1Text($$);
|
---|
24 |
|
---|
25 | # audio formats that we process after an ID3v2 header (in order)
|
---|
26 | my @audioFormats = qw(APE MPC FLAC OGG MP3);
|
---|
27 |
|
---|
28 | # audio formats where the processing proc is in a different module
|
---|
29 | my %audioModule = (
|
---|
30 | MP3 => 'ID3',
|
---|
31 | OGG => 'Vorbis',
|
---|
32 | );
|
---|
33 |
|
---|
34 | # picture types for 'PIC' and 'APIC' tags
|
---|
35 | # (Note: Duplicated in ID3, ASF and FLAC modules!)
|
---|
36 | my %pictureType = (
|
---|
37 | 0 => 'Other',
|
---|
38 | 1 => '32x32 PNG Icon',
|
---|
39 | 2 => 'Other Icon',
|
---|
40 | 3 => 'Front Cover',
|
---|
41 | 4 => 'Back Cover',
|
---|
42 | 5 => 'Leaflet',
|
---|
43 | 6 => 'Media',
|
---|
44 | 7 => 'Lead Artist',
|
---|
45 | 8 => 'Artist',
|
---|
46 | 9 => 'Conductor',
|
---|
47 | 10 => 'Band',
|
---|
48 | 11 => 'Composer',
|
---|
49 | 12 => 'Lyricist',
|
---|
50 | 13 => 'Recording Studio or Location',
|
---|
51 | 14 => 'Recording Session',
|
---|
52 | 15 => 'Performance',
|
---|
53 | 16 => 'Capture from Movie or Video',
|
---|
54 | 17 => 'Bright(ly) Colored Fish',
|
---|
55 | 18 => 'Illustration',
|
---|
56 | 19 => 'Band Logo',
|
---|
57 | 20 => 'Publisher Logo',
|
---|
58 | );
|
---|
59 |
|
---|
60 | my %dateTimeConv = (
|
---|
61 | ValueConv => 'require Image::ExifTool::XMP; Image::ExifTool::XMP::ConvertXMPDate($val)',
|
---|
62 | PrintConv => '$self->ConvertDateTime($val)',
|
---|
63 | );
|
---|
64 |
|
---|
65 | # This table is just for documentation purposes
|
---|
66 | %Image::ExifTool::ID3::Main = (
|
---|
67 | VARS => { NO_ID => 1 },
|
---|
68 | NOTES => q{
|
---|
69 | ExifTool extracts ID3 information from MP3, MPEG, AIFF, OGG, FLAC, APE and
|
---|
70 | RealAudio files. ID3v2 tags which support multiple languages (ie. Comment
|
---|
71 | and Lyrics) are extracted by specifying the tag name, followed by a dash
|
---|
72 | ('-'), then a 3-character
|
---|
73 | ISO 639-2
|
---|
74 | language code (ie. "Comment-spa"). See L<http://www.id3.org/> for the
|
---|
75 | official ID3 specification and
|
---|
76 | L<http://www.loc.gov/standards/iso639-2/php/code_list.php> for a list of ISO
|
---|
77 | 639-2 language codes.
|
---|
78 | },
|
---|
79 | ID3v1 => {
|
---|
80 | Name => 'ID3v1',
|
---|
81 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::v1' },
|
---|
82 | },
|
---|
83 | ID3v1Enh => {
|
---|
84 | Name => 'ID3v1_Enh',
|
---|
85 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::v1_Enh' },
|
---|
86 | },
|
---|
87 | ID3v22 => {
|
---|
88 | Name => 'ID3v2_2',
|
---|
89 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_2' },
|
---|
90 | },
|
---|
91 | ID3v23 => {
|
---|
92 | Name => 'ID3v2_3',
|
---|
93 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_3' },
|
---|
94 | },
|
---|
95 | ID3v24 => {
|
---|
96 | Name => 'ID3v2_4',
|
---|
97 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_4' },
|
---|
98 | },
|
---|
99 | );
|
---|
100 |
|
---|
101 | # Mapping for ID3v1 Genre numbers
|
---|
102 | my %genre = (
|
---|
103 | 0 => 'Blues',
|
---|
104 | 1 => 'Classic Rock',
|
---|
105 | 2 => 'Country',
|
---|
106 | 3 => 'Dance',
|
---|
107 | 4 => 'Disco',
|
---|
108 | 5 => 'Funk',
|
---|
109 | 6 => 'Grunge',
|
---|
110 | 7 => 'Hip-Hop',
|
---|
111 | 8 => 'Jazz',
|
---|
112 | 9 => 'Metal',
|
---|
113 | 10 => 'New Age',
|
---|
114 | 11 => 'Oldies',
|
---|
115 | 12 => 'Other',
|
---|
116 | 13 => 'Pop',
|
---|
117 | 14 => 'R&B',
|
---|
118 | 15 => 'Rap',
|
---|
119 | 16 => 'Reggae',
|
---|
120 | 17 => 'Rock',
|
---|
121 | 18 => 'Techno',
|
---|
122 | 19 => 'Industrial',
|
---|
123 | 20 => 'Alternative',
|
---|
124 | 21 => 'Ska',
|
---|
125 | 22 => 'Death Metal',
|
---|
126 | 23 => 'Pranks',
|
---|
127 | 24 => 'Soundtrack',
|
---|
128 | 25 => 'Euro-Techno',
|
---|
129 | 26 => 'Ambient',
|
---|
130 | 27 => 'Trip-Hop',
|
---|
131 | 28 => 'Vocal',
|
---|
132 | 29 => 'Jazz+Funk',
|
---|
133 | 30 => 'Fusion',
|
---|
134 | 31 => 'Trance',
|
---|
135 | 32 => 'Classical',
|
---|
136 | 33 => 'Instrumental',
|
---|
137 | 34 => 'Acid',
|
---|
138 | 35 => 'House',
|
---|
139 | 36 => 'Game',
|
---|
140 | 37 => 'Sound Clip',
|
---|
141 | 38 => 'Gospel',
|
---|
142 | 39 => 'Noise',
|
---|
143 | 40 => 'AlternRock',
|
---|
144 | 41 => 'Bass',
|
---|
145 | 42 => 'Soul',
|
---|
146 | 43 => 'Punk',
|
---|
147 | 44 => 'Space',
|
---|
148 | 45 => 'Meditative',
|
---|
149 | 46 => 'Instrumental Pop',
|
---|
150 | 47 => 'Instrumental Rock',
|
---|
151 | 48 => 'Ethnic',
|
---|
152 | 49 => 'Gothic',
|
---|
153 | 50 => 'Darkwave',
|
---|
154 | 51 => 'Techno-Industrial',
|
---|
155 | 52 => 'Electronic',
|
---|
156 | 53 => 'Pop-Folk',
|
---|
157 | 54 => 'Eurodance',
|
---|
158 | 55 => 'Dream',
|
---|
159 | 56 => 'Southern Rock',
|
---|
160 | 57 => 'Comedy',
|
---|
161 | 58 => 'Cult',
|
---|
162 | 59 => 'Gangsta',
|
---|
163 | 60 => 'Top 40',
|
---|
164 | 61 => 'Christian Rap',
|
---|
165 | 62 => 'Pop/Funk',
|
---|
166 | 63 => 'Jungle',
|
---|
167 | 64 => 'Native American',
|
---|
168 | 65 => 'Cabaret',
|
---|
169 | 66 => 'New Wave',
|
---|
170 | 67 => 'Psychadelic',
|
---|
171 | 68 => 'Rave',
|
---|
172 | 69 => 'Showtunes',
|
---|
173 | 70 => 'Trailer',
|
---|
174 | 71 => 'Lo-Fi',
|
---|
175 | 72 => 'Tribal',
|
---|
176 | 73 => 'Acid Punk',
|
---|
177 | 74 => 'Acid Jazz',
|
---|
178 | 75 => 'Polka',
|
---|
179 | 76 => 'Retro',
|
---|
180 | 77 => 'Musical',
|
---|
181 | 78 => 'Rock & Roll',
|
---|
182 | 79 => 'Hard Rock',
|
---|
183 | # The following genres are Winamp extensions
|
---|
184 | 80 => 'Folk',
|
---|
185 | 81 => 'Folk-Rock',
|
---|
186 | 82 => 'National Folk',
|
---|
187 | 83 => 'Swing',
|
---|
188 | 84 => 'Fast Fusion',
|
---|
189 | 85 => 'Bebob',
|
---|
190 | 86 => 'Latin',
|
---|
191 | 87 => 'Revival',
|
---|
192 | 88 => 'Celtic',
|
---|
193 | 89 => 'Bluegrass',
|
---|
194 | 90 => 'Avantgarde',
|
---|
195 | 91 => 'Gothic Rock',
|
---|
196 | 92 => 'Progressive Rock',
|
---|
197 | 93 => 'Psychedelic Rock',
|
---|
198 | 94 => 'Symphonic Rock',
|
---|
199 | 95 => 'Slow Rock',
|
---|
200 | 96 => 'Big Band',
|
---|
201 | 97 => 'Chorus',
|
---|
202 | 98 => 'Easy Listening',
|
---|
203 | 99 => 'Acoustic',
|
---|
204 | 100 => 'Humour',
|
---|
205 | 101 => 'Speech',
|
---|
206 | 102 => 'Chanson',
|
---|
207 | 103 => 'Opera',
|
---|
208 | 104 => 'Chamber Music',
|
---|
209 | 105 => 'Sonata',
|
---|
210 | 106 => 'Symphony',
|
---|
211 | 107 => 'Booty Bass',
|
---|
212 | 108 => 'Primus',
|
---|
213 | 109 => 'Porn Groove',
|
---|
214 | 110 => 'Satire',
|
---|
215 | 111 => 'Slow Jam',
|
---|
216 | 112 => 'Club',
|
---|
217 | 113 => 'Tango',
|
---|
218 | 114 => 'Samba',
|
---|
219 | 115 => 'Folklore',
|
---|
220 | 116 => 'Ballad',
|
---|
221 | 117 => 'Power Ballad',
|
---|
222 | 118 => 'Rhythmic Soul',
|
---|
223 | 119 => 'Freestyle',
|
---|
224 | 120 => 'Duet',
|
---|
225 | 121 => 'Punk Rock',
|
---|
226 | 122 => 'Drum Solo',
|
---|
227 | 123 => 'Acapella',
|
---|
228 | 124 => 'Euro-House',
|
---|
229 | 125 => 'Dance Hall',
|
---|
230 | # ref http://yar.hole.ru/MP3Tech/lamedoc/id3.html
|
---|
231 | 126 => 'Goa',
|
---|
232 | 127 => 'Drum & Bass',
|
---|
233 | 128 => 'Club-House',
|
---|
234 | 129 => 'Hardcore',
|
---|
235 | 130 => 'Terror',
|
---|
236 | 131 => 'Indie',
|
---|
237 | 132 => 'BritPop',
|
---|
238 | 133 => 'Negerpunk',
|
---|
239 | 134 => 'Polsk Punk',
|
---|
240 | 135 => 'Beat',
|
---|
241 | 136 => 'Christian Gangsta',
|
---|
242 | 137 => 'Heavy Metal',
|
---|
243 | 138 => 'Black Metal',
|
---|
244 | 139 => 'Crossover',
|
---|
245 | 140 => 'Contemporary C',
|
---|
246 | 141 => 'Christian Rock',
|
---|
247 | 142 => 'Merengue',
|
---|
248 | 143 => 'Salsa',
|
---|
249 | 144 => 'Thrash Metal',
|
---|
250 | 145 => 'Anime',
|
---|
251 | 146 => 'JPop',
|
---|
252 | 147 => 'SynthPop',
|
---|
253 | 255 => 'None',
|
---|
254 | # ID3v2 adds some text short forms...
|
---|
255 | CR => 'Cover',
|
---|
256 | RX => 'Remix',
|
---|
257 | );
|
---|
258 |
|
---|
259 | # Tags for ID3v1
|
---|
260 | %Image::ExifTool::ID3::v1 = (
|
---|
261 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
262 | GROUPS => { 1 => 'ID3v1', 2 => 'Audio' },
|
---|
263 | PRIORITY => 0, # let ID3v2 tags replace these if they come later
|
---|
264 | 3 => {
|
---|
265 | Name => 'Title',
|
---|
266 | Format => 'string[30]',
|
---|
267 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
268 | },
|
---|
269 | 33 => {
|
---|
270 | Name => 'Artist',
|
---|
271 | Groups => { 2 => 'Author' },
|
---|
272 | Format => 'string[30]',
|
---|
273 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
274 | },
|
---|
275 | 63 => {
|
---|
276 | Name => 'Album',
|
---|
277 | Format => 'string[30]',
|
---|
278 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
279 | },
|
---|
280 | 93 => {
|
---|
281 | Name => 'Year',
|
---|
282 | Groups => { 2 => 'Time' },
|
---|
283 | Format => 'string[4]',
|
---|
284 | },
|
---|
285 | 97 => {
|
---|
286 | Name => 'Comment',
|
---|
287 | Format => 'string[30]',
|
---|
288 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
289 | },
|
---|
290 | 125 => { # ID3v1.1 (ref http://en.wikipedia.org/wiki/ID3#Layout)
|
---|
291 | Name => 'Track',
|
---|
292 | Format => 'int8u[2]',
|
---|
293 | Notes => 'v1.1 addition -- last 2 bytes of v1.0 Comment field',
|
---|
294 | RawConv => '($val =~ s/^0 // and $val) ? $val : undef',
|
---|
295 | },
|
---|
296 | 127 => {
|
---|
297 | Name => 'Genre',
|
---|
298 | Notes => 'CR and RX are ID3v2 only',
|
---|
299 | Format => 'int8u',
|
---|
300 | PrintConv => \%genre,
|
---|
301 | PrintConvColumns => 3,
|
---|
302 | },
|
---|
303 | );
|
---|
304 |
|
---|
305 | # ID3v1 "Enhanced TAG" information (ref 3)
|
---|
306 | %Image::ExifTool::ID3::v1_Enh = (
|
---|
307 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
308 | GROUPS => { 1 => 'ID3v1_Enh', 2 => 'Audio' },
|
---|
309 | NOTES => 'ID3 version 1 "Enhanced TAG" information (not part of the official spec).',
|
---|
310 | PRIORITY => 0, # let ID3v2 tags replace these if they come later
|
---|
311 | 4 => {
|
---|
312 | Name => 'Title2',
|
---|
313 | Format => 'string[60]',
|
---|
314 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
315 | },
|
---|
316 | 64 => {
|
---|
317 | Name => 'Artist2',
|
---|
318 | Groups => { 2 => 'Author' },
|
---|
319 | Format => 'string[60]',
|
---|
320 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
321 | },
|
---|
322 | 124 => {
|
---|
323 | Name => 'Album2',
|
---|
324 | Format => 'string[60]',
|
---|
325 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
326 | },
|
---|
327 | 184 => {
|
---|
328 | Name => 'Speed',
|
---|
329 | Format => 'int8u',
|
---|
330 | PrintConv => {
|
---|
331 | 1 => 'Slow',
|
---|
332 | 2 => 'Medium',
|
---|
333 | 3 => 'Fast',
|
---|
334 | 4 => 'Hardcore',
|
---|
335 | },
|
---|
336 | },
|
---|
337 | 185 => {
|
---|
338 | Name => 'Genre',
|
---|
339 | Format => 'string[30]',
|
---|
340 | ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
|
---|
341 | },
|
---|
342 | 215 => {
|
---|
343 | Name => 'StartTime',
|
---|
344 | Format => 'string[6]',
|
---|
345 | },
|
---|
346 | 221 => {
|
---|
347 | Name => 'EndTime',
|
---|
348 | Format => 'string[6]',
|
---|
349 | },
|
---|
350 | );
|
---|
351 |
|
---|
352 | # Tags for ID2v2.2
|
---|
353 | %Image::ExifTool::ID3::v2_2 = (
|
---|
354 | PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
|
---|
355 | GROUPS => { 1 => 'ID3v2_2', 2 => 'Audio' },
|
---|
356 | NOTES => q{
|
---|
357 | ExifTool extracts mainly text-based tags from ID3v2 information. The tags
|
---|
358 | in the tables below are those extracted by ExifTool, and don't represent a
|
---|
359 | complete list of available ID3v2 tags.
|
---|
360 |
|
---|
361 | ID3 version 2.2 tags. (These are the tags written by iTunes 5.0.)
|
---|
362 | },
|
---|
363 | CNT => 'PlayCounter',
|
---|
364 | COM => 'Comment',
|
---|
365 | IPL => 'InvolvedPeople',
|
---|
366 | PIC => {
|
---|
367 | Name => 'Picture',
|
---|
368 | Groups => { 2 => 'Image' },
|
---|
369 | Binary => 1,
|
---|
370 | Notes => 'the 3 tags below are also extracted from this PIC frame',
|
---|
371 | },
|
---|
372 | 'PIC-1' => { Name => 'PictureFormat', Groups => { 2 => 'Image' } },
|
---|
373 | 'PIC-2' => {
|
---|
374 | Name => 'PictureType',
|
---|
375 | Groups => { 2 => 'Image' },
|
---|
376 | PrintConv => \%pictureType,
|
---|
377 | SeparateTable => 1,
|
---|
378 | },
|
---|
379 | 'PIC-3' => { Name => 'PictureDescription', Groups => { 2 => 'Image' } },
|
---|
380 | # POP => 'Popularimeter',
|
---|
381 | SLT => {
|
---|
382 | Name => 'SynchronizedLyricText',
|
---|
383 | Binary => 1,
|
---|
384 | },
|
---|
385 | TAL => 'Album',
|
---|
386 | TBP => 'BeatsPerMinute',
|
---|
387 | TCM => 'Composer',
|
---|
388 | TCO =>{
|
---|
389 | Name => 'Genre',
|
---|
390 | Notes => 'uses same lookup table as ID3v1 Genre',
|
---|
391 | PrintConv => 'Image::ExifTool::ID3::PrintGenre($val)',
|
---|
392 | },
|
---|
393 | TCP => 'Compilation', # not part of spec, but used by iTunes
|
---|
394 | TCR => { Name => 'Copyright', Groups => { 2 => 'Author' } },
|
---|
395 | TDA => { Name => 'Date', Groups => { 2 => 'Time' } },
|
---|
396 | TDY => 'PlaylistDelay',
|
---|
397 | TEN => 'EncodedBy',
|
---|
398 | TFT => 'FileType',
|
---|
399 | TIM => { Name => 'Time', Groups => { 2 => 'Time' } },
|
---|
400 | TKE => 'InitialKey',
|
---|
401 | TLA => 'Language',
|
---|
402 | TLE => 'Length',
|
---|
403 | TMT => 'Media',
|
---|
404 | TOA => { Name => 'OriginalArtist', Groups => { 2 => 'Author' } },
|
---|
405 | TOF => 'OriginalFilename',
|
---|
406 | TOL => 'OriginalLyricist',
|
---|
407 | TOR => 'OriginalReleaseYear',
|
---|
408 | TOT => 'OriginalAlbum',
|
---|
409 | TP1 => { Name => 'Artist', Groups => { 2 => 'Author' } },
|
---|
410 | TP2 => 'Band',
|
---|
411 | TP3 => 'Conductor',
|
---|
412 | TP4 => 'InterpretedBy',
|
---|
413 | TPA => 'PartOfSet',
|
---|
414 | TPB => 'Publisher',
|
---|
415 | TRC => 'ISRC', # (international standard recording code)
|
---|
416 | TRD => 'RecordingDates',
|
---|
417 | TRK => 'Track',
|
---|
418 | TSI => 'Size',
|
---|
419 | TSS => 'EncoderSettings',
|
---|
420 | TT1 => 'Grouping',
|
---|
421 | TT2 => 'Title',
|
---|
422 | TT3 => 'Subtitle',
|
---|
423 | TXT => 'Lyricist',
|
---|
424 | TXX => 'UserDefinedText',
|
---|
425 | TYE => { Name => 'Year', Groups => { 2 => 'Time' } },
|
---|
426 | ULT => 'Lyrics',
|
---|
427 | WAF => 'FileURL',
|
---|
428 | WAR => { Name => 'ArtistURL', Groups => { 2 => 'Author' } },
|
---|
429 | WAS => 'SourceURL',
|
---|
430 | WCM => 'CommercialURL',
|
---|
431 | WCP => { Name => 'CopyrightURL', Groups => { 2 => 'Author' } },
|
---|
432 | WPB => 'PublisherURL',
|
---|
433 | WXX => 'UserDefinedURL',
|
---|
434 | );
|
---|
435 |
|
---|
436 | # tags common to ID3v2.3 and ID3v2.4
|
---|
437 | my %id3v2_common = (
|
---|
438 | # AENC => 'AudioEncryption', # Owner, preview start, preview length, encr data
|
---|
439 | APIC => {
|
---|
440 | Name => 'Picture',
|
---|
441 | Groups => { 2 => 'Image' },
|
---|
442 | Binary => 1,
|
---|
443 | Notes => 'the 3 tags below are also extracted from this PIC frame',
|
---|
444 | },
|
---|
445 | 'APIC-1' => { Name => 'PictureMimeType', Groups => { 2 => 'Image' } },
|
---|
446 | 'APIC-2' => {
|
---|
447 | Name => 'PictureType',
|
---|
448 | Groups => { 2 => 'Image' },
|
---|
449 | PrintConv => \%pictureType,
|
---|
450 | SeparateTable => 1,
|
---|
451 | },
|
---|
452 | 'APIC-3' => { Name => 'PictureDescription', Groups => { 2 => 'Image' } },
|
---|
453 | COMM => 'Comment',
|
---|
454 | # COMR => 'Commercial',
|
---|
455 | # ENCR => 'EncryptionMethod',
|
---|
456 | # ETCO => 'EventTimingCodes',
|
---|
457 | # GEOB => 'GeneralEncapsulatedObject',
|
---|
458 | # GRID => 'GroupIdentification',
|
---|
459 | # LINK => 'LinkedInformation',
|
---|
460 | MCDI => { Name => 'MusicCDIdentifier', Binary => 1 },
|
---|
461 | # MLLT => 'MPEGLocationLookupTable',
|
---|
462 | # OWNE => 'Ownership', # enc(1), _price, 00, _date(8), Seller
|
---|
463 | PCNT => 'PlayCounter',
|
---|
464 | # POPM => 'Popularimeter', # _email, 00, rating(1), counter(4-N)
|
---|
465 | # POSS => 'PostSynchronization',
|
---|
466 | PRIV => {
|
---|
467 | Name => 'Private',
|
---|
468 | SubDirectory => { TagTable => 'Image::ExifTool::ID3::Private' },
|
---|
469 | },
|
---|
470 | # RBUF => 'RecommendedBufferSize',
|
---|
471 | # RVRB => 'Reverb',
|
---|
472 | SYLT => {
|
---|
473 | Name => 'SynchronizedLyricText',
|
---|
474 | Binary => 1,
|
---|
475 | },
|
---|
476 | # SYTC => 'SynchronizedTempoCodes',
|
---|
477 | TALB => 'Album',
|
---|
478 | TBPM => 'BeatsPerMinute',
|
---|
479 | TCMP => { Name => 'Compilation', PrintConv => { 0 => 'No', 1 => 'Yes' } }, #PH (iTunes)
|
---|
480 | TCOM => 'Composer',
|
---|
481 | TCON =>{
|
---|
482 | Name => 'Genre',
|
---|
483 | Notes => 'uses same lookup table as ID3v1 Genre',
|
---|
484 | PrintConv => 'Image::ExifTool::ID3::PrintGenre($val)',
|
---|
485 | },
|
---|
486 | TCOP => { Name => 'Copyright', Groups => { 2 => 'Author' } },
|
---|
487 | TDLY => 'PlaylistDelay',
|
---|
488 | TENC => 'EncodedBy',
|
---|
489 | TEXT => 'Lyricist',
|
---|
490 | TFLT => 'FileType',
|
---|
491 | TIT1 => 'Grouping',
|
---|
492 | TIT2 => 'Title',
|
---|
493 | TIT3 => 'Subtitle',
|
---|
494 | TKEY => 'InitialKey',
|
---|
495 | TLAN => 'Language',
|
---|
496 | TLEN => {
|
---|
497 | Name => 'Length',
|
---|
498 | ValueConv => '$val / 1000',
|
---|
499 | PrintConv => '"$val s"',
|
---|
500 | },
|
---|
501 | TMED => 'Media',
|
---|
502 | TOAL => 'OriginalAlbum',
|
---|
503 | TOFN => 'OriginalFilename',
|
---|
504 | TOLY => 'OriginalLyricist',
|
---|
505 | TOPE => { Name => 'OriginalArtist', Groups => { 2 => 'Author' } },
|
---|
506 | TOWN => 'FileOwner',
|
---|
507 | TPE1 => { Name => 'Artist', Groups => { 2 => 'Author' } },
|
---|
508 | TPE2 => 'Band',
|
---|
509 | TPE3 => 'Conductor',
|
---|
510 | TPE4 => 'InterpretedBy',
|
---|
511 | TPOS => 'PartOfSet',
|
---|
512 | TPUB => 'Publisher',
|
---|
513 | TRCK => 'Track',
|
---|
514 | TRSN => 'InternetRadioStationName',
|
---|
515 | TRSO => 'InternetRadioStationOwner',
|
---|
516 | TSRC => 'ISRC', # (international standard recording code)
|
---|
517 | TSSE => 'EncoderSettings',
|
---|
518 | TXXX => 'UserDefinedText',
|
---|
519 | # UFID => 'UniqueFileID',
|
---|
520 | USER => 'TermsOfUse',
|
---|
521 | USLT => 'Lyrics',
|
---|
522 | WCOM => 'CommercialURL',
|
---|
523 | WCOP => 'CopyrightURL',
|
---|
524 | WOAF => 'FileURL',
|
---|
525 | WOAR => { Name => 'ArtistURL', Groups => { 2 => 'Author' } },
|
---|
526 | WOAS => 'SourceURL',
|
---|
527 | WORS => 'InternetRadioStationURL',
|
---|
528 | WPAY => 'PaymentURL',
|
---|
529 | WPUB => 'PublisherURL',
|
---|
530 | WXXX => 'UserDefinedURL',
|
---|
531 | );
|
---|
532 |
|
---|
533 | # Tags for ID3v2.3 (http://www.id3.org/id3v2.3.0)
|
---|
534 | %Image::ExifTool::ID3::v2_3 = (
|
---|
535 | PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
|
---|
536 | GROUPS => { 1 => 'ID3v2_3', 2 => 'Audio' },
|
---|
537 | NOTES => 'ID3 version 2.3 tags',
|
---|
538 | %id3v2_common, # include common tags
|
---|
539 | # EQUA => 'Equalization',
|
---|
540 | IPLS => 'InvolvedPeople',
|
---|
541 | # RVAD => 'RelativeVolumeAdjustment',
|
---|
542 | TDAT => { Name => 'Date', Groups => { 2 => 'Time' } },
|
---|
543 | TIME => { Name => 'Time', Groups => { 2 => 'Time' } },
|
---|
544 | TORY => 'OriginalReleaseYear',
|
---|
545 | TRDA => 'RecordingDates',
|
---|
546 | TSIZ => 'Size',
|
---|
547 | TYER => { Name => 'Year', Groups => { 2 => 'Time' } },
|
---|
548 | );
|
---|
549 |
|
---|
550 | # Tags for ID3v2.4 (http://www.id3.org/id3v2.4.0-frames)
|
---|
551 | %Image::ExifTool::ID3::v2_4 = (
|
---|
552 | PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
|
---|
553 | GROUPS => { 1 => 'ID3v2_4', 2 => 'Audio' },
|
---|
554 | NOTES => 'ID3 version 2.4 tags',
|
---|
555 | %id3v2_common, # include common tags
|
---|
556 | # EQU2 => 'Equalization',
|
---|
557 | # RVA2 => 'RelativeVolumeAdjustment',
|
---|
558 | # SEEK => 'Seek',
|
---|
559 | # SIGN => 'Signature',
|
---|
560 | TDEN => { Name => 'EncodingTime', Groups => { 2 => 'Time' }, %dateTimeConv },
|
---|
561 | TDOR => { Name => 'OriginalReleaseTime',Groups => { 2 => 'Time' }, %dateTimeConv },
|
---|
562 | TDRC => { Name => 'RecordingTime', Groups => { 2 => 'Time' }, %dateTimeConv },
|
---|
563 | TDRL => { Name => 'ReleaseTime', Groups => { 2 => 'Time' }, %dateTimeConv },
|
---|
564 | TDTG => { Name => 'TaggingTime', Groups => { 2 => 'Time' }, %dateTimeConv },
|
---|
565 | TIPL => 'InvolvedPeople',
|
---|
566 | TMCL => 'MusicianCredits',
|
---|
567 | TMOO => 'Mood',
|
---|
568 | TPRO => 'ProducedNotice',
|
---|
569 | TSOA => 'AlbumSortOrder',
|
---|
570 | TSOP => 'PerformerSortOrder',
|
---|
571 | TSOT => 'TitleSortOrder',
|
---|
572 | TSST => 'SetSubtitle',
|
---|
573 | );
|
---|
574 |
|
---|
575 | # ID3 PRIV tags (ref PH)
|
---|
576 | %Image::ExifTool::ID3::Private = (
|
---|
577 | PROCESS_PROC => \&Image::ExifTool::ID3::ProcessPrivate,
|
---|
578 | GROUPS => { 1 => 'ID3', 2 => 'Audio' },
|
---|
579 | NOTES => 'ID3 private (PRIV) tags.',
|
---|
580 | XMP => {
|
---|
581 | SubDirectory => {
|
---|
582 | DirName => 'XMP',
|
---|
583 | TagTable => 'Image::ExifTool::XMP::Main',
|
---|
584 | },
|
---|
585 | },
|
---|
586 | PeakValue => {
|
---|
587 | ValueConv => 'length($val)==4 ? unpack("V",$val) : \$val',
|
---|
588 | },
|
---|
589 | AverageLevel => {
|
---|
590 | ValueConv => 'length($val)==4 ? unpack("V",$val) : \$val',
|
---|
591 | },
|
---|
592 | );
|
---|
593 |
|
---|
594 | # ID3 Composite tags
|
---|
595 | %Image::ExifTool::ID3::Composite = (
|
---|
596 | GROUPS => { 2 => 'Image' },
|
---|
597 | DateTimeOriginal => {
|
---|
598 | Description => 'Date/Time Original',
|
---|
599 | Groups => { 2 => 'Time' },
|
---|
600 | Priority => 0,
|
---|
601 | Desire => {
|
---|
602 | 0 => 'ID3:RecordingTime',
|
---|
603 | 1 => 'ID3:Year',
|
---|
604 | 2 => 'ID3:Date',
|
---|
605 | 3 => 'ID3:Time',
|
---|
606 | },
|
---|
607 | ValueConv => q{
|
---|
608 | return $val[0] if $val[0];
|
---|
609 | return undef unless $val[1];
|
---|
610 | return $val[1] unless $val[2] and $val[2] =~ /^(\d{2})(\d{2})$/;
|
---|
611 | $val[1] .= ":$1:$2";
|
---|
612 | return $val[1] unless $val[3] and $val[3] =~ /^(\d{2})(\d{2})$/;
|
---|
613 | return "$val[1] $1:$2";
|
---|
614 | },
|
---|
615 | PrintConv => '$self->ConvertDateTime($val)',
|
---|
616 | },
|
---|
617 | );
|
---|
618 |
|
---|
619 | # add our composite tags
|
---|
620 | Image::ExifTool::AddCompositeTags('Image::ExifTool::ID3');
|
---|
621 |
|
---|
622 | # can't share tagInfo hashes between two tables, so we must make
|
---|
623 | # copies of the necessary hashes
|
---|
624 | {
|
---|
625 | my $tag;
|
---|
626 | foreach $tag (keys %id3v2_common) {
|
---|
627 | next unless ref $id3v2_common{$tag} eq 'HASH';
|
---|
628 | my %tagInfo = %{$id3v2_common{$tag}};
|
---|
629 | # must also copy Groups hash if it exists
|
---|
630 | my $groups = $tagInfo{Groups};
|
---|
631 | $tagInfo{Groups} = { %$groups } if $groups;
|
---|
632 | $Image::ExifTool::ID3::v2_4{$tag} = \%tagInfo;
|
---|
633 | }
|
---|
634 | }
|
---|
635 |
|
---|
636 | #------------------------------------------------------------------------------
|
---|
637 | # Convert ID3v1 text to exiftool character set
|
---|
638 | # Inputs: 0) ExifTool object ref, 1) text string
|
---|
639 | # Returns: converted text
|
---|
640 | sub ConvertID3v1Text($$)
|
---|
641 | {
|
---|
642 | my ($exifTool, $val) = @_;
|
---|
643 | return $exifTool->Decode($val, $exifTool->Options('CharsetID3'));
|
---|
644 | }
|
---|
645 |
|
---|
646 | #------------------------------------------------------------------------------
|
---|
647 | # Process ID3 PRIV data
|
---|
648 | # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
|
---|
649 | sub ProcessPrivate($$$)
|
---|
650 | {
|
---|
651 | my ($exifTool, $dirInfo, $tagTablePtr) = @_;
|
---|
652 | my $dataPt = $$dirInfo{DataPt};
|
---|
653 | my ($tag, $start);
|
---|
654 | if ($$dataPt =~ /^(.*?)\0/) {
|
---|
655 | $tag = $1;
|
---|
656 | $start = length($tag) + 1;
|
---|
657 | } else {
|
---|
658 | $tag = '';
|
---|
659 | $start = 0;
|
---|
660 | }
|
---|
661 | unless ($$tagTablePtr{$tag}) {
|
---|
662 | $tag =~ tr{/ }{_}d; # translate '/' to '_' and remove spaces
|
---|
663 | $tag = 'private' unless $tag =~ /^[-\w]{1,24}$/;
|
---|
664 | unless ($$tagTablePtr{$tag}) {
|
---|
665 | Image::ExifTool::AddTagToTable($tagTablePtr, $tag,
|
---|
666 | { Name => ucfirst($tag), Binary => 1 });
|
---|
667 | }
|
---|
668 | }
|
---|
669 | my $key = $exifTool->HandleTag($tagTablePtr, $tag, undef,
|
---|
670 | Size => length($$dataPt) - $start,
|
---|
671 | Start => $start,
|
---|
672 | DataPt => $dataPt,
|
---|
673 | );
|
---|
674 | # set group1 name
|
---|
675 | $exifTool->SetGroup($key, $$exifTool{ID3_Ver}) if $key;
|
---|
676 | }
|
---|
677 |
|
---|
678 | #------------------------------------------------------------------------------
|
---|
679 | # Print ID3v2 Genre
|
---|
680 | # Inputs: TCON or TCO frame data
|
---|
681 | # Returns: Content type with decoded genre numbers
|
---|
682 | sub PrintGenre($)
|
---|
683 | {
|
---|
684 | my $val = shift;
|
---|
685 | # make sure that %genre has an entry for all numbers we are interested in
|
---|
686 | # (genre numbers are in brackets for ID3v2.2 and v2.3)
|
---|
687 | while ($val =~ /\((\d+)\)/g) {
|
---|
688 | $genre{$1} or $genre{$1} = "Unknown ($1)";
|
---|
689 | }
|
---|
690 | # (genre numbers are separated by nulls in ID3v2.4,
|
---|
691 | # but nulls are converted to '/' by DecodeString())
|
---|
692 | while ($val =~ /(?:^|\/)(\d+)/g) {
|
---|
693 | $genre{$1} or $genre{$1} = "Unknown ($1)";
|
---|
694 | }
|
---|
695 | $val =~ s/\((\d+)\)/\($genre{$1}\)/g;
|
---|
696 | $val =~ s/(^|\/)(\d+)/$1$genre{$2}/g;
|
---|
697 | $val =~ s/^\(([^)]+)\)\1?$/$1/; # clean up by removing brackets and duplicates
|
---|
698 | return $val;
|
---|
699 | }
|
---|
700 |
|
---|
701 | #------------------------------------------------------------------------------
|
---|
702 | # Decode ID3 string
|
---|
703 | # Inputs: 0) ExifTool object reference
|
---|
704 | # 1) string beginning with encoding byte unless specified as argument
|
---|
705 | # 2) optional encoding (0=ISO-8859-1, 1=UTF-16 BOM, 2=UTF-16BE, 3=UTF-8)
|
---|
706 | # Returns: Decoded string in scalar context, or list of strings in list context
|
---|
707 | sub DecodeString($$;$)
|
---|
708 | {
|
---|
709 | my ($exifTool, $val, $enc) = @_;
|
---|
710 | return '' unless length $val;
|
---|
711 | unless (defined $enc) {
|
---|
712 | $enc = unpack('C', $val);
|
---|
713 | $val = substr($val, 1); # remove encoding byte
|
---|
714 | }
|
---|
715 | my @vals;
|
---|
716 | if ($enc == 0 or $enc == 3) { # ISO 8859-1 or UTF-8
|
---|
717 | $val =~ s/\0+$//; # remove any null padding
|
---|
718 | # (must split before converting because conversion routines truncate at null)
|
---|
719 | @vals = split "\0", $val;
|
---|
720 | foreach $val (@vals) {
|
---|
721 | $val = $exifTool->Decode($val, $enc ? 'UTF8' : 'Latin');
|
---|
722 | }
|
---|
723 | } elsif ($enc == 1 or $enc == 2) { # UTF-16 with BOM, or UTF-16BE
|
---|
724 | my $bom = "\xfe\xff";
|
---|
725 | my %order = ( "\xfe\xff" => 'MM', "\xff\xfe", => 'II' );
|
---|
726 | for (;;) {
|
---|
727 | my $v;
|
---|
728 | # split string at null terminators on word boundaries
|
---|
729 | if ($val =~ s/((..)*?)\0\0//) {
|
---|
730 | $v = $1;
|
---|
731 | } else {
|
---|
732 | last unless length $val > 1;
|
---|
733 | $v = $val;
|
---|
734 | $val = '';
|
---|
735 | }
|
---|
736 | $bom = $1 if $v =~ s/^(\xfe\xff|\xff\xfe)//;
|
---|
737 | push @vals, $exifTool->Decode($v, 'UCS2', $order{$bom});
|
---|
738 | }
|
---|
739 | } else {
|
---|
740 | $val =~ s/\0+$//;
|
---|
741 | return "<Unknown encoding $enc> $val";
|
---|
742 | }
|
---|
743 | return @vals if wantarray;
|
---|
744 | return join('/',@vals);
|
---|
745 | }
|
---|
746 |
|
---|
747 | #------------------------------------------------------------------------------
|
---|
748 | # Convert sync-safe integer to a number we can use
|
---|
749 | # Inputs: 0) int32u sync-safe value
|
---|
750 | # Returns: actual number or undef on invalid value
|
---|
751 | sub UnSyncSafe($)
|
---|
752 | {
|
---|
753 | my $val = shift;
|
---|
754 | return undef if $val & 0x80808080;
|
---|
755 | return ($val & 0x0000007f) |
|
---|
756 | (($val & 0x00007f00) >> 1) |
|
---|
757 | (($val & 0x007f0000) >> 2) |
|
---|
758 | (($val & 0x7f000000) >> 3);
|
---|
759 | }
|
---|
760 |
|
---|
761 | #------------------------------------------------------------------------------
|
---|
762 | # Process ID3v2 information
|
---|
763 | # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
|
---|
764 | sub ProcessID3v2($$$)
|
---|
765 | {
|
---|
766 | my ($exifTool, $dirInfo, $tagTablePtr) = @_;
|
---|
767 | my $dataPt = $$dirInfo{DataPt};
|
---|
768 | my $offset = $$dirInfo{DirStart};
|
---|
769 | my $size = $$dirInfo{DirLen};
|
---|
770 | my $vers = $$dirInfo{Version};
|
---|
771 | my $verbose = $exifTool->Options('Verbose');
|
---|
772 | my $len; # frame data length
|
---|
773 |
|
---|
774 | $verbose and $exifTool->VerboseDir($tagTablePtr->{GROUPS}->{1}, 0, $size);
|
---|
775 |
|
---|
776 | for (;;$offset+=$len) {
|
---|
777 | my ($id, $flags, $hi);
|
---|
778 | if ($vers < 0x0300) {
|
---|
779 | # version 2.2 frame header is 6 bytes
|
---|
780 | last if $offset + 6 > $size;
|
---|
781 | ($id, $hi, $len) = unpack("x${offset}a3Cn",$$dataPt);
|
---|
782 | last if $id eq "\0\0\0";
|
---|
783 | $len += $hi << 16;
|
---|
784 | $offset += 6;
|
---|
785 | } else {
|
---|
786 | # version 2.3/2.4 frame header is 10 bytes
|
---|
787 | last if $offset + 10 > $size;
|
---|
788 | ($id, $len, $flags) = unpack("x${offset}a4Nn",$$dataPt);
|
---|
789 | last if $id eq "\0\0\0\0";
|
---|
790 | $offset += 10;
|
---|
791 | # length is a "sync-safe" integer by the ID3v2.4 specification, but
|
---|
792 | # reportedly some versions of iTunes write this as a normal integer
|
---|
793 | # (ref http://www.id3.org/iTunes)
|
---|
794 | while ($vers >= 0x0400 and $len > 0x7f and not $len & 0x80808080) {
|
---|
795 | my $oldLen = $len;
|
---|
796 | $len = UnSyncSafe($len);
|
---|
797 | if (not defined $len or $offset + $len + 10 > $size) {
|
---|
798 | $exifTool->Warn('Invalid ID3 frame size');
|
---|
799 | last;
|
---|
800 | }
|
---|
801 | # check next ID to see if it makes sense
|
---|
802 | my $nextID = substr($$dataPt, $offset + $len, 4);
|
---|
803 | last if $$tagTablePtr{$nextID};
|
---|
804 | # try again with the incorrect length word (patch for iTunes bug)
|
---|
805 | last if $offset + $oldLen + 10 > $size;
|
---|
806 | $nextID = substr($$dataPt, $offset + $len, 4);
|
---|
807 | $len = $oldLen if $$tagTablePtr{$nextID};
|
---|
808 | last; # yes, "while" was really a "goto" in disguise
|
---|
809 | }
|
---|
810 | }
|
---|
811 | last if $offset + $len > $size;
|
---|
812 | my $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $id);
|
---|
813 | unless ($tagInfo) {
|
---|
814 | next unless $verbose or $exifTool->Options('Unknown');
|
---|
815 | $id =~ tr/-A-Za-z0-9_//dc;
|
---|
816 | $id = 'unknown' unless length $id;
|
---|
817 | unless ($$tagTablePtr{$id}) {
|
---|
818 | $tagInfo = { Name => "ID3_$id", Binary => 1 };
|
---|
819 | Image::ExifTool::AddTagToTable($tagTablePtr, $id, $tagInfo);
|
---|
820 | }
|
---|
821 | }
|
---|
822 | # decode v2.3 and v2.4 flags
|
---|
823 | my %flags;
|
---|
824 | if ($flags) {
|
---|
825 | if ($vers < 0x0400) {
|
---|
826 | # version 2.3 flags
|
---|
827 | $flags & 0x80 and $flags{Compress} = 1;
|
---|
828 | $flags & 0x40 and $flags{Encrypt} = 1;
|
---|
829 | $flags & 0x20 and $flags{GroupID} = 1;
|
---|
830 | } else {
|
---|
831 | # version 2.4 flags
|
---|
832 | $flags & 0x40 and $flags{GroupID} = 1;
|
---|
833 | $flags & 0x08 and $flags{Compress} = 1;
|
---|
834 | $flags & 0x04 and $flags{Encrypt} = 1;
|
---|
835 | $flags & 0x02 and $flags{Unsync} = 1;
|
---|
836 | $flags & 0x01 and $flags{DataLen} = 1;
|
---|
837 | }
|
---|
838 | }
|
---|
839 | if ($flags{Encrypt}) {
|
---|
840 | $exifTool->WarnOnce('Encrypted frames currently not supported');
|
---|
841 | next;
|
---|
842 | }
|
---|
843 | # extract the value
|
---|
844 | my $val = substr($$dataPt, $offset, $len);
|
---|
845 |
|
---|
846 | # reverse the unsynchronization
|
---|
847 | $val =~ s/\xff\x00/\xff/g if $flags{Unsync};
|
---|
848 |
|
---|
849 | # read grouping identity
|
---|
850 | if ($flags{GroupID}) {
|
---|
851 | length($val) >= 1 or $exifTool->Warn("Short $id frame"), next;
|
---|
852 | $val = substr($val, 1); # (ignore it)
|
---|
853 | }
|
---|
854 | # read data length
|
---|
855 | my $dataLen;
|
---|
856 | if ($flags{DataLen} or $flags{Compress}) {
|
---|
857 | length($val) >= 4 or $exifTool->Warn("Short $id frame"), next;
|
---|
858 | $dataLen = unpack('N', $val); # save the data length word
|
---|
859 | $val = substr($val, 4);
|
---|
860 | }
|
---|
861 | # uncompress data
|
---|
862 | if ($flags{Compress}) {
|
---|
863 | if (eval 'require Compress::Zlib') {
|
---|
864 | my $inflate = Compress::Zlib::inflateInit();
|
---|
865 | my ($buff, $stat);
|
---|
866 | $inflate and ($buff, $stat) = $inflate->inflate($val);
|
---|
867 | if ($inflate and $stat == Compress::Zlib::Z_STREAM_END()) {
|
---|
868 | $val = $buff;
|
---|
869 | } else {
|
---|
870 | $exifTool->Warn("Error inflating $id frame");
|
---|
871 | next;
|
---|
872 | }
|
---|
873 | } else {
|
---|
874 | $exifTool->WarnOnce('Install Compress::Zlib to decode compressed frames');
|
---|
875 | next;
|
---|
876 | }
|
---|
877 | }
|
---|
878 | # validate data length
|
---|
879 | if (defined $dataLen) {
|
---|
880 | $dataLen = UnSyncSafe($dataLen);
|
---|
881 | defined $dataLen or $exifTool->Warn("Invalid length for $id frame"), next;
|
---|
882 | $dataLen == length($val) or $exifTool->Warn("Wrong length for $id frame"), next;
|
---|
883 | }
|
---|
884 | $verbose and $exifTool->VerboseInfo($id, $tagInfo,
|
---|
885 | Table => $tagTablePtr,
|
---|
886 | Value => $val,
|
---|
887 | DataPt => $dataPt,
|
---|
888 | DataPos => $$dirInfo{DataPos},
|
---|
889 | Size => $len,
|
---|
890 | Start => $offset,
|
---|
891 | );
|
---|
892 | next unless $tagInfo;
|
---|
893 | #
|
---|
894 | # decode data in this frame
|
---|
895 | #
|
---|
896 | my $lang;
|
---|
897 | my $valLen = length($val); # actual value length (after decompression, etc)
|
---|
898 | if ($id =~ /^(TXX|TXXX)$/) {
|
---|
899 | # two encoded strings separated by a null
|
---|
900 | my @vals = DecodeString($exifTool, $val);
|
---|
901 | foreach (0..1) { $vals[$_] = '' unless defined $vals[$_]; }
|
---|
902 | ($val = "($vals[0]) $vals[1]") =~ s/^\(\) //;
|
---|
903 | } elsif ($id =~ /^T/ or $id =~ /^(IPL|IPLS)$/) {
|
---|
904 | $val = DecodeString($exifTool, $val);
|
---|
905 | } elsif ($id =~ /^(WXX|WXXX)$/) {
|
---|
906 | # one encoded string and one Latin string separated by a null
|
---|
907 | my $enc = unpack('C', $val);
|
---|
908 | my $url;
|
---|
909 | if ($enc == 1 or $enc == 2) {
|
---|
910 | ($val, $url) = ($val =~ /^(.(?:..)*?)\0\0(.*)/);
|
---|
911 | } else {
|
---|
912 | ($val, $url) = ($val =~ /^(..*?)\0(.*)/);
|
---|
913 | }
|
---|
914 | unless (defined $val and defined $url) {
|
---|
915 | $exifTool->Warn("Invalid $id frame value");
|
---|
916 | next;
|
---|
917 | }
|
---|
918 | $val = DecodeString($exifTool, $val);
|
---|
919 | $url =~ s/\0.*//;
|
---|
920 | $val = length($val) ? "($val) $url" : $url;
|
---|
921 | } elsif ($id =~ /^W/) {
|
---|
922 | $val =~ s/\0.*//; # truncate at null
|
---|
923 | } elsif ($id =~ /^(COM|COMM|ULT|USLT)$/) {
|
---|
924 | $valLen > 4 or $exifTool->Warn("Short $id frame"), next;
|
---|
925 | $lang = substr($val,1,3);
|
---|
926 | my @vals = DecodeString($exifTool, substr($val,4), Get8u(\$val,0));
|
---|
927 | foreach (0..1) { $vals[$_] = '' unless defined $vals[$_]; }
|
---|
928 | $val = length($vals[0]) ? "($vals[0]) $vals[1]" : $vals[1];
|
---|
929 | } elsif ($id eq 'USER') {
|
---|
930 | $valLen > 4 or $exifTool->Warn('Short USER frame'), next;
|
---|
931 | $lang = substr($val,1,3);
|
---|
932 | $val = DecodeString($exifTool, substr($val,4), Get8u(\$val,0));
|
---|
933 | } elsif ($id =~ /^(CNT|PCNT)$/) {
|
---|
934 | $valLen >= 4 or $exifTool->Warn("Short $id frame"), next;
|
---|
935 | my $cnt = unpack('N', $val);
|
---|
936 | my $i;
|
---|
937 | for ($i=4; $i<$valLen; ++$i) {
|
---|
938 | $cnt = $cnt * 256 + unpack("x${i}C", $val);
|
---|
939 | }
|
---|
940 | $val = $cnt;
|
---|
941 | } elsif ($id =~ /^(PIC|APIC)$/) {
|
---|
942 | $valLen >= 4 or $exifTool->Warn("Short $id frame"), next;
|
---|
943 | my ($hdr, $attr);
|
---|
944 | my $enc = unpack('C', $val);
|
---|
945 | if ($enc == 1 or $enc == 2) {
|
---|
946 | $hdr = ($id eq 'PIC') ? ".(...)(.)((?:..)*?)\0\0" : ".(.*?)\0(.)((?:..)*?)\0\0";
|
---|
947 | } else {
|
---|
948 | $hdr = ($id eq 'PIC') ? ".(...)(.)(.*?)\0" : ".(.*?)\0(.)(.*?)\0";
|
---|
949 | }
|
---|
950 | # remove header (encoding, image format or MIME type, picture type, description)
|
---|
951 | $val =~ s/^$hdr//s or $exifTool->Warn("Invalid $id frame"), next;
|
---|
952 | my @attrs = ($1, ord($2), DecodeString($exifTool, $3, $enc));
|
---|
953 | my $i = 1;
|
---|
954 | foreach $attr (@attrs) {
|
---|
955 | # must store descriptions even if they are empty to maintain
|
---|
956 | # sync between copy numbers when multiple images
|
---|
957 | $exifTool->HandleTag($tagTablePtr, "$id-$i", $attr);
|
---|
958 | ++$i;
|
---|
959 | }
|
---|
960 | } elsif ($id eq 'PRIV') {
|
---|
961 | # save version number to set group1 name for tag later
|
---|
962 | $exifTool->{ID3_Ver} = $tagTablePtr->{GROUPS}->{1};
|
---|
963 | $exifTool->HandleTag($tagTablePtr, $id, $val);
|
---|
964 | next;
|
---|
965 | } elsif (not $$tagInfo{Binary}) {
|
---|
966 | $exifTool->Warn("Don't know how to handle $id frame");
|
---|
967 | next;
|
---|
968 | }
|
---|
969 | if ($lang and $lang =~ /^[a-z]{3}$/i and $lang ne 'eng') {
|
---|
970 | $tagInfo = Image::ExifTool::GetLangInfo($tagInfo, lc $lang);
|
---|
971 | }
|
---|
972 | $exifTool->FoundTag($tagInfo, $val);
|
---|
973 | }
|
---|
974 | }
|
---|
975 |
|
---|
976 | #------------------------------------------------------------------------------
|
---|
977 | # Extract ID3 information from an audio file
|
---|
978 | # Inputs: 0) ExifTool object reference, 1) dirInfo reference
|
---|
979 | # Returns: 1 on success, 0 if this file didn't contain ID3 information
|
---|
980 | # - also processes audio data if any ID3 information was found
|
---|
981 | # - sets ExifTool DoneID3 to 1 when called, or to 2 if an ID3v1 trailer exists
|
---|
982 | sub ProcessID3($$)
|
---|
983 | {
|
---|
984 | my ($exifTool, $dirInfo) = @_;
|
---|
985 |
|
---|
986 | return 0 if $exifTool->{DoneID3}; # avoid infinite recursion
|
---|
987 | $exifTool->{DoneID3} = 1;
|
---|
988 |
|
---|
989 | # allow this to be called with either RAF or DataPt
|
---|
990 | my $raf = $$dirInfo{RAF} || new File::RandomAccess($$dirInfo{DataPt});
|
---|
991 | my ($buff, %id3Header, %id3Trailer, $hBuff, $tBuff, $eBuff, $tagTablePtr);
|
---|
992 | my $rtnVal = 0;
|
---|
993 | my $hdrEnd = 0;
|
---|
994 | my $id3Len = 0;
|
---|
995 |
|
---|
996 | # read first 3 bytes of file
|
---|
997 | $raf->Seek(0, 0);
|
---|
998 | return 0 unless $raf->Read($buff, 3) == 3;
|
---|
999 | #
|
---|
1000 | # identify ID3v2 header
|
---|
1001 | #
|
---|
1002 | while ($buff =~ /^ID3/) {
|
---|
1003 | $rtnVal = 1;
|
---|
1004 | $raf->Read($hBuff, 7) == 7 or $exifTool->Warn('Short ID3 header'), last;
|
---|
1005 | my ($vers, $flags, $size) = unpack('nCN', $hBuff);
|
---|
1006 | $size = UnSyncSafe($size);
|
---|
1007 | defined $size or $exifTool->Warn('Invalid ID3 header'), last;
|
---|
1008 | my $verStr = sprintf("2.%d.%d", $vers >> 8, $vers & 0xff);
|
---|
1009 | if ($vers >= 0x0500) {
|
---|
1010 | $exifTool->Warn("Unsupported ID3 version: $verStr");
|
---|
1011 | last;
|
---|
1012 | }
|
---|
1013 | unless ($raf->Read($hBuff, $size) == $size) {
|
---|
1014 | $exifTool->Warn('Truncated ID3 data');
|
---|
1015 | last;
|
---|
1016 | }
|
---|
1017 | # this flag only indicates use of unsynchronized frames in ID3v2.4
|
---|
1018 | if ($flags & 0x80 and $vers < 0x0400) {
|
---|
1019 | # reverse the unsynchronization
|
---|
1020 | $hBuff =~ s/\xff\x00/\xff/g;
|
---|
1021 | }
|
---|
1022 | my $pos = 10;
|
---|
1023 | if ($flags & 0x40) {
|
---|
1024 | # skip the extended header
|
---|
1025 | $size >= 4 or $exifTool->Warn('Bad ID3 extended header'), last;
|
---|
1026 | my $len = unpack('N', $hBuff);
|
---|
1027 | if ($len > length($hBuff) - 4) {
|
---|
1028 | $exifTool->Warn('Truncated ID3 extended header');
|
---|
1029 | last;
|
---|
1030 | }
|
---|
1031 | $hBuff = substr($hBuff, $len + 4);
|
---|
1032 | $pos += $len + 4;
|
---|
1033 | }
|
---|
1034 | if ($flags & 0x10) {
|
---|
1035 | # ignore v2.4 footer (10 bytes long)
|
---|
1036 | $raf->Seek(10, 1);
|
---|
1037 | }
|
---|
1038 | %id3Header = (
|
---|
1039 | DataPt => \$hBuff,
|
---|
1040 | DataPos => $pos,
|
---|
1041 | DirStart => 0,
|
---|
1042 | DirLen => length($hBuff),
|
---|
1043 | Version => $vers,
|
---|
1044 | DirName => "ID3v$verStr",
|
---|
1045 | );
|
---|
1046 | $id3Len += length($hBuff) + 10;
|
---|
1047 | if ($vers >= 0x0400) {
|
---|
1048 | $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_4');
|
---|
1049 | } elsif ($vers >= 0x0300) {
|
---|
1050 | $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_3');
|
---|
1051 | } else {
|
---|
1052 | $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_2');
|
---|
1053 | }
|
---|
1054 | $hdrEnd = $raf->Tell();
|
---|
1055 | last;
|
---|
1056 | }
|
---|
1057 | #
|
---|
1058 | # read ID3v1 trailer if it exists
|
---|
1059 | #
|
---|
1060 | if ($raf->Seek(-128, 2) and $raf->Read($tBuff, 128) == 128 and $tBuff =~ /^TAG/) {
|
---|
1061 | $exifTool->{DoneID3} = 2; # set to 2 as flag that trailer exists
|
---|
1062 | %id3Trailer = (
|
---|
1063 | DataPt => \$tBuff,
|
---|
1064 | DataPos => $raf->Tell() - 128,
|
---|
1065 | DirStart => 0,
|
---|
1066 | DirLen => length($tBuff),
|
---|
1067 | );
|
---|
1068 | $id3Len += length($tBuff);
|
---|
1069 | $rtnVal = 1;
|
---|
1070 | # load 'Enhanced TAG' information if available
|
---|
1071 | if ($raf->Seek(-355, 2) and $raf->Read($eBuff, 227) == 227 and $eBuff =~ /^TAG+/) {
|
---|
1072 | $id3Trailer{EnhancedTAG} = \$eBuff;
|
---|
1073 | }
|
---|
1074 | }
|
---|
1075 | #
|
---|
1076 | # process the the information
|
---|
1077 | #
|
---|
1078 | if ($rtnVal) {
|
---|
1079 | # first process audio data if it exists
|
---|
1080 | if ($$dirInfo{RAF}) {
|
---|
1081 | my $oldType = $exifTool->{FILE_TYPE}; # save file type
|
---|
1082 | # check current file type first
|
---|
1083 | my @types = grep /^$oldType$/, @audioFormats;
|
---|
1084 | push @types, grep(!/^$oldType$/, @audioFormats);
|
---|
1085 | my $type;
|
---|
1086 | foreach $type (@types) {
|
---|
1087 | # seek to end of ID3 header
|
---|
1088 | $raf->Seek($hdrEnd, 0);
|
---|
1089 | # set type for this file if we are successful
|
---|
1090 | $exifTool->{FILE_TYPE} = $type;
|
---|
1091 | my $module = $audioModule{$type} || $type;
|
---|
1092 | require "Image/ExifTool/$module.pm" or next;
|
---|
1093 | my $func = "Image::ExifTool::${module}::Process$type";
|
---|
1094 | # process the file
|
---|
1095 | no strict 'refs';
|
---|
1096 | &$func($exifTool, $dirInfo) and last;
|
---|
1097 | use strict 'refs';
|
---|
1098 | }
|
---|
1099 | $exifTool->{FILE_TYPE} = $oldType; # restore original file type
|
---|
1100 | }
|
---|
1101 | # set file type to MP3 if we didn't find audio data
|
---|
1102 | $exifTool->SetFileType('MP3');
|
---|
1103 | # record the size if the ID3 metadata
|
---|
1104 | $exifTool->FoundTag('ID3Size', $id3Len);
|
---|
1105 | # process ID3v2 header if it exists
|
---|
1106 | if (%id3Header) {
|
---|
1107 | $exifTool->VPrint(0, "$id3Header{DirName}:\n");
|
---|
1108 | $exifTool->ProcessDirectory(\%id3Header, $tagTablePtr);
|
---|
1109 | }
|
---|
1110 | # process ID3v1 trailer if it exists
|
---|
1111 | if (%id3Trailer) {
|
---|
1112 | $exifTool->VPrint(0, "ID3v1:\n");
|
---|
1113 | SetByteOrder('MM');
|
---|
1114 | $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v1');
|
---|
1115 | $exifTool->ProcessDirectory(\%id3Trailer, $tagTablePtr);
|
---|
1116 | # process "Enhanced TAG" information if available
|
---|
1117 | if ($id3Trailer{EnhancedTAG}) {
|
---|
1118 | $exifTool->VPrint(0, "ID3v1 Enhanced TAG:\n");
|
---|
1119 | $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v1_Enh');
|
---|
1120 | $id3Trailer{DataPt} = $id3Trailer{EnhancedTAG};
|
---|
1121 | $id3Trailer{DataPos} -= 227; # (227 = length of Enhanced TAG block)
|
---|
1122 | $id3Trailer{DirLen} = 227;
|
---|
1123 | $exifTool->ProcessDirectory(\%id3Trailer, $tagTablePtr);
|
---|
1124 | }
|
---|
1125 | }
|
---|
1126 | }
|
---|
1127 | # return file pointer to start of file to read audio data if necessary
|
---|
1128 | $raf->Seek(0, 0);
|
---|
1129 | return $rtnVal;
|
---|
1130 | }
|
---|
1131 |
|
---|
1132 | #------------------------------------------------------------------------------
|
---|
1133 | # Extract ID3 information from an MP3 audio file
|
---|
1134 | # Inputs: 0) ExifTool object reference, 1) dirInfo reference
|
---|
1135 | # Returns: 1 on success, 0 if this wasn't a valid MP3 file
|
---|
1136 | sub ProcessMP3($$)
|
---|
1137 | {
|
---|
1138 | my ($exifTool, $dirInfo) = @_;
|
---|
1139 |
|
---|
1140 | # must first check for leading/trailing ID3 information
|
---|
1141 | unless ($exifTool->{DoneID3}) {
|
---|
1142 | ProcessID3($exifTool, $dirInfo) and return 1;
|
---|
1143 | }
|
---|
1144 | my $raf = $$dirInfo{RAF};
|
---|
1145 | my $rtnVal = 0;
|
---|
1146 | my $buff;
|
---|
1147 | #
|
---|
1148 | # extract information from first audio/video frame headers
|
---|
1149 | # (if found in the first $scanLen bytes)
|
---|
1150 | #
|
---|
1151 | # scan further into a file that should be an MP3
|
---|
1152 | my $scanLen = ($$exifTool{FILE_EXT} and $$exifTool{FILE_EXT} eq 'MP3') ? 8192 : 256;
|
---|
1153 | if ($raf->Read($buff, $scanLen)) {
|
---|
1154 | require Image::ExifTool::MPEG;
|
---|
1155 | if ($buff =~ /\0\0\x01(\xb3|\xc0)/) {
|
---|
1156 | # look for A/V headers in first 64kB
|
---|
1157 | my $buf2;
|
---|
1158 | $raf->Read($buf2, 0x10000 - $scanLen) and $buff .= $buf2;
|
---|
1159 | $rtnVal = 1 if Image::ExifTool::MPEG::ParseMPEGAudioVideo($exifTool, \$buff);
|
---|
1160 | } else {
|
---|
1161 | # look for audio frame sync in first $scanLen bytes
|
---|
1162 | $rtnVal = 1 if Image::ExifTool::MPEG::ParseMPEGAudio($exifTool, \$buff);
|
---|
1163 | }
|
---|
1164 | }
|
---|
1165 | return $rtnVal;
|
---|
1166 | }
|
---|
1167 |
|
---|
1168 | 1; # end
|
---|
1169 |
|
---|
1170 | __END__
|
---|
1171 |
|
---|
1172 | =head1 NAME
|
---|
1173 |
|
---|
1174 | Image::ExifTool::ID3 - Read ID3 meta information
|
---|
1175 |
|
---|
1176 | =head1 SYNOPSIS
|
---|
1177 |
|
---|
1178 | This module is used by Image::ExifTool
|
---|
1179 |
|
---|
1180 | =head1 DESCRIPTION
|
---|
1181 |
|
---|
1182 | This module contains definitions required by Image::ExifTool to extract ID3
|
---|
1183 | information from audio files. ID3 information is found in MP3 and various
|
---|
1184 | other types of audio files.
|
---|
1185 |
|
---|
1186 | =head1 AUTHOR
|
---|
1187 |
|
---|
1188 | Copyright 2003-2011, Phil Harvey (phil at owl.phy.queensu.ca)
|
---|
1189 |
|
---|
1190 | This library is free software; you can redistribute it and/or modify it
|
---|
1191 | under the same terms as Perl itself.
|
---|
1192 |
|
---|
1193 | =head1 REFERENCES
|
---|
1194 |
|
---|
1195 | =over 4
|
---|
1196 |
|
---|
1197 | =item L<http://www.id3.org/>
|
---|
1198 |
|
---|
1199 | =item L<http://www.mp3-tech.org/>
|
---|
1200 |
|
---|
1201 | =item L<http://www.fortunecity.com/underworld/sonic/3/id3tag.html>
|
---|
1202 |
|
---|
1203 | =back
|
---|
1204 |
|
---|
1205 | =head1 SEE ALSO
|
---|
1206 |
|
---|
1207 | L<Image::ExifTool::TagNames/ID3 Tags>,
|
---|
1208 | L<Image::ExifTool(3pm)|Image::ExifTool>
|
---|
1209 |
|
---|
1210 | =cut
|
---|
1211 |
|
---|