Simple ID3v2.3.x Tag Reader for MP3 Files

Share your advanced PureBasic knowledge/code with the community.
ebs
Enthusiast
Enthusiast
Posts: 557
Joined: Fri Apr 25, 2003 11:08 pm

Simple ID3v2.3.x Tag Reader for MP3 Files

Post by ebs »

The subject says it all! Here's the code:

Code: Select all

Structure ID3v2
  Title.s
  Artist.s
  Album.s
  Comment.s
  Year.s
  Genre.s
  Composer.s
  URL.s
  OrgArtist.s
  Copyright.s
  EncodedBy.s
  Encoder.s
  Track.s
EndStructure
Global TagID3v2.ID3v2

Declare.l GetID3v2Tag(FileName.s)

Repeat 
  MP3File.s = OpenFileRequester("Select MP3 File", "", "MP3 Files (*.mp3)|*.mp3", 0)
  If GetID3v2Tag(MP3File)
    ; display tag contents
    Debug "Album: " + TagID3v2\Album
    Debug "Artist: " + TagID3v2\Artist
    Debug "Comment: " + TagID3v2\Comment
    Debug "Composer: " + TagID3v2\Composer
    Debug "Copyright: " + TagID3v2\Copyright
    Debug "Encoded by: " + TagID3v2\EncodedBy
    Debug "Encoder: " + TagID3v2\Encoder
    Debug "Genre: " + TagID3v2\Genre
    Debug "Original Artist: " + TagID3v2\OrgArtist
    Debug "Title: " + TagID3v2\Title
    Debug "Track: " + TagID3v2\Track
    Debug "URL: " + TagID3v2\URL
    Debug "Year: " + TagID3v2\Year
    Debug ""
  EndIf
Until MP3File = ""
End

;- read ID3v2.3.x tag fields from MP3 file
Procedure.l GetID3v2Tag(FileName.s)
  If FileName = ""
    ProcedureReturn #False
  EndIf
  
  ReadFile(1, FileName)
  
  ID3.s = Space(3)
  ReadData(@ID3, 3)
  ; must be ID3v2.3.x
  If ID3 = "ID3" And ReadByte() = $03
    ID3Exists = #True 
    ; skip revision and flag bytes
    ReadWord()
    ; get tag size
    ID3Size.l = 0
    For Byte.l = 3 To 0 Step -1
      ID3Size + (ReadByte() << (7*Byte)) 
    Next
  Else
    ; no ID3v2.3.x tag present
    CloseFile(1)
    ProcedureReturn #False
  EndIf
  
  ; clear tag contents
  TagID3v2\Album = ""
  TagID3v2\Artist = ""
  TagID3v2\Comment = ""
  TagID3v2\Composer = ""
  TagID3v2\Copyright = ""
  TagID3v2\EncodedBy = ""
  TagID3v2\Encoder = ""
  TagID3v2\Genre = ""
  TagID3v2\OrgArtist = ""
  TagID3v2\Title = ""
  TagID3v2\Track = ""
  TagID3v2\URL = ""
  TagID3v2\Year = ""
  
  Size.l = 0
  Repeat
    ; get frames until no more
    FrameID.s = Space(4)
    ReadData(@FrameID, 4)
    If Asc(Left(FrameID, 1)) = 0
      ; no more frames
      CloseFile(1)
      ProcedureReturn #True
    EndIf
    
    ; get frame size
    FrameSize.l = 0
    For Byte.l = 3 To 0 Step -1
      FrameSize + (ReadByte() << (7*Byte)) 
    Next
    ; add frame size to total size
    Size + FrameSize
    
    ; skip flag and language bytes
    ReadWord()
    ReadByte()
    
    ; get frame contents
    ; subtract one for language byte
    FrameSize - 1
    Contents.s = Space(FrameSize)
    ReadData(@Contents, FrameSize)
    
    ; put frame contents into structure
    Select FrameID
      Case "TALB"
        TagID3v2\Album = Contents
      Case "TCOM"
        TagID3v2\Composer = Contents
      Case "TCON"
        TagID3v2\Genre = Contents
      Case "TCOP"
        TagID3v2\Copyright = Contents
      Case "TENC"
        TagID3v2\EncodedBy = Contents
      Case "TIT2"
        TagID3v2\Title = Contents
      Case "TOPE"
        TagID3v2\OrgArtist = Contents
      Case "TPE1"
        TagID3v2\Artist = Contents
      Case "TRCK"
        TagID3v2\Track = Contents
      Case "TSSE"
        TagID3v2\Encoder = Contents
      Case "TYER"
        TagID3v2\Year = Contents
      Case "COMM"
        TagID3v2\Comment = Contents
      Case "WXXX"
        TagID3v2\URL = Contents
    EndSelect
  ; stop if tag size reached/exceeded  
  Until Size >= ID3Size

  CloseFile(1)
  ProcedureReturn #True
