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