Updated 2011-04-16 19:49:42 by RLE

Keith Vetter 2005-02-28 : Recently I've had to dig deep into various audio and video file formats. Here's a little tool I wrote to parse and dump out information about WAV audio files. (There's also code on the WAV page to do this but it's not very robust. It assumes that there are only two chunks and that the first chunk is always 'fmt '. It also can't handle the bizarre big-endian format, also known as RIFX files--but then again RealPlayer can't handle them either.)
 ##+##########################################################################
 #
 # wavDump.tsh -- Parses and dumps wav audio files
 # by Keith Vetter - Feb 28, 2005
 #
 ##+##########################################################################
 #############################################################################

 proc comma { num } {
    while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {}
    return $num
 }
 ##+##########################################################################
 #
 # fmtChunk -- parses and dumps the fmt chunk
 #
 proc fmtChunk {len} {
    global bytesSec scanFmt fin

    binary scan [read $fin 16] $scanFmt(fmt) \
        fmt chan sampleSec bytesSec align bitsSample

    set line " fmt: $fmt "
    append line [expr {$chan == 1 ? "mono" : $chan == 2 ? "stereo" : "$chan channels"}]
    append line " [comma $sampleSec]Hz "
    append line "[comma $bytesSec] bytes/sec "
    append line "[comma $align] bytes/sample "
    append line "[comma $bitsSample] bits/sample "

    if {$fmt != 1} {                            ;# Extra header stuff
        binary scan [read $fin 2] $scanFmt(16) extra
        append line "extra: $extra"
    }
    puts $line
 }
 ##+##########################################################################
 #
 # DoFile -- Parse one wave file
 #
 proc DoFile {fname} {
    global fin scanFmt fmtScan

    puts $fname
    set fin [open $fname "r"]
    fconfigure $fin -translation binary

    set magic [read $fin 4]
    array set scanFmt {16 s 32 i fmt "ssiiss"}  ;# Little endian by default
    if {$magic eq "RIFX"} {                     ;# This is big-endian format
        puts " big-endian format"
        array set scanFmt {16 S 32 I fmt "SSIISS"}
    } elseif {$magic ne "RIFF"} {
        puts "Bad magic number: '$magic'"
        exit
    }
    binary scan [read $fin 4] $scanFmt(32) len  ;# Should be file length - 8
    set type [read $fin 4]
    if {$type ne "WAVE"} {
        puts "Not a WAVE file: '$type'"
        exit
    }

    set dataLen 0
    while {1} {                                 ;# Loop through every chunk
        set chunk [read $fin 4]                 ;# Chunk type
        if {[eof $fin]} break
        binary scan [read $fin 4] $scanFmt(32) len ;# Chunk length
        set eoc [expr {[tell $fin] + $len}]     ;# End of chunk

        set proc "[string tolower [string trim $chunk]]Chunk"
        if {[info procs $proc] ne ""} {
            $proc $len
        } else {
            puts " $chunk: length [comma $len]"
            if {$chunk eq "data"} {             ;# Possibly multiple data chunks
                incr dataLen $len
            }
        }
        seek $fin $eoc start
    }
    set duration [expr {double($dataLen) / $::bytesSec}]
    puts " => duration: [format %.3g $duration] sec"
    close $fin
    puts ""
 }

 ################################################################
 ################################################################

 if {$argc == 0} {
    puts stderr "usage: wavDump.tsh <wave files>"
    exit
 }
 foreach arg $argv {
    set arg [file normalize $arg]               ;# Make ok for glob
    foreach fname [glob -nocomplain $arg] {     ;# Do wildcard expansion if any
        DoFile $fname
    }
 }