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; variable genres ; #PWQ 26 Nov 04 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 should be a3a30a30a30a4a28ccc instead of A3A30A30A30A4A28ccc (tag fields are null-padded, not space-padded). ''[PWQ]'' 26 Nov 04, added missing variable command to mp3InfoReader proc. ---- [LV] Has anyone taken a look at what it would take to access CD-TEXT information (see for example http://www.ncf.carleton.ca/~aa571/cdtext.htm for details) from a CD to get information to add into the MP3 files? ---- See also: * [mp3info] ---- [Category Music] | [Category Sound]