EndProcedure
naw
Enthusiast
Enthusiast
Posts: 573
Joined: Fri Apr 25, 2003 4:57 pm

Post by naw »

Nice Stuff - I'm in the process of converting something I found on the German forum - but the code is quite complex - I find this easier to understand...

I wonder, though - I use MusicMatch JukeBox which seems to use some non-standard ID3 Tags that it appears to dump into the "Comments" TAG...

I'm struggling to extract those TAGS & their values...
Ta - N
User avatar
CONVERT
Enthusiast
Enthusiast
Posts: 130
Joined: Fri May 02, 2003 12:19 pm
Location: France

Thank you very much.

Post by CONVERT »

Thank you very much, ebs, for your code. It allows me to make jackets for my CD.
Jean.
PureBasic 6.20 beta 2 (x64) | Windows 10 Pro x64 | Intel(R) Core(TM) i7-8700 CPU @ 3.20Ghz 16 GB RAM, SSD 500 GB, PC locally assembled.
Come back to 6.11 LTS 64 bits because of an issue with #PB_ComboBox_UpperCase in ComboBoxGadget() (Oct. 10, 2024).
Killswitch
Enthusiast
Enthusiast
Posts: 731
Joined: Wed Apr 21, 2004 7:12 pm

Post by Killswitch »

Is this code freeware? i.e. Is it ok to be used in commercial applications?
~I see one problem with your reasoning: the fact is thats not a chicken~
ebs
Enthusiast
Enthusiast
Posts: 557
Joined: Fri Apr 25, 2003 11:08 pm

Post by ebs »

Killswitch wrote:Is this code freeware? i.e. Is it ok to be used in commercial applications?
Anything I post here is meant to be used by others, in any application you like (freeware, commercial or otherwise).
Go right ahead, but please include me in your "about" box or readme file!

Regards,
Eric Schuyler
orange-blue
User
User
Posts: 24
Joined: Wed Apr 06, 2005 10:21 am

Post by orange-blue »

nice, but I don't have many songs with ID3 v2.x Tags :(
Do you also have a code for reading ID3 v1 Tags?
Would be nice. :D
KarLKoX
Enthusiast
Enthusiast
Posts: 681
Joined: Mon Oct 06, 2003 7:13 pm
Location: France
Contact:

Post by KarLKoX »

Code: Select all

Structure ID3TagV1Tag
     tag.s
     title.s
     artist.s
     album.s
     year.s
     comments.s
     genre.s
     track.s
EndStructure

#READ_INPUT_FILE_TAG          = 1
#WRITE_INPUT_FILE_TAG         = 2

Procedure.s padString(theString.s, dummy.b)
Protected m_idx.b
Protected m_lent.l
Protected m_newString.s

     If dummy
          m_len = 28
     Else
          m_len = 30
     EndIf
     
     m_newString = theString
     For m_idx = Len(theString) To m_len
          m_newString + Chr(0)
     Next m_idx
     Debug "m_newString.s : " + m_newString.s

     ProcedureReturn m_newString

EndProcedure

