Version 7 of Reading MP3 file info

Updated 2004-11-25 00:13:58

by Martin Lemburg aka male


I searched the internet to get information about reading information from MP3 files like sampling rate, bit rate, duration and so on.

So I found the page http://www.dv.co.yu/mpgscript/mpeghdr.htm , from where I got my first impression how to start.

Then I downloaded the sources from the author of this page and the sources of snack to build this MP3 file information reader in pure tcl.

I tried it with several MP3 files and got satisfying results, what doesn't mean, that there are no errors!

Please feel so free to test and to give comments, suggestions, ...! Thanks!


usage: mp3InfoReader fileName arrayVarName ?debugFlag?

fileName
path to the MP3 file
arrayVarName
name of the array variable to be used as container for the MP3 information, will be erased before filled
debugFlag
boolean - true if information about the first 2 frame headers shouldn't be deleted and be returned

 namespace eval ::mp3InfoReader {
    variable bitRateTable;
    variable sampleRateTable;
    variable channelModeTable;

    array set bitRateTable {
       0.1.0      0   0.1.1      8   0.1.2     16   0.1.3     24
       0.1.4     32   0.1.5     40   0.1.6     48   0.1.7     56
       0.1.8     64   0.1.9     80   0.1.10    96   0.1.11   112
       0.1.12   128   0.1.13   144   0.1.14   160   0.1.15    -1
       0.3.0      0   0.3.1     32   0.3.2     48   0.3.3     56
       0.3.4     64   0.3.5     80   0.3.6     96   0.3.7    112
       0.3.8    128   0.3.9    144   0.3.10   160   0.3.11   176
       0.3.12   192   0.3.13   224   0.3.14   256   0.3.15    -1
       3.1.0      0   3.1.1     32   3.1.2     40   3.1.3     48
       3.1.4     56   3.1.5     64   3.1.6     80   3.1.7     96
       3.1.8    112   3.1.9    128   3.1.10   160   3.1.11   192
       3.1.12   224   3.1.13   256   3.1.14   320   3.1.15    -1
       3.2.0      0   3.2.1     32   3.2.2     48   3.2.3     56
       3.2.4     64   3.2.5     80   3.2.6     96   3.2.7    112
       3.2.8    128   3.2.9    160   3.2.10   192   3.2.11   224
       3.2.12   256   3.2.13   320   3.2.14   384   3.2.15    -1
       3.3.0      0   3.3.1     32   3.3.2     64   3.3.3     96
       3.3.4    128   3.3.5    160   3.3.6    192   3.3.7    224
       3.3.8    256   3.3.9    288   3.3.10   320   3.3.11   352
       3.3.12   384   3.3.13   416   3.3.14   448   3.3.15    -1
    }

    array set sampleRateTable {
       0.0   11025   0.1   12000   0.2    8000
       2.0   22050   2.1   24000   2.2   16000
       3.0   44100   3.1   48000   3.2   32000
    }

    array set channelModeTable {
       3.0   {4 31}   3.1   {8 31}                 3.2   {12 31}         3.3   {16 31}
       1.0   {}       1.1   {"intensity stereo"}   1.2   {"ms stereo"}   1.3   {"intensity stereo" "ms stereo"}
    };

    proc readI4 {fileId} {
       if {[binary scan [read $fileId 4] I I4] != 1} {
          error "couldn't read a 4byte bit-endian integer value from \"$fileId\"";
       }

       return $I4
    }

    variable genres {
        Blues                        {Classic Rock}                Country                        Dance
        Disco                        Funk                        Grunge                        Hip-Hop
        Jazz                        Metal                        {New Age}                Oldies
        Other                        Pop                        R&B                        Rap
        Reggae                        Rock                        Techno                        Industrial
        Alternative                Ska                        {Death Metal}                Pranks
        Soundtrack                Euro-Techno                Ambient                        Trip-Hop
        Vocal                        Jazz+Funk                Fusion                        Trance
        Classical                Instrumental                Acid                        House
        Game                        {Sound Clip}                Gospel                        Noise
        AlternRock                Bass                        Soul                        Punk
        Space                        Meditative                {Instrumental Pop}        {Instrumental Rock}
        Ethnic                        Gothic                        Darkwave                Techno-Industrial
        Electronic                Pop-Folk                Eurodance                Dream
        {Southern Rock}        Comedy                        Cult                        Gangsta
        {Top 40}                {Christian Rap}                Pop/Funk                Jungle
        {Native American}        Cabaret                        {New Wave}                Psychadelic
        Rave                        Showtunes                Trailer                        Lo-Fi
        Tribal                        {Acid Punk}                {Acid Jazz}                Polka
        Retro                        Musical                        {Rock & Roll}                {Hard Rock}
        Folk                        Folk-Rock                {National Folk}                Swing
        {Fast Fusion}                Bebob                        Latin                        Revival
        Celtic                        Bluegrass                Avantgarde                {Gothic Rock}
        {Progressive Rock}        {Psychedelic Rock}        {Symphonic Rock}        {Slow Rock}
        {Big Band}                Chorus                        {Easy Listening}        Acoustic
        Humour                        Speech                        Chanson                        Opera
        {Chamber Music}        Sonata                        Symphony                {Booty Brass}
        Primus                        {Porn Groove}                Satire                        {Slow Jam}
        Club                        Tango                        Samba                        Folklore
        Ballad                        {Power Ballad}                {Rhytmic Soul}                Freestyle
        Duet                        {Punk Rock}                {Drum Solo}                {A Capela}
        Euro-House                {Dance Hall}
    }


    proc mpegBin {byte2} {
       return [expr {($byte2 >> 3) & 0x3}];
    }

    proc mpeg {byte2} {
       switch -exact -- [mpegBin $byte2] {
          0 {return 2.5;}
          2 {return 2;}
          3 {return 1;}
       }

       return 0;
    } 

    proc layerBin {byte2} {
       return [expr {($byte2 >> 1) & 0x3}];
    }

    proc layer {byte2} {
       set layer [expr {4 - [layerBin $byte2]}];

       if {$layer > 3} {
          return 0;
       }

       return $layer
    } 

    proc protection {byte2} {
       return [expr {($byte2 & 0x1) == 0}];
    }

    proc bitRateBin {byte3} {
       return [expr {($byte3 >> 4) & 0xF}];
    }

    proc bitRate {byte2 byte3} {
       if {[set bitRateIdx [bitRateBin $byte3]] == 0xF} {
          return -1;
       }

       if {[set layerIdx [layerBin $byte2]] == 0} {
          return -1;
       }

       if {[set mpegIdx [mpegBin $byte2]] == -1} {
          return -1;
       } elseif {$mpegIdx == 2} {
          if {$layerIdx == 2} {
             set layerIdx 1;
          }

          set mpegIdx 0;
       }

       variable bitRateTable;

       return $bitRateTable($mpegIdx.$layerIdx.$bitRateIdx);
    }

    proc sampleRateBin {byte3} {
       return [expr {($byte3 >> 2) & 0x3}];
    }

    proc sampleRate {byte2 byte3} {
       if {[set sampleRateIdx [sampleRateBin $byte3]] == 0x3} {
          return 0;
       }

       if {[set mpegIdx [mpegBin $byte2]] == 1} {
          return 0;
       }

       variable sampleRateTable;

       return $sampleRateTable($mpegIdx.$sampleRateIdx);
    }

    proc private {byte3} {
       return [expr {($byte3 & 0x1) == 1}];
    }

    proc padding {byte3} {
       return [expr {(($byte3 >> 1) & 0x1) == 1}];
    }

    proc channelModeBin {byte4} {
       return [expr {($byte4 >> 6) & 0x3}];
    }

    proc channelMode {byte2 byte4} {
       switch -exact -- [channelModeBin $byte4] {
          3 {return "single";}
          2 {return "dual";}
          0 {return "stereo";}
       }

       if {[set layerIdx [layerBin $byte2]] == 0} {
          return "joint";
       } elseif {$layerIdx == 2} {
          set layerIdx 3;
       }

       variable channelModeTable;

       return [list "joint" $channelModeTable($layerIdx.[expr {($byte4 >> 4) & 0x3}])];
    } 

    proc copyright {byte4} {
       return [expr {(($byte4 >> 3) & 0x1) == 1}];
    }

    proc original {byte4} {
       return [expr {(($byte4 >> 2) & 0x1) == 1}];
    }

    proc emphasisBin {byte4} {
       return [expr {$byte4 & 0x3}];
    }

    proc emphasisDesc {byte4} {
       switch -exact -- [emphasisBin $byte4] {
          3 {return "CCIT J.17";}
          2 {return "reserved";}
          1 {return "50/15 ms";}
       }

       return "none";
    } 

    proc frameLength {byte1 byte2 byte3} {
       set mpeg       [mpegBin $byte2];
       set layer      [layerBin $byte2];
       set protection [protection $byte2];
       set padding    [padding $byte3];

       set bitRate    [bitRate $byte2 $byte3];
       set sampleRate [sampleRate $byte2 $byte3];

       if {!$bitRate} {
          # (Free bit rate) This will move the scanner one step forward
          #
          set frameLength 1;
       } else {
          if {$layer == 3} {
             set frameLength [expr {(12 * $bitRate * 1000 / $sampleRate) + (4 * $padding) + (2 * $protection)}]
          } else {
             set frameLength [expr {(144 * $bitRate * 1000 / $sampleRate) + $padding + (2 * $protection)}]
          }
       }

       return $frameLength;
    }

    proc isValidFrameHeader {byte1 byte2 byte3 byte4} {
       # 1. MPEG version not unknown
       # 2. layer not reserved
       # 2. sample rate index, 3 not allowed 
       # 3. bitrate, 15 not allowed 
       #
       if {([mpegBin $byte2]       != 0x1) &&
           ([layer $byte2]         != 0x0) &&
           ((($byte3 >> 2) & 0x3)  != 0x3) &&                          
           ((($byte3 >> 4) & 0xF)  != 0xf)} {     
          return 1;
       }

       return 0;
    } 

    proc mp3InfoReader {fileName arrayVar {debug 0}} {
       upvar $arrayVar info;

       catch {unset info;};

       if {[catch {set fd [open $fileName r];} reason]} {
          error $reason $::errorInfo $::errorCode;
       }

       fconfigure $fd -encoding binary -translation binary -buffering full -buffersize 1000000;

       set idx          1;
       set result      "";

       while {![eof $fd]} {
          # read until frame header is complete and valid
          #
          while {![eof $fd]} {
             scan [read $fd 1] %c byte1;

             if {($byte1 & 0xFF) == 0xFF} {
                scan [read $fd 1] %c byte2;

                if {($byte2 & 0xE0) == 0xE0} {
                   scan [read $fd 2] %c%c byte3 byte4;

                   if {[isValidFrameHeader $byte1 $byte2 $byte3 $byte4]} {
                      break;
                   }
                }
             }
          }

          # recognize all (normally) frame independent header data
          #
          set info($idx.mpeg)        [mpeg $byte2];
          set info($idx.layer)       [layer $byte2];
          set info($idx.protection)  [protection $byte2];
          set info($idx.sampleRate)  [sampleRate $byte2 $byte3];
          set info($idx.bitRate)     [bitRate $byte2 $byte3];
          set info($idx.private)     [private $byte3];
          set info($idx.channelMode) [channelMode $byte2 $byte4];
          set info($idx.copyright)   [copyright $byte4];
          set info($idx.original)    [original $byte4];
          set info($idx.emphasis)    [emphasisDesc $byte4];
          set info($idx.padding)     [padding $byte3];

          if {$info($idx.layer) == 1} {
             set info($idx.samplesPerFrame) 384;
          } else {
             set info($idx.samplesPerFrame) 1152;
          }


          if {$idx == 1} {
             # looking for a Xing VBR header (variable bitrate)
             #
             set info(vbr)   0;

             if {$info($idx.mpeg) == 1} {
                set xingHeaderStart [expr {$info($idx.channelMode) != "single" ? 32 : 17}];
             } else {
                set xingHeaderStart [expr {$info($idx.channelMode) != "single" ? 17 : 9}];
             }

             seek $fd $xingHeaderStart current;

             if {[read $fd 4] == "Xing"} {
                # found a Xing VBR header - looking for the average bit rate
                #
                set info(vbr) 1;
                set xingFrames 0;
                set xingBytes  0;

                if {[set xingHeadFlags [readI4 $fd]] & 0x0001} {
                   set xingFrames [readI4 $fd];
                }

                if {$xingHeadFlags & 0x0002} {
                   set xingBytes  [readI4 $fd];
                }

                if {($xingFrames > 0) &&
                    ($xingBytes  > 0) &&
                    ($xingHeadFlags & (0x0002 | 0x0001))} {
                   set info(bitRate)   [expr {(($xingBytes / $xingFrames) * $info($idx.sampleRate)) / ($info($idx.mpeg) == 1 ? 144000 : 72000)}];
                }
             } else {
                # first recognized bit rate is assumed to be the global one
                #
                set info(bitRate)      $info($idx.bitRate);
                set info(bitRate.calc) 0;
             }
          } elseif {!$info(vbr) && (!$info($idx.bitRate) || ($info(bitRate) != $info($idx.bitRate)))} {
             # another bit rate is not identical to the global (first) one
             # => calculate bit rate later on!
             #
             set info(bitRate.calc) 1;
             set info(bitRate.list) [list $info(bitRate) $info($idx.bitRate)];
          }

          # jump over audio data, if frame length calculated
          #
          seek $fd [frameLength $byte1 $byte2 $byte3] current;

          if {$idx == 2} {
             break;
          }

          incr idx;
       }

        # Now reread the last 128 bytes to decode the MP3 Tag
        if {[catch {seek $fd -128 end}]} {
            set tag ""
        } else {
            set tag [read $fd]
        }
      close $fd;

       # setting or calculating the bit rate
       #
       if {!$info(vbr)} {
          if {$info(bitRate.calc)} {
             # calculating the mean bit rate
             #
             set result        "warning: variable bit rate - published approximated duration and average bit rate!";
             set info(bitRate) 0;

             foreach value [set info(bitRate.list) [lsort -unique $info(bitRate.list)]] {
                incr info(bitRate) $value;
             }

             set info(bitRate) [expr {int($info(bitRate) / double($idx))}];
          }
       }

       # calculating the durating using the bit rate and the file size
       #
       set info(duration) [expr {int([file size $fileName]*8 / double(1000*$info(bitRate)))}];

       # copy all frame independent data into the return array
       #
       foreach {name value} [array get info 1.*] {
          set name   [join [lrange [split $name "."] 1 end] "."];

          if {[string match "bitRate*" $name] ||
              ($name == "crc")                ||
              ($name == "duration")           ||
              ($name == "frameLength")        ||
              ($name == "padding")            ||
              ($name == "protection")} {
             continue;
          }

          set info($name) $value;
       }

       if {!$debug} {
          array unset info {[0-9]*};
       }

        binary scan $tag A3 id
        if {[string equal $id TAG]} {
            set info(hastag) 1
            set info(genre) 12
            binary scan $tag a3a30a30a30a4a28ccc id info(title) info(artist) info(album) info(year) info(comment) zero info(track) info(genreid)
        } else {
            array set info {
                hastag 0
                title ""
                artist ""
                album ""
                year ""
                comment ""
                track 0
                genreid -1
            }
        }
        set info(genre) [lindex $genres $info(genreid)]

       return $result;
    }

    namespace export -clear mp3InfoReader;
 }

ps 23April2004

Tagging MP3 files is a reader/writer for MP3 tags, which contains the artist name, album info, etc. I have integrated the reading of that info into the code above. Have fun.


MGS [2004/11/24\] - Two minor changes above: sampleRatesTable entry should be 3.2, not 3.3,, and binary scan format in proc mp3InfoReader shloud be a3a30a30a30a4a28ccc instead of A3A30A30A30A4A28ccc (tag fields are null-padded, not space-padded).


[ Category Multimedia Category Sound ]