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?
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 0.2.10 -1 0.2.12 -1 0.2.7 -1 0.2.0 -1 0.2.4 -1 0.2.8 -1 0.2.6 -1 0.2.3 -1 0.2.14 -1 0.2.5 -1 0.2.2 -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?
nihaopaul [2005/09/10] Added the 0.2.x to the bitRate array, but couldnt find any information on them so used -1 I'm indexing ~4000 mp3s with this to a mysql database, via.. eggdrop :D
bll [2012/06/15] Here are some code fragments that will handle id3v2. The following is untested/unimplemented: unsychronized, extended header, footer, SEEK. Also note there is a disconnect between the ID3v2 frame names and the ones built in to mp3info.
proc mungeSize { len } { set nlen 0 for { set p 0 } { $p < 4 } { incr p } { set t [expr ($len >> ( $p * 8 )) & 0x7f]; set nlen [expr $nlen | [expr $t << ( $p * 7 )]]; } return $nlen } proc readID3v2Info { infov fdv tsz } { upvar $infov info upvar $fdv fd set info(hastag) 1 while {![eof $fd]} { scan [read $fd 2] %c%c byte1 byte2; if {($byte1 & 0xFF) == 0x00 && ($byte2 & 0xFF) == 0x00} { seek $fd $tsz start; return; } if {($byte1 & 0xFF) == 0xFF && ($byte2 & 0xE0) == 0xE0} { seek $fd -2 current; return; } seek $fd -2 current; scan [read $fd 4] %4s tag; set len [readI4 $fd] set len [mungeSize $len] scan [read $fd 2] %c%c flag1 flag2; set enc1 0 if { $tag != "UFID" } { # is a byte less than some value an encoding byte? scan [read $fd 1] %c enc1; incr len -1; # skipped encoding byte already } incr len -1; # don't read null set val [read $fd $len] if { $enc1 == 0x00 } { set val [encoding convertfrom iso8859-1 $val] } elseif { $enc1 == 0x01 } { set val [encoding convertfrom unicode [string range $val 2 end]] } elseif { $enc1 == 0x02 } { set val [encoding convertfrom unicode $val] } elseif { $enc1 == 0x03 } { set val [encoding convertfrom utf-8 $val] } elseif { $enc1 != 0x00 } { puts "Unknown encoding value" } read $fd 1; # null byte if { $tag != "APIC" && $val != "" } { set info($tag) $val } } } # this code goes before the '# read until frame header is complete and valid' loop. if { ! $headerChecked } { binary scan [read $fd 3] A3 hd if { $hd == "ID3" } { set isid3v2 true; # ID3(1-3) min-ver(4-5) flags(6) size(7-10) scan [read $fd 3] %c%c%c byte4 byte5 flags; set tsz [readI4 $fd] set tsz [mungeSize $tsz] incr tsz -10 if { ($flags & 0x80) == 0x80 } { set id3v2unsync true; puts "mp3: unsynchronized" } if { ($flags & 0x40) == 0x40 } { set id3v2ext true; puts "mp3: has extended header" } if { ($flags & 0x10) == 0x10 } { set id3v2footer true; puts "mp3: has footer" incr tsz -10 } seek $fd 10 start; if { $id3v2ext } { set sz [readI4 $fd] set sz [mungeSize $sz] seek $fd $sz current; # skip extended header } readID3v2Info info fd $tsz } else { seek $fd -3 current; } set headerChecked true }
FrBa 2022-07-12: The SnackAmp music player by Tom Wilkason provides a file named id3.tcl that is very capable to read and write id3 v1/v2 tags. To use this code unmodified, your code will need a few additions to avoid errors:
# WORKAROUNDS FOR USE OF ID3.TCL OUTSIDE OF ITS NORMAL PACKAGE proc Trace {args} {} proc debug {args} {} set snackAmpSettings(zeroPadTrack) 0 proc fromDb {args} {return ?}
Then to retrieve tags from an mp3 file you could simply issue:
source id3.tcl id3Tag::id3V2Get $filename data parray data
See also: