EG I have a portable music player which supports Ogg files. While any modern unix/linux workstation comes with the tools to convert and otherwise handle this format, I want to do it with tcl.
So, after a while digging on the somewhat confusing documentation, I've come with this code to read the Ogg/Vorbis metadata and write comments to a new file (as the 'vorbiscomment' utility do). The readMetaData procedure takes a file name and returns a dictionary with the relevant information about the file (I've used the official documentation names as dictionary keys). The writeComments procedure takes a input filename, an output filename and a dict with key-value pairs of tags. They will be applied to the output file stripping any existing comments the input file already have.
For a list of standard key names see [L1 ]
package require Tcl 8.5 namespace eval ogg { # Relevant documents : # http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html # http://www.xiph.org/ogg/doc/framing.html variable reflected 0 variable crc_table } proc ogg::writeComments { oggsource oggdest comments } { variable crc_table variable reflected # as this is needed for only for this proc, put it here package require crc16 if {![info exists crc_table]} { set crc_table [::crc::Crc_table 32 0x04c11db7 $reflected] } set fdin [open $oggsource] chan configure $fdin -translation binary -encoding binary # verify that's a Ogg/Vorbis file if {![IsOgg $fdin]} { chan close $fdin return -code error "$oggsource is not an Ogg/Vorbis file" } # read the first Ogg page. it's fixed size, and contains the Ogg # header plus the Vorbis identification packet set oggpage1 [chan read $fdin 58] # read the number of page segments and the segments table set oggp2 [chan read $fdin 22] # read 4 bytes of the crc and replace them with zeros chan read $fdin 4; append oggp2 \x00\x00\x00\x00 binary scan [chan read $fdin 1] cu page_segments binary scan [chan read $fdin $page_segments] cu* segment_table # determine the 2nd packet size and read it. read the rest of the data # until the end of the second ogg page set i [lsearch -not -integer $segment_table 255] set vp2_lv [lrange $segment_table 0 $i] set vp3_lv [lrange $segment_table $i+1 end] set vp2_data [chan read $fdin [expr [join $vp2_lv +]]] set vp3_data [chan read $fdin [expr [join $vp3_lv +]]] # reuse the vendor string of the file binary scan [string range $vp2_data 7 10] iu vs_size binary scan [string range $vp2_data 11 10+$vs_size] A* vendor_string # build a new 2nd vorbis packet set vorbisp2 \x03vorbis # add the vendor string append vorbisp2 [binary format iuA* $vs_size $vendor_string] # hoy many fields of comments we have append vorbisp2 [binary format iu [dict size $comments]] # now, add the comments dict for {key value} $comments { set field "${key}=[encoding convertto utf-8 $value]" set size [string length $field] append vorbisp2 [binary format iuA* $size $field] } # finalize the second Vorbis packet append vorbisp2 [binary format c 1] # compute its size set vp2_size [string length $vorbisp2] # determine the new lacing values set lvalues [lrepeat [expr {$vp2_size / 255}] 255 ] lappend lvalues [expr {$vp2_size % 255}] lappend lvalues {*}$vp3_lv # reconstruct the second Ogg page. We have already the first 26 bytes append oggp2 [binary format cucu* [llength $lvalues] $lvalues] append oggp2 $vorbisp2 append oggp2 $vp3_data # we are almost done. calculate the crc32 value as instructed in the # specification and put it on the bytes 22-25 set crc_new [::crc::Crc $oggp2 32 [namespace current]::crc_table 0 0 $reflected] set oggpage2 [string replace $oggp2 22 25 [binary format iu $crc_new]] # write the results to the output file set fdout [open $oggdest w] chan configure $fdout -encoding binary -translation binary chan puts -nonewline $fdout $oggpage1 chan puts -nonewline $fdout $oggpage2 # the rest of the file is unchanged. simply copy it to the new file chan copy $fdin $fdout chan close $fdin chan close $fdout } proc ogg::readMetaData { oggfile } { set fd [open $oggfile] chan configure $fd -translation binary -encoding binary # verify that's a Ogg/Vorbis file if {![IsOgg $fd]} { chan close $fd return -code error "$oggfile is not an Ogg/Vorbis file" } #this will be our returning value: a dict set result [dict create] # here comes the data of the identification packet # extract only the interesting information chan seek $fd 35 binary scan [chan read $fd 21] iucuiuiii \ version channels sample_r br_max br_nom br_min dict set result version $version dict set result audio_channels $channels dict set result audio_sample_rate $sample_r dict set result bitrate_maximum $br_max dict set result bitrate_nominal $br_nom dict set result bitrate_minimum $br_min # towards the end of the second Ogg header, this # byte counts how many bytes left are in the header chan seek $fd 84 binary scan [chan read $fd 1] c bytesleft chan seek $fd [expr {85 + $bytesleft + 7}] # follows a vendor string binary scan [chan read $fd 4] iu size binary scan [chan read $fd $size] A* vendor dict set result vendor_string $vendor # and now a list of attributes in the form "key=value" binary scan [chan read $fd 4] iu numfields for {set i 0} {$i < $numfields} {incr i} { binary scan [chan read $fd 4] iu size binary scan [chan read $fd $size] a* field set field [encoding convertfrom utf-8 $field] lassign [split $field =] key value dict set result user_comment $key $value } chan close $fd return $result } proc ogg::IsOgg { fd } { set pos [chan tell $fd] chan seek $fd 0 set data [chan read $fd 35] chan seek $fd $pos return [expr {[string range $data 0 3] eq "OggS" \ && [string range $data 28 34] eq "\x01vorbis" ? 1 : 0}] } # simple test if 0 { # ogg::writeComments set f [lindex $argv 0] set nf [file rootname $f].new.ogg puts "Input File: $f" puts "Output File: $nf" puts "Tags to apply: title: \"My tune\" artist: \"My favourite artist\"" ogg::writeComments $f $nf {title "My tune" artist "My favourite artist"} } if 0 { # ogg::readMetaData set f [lindex $argv 0] puts "File: $f" puts [ogg::readMetaData $f] }