1#------------------------------------------------------------------------------
2# File:         ID3.pm
3#
4# Description:  Read ID3 and Lyrics3 meta information
5#
6# Revisions:    09/12/2005 - P. Harvey Created
7#               09/08/2020 - PH Added Lyrics3 support
8#
9# References:   1) http://www.id3.org/
10#               2) http://www.mp3-tech.org/
11#               3) http://www.fortunecity.com/underworld/sonic/3/id3tag.html
12#               4) https://id3.org/Lyrics3
13#------------------------------------------------------------------------------
14
15package Image::ExifTool::ID3;
16
17use strict;
18use vars qw($VERSION);
19use Image::ExifTool qw(:DataAccess :Utils);
20
21$VERSION = '1.57';
22
23sub ProcessID3v2($$$);
24sub ProcessPrivate($$$);
25sub ProcessSynText($$$);
26sub ProcessID3Dir($$$);
27sub ConvertID3v1Text($$);
28sub ConvertTimeStamp($);
29
30# audio formats that we process after an ID3v2 header (in order)
31my @audioFormats = qw(APE MPC FLAC OGG MP3);
32
33# audio formats where the processing proc is in a differently-named module
34my %audioModule = (
35    MP3 => 'ID3',
36    OGG => 'Ogg',
37);
38
39# picture types for 'PIC' and 'APIC' tags
40# (Note: Duplicated in ID3, ASF and FLAC modules!)
41my %pictureType = (
42    0 => 'Other',
43    1 => '32x32 PNG Icon',
44    2 => 'Other Icon',
45    3 => 'Front Cover',
46    4 => 'Back Cover',
47    5 => 'Leaflet',
48    6 => 'Media',
49    7 => 'Lead Artist',
50    8 => 'Artist',
51    9 => 'Conductor',
52    10 => 'Band',
53    11 => 'Composer',
54    12 => 'Lyricist',
55    13 => 'Recording Studio or Location',
56    14 => 'Recording Session',
57    15 => 'Performance',
58    16 => 'Capture from Movie or Video',
59    17 => 'Bright(ly) Colored Fish',
60    18 => 'Illustration',
61    19 => 'Band Logo',
62    20 => 'Publisher Logo',
63);
64
65my %dateTimeConv = (
66    ValueConv => 'require Image::ExifTool::XMP; Image::ExifTool::XMP::ConvertXMPDate($val)',
67    PrintConv => '$self->ConvertDateTime($val)',
68);
69
70# This table is just for documentation purposes
71%Image::ExifTool::ID3::Main = (
72    VARS => { NO_ID => 1 },
73    PROCESS_PROC => \&ProcessID3Dir, # (used to process 'id3 ' chunk in WAV files)
74    NOTES => q{
75        ExifTool extracts ID3 and Lyrics3 information from MP3, MPEG, WAV, AIFF,
76        OGG, FLAC, APE, MPC and RealAudio files.  ID3v2 tags which support multiple
77        languages (eg. Comment and Lyrics) are extracted by specifying the tag name,
78        followed by a dash ('-'), then a 3-character ISO 639-2 language code (eg.
79        "Comment-spa"). See L<http://www.id3.org/> for the official ID3
80        specification and L<http://www.loc.gov/standards/iso639-2/php/code_list.php>
81        for a list of ISO 639-2 language codes.
82    },
83    ID3v1 => {
84        Name => 'ID3v1',
85        SubDirectory => { TagTable => 'Image::ExifTool::ID3::v1' },
86    },
87    ID3v1Enh => {
88        Name => 'ID3v1_Enh',
89        SubDirectory => { TagTable => 'Image::ExifTool::ID3::v1_Enh' },
90    },
91    ID3v22 => {
92        Name => 'ID3v2_2',
93        SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_2' },
94    },
95    ID3v23 => {
96        Name => 'ID3v2_3',
97        SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_3' },
98    },
99    ID3v24 => {
100        Name => 'ID3v2_4',
101        SubDirectory => { TagTable => 'Image::ExifTool::ID3::v2_4' },
102    },
103);
104
105# Lyrics3 tags (ref 4)
106%Image::ExifTool::ID3::Lyrics3 = (
107    GROUPS => { 1 => 'Lyrics3', 2 => 'Audio' },
108    NOTES => q{
109        ExifTool extracts Lyrics3 version 1.00 and 2.00 tags from any file that
110        supports ID3.  See L<https://id3.org/Lyrics3> for the specification.
111    },
112    IND => 'Indications',
113    LYR => 'Lyrics',
114    INF => 'AdditionalInfo',
115    AUT => { Name => 'Author', Groups => { 2 => 'Author' } },
116    EAL => 'ExtendedAlbumName',
117    EAR => 'ExtendedArtistName',
118    ETT => 'ExtendedTrackTitle',
119    IMG => 'AssociatedImageFile',
120    CRC => 'CRC', #PH
121);
122
123# Mapping for ID3v1 Genre numbers
124my %genre = (
125      0 => 'Blues',
126      1 => 'Classic Rock',
127      2 => 'Country',
128      3 => 'Dance',
129      4 => 'Disco',
130      5 => 'Funk',
131      6 => 'Grunge',
132      7 => 'Hip-Hop',
133      8 => 'Jazz',
134      9 => 'Metal',
135     10 => 'New Age',
136     11 => 'Oldies',
137     12 => 'Other',
138     13 => 'Pop',
139     14 => 'R&B',
140     15 => 'Rap',
141     16 => 'Reggae',
142     17 => 'Rock',
143     18 => 'Techno',
144     19 => 'Industrial',
145     20 => 'Alternative',
146     21 => 'Ska',
147     22 => 'Death Metal',
148     23 => 'Pranks',
149     24 => 'Soundtrack',
150     25 => 'Euro-Techno',
151     26 => 'Ambient',
152     27 => 'Trip-Hop',
153     28 => 'Vocal',
154     29 => 'Jazz+Funk',
155     30 => 'Fusion',
156     31 => 'Trance',
157     32 => 'Classical',
158     33 => 'Instrumental',
159     34 => 'Acid',
160     35 => 'House',
161     36 => 'Game',
162     37 => 'Sound Clip',
163     38 => 'Gospel',
164     39 => 'Noise',
165     40 => 'Alt. Rock', # (was AlternRock)
166     41 => 'Bass',
167     42 => 'Soul',
168     43 => 'Punk',
169     44 => 'Space',
170     45 => 'Meditative',
171     46 => 'Instrumental Pop',
172     47 => 'Instrumental Rock',
173     48 => 'Ethnic',
174     49 => 'Gothic',
175     50 => 'Darkwave',
176     51 => 'Techno-Industrial',
177     52 => 'Electronic',
178     53 => 'Pop-Folk',
179     54 => 'Eurodance',
180     55 => 'Dream',
181     56 => 'Southern Rock',
182     57 => 'Comedy',
183     58 => 'Cult',
184     59 => 'Gangsta Rap', # (was Gansta)
185     60 => 'Top 40',
186     61 => 'Christian Rap',
187     62 => 'Pop/Funk',
188     63 => 'Jungle',
189     64 => 'Native American',
190     65 => 'Cabaret',
191     66 => 'New Wave',
192     67 => 'Psychedelic', # (was misspelt)
193     68 => 'Rave',
194     69 => 'Showtunes',
195     70 => 'Trailer',
196     71 => 'Lo-Fi',
197     72 => 'Tribal',
198     73 => 'Acid Punk',
199     74 => 'Acid Jazz',
200     75 => 'Polka',
201     76 => 'Retro',
202     77 => 'Musical',
203     78 => 'Rock & Roll',
204     79 => 'Hard Rock',
205     # The following genres are Winamp extensions
206     80 => 'Folk',
207     81 => 'Folk-Rock',
208     82 => 'National Folk',
209     83 => 'Swing',
210     84 => 'Fast-Fusion', # (was Fast Fusion)
211     85 => 'Bebop', # (was misspelt)
212     86 => 'Latin',
213     87 => 'Revival',
214     88 => 'Celtic',
215     89 => 'Bluegrass',
216     90 => 'Avantgarde',
217     91 => 'Gothic Rock',
218     92 => 'Progressive Rock',
219     93 => 'Psychedelic Rock',
220     94 => 'Symphonic Rock',
221     95 => 'Slow Rock',
222     96 => 'Big Band',
223     97 => 'Chorus',
224     98 => 'Easy Listening',
225     99 => 'Acoustic',
226    100 => 'Humour',
227    101 => 'Speech',
228    102 => 'Chanson',
229    103 => 'Opera',
230    104 => 'Chamber Music',
231    105 => 'Sonata',
232    106 => 'Symphony',
233    107 => 'Booty Bass',
234    108 => 'Primus',
235    109 => 'Porn Groove',
236    110 => 'Satire',
237    111 => 'Slow Jam',
238    112 => 'Club',
239    113 => 'Tango',
240    114 => 'Samba',
241    115 => 'Folklore',
242    116 => 'Ballad',
243    117 => 'Power Ballad',
244    118 => 'Rhythmic Soul',
245    119 => 'Freestyle',
246    120 => 'Duet',
247    121 => 'Punk Rock',
248    122 => 'Drum Solo',
249    123 => 'A Cappella', # (was Acapella)
250    124 => 'Euro-House',
251    125 => 'Dance Hall',
252    # ref http://yar.hole.ru/MP3Tech/lamedoc/id3.html
253    126 => 'Goa',
254    127 => 'Drum & Bass',
255    128 => 'Club-House',
256    129 => 'Hardcore',
257    130 => 'Terror',
258    131 => 'Indie',
259    132 => 'BritPop',
260    133 => 'Afro-Punk', # (was Negerpunk)
261    134 => 'Polsk Punk',
262    135 => 'Beat',
263    136 => 'Christian Gangsta Rap', # (was Christian Gangsta)
264    137 => 'Heavy Metal',
265    138 => 'Black Metal',
266    139 => 'Crossover',
267    140 => 'Contemporary Christian', # (was Contemporary C)
268    141 => 'Christian Rock',
269    142 => 'Merengue',
270    143 => 'Salsa',
271    144 => 'Thrash Metal',
272    145 => 'Anime',
273    146 => 'JPop',
274    147 => 'Synthpop', # (was SynthPop)
275    # ref http://alicja.homelinux.com/~mats/text/Music/MP3/ID3/Genres.txt
276    # (also used to update some Genres above)
277    148 => 'Abstract',
278    149 => 'Art Rock',
279    150 => 'Baroque',
280    151 => 'Bhangra',
281    152 => 'Big Beat',
282    153 => 'Breakbeat',
283    154 => 'Chillout',
284    155 => 'Downtempo',
285    156 => 'Dub',
286    157 => 'EBM',
287    158 => 'Eclectic',
288    159 => 'Electro',
289    160 => 'Electroclash',
290    161 => 'Emo',
291    162 => 'Experimental',
292    163 => 'Garage',
293    164 => 'Global',
294    165 => 'IDM',
295    166 => 'Illbient',
296    167 => 'Industro-Goth',
297    168 => 'Jam Band',
298    169 => 'Krautrock',
299    170 => 'Leftfield',
300    171 => 'Lounge',
301    172 => 'Math Rock',
302    173 => 'New Romantic',
303    174 => 'Nu-Breakz',
304    175 => 'Post-Punk',
305    176 => 'Post-Rock',
306    177 => 'Psytrance',
307    178 => 'Shoegaze',
308    179 => 'Space Rock',
309    180 => 'Trop Rock',
310    181 => 'World Music',
311    182 => 'Neoclassical',
312    183 => 'Audiobook',
313    184 => 'Audio Theatre',
314    185 => 'Neue Deutsche Welle',
315    186 => 'Podcast',
316    187 => 'Indie Rock',
317    188 => 'G-Funk',
318    189 => 'Dubstep',
319    190 => 'Garage Rock',
320    191 => 'Psybient',
321    255 => 'None',
322    # ID3v2 adds some text short forms...
323    CR  => 'Cover',
324    RX  => 'Remix',
325);
326
327# Tags for ID3v1
328%Image::ExifTool::ID3::v1 = (
329    PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
330    GROUPS => { 1 => 'ID3v1', 2 => 'Audio' },
331    PRIORITY => 0,  # let ID3v2 tags replace these if they come later
332    3 => {
333        Name => 'Title',
334        Format => 'string[30]',
335        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
336    },
337    33 => {
338        Name => 'Artist',
339        Groups => { 2 => 'Author' },
340        Format => 'string[30]',
341        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
342    },
343    63 => {
344        Name => 'Album',
345        Format => 'string[30]',
346        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
347    },
348    93 => {
349        Name => 'Year',
350        Groups => { 2 => 'Time' },
351        Format => 'string[4]',
352    },
353    97 => {
354        Name => 'Comment',
355        Format => 'string[30]',
356        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
357    },
358    125 => { # ID3v1.1 (ref http://en.wikipedia.org/wiki/ID3#Layout)
359        Name => 'Track',
360        Format => 'int8u[2]',
361        Notes => 'v1.1 addition -- last 2 bytes of v1.0 Comment field',
362        RawConv => '($val =~ s/^0 // and $val) ? $val : undef',
363    },
364    127 => {
365        Name => 'Genre',
366        Notes => 'CR and RX are ID3v2 only',
367        Format => 'int8u',
368        PrintConv => \%genre,
369        PrintConvColumns => 3,
370    },
371);
372
373# ID3v1 "Enhanced TAG" information (ref 3)
374%Image::ExifTool::ID3::v1_Enh = (
375    PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
376    GROUPS => { 1 => 'ID3v1_Enh', 2 => 'Audio' },
377    NOTES => 'ID3 version 1 "Enhanced TAG" information (not part of the official spec).',
378    PRIORITY => 0,  # let ID3v2 tags replace these if they come later
379    4 => {
380        Name => 'Title2',
381        Format => 'string[60]',
382        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
383    },
384    64 => {
385        Name => 'Artist2',
386        Groups => { 2 => 'Author' },
387        Format => 'string[60]',
388        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
389    },
390    124 => {
391        Name => 'Album2',
392        Format => 'string[60]',
393        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
394    },
395    184 => {
396        Name => 'Speed',
397        Format => 'int8u',
398        PrintConv => {
399            1 => 'Slow',
400            2 => 'Medium',
401            3 => 'Fast',
402            4 => 'Hardcore',
403        },
404    },
405    185 => {
406        Name => 'Genre',
407        Format => 'string[30]',
408        ValueConv => 'Image::ExifTool::ID3::ConvertID3v1Text($self,$val)',
409    },
410    215 => {
411        Name => 'StartTime',
412        Format => 'string[6]',
413    },
414    221 => {
415        Name => 'EndTime',
416        Format => 'string[6]',
417    },
418);
419
420# Tags for ID2v2.2
421%Image::ExifTool::ID3::v2_2 = (
422    PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
423    GROUPS => { 1 => 'ID3v2_2', 2 => 'Audio' },
424    NOTES => q{
425        ExifTool extracts mainly text-based tags from ID3v2 information.  The tags
426        in the tables below are those extracted by ExifTool, and don't represent a
427        complete list of available ID3v2 tags.
428
429        ID3 version 2.2 tags.  (These are the tags written by iTunes 5.0.)
430    },
431    CNT => 'PlayCounter',
432    COM => 'Comment',
433    IPL => 'InvolvedPeople',
434    PIC => {
435        Name => 'Picture',
436        Groups => { 2 => 'Preview' },
437        Binary => 1,
438        Notes => 'the 3 tags below are also extracted from this PIC frame',
439    },
440    'PIC-1' => { Name => 'PictureFormat',      Groups => { 2 => 'Image' } },
441    'PIC-2' => {
442        Name => 'PictureType',
443        Groups => { 2 => 'Image' },
444        PrintConv => \%pictureType,
445        SeparateTable => 1,
446    },
447    'PIC-3' => { Name => 'PictureDescription', Groups => { 2 => 'Image' } },
448    POP => {
449        Name => 'Popularimeter',
450        PrintConv => '$val=~s/^(.*?) (\d+) (\d+)$/$1 Rating=$2 Count=$3/s; $val',
451    },
452    SLT => {
453        Name => 'SynLyrics',
454        SubDirectory => { TagTable => 'Image::ExifTool::ID3::SynLyrics' },
455    },
456    TAL => 'Album',
457    TBP => 'BeatsPerMinute',
458    TCM => 'Composer',
459    TCO =>{
460        Name => 'Genre',
461        Notes => 'uses same lookup table as ID3v1 Genre',
462        PrintConv => 'Image::ExifTool::ID3::PrintGenre($val)',
463    },
464    TCP => { Name => 'Compilation', PrintConv => { 0 => 'No', 1 => 'Yes' } }, # iTunes
465    TCR => { Name => 'Copyright', Groups => { 2 => 'Author' } },
466    TDA => { Name => 'Date', Groups => { 2 => 'Time' } },
467    TDY => 'PlaylistDelay',
468    TEN => 'EncodedBy',
469    TFT => 'FileType',
470    TIM => { Name => 'Time', Groups => { 2 => 'Time' } },
471    TKE => 'InitialKey',
472    TLA => 'Language',
473    TLE => 'Length',
474    TMT => 'Media',
475    TOA => { Name => 'OriginalArtist', Groups => { 2 => 'Author' } },
476    TOF => 'OriginalFileName',
477    TOL => 'OriginalLyricist',
478    TOR => 'OriginalReleaseYear',
479    TOT => 'OriginalAlbum',
480    TP1 => { Name => 'Artist', Groups => { 2 => 'Author' } },
481    TP2 => 'Band',
482    TP3 => 'Conductor',
483    TP4 => 'InterpretedBy',
484    TPA => 'PartOfSet',
485    TPB => 'Publisher',
486    TRC => 'ISRC', # (international standard recording code)
487    TRD => 'RecordingDates',
488    TRK => 'Track',
489    TSI => 'Size',
490    TSS => 'EncoderSettings',
491    TT1 => 'Grouping',
492    TT2 => 'Title',
493    TT3 => 'Subtitle',
494    TXT => 'Lyricist',
495    TXX => 'UserDefinedText',
496    TYE => { Name => 'Year', Groups => { 2 => 'Time' } },
497    ULT => 'Lyrics',
498    WAF => 'FileURL',
499    WAR => { Name => 'ArtistURL', Groups => { 2 => 'Author' } },
500    WAS => 'SourceURL',
501    WCM => 'CommercialURL',
502    WCP => { Name => 'CopyrightURL', Groups => { 2 => 'Author' } },
503    WPB => 'PublisherURL',
504    WXX => 'UserDefinedURL',
505    # the following written by iTunes 10.5 (ref PH)
506    RVA => 'RelativeVolumeAdjustment',
507    TST => 'TitleSortOrder',
508    TSA => 'AlbumSortOrder',
509    TSP => 'PerformerSortOrder',
510    TS2 => 'AlbumArtistSortOrder',
511    TSC => 'ComposerSortOrder',
512    ITU => { Name => 'iTunesU', Description => 'iTunes U', Binary => 1, Unknown => 1 },
513    PCS => { Name => 'Podcast', Binary => 1, Unknown => 1 },
514);
515
516# tags common to ID3v2.3 and ID3v2.4
517my %id3v2_common = (
518  # AENC => 'AudioEncryption', # Owner, preview start, preview length, encr data
519    APIC => {
520        Name => 'Picture',
521        Groups => { 2 => 'Preview' },
522        Binary => 1,
523        Notes => 'the 3 tags below are also extracted from this APIC frame',
524    },
525    'APIC-1' => { Name => 'PictureMIMEType',    Groups => { 2 => 'Image' } },
526    'APIC-2' => {
527        Name => 'PictureType',
528        Groups => { 2 => 'Image' },
529        PrintConv => \%pictureType,
530        SeparateTable => 1,
531    },
532    'APIC-3' => { Name => 'PictureDescription', Groups => { 2 => 'Image' } },
533    COMM => 'Comment',
534  # COMR => 'Commercial',
535  # ENCR => 'EncryptionMethod',
536  # ETCO => 'EventTimingCodes',
537  # GEOB => 'GeneralEncapsulatedObject',
538  # GRID => 'GroupIdentification',
539  # LINK => 'LinkedInformation',
540    MCDI => { Name => 'MusicCDIdentifier', Binary => 1 },
541  # MLLT => 'MPEGLocationLookupTable',
542    OWNE => 'Ownership',
543    PCNT => 'PlayCounter',
544    POPM => {
545        Name => 'Popularimeter',
546        PrintConv => '$val=~s/^(.*?) (\d+) (\d+)$/$1 Rating=$2 Count=$3/s; $val',
547    },
548  # POSS => 'PostSynchronization',
549    PRIV => {
550        Name => 'Private',
551        SubDirectory => { TagTable => 'Image::ExifTool::ID3::Private' },
552    },
553  # RBUF => 'RecommendedBufferSize',
554  # RVRB => 'Reverb',
555    SYLT => {
556        Name => 'SynLyrics',
557        SubDirectory => { TagTable => 'Image::ExifTool::ID3::SynLyrics' },
558    },
559  # SYTC => 'SynchronizedTempoCodes',
560    TALB => 'Album',
561    TBPM => 'BeatsPerMinute',
562    TCMP => { Name => 'Compilation', PrintConv => { 0 => 'No', 1 => 'Yes' } }, #PH (iTunes)
563    TCOM => 'Composer',
564    TCON =>{
565        Name => 'Genre',
566        Notes => 'uses same lookup table as ID3v1 Genre',
567        PrintConv => 'Image::ExifTool::ID3::PrintGenre($val)',
568    },
569    TCOP => { Name => 'Copyright', Groups => { 2 => 'Author' } },
570    TDLY => 'PlaylistDelay',
571    TENC => 'EncodedBy',
572    TEXT => 'Lyricist',
573    TFLT => 'FileType',
574    TIT1 => 'Grouping',
575    TIT2 => 'Title',
576    TIT3 => 'Subtitle',
577    TKEY => 'InitialKey',
578    TLAN => 'Language',
579    TLEN => {
580        Name => 'Length',
581        ValueConv => '$val / 1000',
582        PrintConv => '"$val s"',
583    },
584    TMED => 'Media',
585    TOAL => 'OriginalAlbum',
586    TOFN => 'OriginalFileName',
587    TOLY => 'OriginalLyricist',
588    TOPE => { Name => 'OriginalArtist', Groups => { 2 => 'Author' } },
589    TOWN => 'FileOwner',
590    TPE1 => { Name => 'Artist', Groups => { 2 => 'Author' } },
591    TPE2 => 'Band',
592    TPE3 => 'Conductor',
593    TPE4 => 'InterpretedBy',
594    TPOS => 'PartOfSet',
595    TPUB => 'Publisher',
596    TRCK => 'Track',
597    TRSN => 'InternetRadioStationName',
598    TRSO => 'InternetRadioStationOwner',
599    TSRC => 'ISRC', # (international standard recording code)
600    TSSE => 'EncoderSettings',
601    TXXX => 'UserDefinedText',
602  # UFID => 'UniqueFileID', (not extracted because it is long and nasty and not very useful)
603    USER => 'TermsOfUse',
604    USLT => 'Lyrics',
605    WCOM => 'CommercialURL',
606    WCOP => 'CopyrightURL',
607    WOAF => 'FileURL',
608    WOAR => { Name => 'ArtistURL', Groups => { 2 => 'Author' } },
609    WOAS => 'SourceURL',
610    WORS => 'InternetRadioStationURL',
611    WPAY => 'PaymentURL',
612    WPUB => 'PublisherURL',
613    WXXX => 'UserDefinedURL',
614#
615# non-standard frames
616#
617    # the following are written by iTunes 10.5 (ref PH)
618    TSO2 => 'AlbumArtistSortOrder',
619    TSOC => 'ComposerSortOrder',
620    ITNU => { Name => 'iTunesU', Description => 'iTunes U', Binary => 1, Unknown => 1 },
621    PCST => { Name => 'Podcast', Binary => 1, Unknown => 1 },
622    # other proprietary Apple tags (ref http://help.mp3tag.de/main_tags.html)
623    TDES => 'PodcastDescription',
624    TGID => 'PodcastID',
625    WFED => 'PodcastURL',
626    TKWD => 'PodcastKeywords',
627    TCAT => 'PodcastCategory',
628    # more non-standard tags (ref http://eyed3.nicfit.net/compliance.html)
629    # NCON - unknown MusicMatch binary data
630    XDOR => { Name => 'OriginalReleaseTime',Groups => { 2 => 'Time' }, %dateTimeConv },
631    XSOA => 'AlbumSortOrder',
632    XSOP => 'PerformerSortOrder',
633    XSOT => 'TitleSortOrder',
634    XOLY => {
635        Name => 'OlympusDSS',
636        SubDirectory => { TagTable => 'Image::ExifTool::Olympus::DSS' },
637    },
638    GRP1 => 'Grouping',
639    MVNM => 'MovementName', # (NC)
640    MVIN => 'MovementNumber', # (NC)
641);
642
643# Tags for ID3v2.3 (http://www.id3.org/id3v2.3.0)
644%Image::ExifTool::ID3::v2_3 = (
645    PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
646    GROUPS => { 1 => 'ID3v2_3', 2 => 'Audio' },
647    NOTES => q{
648        ID3 version 2.3 tags.  Includes some non-standard tags written by other
649        software.
650    },
651    %id3v2_common,  # include common tags
652  # EQUA => 'Equalization',
653    IPLS => 'InvolvedPeople',
654  # RVAD => 'RelativeVolumeAdjustment',
655    TDAT => { Name => 'Date', Groups => { 2 => 'Time' } },
656    TIME => { Name => 'Time', Groups => { 2 => 'Time' } },
657    TORY => 'OriginalReleaseYear',
658    TRDA => 'RecordingDates',
659    TSIZ => 'Size',
660    TYER => { Name => 'Year', Groups => { 2 => 'Time' } },
661);
662
663# Tags for ID3v2.4 (http://www.id3.org/id3v2.4.0-frames)
664%Image::ExifTool::ID3::v2_4 = (
665    PROCESS_PROC => \&Image::ExifTool::ID3::ProcessID3v2,
666    GROUPS => { 1 => 'ID3v2_4', 2 => 'Audio' },
667    NOTES => q{
668        ID3 version 2.4 tags.  Includes some non-standard tags written by other
669        software.
670    },
671    %id3v2_common,  # include common tags
672  # EQU2 => 'Equalization',
673    RVA2 => 'RelativeVolumeAdjustment',
674  # SEEK => 'Seek',
675  # SIGN => 'Signature',
676    TDEN => { Name => 'EncodingTime',       Groups => { 2 => 'Time' }, %dateTimeConv },
677    TDOR => { Name => 'OriginalReleaseTime',Groups => { 2 => 'Time' }, %dateTimeConv },
678    TDRC => { Name => 'RecordingTime',      Groups => { 2 => 'Time' }, %dateTimeConv },
679    TDRL => { Name => 'ReleaseTime',        Groups => { 2 => 'Time' }, %dateTimeConv },
680    TDTG => { Name => 'TaggingTime',        Groups => { 2 => 'Time' }, %dateTimeConv },
681    TIPL => 'InvolvedPeople',
682    TMCL => 'MusicianCredits',
683    TMOO => 'Mood',
684    TPRO => 'ProducedNotice',
685    TSOA => 'AlbumSortOrder',
686    TSOP => 'PerformerSortOrder',
687    TSOT => 'TitleSortOrder',
688    TSST => 'SetSubtitle',
689);
690
691# Synchronized lyrics/text
692%Image::ExifTool::ID3::SynLyrics = (
693    GROUPS => { 1 => 'ID3', 2 => 'Audio' },
694    VARS => { NO_ID => 1 },
695    PROCESS_PROC => \&ProcessSynText,
696    NOTES => 'The following tags are extracted from synchronized lyrics/text frames.',
697    desc => { Name => 'SynchronizedLyricsDescription' },
698    type => {
699        Name => 'SynchronizedLyricsType',
700        PrintConv => {
701            0 => 'Other',
702            1 => 'Lyrics',
703            2 => 'Text Transcription',
704            3 => 'Movement/part Name',
705            4 => 'Events',
706            5 => 'Chord',
707            6 => 'Trivia/"pop-up" Information',
708            7 => 'Web Page URL',
709            8 => 'Image URL',
710        },
711    },
712    text => {
713        Name => 'SynchronizedLyricsText',
714        List => 1,
715        Notes => q{
716            each list item has a leading time stamp in square brackets.  Time stamps may
717            be in seconds with format [MM:SS.ss], or MPEG frames with format [FFFF],
718            depending on how this information was stored
719        },
720        PrintConv => \&ConvertTimeStamp,
721    },
722);
723
724# ID3 PRIV tags (ref PH)
725%Image::ExifTool::ID3::Private = (
726    PROCESS_PROC => \&Image::ExifTool::ID3::ProcessPrivate,
727    GROUPS => { 1 => 'ID3', 2 => 'Audio' },
728    VARS => { NO_ID => 1 },
729    NOTES => q{
730        ID3 private (PRIV) tags.  ExifTool will decode any private tags found, even
731        if they do not appear in this table.
732    },
733    XMP => {
734        SubDirectory => {
735            DirName => 'XMP',
736            TagTable => 'Image::ExifTool::XMP::Main',
737        },
738    },
739    PeakValue => {
740        ValueConv => 'length($val)==4 ? unpack("V",$val) : \$val',
741    },
742    AverageLevel => {
743        ValueConv => 'length($val)==4 ? unpack("V",$val) : \$val',
744    },
745    # Windows Media attributes ("/" in tag ID is converted to "_" by ProcessPrivate)
746    WM_WMContentID => {
747        Name => 'WM_ContentID',
748        ValueConv => 'require Image::ExifTool::ASF; Image::ExifTool::ASF::GetGUID($val)',
749    },
750    WM_WMCollectionID => {
751        Name => 'WM_CollectionID',
752        ValueConv => 'require Image::ExifTool::ASF; Image::ExifTool::ASF::GetGUID($val)',
753    },
754    WM_WMCollectionGroupID => {
755        Name => 'WM_CollectionGroupID',
756        ValueConv => 'require Image::ExifTool::ASF; Image::ExifTool::ASF::GetGUID($val)',
757    },
758    WM_MediaClassPrimaryID => {
759        ValueConv => 'require Image::ExifTool::ASF; Image::ExifTool::ASF::GetGUID($val)',
760    },
761    WM_MediaClassSecondaryID => {
762        ValueConv => 'require Image::ExifTool::ASF; Image::ExifTool::ASF::GetGUID($val)',
763    },
764    WM_Provider => {
765        ValueConv => '$self->Decode($val,"UCS2","II")', #PH (NC)
766    },
767    # there are lots more WM tags that could be decoded if I had samples or documentation - PH
768    # WM/AlbumArtist
769    # WM/AlbumTitle
770    # WM/Category
771    # WM/Composer
772    # WM/Conductor
773    # WM/ContentDistributor
774    # WM/ContentGroupDescription
775    # WM/EncodingTime
776    # WM/Genre
777    # WM/GenreID
778    # WM/InitialKey
779    # WM/Language
780    # WM/Lyrics
781    # WM/MCDI
782    # WM/MediaClassPrimaryID
783    # WM/MediaClassSecondaryID
784    # WM/Mood
785    # WM/ParentalRating
786    # WM/Period
787    # WM/ProtectionType
788    # WM/Provider
789    # WM/ProviderRating
790    # WM/ProviderStyle
791    # WM/Publisher
792    # WM/SubscriptionContentID
793    # WM/SubTitle
794    # WM/TrackNumber
795    # WM/UniqueFileIdentifier
796    # WM/WMCollectionGroupID
797    # WM/WMCollectionID
798    # WM/WMContentID
799    # WM/Writer
800    # WM/Year
801);
802
803# lookup to check for existence of tags in other ID3 versions
804my %otherTable = (
805    \%Image::ExifTool::ID3::v2_4 => \%Image::ExifTool::ID3::v2_3,
806    \%Image::ExifTool::ID3::v2_3 => \%Image::ExifTool::ID3::v2_4,
807);
808
809# ID3 Composite tags
810%Image::ExifTool::ID3::Composite = (
811    GROUPS => { 2 => 'Image' },
812    DateTimeOriginal => {
813        Description => 'Date/Time Original',
814        Groups => { 2 => 'Time' },
815        Priority => 0,
816        Desire => {
817            0 => 'ID3:RecordingTime',
818            1 => 'ID3:Year',
819            2 => 'ID3:Date',
820            3 => 'ID3:Time',
821        },
822        ValueConv => q{
823            return $val[0] if $val[0];
824            return undef unless $val[1];
825            return $val[1] unless $val[2] and $val[2] =~ /^(\d{2})(\d{2})$/;
826            $val[1] .= ":$1:$2";
827            return $val[1] unless $val[3] and $val[3] =~ /^(\d{2})(\d{2})$/;
828            return "$val[1] $1:$2";
829        },
830        PrintConv => '$self->ConvertDateTime($val)',
831    },
832);
833
834# add our composite tags
835Image::ExifTool::AddCompositeTags('Image::ExifTool::ID3');
836
837# can't share tagInfo hashes between two tables, so we must make
838# copies of the necessary hashes
839{
840    my $tag;
841    foreach $tag (keys %id3v2_common) {
842        next unless ref $id3v2_common{$tag} eq 'HASH';
843        my %tagInfo = %{$id3v2_common{$tag}};
844        # must also copy Groups hash if it exists
845        my $groups = $tagInfo{Groups};
846        $tagInfo{Groups} = { %$groups } if $groups;
847        $Image::ExifTool::ID3::v2_4{$tag} = \%tagInfo;
848    }
849}
850
851#------------------------------------------------------------------------------
852# Convert ID3v1 text to exiftool character set
853# Inputs: 0) ExifTool object ref, 1) text string
854# Returns: converted text
855sub ConvertID3v1Text($$)
856{
857    my ($et, $val) = @_;
858    return $et->Decode($val, $et->Options('CharsetID3'));
859}
860
861#------------------------------------------------------------------------------
862# Re-format time stamp in synchronized lyrics
863# Inputs: 0) synchronized lyrics entry (eg. "[84.030]Da do do do")
864# Returns: entry with formatted timestamp (eg. "[01:24.03]Da do do do")
865sub ConvertTimeStamp($)
866{
867    my $val = shift;
868    # do nothing if this isn't a time stamp (frame count doesn't contain a decimal)
869    return $val unless $val =~ /^\[(\d+\.\d+)\]/g;
870    my $time = $1;
871    # print hours only if more than 60 minutes
872    my $h = int($time / 3600);
873    if ($h) {
874        $time -= $h * 3600;
875        $h = "$h:";
876    } else {
877        $h = '';
878    }
879    my $m = int($time / 60);
880    my $s = $time - $m * 60;
881    my $ss = sprintf('%05.2f', $s);
882    if ($ss >= 60) {
883        $ss = '00.00';
884        ++$m >= 60 and $m -= 60, ++$h;
885    }
886    return sprintf('[%s%.2d:%s]', $h, $m, $ss) . substr($val, pos($val));
887}
888
889#------------------------------------------------------------------------------
890# Process ID3 synchronized lyrics/text
891# Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
892sub ProcessSynText($$$)
893{
894    my ($et, $dirInfo, $tagTablePtr) = @_;
895    my $dataPt = $$dirInfo{DataPt};
896
897    $et->VerboseDir('SynLyrics', 0, length $$dataPt);
898    return unless length $$dataPt > 6;
899
900    my ($enc,$lang,$timeCode,$type) = unpack('Ca3CC', $$dataPt);
901    $lang = lc $lang;
902    undef $lang if $lang !~ /^[a-z]{3}$/ or $lang eq 'eng';
903    pos($$dataPt) = 6;
904    my ($termLen, $pat);
905    if ($enc == 1 or $enc == 2) {
906        $$dataPt =~ /\G(..)*?\0\0/sg or return;
907        $termLen = 2;
908        $pat = '\G(?:..)*?\0\0(....)';
909    } else {
910        $$dataPt =~ /\0/g or return;
911        $termLen = 1;
912        $pat = '\0(....)';
913    }
914    my $desc = substr($$dataPt, 6, pos($$dataPt) - 6 - $termLen);
915    $desc = DecodeString($et, $desc, $enc);
916
917    my $tagInfo = $et->GetTagInfo($tagTablePtr, 'desc');
918    $tagInfo = Image::ExifTool::GetLangInfo($tagInfo, $lang) if $lang;
919    $et->HandleTag($tagTablePtr, 'type', $type);
920    $et->HandleTag($tagTablePtr, 'desc', $desc, TagInfo => $tagInfo);
921    $tagInfo = $et->GetTagInfo($tagTablePtr, 'text');
922    $tagInfo = Image::ExifTool::GetLangInfo($tagInfo, $lang) if $lang;
923
924    for (;;) {
925        my $pos = pos $$dataPt;
926        last unless $$dataPt =~ /$pat/sg;
927        my $time = unpack('N', $1);
928        my $text = substr($$dataPt, $pos, pos($$dataPt) - $pos - 4 - $termLen);
929        $text = DecodeString($et, $text, $enc);
930        my $timeStr;
931        if ($timeCode == 2) { # time in ms
932            $timeStr = sprintf('%.3f', $time / 1000);
933        } else {              # time in MPEG frames
934            $timeStr = sprintf('%.4d', $time);
935            $timeStr .= '?' if $timeCode != 1;
936        }
937        $et->HandleTag($tagTablePtr, 'text', "[$timeStr]$text", TagInfo => $tagInfo);
938    }
939}
940
941#------------------------------------------------------------------------------
942# Process ID3 PRIV data
943# Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
944sub ProcessPrivate($$$)
945{
946    my ($et, $dirInfo, $tagTablePtr) = @_;
947    my $dataPt = $$dirInfo{DataPt};
948    my ($tag, $start);
949    $et->VerboseDir('PRIV', 0, length $$dataPt);
950    if ($$dataPt =~ /^(.*?)\0/s) {
951        $tag = $1;
952        $start = length($tag) + 1;
953    } else {
954        $tag = '';
955        $start = 0;
956    }
957    unless ($$tagTablePtr{$tag}) {
958        $tag =~ tr{/ }{_}d; # translate '/' to '_' and remove spaces
959        $tag = 'private' unless $tag =~ /^[-\w]{1,24}$/;
960        unless ($$tagTablePtr{$tag}) {
961            AddTagToTable($tagTablePtr, $tag,
962                { Name => ucfirst($tag), Binary => 1 });
963        }
964    }
965    my $key = $et->HandleTag($tagTablePtr, $tag, undef,
966        Size  => length($$dataPt) - $start,
967        Start => $start,
968        DataPt => $dataPt,
969    );
970    # set group1 name
971    $et->SetGroup($key, $$et{ID3_Ver}) if $key;
972}
973
974#------------------------------------------------------------------------------
975# Print ID3v2 Genre
976# Inputs: TCON or TCO frame data
977# Returns: Content type with decoded genre numbers
978sub PrintGenre($)
979{
980    my $val = shift;
981    # make sure that %genre has an entry for all numbers we are interested in
982    # (genre numbers are in brackets for ID3v2.2 and v2.3)
983    while ($val =~ /\((\d+)\)/g) {
984        $genre{$1} or $genre{$1} = "Unknown ($1)";
985    }
986    # (genre numbers are separated by nulls in ID3v2.4,
987    #  but nulls are converted to '/' by DecodeString())
988    while ($val =~ /(?:^|\/)(\d+)(\/|$)/g) {
989        $genre{$1} or $genre{$1} = "Unknown ($1)";
990    }
991    $val =~ s/\((\d+)\)/\($genre{$1}\)/g;
992    $val =~ s/(^|\/)(\d+)(?=\/|$)/$1$genre{$2}/g;
993    $val =~ s/^\(([^)]+)\)\1?$/$1/; # clean up by removing brackets and duplicates
994    return $val;
995}
996
997#------------------------------------------------------------------------------
998# Get Genre ID
999# Inputs: 0) Genre name
1000# Returns: genre ID number, or undef
1001sub GetGenreID($)
1002{
1003    return Image::ExifTool::ReverseLookup(shift, \%genre);
1004}
1005
1006#------------------------------------------------------------------------------
1007# Decode ID3 string
1008# Inputs: 0) ExifTool object reference
1009#         1) string beginning with encoding byte unless specified as argument
1010#         2) optional encoding (0=ISO-8859-1, 1=UTF-16 BOM, 2=UTF-16BE, 3=UTF-8)
1011# Returns: Decoded string in scalar context, or list of strings in list context
1012sub DecodeString($$;$)
1013{
1014    my ($et, $val, $enc) = @_;
1015    return '' unless length $val;
1016    unless (defined $enc) {
1017        $enc = unpack('C', $val);
1018        $val = substr($val, 1); # remove encoding byte
1019    }
1020    my @vals;
1021    if ($enc == 0 or $enc == 3) { # ISO 8859-1 or UTF-8
1022        $val =~ s/\0+$//;   # remove any null padding
1023        # (must split before converting because conversion routines truncate at null)
1024        @vals = split "\0", $val;
1025        foreach $val (@vals) {
1026            $val = $et->Decode($val, $enc ? 'UTF8' : 'Latin');
1027        }
1028    } elsif ($enc == 1 or $enc == 2) {  # UTF-16 with BOM, or UTF-16BE
1029        my $bom = "\xfe\xff";
1030        my %order = ( "\xfe\xff" => 'MM', "\xff\xfe", => 'II' );
1031        for (;;) {
1032            my $v;
1033            # split string at null terminators on word boundaries
1034            if ($val =~ s/((..)*?)\0\0//s) {
1035                $v = $1;
1036            } else {
1037                last unless length $val > 1;
1038                $v = $val;
1039                $val = '';
1040            }
1041            $bom = $1 if $v =~ s/^(\xfe\xff|\xff\xfe)//;
1042            push @vals, $et->Decode($v, 'UCS2', $order{$bom});
1043        }
1044    } else {
1045        $val =~ s/\0+$//;
1046        return "<Unknown encoding $enc> $val";
1047    }
1048    return @vals if wantarray;
1049    return join('/',@vals);
1050}
1051
1052#------------------------------------------------------------------------------
1053# Convert sync-safe integer to a number we can use
1054# Inputs: 0) int32u sync-safe value
1055# Returns: actual number or undef on invalid value
1056sub UnSyncSafe($)
1057{
1058    my $val = shift;
1059    return undef if $val & 0x80808080;
1060    return ($val & 0x0000007f) |
1061          (($val & 0x00007f00) >> 1) |
1062          (($val & 0x007f0000) >> 2) |
1063          (($val & 0x7f000000) >> 3);
1064}
1065
1066#------------------------------------------------------------------------------
1067# Process ID3v2 information
1068# Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1069sub ProcessID3v2($$$)
1070{
1071    my ($et, $dirInfo, $tagTablePtr) = @_;
1072    my $dataPt  = $$dirInfo{DataPt};
1073    my $offset  = $$dirInfo{DirStart};
1074    my $size    = $$dirInfo{DirLen};
1075    my $vers    = $$dirInfo{Version};
1076    my $verbose = $et->Options('Verbose');
1077    my $len;    # frame data length
1078
1079    $et->VerboseDir($tagTablePtr->{GROUPS}->{1}, 0, $size);
1080    $et->VerboseDump($dataPt, Len => $size, Start => $offset);
1081
1082    for (;;$offset+=$len) {
1083        my ($id, $flags, $hi);
1084        if ($vers < 0x0300) {
1085            # version 2.2 frame header is 6 bytes
1086            last if $offset + 6 > $size;
1087            ($id, $hi, $len) = unpack("x${offset}a3Cn",$$dataPt);
1088            last if $id eq "\0\0\0";
1089            $len += $hi << 16;
1090            $offset += 6;
1091        } else {
1092            # version 2.3/2.4 frame header is 10 bytes
1093            last if $offset + 10 > $size;
1094            ($id, $len, $flags) = unpack("x${offset}a4Nn",$$dataPt);
1095            last if $id eq "\0\0\0\0";
1096            $offset += 10;
1097            # length is a "sync-safe" integer by the ID3v2.4 specification, but
1098            # reportedly some versions of iTunes write this as a normal integer
1099            # (ref http://www.id3.org/iTunes)
1100            while ($vers >= 0x0400 and $len > 0x7f and not $len & 0x80808080) {
1101                my $oldLen = $len;
1102                $len =  UnSyncSafe($len);
1103                if (not defined $len or $offset + $len + 10 > $size) {
1104                    $et->Warn('Invalid ID3 frame size');
1105                    last;
1106                }
1107                # check next ID to see if it makes sense
1108                my $nextID = substr($$dataPt, $offset + $len, 4);
1109                last if $$tagTablePtr{$nextID};
1110                # try again with the incorrect length word (patch for iTunes bug)
1111                last if $offset + $oldLen + 10 > $size;
1112                $nextID = substr($$dataPt, $offset + $len, 4);
1113                $len = $oldLen if $$tagTablePtr{$nextID};
1114                last; # yes, "while" was really a "goto" in disguise
1115            }
1116        }
1117        last if $offset + $len > $size;
1118        my $tagInfo = $et->GetTagInfo($tagTablePtr, $id);
1119        unless ($tagInfo) {
1120            my $otherTable = $otherTable{$tagTablePtr};
1121            $tagInfo = $et->GetTagInfo($otherTable, $id) if $otherTable;
1122            if ($tagInfo) {
1123                $et->WarnOnce("Frame '${id}' is not valid for this ID3 version", 1);
1124            } else {
1125                next unless $verbose or $et->Options('Unknown');
1126                $id =~ tr/-A-Za-z0-9_//dc;
1127                $id = 'unknown' unless length $id;
1128                unless ($$tagTablePtr{$id}) {
1129                    $tagInfo = { Name => "ID3_$id", Binary => 1 };
1130                    AddTagToTable($tagTablePtr, $id, $tagInfo);
1131                }
1132            }
1133        }
1134        # decode v2.3 and v2.4 flags
1135        my (%flags, %extra);
1136        if ($flags) {
1137            if ($vers < 0x0400) {
1138                # version 2.3 flags
1139                $flags & 0x80 and $flags{Compress} = 1;
1140                $flags & 0x40 and $flags{Encrypt}  = 1;
1141                $flags & 0x20 and $flags{GroupID}  = 1;
1142            } else {
1143                # version 2.4 flags
1144                $flags & 0x40 and $flags{GroupID}  = 1;
1145                $flags & 0x08 and $flags{Compress} = 1;
1146                $flags & 0x04 and $flags{Encrypt}  = 1;
1147                $flags & 0x02 and $flags{Unsync}   = 1;
1148                $flags & 0x01 and $flags{DataLen}  = 1;
1149            }
1150        }
1151        if ($flags{Encrypt}) {
1152            $et->WarnOnce('Encrypted frames currently not supported');
1153            next;
1154        }
1155        # extract the value
1156        my $val = substr($$dataPt, $offset, $len);
1157
1158        # reverse the unsynchronization
1159        $val =~ s/\xff\x00/\xff/g if $flags{Unsync};
1160
1161        # read grouping identity
1162        if ($flags{GroupID}) {
1163            length($val) >= 1 or $et->Warn("Short $id frame"), next;
1164            $val = substr($val, 1); # (ignore it)
1165        }
1166        # read data length
1167        my $dataLen;
1168        if ($flags{DataLen} or $flags{Compress}) {
1169            length($val) >= 4 or $et->Warn("Short $id frame"), next;
1170            $dataLen = unpack('N', $val);   # save the data length word
1171            $val = substr($val, 4);
1172        }
1173        # uncompress data
1174        if ($flags{Compress}) {
1175            if (eval { require Compress::Zlib }) {
1176                my $inflate = Compress::Zlib::inflateInit();
1177                my ($buff, $stat);
1178                $inflate and ($buff, $stat) = $inflate->inflate($val);
1179                if ($inflate and $stat == Compress::Zlib::Z_STREAM_END()) {
1180                    $val = $buff;
1181                } else {
1182                    $et->Warn("Error inflating $id frame");
1183                    next;
1184                }
1185            } else {
1186                $et->WarnOnce('Install Compress::Zlib to decode compressed frames');
1187                next;
1188            }
1189        }
1190        # validate data length
1191        if (defined $dataLen) {
1192            $dataLen = UnSyncSafe($dataLen);
1193            defined $dataLen or $et->Warn("Invalid length for $id frame"), next;
1194            $dataLen == length($val) or $et->Warn("Wrong length for $id frame"), next;
1195        }
1196        unless ($tagInfo) {
1197            next unless $verbose;
1198            %flags and $extra{Extra} = ', Flags=' . join(',', sort keys %flags);
1199            $et->VerboseInfo($id, $tagInfo,
1200                Table   => $tagTablePtr,
1201                Value   => $val,
1202                DataPt  => $dataPt,
1203                DataPos => $$dirInfo{DataPos},
1204                Size    => $len,
1205                Start   => $offset,
1206                %extra
1207            );
1208            next;
1209        }
1210#
1211# decode data in this frame (it is bad form to hard-code these, but the ID3 frame formats
1212# are so variable that it would be more work to define format types for each of them)
1213#
1214        my $lang;
1215        my $valLen = length($val);  # actual value length (after decompression, etc)
1216        if ($id =~ /^(TXX|TXXX)$/) {
1217            # two encoded strings separated by a null
1218            my @vals = DecodeString($et, $val);
1219            foreach (0..1) { $vals[$_] = '' unless defined $vals[$_]; }
1220            ($val = "($vals[0]) $vals[1]") =~ s/^\(\) //;
1221        } elsif ($id =~ /^T/ or $id =~ /^(IPL|IPLS)$/) {
1222            $val = DecodeString($et, $val);
1223        } elsif ($id =~ /^(WXX|WXXX)$/) {
1224            # one encoded string and one Latin string separated by a null
1225            my $enc = unpack('C', $val);
1226            my $url;
1227            if ($enc == 1 or $enc == 2) {
1228                ($val, $url) = ($val =~ /^(.(?:..)*?)\0\0(.*)/s);
1229            } else {
1230                ($val, $url) = ($val =~ /^(..*?)\0(.*)/s);
1231            }
1232            unless (defined $val and defined $url) {
1233                $et->Warn("Invalid $id frame value");
1234                next;
1235            }
1236            $val = DecodeString($et, $val);
1237            $url =~ s/\0.*//s;
1238            $val = length($val) ? "($val) $url" : $url;
1239        } elsif ($id =~ /^W/) {
1240            $val =~ s/\0.*//s;  # truncate at null
1241        } elsif ($id =~ /^(COM|COMM|ULT|USLT)$/) {
1242            $valLen > 4 or $et->Warn("Short $id frame"), next;
1243            $lang = substr($val,1,3);
1244            my @vals = DecodeString($et, substr($val,4), Get8u(\$val,0));
1245            foreach (0..1) { $vals[$_] = '' unless defined $vals[$_]; }
1246            $val = length($vals[0]) ? "($vals[0]) $vals[1]" : $vals[1];
1247        } elsif ($id eq 'USER') {
1248            $valLen > 4 or $et->Warn("Short $id frame"), next;
1249            $lang = substr($val,1,3);
1250            $val = DecodeString($et, substr($val,4), Get8u(\$val,0));
1251        } elsif ($id =~ /^(CNT|PCNT)$/) {
1252            $valLen >= 4 or $et->Warn("Short $id frame"), next;
1253            my ($cnt, @xtra) = unpack('NC*', $val);
1254            $cnt = ($cnt << 8) + $_ foreach @xtra;
1255            $val = $cnt;
1256        } elsif ($id =~ /^(PIC|APIC)$/) {
1257            $valLen >= 4 or $et->Warn("Short $id frame"), next;
1258            my ($hdr, $attr);
1259            my $enc = unpack('C', $val);
1260            if ($enc == 1 or $enc == 2) {
1261                $hdr = ($id eq 'PIC') ? ".(...)(.)((?:..)*?)\0\0" : ".(.*?)\0(.)((?:..)*?)\0\0";
1262            } else {
1263                $hdr = ($id eq 'PIC') ? ".(...)(.)(.*?)\0"        : ".(.*?)\0(.)(.*?)\0";
1264            }
1265            # remove header (encoding, image format or MIME type, picture type, description)
1266            $val =~ s/^$hdr//s or $et->Warn("Invalid $id frame"), next;
1267            my @attrs = ($1, ord($2), DecodeString($et, $3, $enc));
1268            my $i = 1;
1269            foreach $attr (@attrs) {
1270                # must store descriptions even if they are empty to maintain
1271                # sync between copy numbers when multiple images
1272                $et->HandleTag($tagTablePtr, "$id-$i", $attr);
1273                ++$i;
1274            }
1275        } elsif ($id eq 'POP' or $id eq 'POPM') {
1276            # _email, 00, rating(1), counter(4-N)
1277            my ($email, $dat) = ($val =~ /^([^\0]*)\0(.*)$/s);
1278            unless (defined $dat and length($dat)) {
1279                $et->Warn("Invalid $id frame");
1280                next;
1281            }
1282            my ($rating, @xtra) = unpack('C*', $dat);
1283            my $cnt = 0;
1284            $cnt = ($cnt << 8) + $_ foreach @xtra;
1285            $val = "$email $rating $cnt";
1286        } elsif ($id eq 'OWNE') {
1287            # enc(1), _price, 00, _date(8), Seller
1288            my @strs = DecodeString($et, $val);
1289            $strs[1] =~ s/^(\d{4})(\d{2})(\d{2})/$1:$2:$3 /s if $strs[1]; # format date
1290            $val = "@strs";
1291        } elsif ($id eq 'RVA' or $id eq 'RVAD') {
1292            my @dat = unpack('C*', $val);
1293            my $flag = shift @dat;
1294            my $bits = shift @dat or $et->Warn("Short $id frame"), next;
1295            my $bytes = int(($bits + 7) / 8);
1296            my @parse = (['Right',0,2,0x01],['Left',1,3,0x02],['Back-right',4,6,0x04],
1297                         ['Back-left',5,7,0x08],['Center',8,9,0x10],['Bass',10,11,0x20]);
1298            $val = '';
1299            while (@parse) {
1300                my $elem = shift @parse;
1301                my $j = $$elem[2] * $bytes;
1302                last if scalar(@dat) < $j + $bytes;
1303                my $i = $$elem[1] * $bytes;
1304                $val .= ', ' if $val;
1305                my ($rel, $pk, $b);
1306                for ($rel=0, $pk=0, $b=0; $b<$bytes; ++$b) {
1307                    $rel = $rel * 256 + $dat[$i + $b];
1308                    $pk  = $pk  * 256 + $dat[$j + $b]; # (peak - not used in printout)
1309                }
1310                $rel =-$rel unless $flag & $$elem[3];
1311                $val .= sprintf("%+.1f%% %s", 100 * $rel / ((1<<$bits)-1), $$elem[0]);
1312            }
1313        } elsif ($id eq 'RVA2') {
1314            my ($pos, $id) = $val=~/^([^\0]*)\0/s ? (length($1)+1, $1) : (1, '');
1315            my @vals;
1316            while ($pos + 4 <= $valLen) {
1317                my $type = Get8u(\$val, $pos);
1318                my $str = ({
1319                    0 => 'Other',
1320                    1 => 'Master',
1321                    2 => 'Front-right',
1322                    3 => 'Front-left',
1323                    4 => 'Back-right',
1324                    5 => 'Back-left',
1325                    6 => 'Front-centre',
1326                    7 => 'Back-centre',
1327                    8 => 'Subwoofer',
1328                }->{$type} || "Unknown($type)");
1329                my $db = Get16s(\$val,$pos+1) / 512;
1330                # convert dB to percent as displayed by iTunes 10.5
1331                # (not sure why I need to divide by 20 instead of 10 as expected - PH)
1332                push @vals, sprintf('%+.1f%% %s', 10**($db/20+2)-100, $str);
1333                # step to next channel (ignoring peak volume)
1334                $pos += 4 + int((Get8u(\$val,$pos+3) + 7) / 8);
1335            }
1336            $val = join ', ', @vals;
1337            $val .= " ($id)" if $id;
1338        } elsif ($id eq 'PRIV') {
1339            # save version number to set group1 name for tag later
1340            $$et{ID3_Ver} = $$tagTablePtr{GROUPS}{1};
1341            $et->HandleTag($tagTablePtr, $id, $val);
1342            next;
1343        } elsif ($$tagInfo{Format} or $$tagInfo{SubDirectory}) {
1344            $et->HandleTag($tagTablePtr, $id, undef, DataPt => \$val);
1345            next;
1346        } elsif ($id eq 'GRP1' or $id eq 'MVNM' or $id eq 'MVIN') {
1347            $val =~ s/(^\0+|\0+$)//g;   # (PH guess)
1348        } elsif (not $$tagInfo{Binary}) {
1349            $et->Warn("Don't know how to handle $id frame");
1350            next;
1351        }
1352        if ($lang and $lang =~ /^[a-z]{3}$/i and $lang ne 'eng') {
1353            $tagInfo = Image::ExifTool::GetLangInfo($tagInfo, lc $lang);
1354        }
1355        %flags and $extra{Extra} = ', Flags=' . join(',', sort keys %flags);
1356        $et->HandleTag($tagTablePtr, $id, $val,
1357            TagInfo => $tagInfo,
1358            DataPt  => $dataPt,
1359            DataPos => $$dirInfo{DataPos},
1360            Size    => $len,
1361            Start   => $offset,
1362            %extra
1363        );
1364    }
1365}
1366
1367#------------------------------------------------------------------------------
1368# Extract ID3 information from an audio file
1369# Inputs: 0) ExifTool object reference, 1) dirInfo reference
1370# Returns: 1 on success, 0 if this file didn't contain ID3 information
1371# - also processes audio data if any ID3 information was found
1372# - sets ExifTool DoneID3 to 1 when called, or to trailer size if an ID3v1 trailer exists
1373sub ProcessID3($$)
1374{
1375    my ($et, $dirInfo) = @_;
1376
1377    return 0 if $$et{DoneID3};  # avoid infinite recursion
1378    $$et{DoneID3} = 1;
1379
1380    # allow this to be called with either RAF or DataPt
1381    my $raf = $$dirInfo{RAF} || new File::RandomAccess($$dirInfo{DataPt});
1382    my ($buff, %id3Header, %id3Trailer, $hBuff, $tBuff, $eBuff, $tagTablePtr);
1383    my $rtnVal = 0;
1384    my $hdrEnd = 0;
1385    my $id3Len = 0;
1386
1387    # read first 3 bytes of file
1388    $raf->Seek(0, 0);
1389    return 0 unless $raf->Read($buff, 3) == 3;
1390#
1391# identify ID3v2 header
1392#
1393    while ($buff =~ /^ID3/) {
1394        $rtnVal = 1;
1395        $raf->Read($hBuff, 7) == 7 or $et->Warn('Short ID3 header'), last;
1396        my ($vers, $flags, $size) = unpack('nCN', $hBuff);
1397        $size = UnSyncSafe($size);
1398        defined $size or $et->Warn('Invalid ID3 header'), last;
1399        my $verStr = sprintf("2.%d.%d", $vers >> 8, $vers & 0xff);
1400        if ($vers >= 0x0500) {
1401            $et->Warn("Unsupported ID3 version: $verStr");
1402            last;
1403        }
1404        unless ($raf->Read($hBuff, $size) == $size) {
1405            $et->Warn('Truncated ID3 data');
1406            last;
1407        }
1408        # this flag only indicates use of unsynchronized frames in ID3v2.4
1409        if ($flags & 0x80 and $vers < 0x0400) {
1410            # reverse the unsynchronization
1411            $hBuff =~ s/\xff\x00/\xff/g;
1412        }
1413        my $pos = 10;
1414        if ($flags & 0x40) {
1415            # skip the extended header
1416            $size >= 4 or $et->Warn('Bad ID3 extended header'), last;
1417            my $len = unpack('N', $hBuff);
1418            if ($len > length($hBuff) - 4) {
1419                $et->Warn('Truncated ID3 extended header');
1420                last;
1421            }
1422            $hBuff = substr($hBuff, $len + 4);
1423            $pos += $len + 4;
1424        }
1425        if ($flags & 0x10) {
1426            # ignore v2.4 footer (10 bytes long)
1427            $raf->Seek(10, 1);
1428        }
1429        %id3Header = (
1430            DataPt   => \$hBuff,
1431            DataPos  => $pos,
1432            DirStart => 0,
1433            DirLen   => length($hBuff),
1434            Version  => $vers,
1435            DirName  => "ID3v$verStr",
1436        );
1437        $id3Len += length($hBuff) + 10;
1438        if ($vers >= 0x0400) {
1439            $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_4');
1440        } elsif ($vers >= 0x0300) {
1441            $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_3');
1442        } else {
1443            $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v2_2');
1444        }
1445        $hdrEnd = $raf->Tell();
1446        last;
1447    }
1448#
1449# read ID3v1 trailer if it exists
1450#
1451    my $trailSize = 0;
1452    if ($raf->Seek(-128, 2) and $raf->Read($tBuff, 128) == 128 and $tBuff =~ /^TAG/) {
1453        $trailSize = 128;
1454        %id3Trailer = (
1455            DataPt   => \$tBuff,
1456            DataPos  => $raf->Tell() - 128,
1457            DirStart => 0,
1458            DirLen   => length($tBuff),
1459        );
1460        $id3Len += length($tBuff);
1461        $rtnVal = 1;
1462        # load 'Enhanced TAG' information if available
1463        my $eSize = 227;    # size of ID3 Enhanced TAG info
1464        if ($raf->Seek(-$trailSize - $eSize, 2) and $raf->Read($eBuff, $eSize) == $eSize and $eBuff =~ /^TAG+/) {
1465            $id3Trailer{EnhancedTAG} = \$eBuff;
1466            $trailSize += $eSize;
1467        }
1468        $$et{DoneID3} = $trailSize; # save trailer size
1469    }
1470#
1471# read Lyrics3 trailer if it exists
1472#
1473    if ($raf->Seek(-$trailSize-15, 2) and $raf->Read($buff, 15) == 15 and $buff =~ /^(.{6})LYRICS(END|200)$/) {
1474        my $ver = $2;   # Lyrics3 version ('END' for version 1)
1475        my $len = ($ver eq 'END') ? 5100 : $1 + 15; # max Lyrics3 length
1476        my $tbl = GetTagTable('Image::ExifTool::ID3::Lyrics3');
1477        $len = $raf->Tell() if $len > $raf->Tell();
1478        if ($raf->Seek(-$len, 1) and $raf->Read($buff, $len) == $len and $buff =~ /LYRICSBEGIN/g) {
1479            my $pos = pos($buff);
1480            $$et{DoneID3} = $trailSize + $len - $pos + 11;  # update trailer length
1481            my $oldIndent = $$et{INDENT};
1482            $$et{INDENT} .= '| ';
1483            if ($et->Options('Verbose')) {
1484                $et->VPrint(0, "Lyrics3:\n");
1485                $et->VerboseDir('Lyrics3', undef, $len);
1486                if ($pos > 11) {
1487                    $buff = substr($buff, $pos - 11);
1488                    $pos = 11;
1489                }
1490                $et->VerboseDump(\$buff);
1491            }
1492            if ($ver eq 'END') {
1493                # Lyrics3 v1.00
1494                my $val = substr($buff, $pos, $len - $pos - 9);
1495                $et->HandleTag($tbl, 'LYR', $et->Decode($val, 'Latin'));
1496            } else {
1497                # Lyrics3 v2.00
1498                for (;;) {
1499                    # (note: the size field is 5 digits,, not 6 as per the documentation)
1500                    last unless $buff =~ /\G(.{3})(\d{5})/g;
1501                    my ($tag, $size) = ($1, $2);
1502                    $pos += 8;
1503                    last if $pos + $size > length($buff);
1504                    unless ($$tbl{$tag}) {
1505                        AddTagToTable($tbl, $tag, { Name => Image::ExifTool::MakeTagName("Lyrics3_$tag") });
1506                    }
1507                    $et->HandleTag($tbl, $tag, $et->Decode(substr($buff, $pos, $size), 'Latin'));
1508                    $pos += $size;
1509                    pos($buff) = $pos;
1510                }
1511                $pos == length($buff) - 15 or $et->Warn('Malformed Lyrics3 v2.00 block');
1512            }
1513            $$et{INDENT} = $oldIndent;
1514        } else {
1515            $et->Warn('Error reading Lyrics3 trailer');
1516        }
1517    }
1518#
1519# process the the information
1520#
1521    if ($rtnVal) {
1522        # first process audio data if it exists
1523        if ($$dirInfo{RAF}) {
1524            my $oldType = $$et{FILE_TYPE};   # save file type
1525            # check current file type first
1526            my @types = grep /^$oldType$/, @audioFormats;
1527            push @types, grep(!/^$oldType$/, @audioFormats);
1528            my $type;
1529            foreach $type (@types) {
1530                # seek to end of ID3 header
1531                $raf->Seek($hdrEnd, 0);
1532                # set type for this file if we are successful
1533                $$et{FILE_TYPE} = $type;
1534                my $module = $audioModule{$type} || $type;
1535                require "Image/ExifTool/$module.pm" or next;
1536                my $func = "Image::ExifTool::${module}::Process$type";
1537                # process the file
1538                no strict 'refs';
1539                &$func($et, $dirInfo) and last;
1540                use strict 'refs';
1541            }
1542            $$et{FILE_TYPE} = $oldType;      # restore original file type
1543        }
1544        # set file type to MP3 if we didn't find audio data
1545        $et->SetFileType('MP3');
1546        # record the size of the ID3 metadata
1547        $et->FoundTag('ID3Size', $id3Len);
1548        # process ID3v2 header if it exists
1549        if (%id3Header) {
1550            $et->VPrint(0, "$id3Header{DirName}:\n");
1551            $et->ProcessDirectory(\%id3Header, $tagTablePtr);
1552        }
1553        # process ID3v1 trailer if it exists
1554        if (%id3Trailer) {
1555            $et->VPrint(0, "ID3v1:\n");
1556            SetByteOrder('MM');
1557            $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v1');
1558            $et->ProcessDirectory(\%id3Trailer, $tagTablePtr);
1559            # process "Enhanced TAG" information if available
1560            if ($id3Trailer{EnhancedTAG}) {
1561                $et->VPrint(0, "ID3v1 Enhanced TAG:\n");
1562                $tagTablePtr = GetTagTable('Image::ExifTool::ID3::v1_Enh');
1563                $id3Trailer{DataPt} = $id3Trailer{EnhancedTAG};
1564                $id3Trailer{DataPos} -= 227; # (227 = length of Enhanced TAG block)
1565                $id3Trailer{DirLen} = 227;
1566                $et->ProcessDirectory(\%id3Trailer, $tagTablePtr);
1567            }
1568        }
1569    }
1570    # return file pointer to start of file to read audio data if necessary
1571    $raf->Seek(0, 0);
1572    return $rtnVal;
1573}
1574
1575#------------------------------------------------------------------------------
1576# Process ID3 directory
1577# Inputs: 0) ExifTool object reference, 1) dirInfo reference, 2) dummy tag table ref
1578sub ProcessID3Dir($$$)
1579{
1580    my ($et, $dirInfo, $tagTablePtr) = @_;
1581    $et->VerboseDir('ID3', undef, length ${$$dirInfo{DataPt}});
1582    return ProcessID3($et, $dirInfo);
1583}
1584
1585#------------------------------------------------------------------------------
1586# Extract ID3 information from an MP3 audio file
1587# Inputs: 0) ExifTool object reference, 1) dirInfo reference
1588# Returns: 1 on success, 0 if this wasn't a valid MP3 file
1589sub ProcessMP3($$)
1590{
1591    my ($et, $dirInfo) = @_;
1592    my $rtnVal = 0;
1593
1594    # must first check for leading/trailing ID3 information
1595    # (and process the rest of the file if found)
1596    unless ($$et{DoneID3}) {
1597        $rtnVal = ProcessID3($et, $dirInfo);
1598    }
1599
1600    # check for MPEG A/V data if not already processed above
1601    unless ($rtnVal) {
1602        my $raf = $$dirInfo{RAF};
1603        my $buff;
1604#
1605# extract information from first audio/video frame headers
1606# (if found in the first $scanLen bytes)
1607#
1608        # scan further into a file that should be an MP3
1609        my $scanLen = ($$et{FILE_EXT} and $$et{FILE_EXT} eq 'MP3') ? 8192 : 256;
1610        if ($raf->Read($buff, $scanLen)) {
1611            require Image::ExifTool::MPEG;
1612            if ($buff =~ /\0\0\x01(\xb3|\xc0)/) {
1613                # look for A/V headers in first 64kB
1614                my $buf2;
1615                $raf->Read($buf2, 0x10000 - $scanLen) and $buff .= $buf2;
1616                $rtnVal = 1 if Image::ExifTool::MPEG::ParseMPEGAudioVideo($et, \$buff);
1617            } else {
1618                # look for audio frame sync in first $scanLen bytes
1619                # (set MP3 flag to 1 so this will fail unless layer 3 audio)
1620                my $ext = $$et{FILE_EXT} || '';
1621                my $mp3 = ($ext eq 'MUS') ? 0 : 1;  # MUS files are MP2
1622                $rtnVal = 1 if Image::ExifTool::MPEG::ParseMPEGAudio($et, \$buff, $mp3);
1623            }
1624        }
1625    }
1626
1627    # check for an APE trailer if this was a valid A/V file and we haven't already done it
1628    if ($rtnVal and not $$et{DoneAPE}) {
1629        require Image::ExifTool::APE;
1630        Image::ExifTool::APE::ProcessAPE($et, $dirInfo);
1631    }
1632    return $rtnVal;
1633}
1634
16351;  # end
1636
1637__END__
1638
1639=head1 NAME
1640
1641Image::ExifTool::ID3 - Read ID3 meta information
1642
1643=head1 SYNOPSIS
1644
1645This module is used by Image::ExifTool
1646
1647=head1 DESCRIPTION
1648
1649This module contains definitions required by Image::ExifTool to extract ID3
1650information from audio files.  ID3 information is found in MP3 and various
1651other types of audio files.
1652
1653=head1 AUTHOR
1654
1655Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
1656
1657This library is free software; you can redistribute it and/or modify it
1658under the same terms as Perl itself.
1659
1660=head1 REFERENCES
1661
1662=over 4
1663
1664=item L<http://www.id3.org/>
1665
1666=item L<http://www.mp3-tech.org/>
1667
1668=item L<http://www.fortunecity.com/underworld/sonic/3/id3tag.html>
1669
1670=item L<https://id3.org/Lyrics3>
1671
1672=back
1673
1674=head1 SEE ALSO
1675
1676L<Image::ExifTool::TagNames/ID3 Tags>,
1677L<Image::ExifTool(3pm)|Image::ExifTool>
1678
1679=cut
1680
1681