;---- HasTagV1
; Purpose: Check if a Tag V1.x exists
; Require: file ==> input file
; Return : #TRUE if the tag exists
;          #FALSE otherwise
Procedure HasTagV1(file.s)
Protected hOrgFile.l
Protected m_tag.s

     hOrgFile = ReadFile(#pb_any, file)
     If (hOrgFile <= 0)
          MessageRequester("HasTagV1::Error", "Error occured while reading " + file, 0)
          ProcedureReturn #FALSE
     EndIf     
     FileSeek(Lof()-128)
     m_tag = Space(3)
     ReadData(@m_tag, 3)
     ; Rewind
     FileSeek(0)
     CloseFile(hOrgFile)
    
     If (m_tag = "TAG")
         ProcedureReturn #TRUE
     Else
         ProcedureReturn #FALSE
     EndIf
    
EndProcedure

;---- ReadTagV1
; Purpose: Read a IDTag V1.0/1.1
; Require: file ==> input file
; Return : fill the m_tagV1Info structure if the tag exist
Procedure.b ReadTagV1(file.s)
Protected hOrgFile.l
Dim        m_temp.b(128)

     If HasTagV1(file) = #FALSE
        ;MessageRequester("Error", "No IdTagV1 found.", 0)
        ProcedureReturn #FALSE
     EndIf
    
     hOrgFile = ReadFile(#READ_INPUT_FILE_TAG, file)
     If (hOrgFile <= 0)
          MessageRequester("ReadTagV1::Error", "Error occured while reading " + file, 0)
        ProcedureReturn #FALSE
     EndIf
     FileSeek(Lof()-128)
     ReadData(@m_temp(), 128)
     CloseFile(#READ_INPUT_FILE_TAG)
            
     m_tagV1Info\tag           = PeekS(m_temp(),3)
     m_tagV1Info\title         = PeekS(m_temp()+3,30)
     m_tagV1Info\artist        = PeekS(m_temp()+33,30)
     m_tagV1Info\album         = PeekS(m_temp()+63,30)
     m_tagV1Info\year          = PeekS(m_temp()+93,4)
     m_tagV1Info\comments      = PeekS(m_temp()+97,30)
     ; IdTag V1.1 extension : the last comments byte is the track number
     If Right(m_tagV1Info\comments, 1) <> ""
         m_tagV1Info\track = PeekS(m_temp()+126, 1) ;Right(m_tagV1Info\comments, 1)
     EndIf
     m_tagV1Info\genre = Chr(PeekB(m_temp()+127))

     ProcedureReturn #TRUE
EndProcedure

;---- WriteTagV1
; Purpose: Write a IDtag V1.0/1.1
; Require: file ==> input file
;          bRemove ==> #TRUE if you want only to remove the tag
;                      #FALSE otherwise
; Return : nothing
Procedure WriteTagV1(file.s, bRemove.b, *theTag.ID3TagV1Tag)
Protected hOrgFile.l
Protected m_compteur.l
Protected m_ID3.s
Protected m_offset.l

     If HasTagV1(fichier)
       m_offset = 128 
     Else
      m_offset = 0
     EndIf
    
     hOrgFile = OpenFile(#WRITE_INPUT_FILE_TAG, file)
     If (hOrgFile = 0)
          MessageRequester("WriteTagV1::Error", "Error occured while reading " + file, 0)
          ProcedureReturn
     EndIf
     
     UseFile(#WRITE_INPUT_FILE_TAG)
     FileSeek(Lof()-m_offset)
     If bRemove = #FALSE
        m_ID3 = Space(3)
        m_ID3 = "TAG"
        WriteData(@m_ID3, 3)
        WriteData(*theTag\title, 30)
        WriteData(*theTag\artist, 30)
        WriteData(*theTag\album, 30)
        WriteData(*theTag\year, 4)
        ; Tag 1.1 extension : write the track
        If (Str(*theTag\track) <> "")
          dummy$ = padString(*theTag\comments, #TRUE)
          WriteData(@dummy$, 28)
          WriteByte(0)
          WriteByte(Val(*theTag\track))
        Else
          WriteData(padString(*theTag\comments, #FALSE), 30)
        EndIf
        WriteByte(Val(*theTag\genre))
     EndIf
    
     CloseFile(#WRITE_INPUT_FILE_TAG)
    
EndProcedure

Last edited by KarLKoX on Fri Aug 05, 2005 11:40 pm, edited 2 times in total.
"Qui baise trop bouffe un poil." P. Desproges

http://karlkox.blogspot.com/
orange-blue
User
User
Posts: 24
Joined: Wed Apr 06, 2005 10:21 am

Post by orange-blue »

hey cool! THX! :D
User avatar
Joakim Christiansen
Addict
Addict
Posts: 2452
Joined: Wed Dec 22, 2004 4:12 pm
Location: Norway
Contact:

Post by Joakim Christiansen »

I converted this to PB4:

Code: Select all

Structure ID3v2 
  Length.s
  Title.s 
  Artist.s 
  Album.s 
  Comment.s 
  Year.s 
  Genre.s 
  Composer.s 
  URL.s 
  OrgArtist.s 
  Copyright.s 
  EncodedBy.s 
  Encoder.s 
  Track.s 
EndStructure 
Global TagID3v2.ID3v2 

Declare.l GetID3v2Tag(FileName.s) 

Repeat 
  MP3File.s = OpenFileRequester("Select MP3 File", "", "MP3 Files (*.mp3)|*.mp3", 0) 
  If GetID3v2Tag(MP3File) 
    ; display tag contents 
    Debug "Album: " + TagID3v2\Album 
    Debug "Artist: " + TagID3v2\Artist 
    Debug "Comment: " + TagID3v2\Comment 
    Debug "Composer: " + TagID3v2\Composer 
    Debug "Copyright: " + TagID3v2\Copyright 
    Debug "Encoded by: " + TagID3v2\EncodedBy 
    Debug "Encoder: " + TagID3v2\Encoder 
    Debug "Genre: " + TagID3v2\Genre 
    Debug "Original Artist: " + TagID3v2\OrgArtist 
    Debug "Length: " + TagID3v2\Length
    Debug "Title: " + TagID3v2\Title 
    Debug "Track: " + TagID3v2\Track 
    Debug "URL: " + TagID3v2\URL 
    Debug "Year: " + TagID3v2\Year 
    Debug "" 
  EndIf 
Until MP3File = "" 
End 

;- read ID3v2.3.x tag fields from MP3 file 
Procedure.l GetID3v2Tag(FileName.s) 
  If FileName = "" 
    ProcedureReturn #False 
  EndIf 
  
  ReadFile(1, FileName) 
  
  ID3.s = Space(3) 
  ReadData(1,@ID3, 3) 
  ; must be ID3v2.3.x 
  If ID3 = "ID3" And ReadByte(1) = $03 
    ID3Exists = #True 
    ; skip revision and flag bytes 
    ReadWord(1) 
    ; get tag size 
    ID3Size.l = 0 
    For Byte.l = 3 To 0 Step -1 
      ID3Size + (ReadByte(1) << (7*Byte)) 
    Next 
  Else 
    ; no ID3v2.3.x tag present 
    CloseFile(1) 
    ProcedureReturn #False 
  EndIf 
  
  ; clear tag contents 
  TagID3v2\Album = "" 
  TagID3v2\Artist = "" 
  TagID3v2\Comment = "" 
  TagID3v2\Composer = "" 
  TagID3v2\Copyright = "" 
  TagID3v2\EncodedBy = "" 
  TagID3v2\Encoder = "" 
  TagID3v2\Genre = "" 
  TagID3v2\OrgArtist = "" 
  TagID3v2\Length = ""
  TagID3v2\Title = "" 
  TagID3v2\Track = "" 
  TagID3v2\URL = "" 
  TagID3v2\Year = "" 
  
  Size.l = 0 
  Repeat 
    ; get frames until no more 
    FrameID.s = Space(4) 
    ReadData(1,@FrameID, 4) 
    If Asc(Left(FrameID, 1)) = 0 
      ; no more frames 
      CloseFile(1) 
      ProcedureReturn #True 
    EndIf 
    
    ; get frame size 
    FrameSize.l = 0 
    For Byte.l = 3 To 0 Step -1 
      FrameSize + (ReadByte(1) << (7*Byte)) 
    Next 
    ; add frame size to total size 
    Size + FrameSize 
    
    ; skip flag and language bytes 
    ReadWord(1) 
    ReadByte(1) 
    
    ; get frame contents 
    ; subtract one for language byte 
    FrameSize - 1 
    Contents.s = Space(FrameSize) 
    ReadData(1,@Contents, FrameSize) 
    
    ; put frame contents into structure 
    Select FrameID 
      Case "TALB" 
        TagID3v2\Album = Contents 
      Case "TCOM" 
        TagID3v2\Composer = Contents 
      Case "TCON" 
        TagID3v2\Genre = Contents 
      Case "TCOP" 
        TagID3v2\Copyright = Contents 
      Case "TENC" 
        TagID3v2\EncodedBy = Contents 
      Case "TIT2" 
        TagID3v2\Title = Contents 
      Case "TOPE" 
        TagID3v2\OrgArtist = Contents 
      Case "TPE1" 
        TagID3v2\Artist = Contents 
      Case "TRCK" 
        TagID3v2\Track = Contents 
      Case "TLEN"
        TagID3v2\Length = Contents 
      Case "TSSE" 
        TagID3v2\Encoder = Contents 
      Case "TYER" 
        TagID3v2\Year = Contents 
      Case "COMM" 
        TagID3v2\Comment = Contents 
      Case "WXXX" 
        TagID3v2\URL = Contents 
    EndSelect 
  ; stop if tag size reached/exceeded  
  Until Size >= ID3Size 

  CloseFile(1) 
  ProcedureReturn #True 
EndProcedure
But I'm having problems with some (most) mp3 files, they make it crash... :?
I like logic, hence I dislike humans but love computers.
User avatar
kenmo
Addict
Addict
Posts: 2032
Joined: Tue Dec 23, 2003 3:54 am

Post by kenmo »

JC,
I wrote my own ID3 read/write routines a while back, and they are still in PB3 syntax so I won't bother to post them... but comparing yours and mine the only significant difference I see is that where you have

Code: Select all

ID3Size + (ReadByte(1) << (7*Byte))
I have

Code: Select all

ID3Size + ((ReadByte()&%01111111) << (7 * Byte))
and the same for FrameSize.
I actually don't remember what the bit masking is for, but I'm pretty sure it fixed my program from crashing almost 100%.

ETA - I just tried your procedure, found a mp3 that crashed, added my fix, and it works.
User avatar
Joakim Christiansen
Addict
Addict
Posts: 2452
Joined: Wed Dec 22, 2004 4:12 pm
Location: Norway
Contact:

Post by Joakim Christiansen »

kenmo wrote:JC,
I wrote my own ID3 read/write routines a while back, and they are still in PB3 syntax so I won't bother to post them... but comparing yours and mine the only significant difference I see is that where you have

Code: Select all

ID3Size + (ReadByte(1) << (7*Byte))
I have

Code: Select all

ID3Size + ((ReadByte()&%01111111) << (7 * Byte))
and the same for FrameSize.
I actually don't remember what the bit masking is for, but I'm pretty sure it fixed my program from crashing almost 100%.

ETA - I just tried your procedure, found a mp3 that crashed, added my fix, and it works.
Thank you kenmo! :)
But I will try to make my own anyway, just to learn.
I like logic, hence I dislike humans but love computers.
User avatar
kenmo
Addict
Addict
Posts: 2032
Joined: Tue Dec 23, 2003 3:54 am

Post by kenmo »

About that bitmasking, on the ID3 specifications on id3.org it reads

Code: Select all

The ID3v2 tag size is encoded with four bytes where the most significant bit (bit 7) is set to zero in every byte, making a total of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is represented as $00 00 02 01.
So if the MSB is supposed to be 0, I don't know why any mp3 encoder would set it to 1... and if the tag reader doesn't cancel that out, I believe the 1 on the MSB would make the length value negative or too large depending on whether or not it is signed... either way would explain the crashing.
KarLKoX
Enthusiast
Enthusiast
Posts: 681
Joined: Mon Oct 06, 2003 7:13 pm
Location: France
Contact:

Post by KarLKoX »

Code: Select all

Procedure.l GetSizeV2(fichier.s)
Protected m_taille.l, FIO.l
Dim        m_byte.b(3)

     FIO = ReadFile(#Pb_Any,fichier)
     if FIO <= 0
       procedurereturn 0
     endif
     FileSeek(FIO,6)    
     ReadData(FIO,@m_byte(), 3)
    
     m_taille = (m_byte(0) << 21) | (m_byte(1) << 14) | (m_byte(2) << 7) | m_byte(3)
     ; Add the size of the frame header    
     m_taille + 10    
     closefile(FIO)

     procedurereturn m_taille

EndProcedure
http://sbougribate.free.fr/Files/PureBasic/IDTags.pb
"Qui baise trop bouffe un poil." P. Desproges

http://karlkox.blogspot.com/
User avatar
Joakim Christiansen
Addict
Addict
Posts: 2452
Joined: Wed Dec 22, 2004 4:12 pm
Location: Norway
Contact:

Post by Joakim Christiansen »

Neat KarLKoX! :D
I like logic, hence I dislike humans but love computers.
User avatar
kenmo
Addict
Addict
Posts: 2032
Joined: Tue Dec 23, 2003 3:54 am

Post by kenmo »

Don't forget to check for the ID3 $03 header though... :wink: I just tried that on a mp3 without ID3 and I got a negative result.
Post Reply