Reading ID3V2

Instruction how to read ID3 tag version 2.

According to the ID3v2.3 specification header contians following information:

  • File identifier (3 bytes): ID3
  • Version (2 bytes): $03 00
  • Flags (1 byte): %abc00000
  • Size (4 bytes): 4 * %0xxxxxxx

So first of all script should check first 10 bytes:

set fr [open $fileN r]
set data [read $fr 10]
binary scan $data a3c2b8I1 id3header ver flags headSize

Let's try to extract some info if we are sure that there is ID3 tag.

According to the specification each frame has header contains following informations:

  • Frame ID (4 bytes/characters): $xx xx xx xx
  • Size (4 bytes): $xx xx xx xx
  • Flags (2 bytes): $xx xx

All we need to do is find Frame ID extract its size and print out its content:

set data [read $fr 10]
binary scan $data a4I1B16 frameType frameSize frameFlags

Everything is quite clear and easy but ... sometimes saved frame size is wrong, then is not so easy to find next frame. To avoid situation that script is trying to read wrond data I've crated $allFrames list of almost all possible frames. Script checks all received data and corrects position in the file if wrong frame ID is detected.

Additionaly $frameToDisplay list is created to limit printed out data. All printed out data has a human readable names defined via frameMap array:

set frameMap(COMM) Comment
set frameMap(TCOM) Composer
set frameMap(TIT1) "Content group description"
set frameMap(TIT2) "Title"
set frameMap(TPE1) "Lead performer(s)/Soloist(s)"
set frameMap(TPE2) "Band"
set frameMap(TPE3) "Conductor"
set frameMap(TPE4) "Interpreted, remixed, or otherwise modified by"
set frameMap(TYER) "Year"

Script:

set debugMode 0
#set debugMode 1

#procedure to print out debugs if enabled
proc putss {str} {
        global debugMode
        if {$debugMode} {puts $str}
}

proc parseID3v2 {fileN} {
        set fr [open $fileN r]
        set frameToDisplay {COMM TCOM TIT1 TIT2 TPE1 TPE2 TPE3 TPE4 TYER}
        set allFrames {AENC APIC COMM COMR ENCR EQUA ETCO GEOB GRID IPLS LINK MCDI MLLT OWNE PRIV PCNT POPM POSS RBUF RVAD\
        RVRB SYLT SYTC TALB TBPM TCOM TCON TCOP TDAT TDLY TENC TEXT TFLT TIME TIT1 TIT2 TIT3 TKEY TLAN TLEN TMED TOAL TOFN\
        TOLY TOPE TORY TOWN TPE1 TPE2 TPE3 TPE4 TPOS TPUB TRCK TRDA TRSN TRSO TSIZ TSRC TSSE TYER TXXX UFID USER USLT WCOM\
        WCOP WOAF WOAR WOAS WORS WPAY WPUB WXXX}
        
        set frameMap(COMM) Comment
        set frameMap(TCOM) Composer
        set frameMap(TIT1) "Content group description"
        set frameMap(TIT2) "Title"
        set frameMap(TPE1) "Lead performer(s)/Soloist(s)"
        set frameMap(TPE2) "Band"
        set frameMap(TPE3) "Conductor"
        set frameMap(TPE4) "Interpreted, remixed, or otherwise modified by"
        set frameMap(TYER) "Year"

        #read header and check basic info
        set data [read $fr 10]
        binary scan $data a3c2b8I1 id3header ver flags headSize
        putss "### Basic file info ###"
        putss $id3header
        putss $ver
        putss $flags
        putss $headSize
        putss "########################"
        
        #read and check frame basic info
        set frameSize 0
        while {[tell $fr] < $headSize} {
                set data [read $fr 10]
                set prevFrameSize $frameSize
                binary scan $data a4I1B16 frameType frameSize frameFlags
                if {[expr 0x[format %x [scan 0100000000000000 %i]] & 0x[format %x [scan $frameFlags %i]]] eq 2147483647} {
                        putss "alter file detected"
                }
                putss "[tell $fr] frameType: $frameType, frameSize: $frameSize, frameFlags: $frameFlags" 
                if {[lsearch $allFrames $frameType] < 0} {
                        putss "Unknown frame $frameType detected!"
                        putss [tell $fr]
                        set currentPos [tell $fr]
                        putss "Move back $prevFrameSize and find first defined frame"
                        seek $fr [expr $currentPos - $prevFrameSize]
                        putss [tell $fr]
                        if {[set newPos [___findNextFrame $fr $allFrames $headSize]] > 0} {
                                seek $fr $newPos
                                continue
                        }
                }
                if {$frameSize < 0} {puts "Wrong id3 tag structure!"; return -1}
                set data [read $fr [expr $frameSize]]
                if {[lsearch $frameToDisplay $frameType] >= 0} {
                        puts "$frameMap($frameType) : $data"
                }
        }
        close $fr
}

proc ___findNextFrame {fileHandler frameList headerSize} {
        set curretnPos [tell $fileHandler]
        set index 0
        while {[tell $fileHandler] < $headerSize} {
                seek $fileHandler [expr $curretnPos + $index]
                set data [read $fileHandler 4]
                if {[lsearch $frameList $data] >= 0} {
                        set newPos [expr $curretnPos + $index]
                        putss "New frame $data detected under position $newPos"
                        return $newPos
                } 
                incr index
        }
        return -1
}

To test the script call:

parseID3v2 test.mp3