source: main/trunk/greenstone2/perllib/cpan/Image/ExifTool/ID3.pm@ 24107

Last change on this file since 24107 was 24107, checked in by sjm84, 13 years ago

Updating the ExifTool perl modules

File size: 39.8 KB
Line 
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
13package Image::ExifTool::ID3;
14
15use strict;
16use vars qw($VERSION);
17use Image::ExifTool qw(:DataAccess :Utils);
18
19$VERSION = '1.28';
20
21sub ProcessID3v2($$$);
22sub ProcessPrivate($$$);
23sub ConvertID3v1Text($$);
24
25# audio formats that we process after an ID3v2 header (in order)
26my @audioFormats = qw(APE MPC FLAC OGG MP3);
27
28# audio formats where the processing proc is in a different module
29my %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!)
36my %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
60my %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
102my %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
437my %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
620Image::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
640sub 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
649sub 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
682sub 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
707sub 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
751sub 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
764sub 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
982sub 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
1136sub 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
11681; # end
1169
1170__END__
1171
1172=head1 NAME
1173
1174Image::ExifTool::ID3 - Read ID3 meta information
1175
1176=head1 SYNOPSIS
1177
1178This module is used by Image::ExifTool
1179
1180=head1 DESCRIPTION
1181
1182This module contains definitions required by Image::ExifTool to extract ID3
1183information from audio files. ID3 information is found in MP3 and various
1184other types of audio files.
1185
1186=head1 AUTHOR
1187
1188Copyright 2003-2011, Phil Harvey (phil at owl.phy.queensu.ca)
1189
1190This library is free software; you can redistribute it and/or modify it
1191under 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
1207L<Image::ExifTool::TagNames/ID3 Tags>,
1208L<Image::ExifTool(3pm)|Image::ExifTool>
1209
1210=cut
1211
Note: See TracBrowser for help on using the repository browser.