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