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