Another Shoutcast Player

While experimenting with Tom Wilkason's snackAmp player I discovered that it did not support obtaining song information from the ShoutCast stream. I found DZ's basic snack stream player 2 - shoutcast but it had some bugs (such as stopping/starting streams). I took DZ's code and fixed a bunch of things to produce yet another Shoutcast player.

http://tcl.jtang.org/shoutcast_player/player.png

This program requires both snack and memchan, both of which are supplied in the ActiveTcl distribution. It has been tested under Tcl 8.4.9 for Linux/x86; it ought to work for other platforms. The default URL goes to http://www.club977.com , a free 80's pop music station.

The program is far from perfect; I get weird "popping" sounds every now and then. I suspect this is caused when snack finishes decoding all of its audio data, but the main program is still busy parsing the song information. I am sure that I can optimize it further. Furthermore I would like to add a bookmarks feature so that I don't have to look up each station's URL.

Convenient downloadable tarball here: http://tcl.jtang.org/shoutcast_player/shoutcast_player.tar.gz

Alternatively copy the two code snippets below to player-cmd.tcl and player-gui.tcl. Run player-gui.tcl to bathe in music goodness.

player-cmd.tcl below:

 #//#
 # Shoutcast stream player.  Based on Daniel Zlobec's basic snack
 # stream player (https://wiki.tcl-lang.org/13305)
 #
 # @author Jason Tang ([email protected])
 #//#

 package require Memchan
 package require snack

 namespace eval shoutcast {
     namespace export *
     set doDebug 0
     set title "No data"
     set total 0
     set s {}
 }

 proc shoutcast::connect {server port path} {
     variable sock

     shoutcast::init
     shoutcast::openChannel
     shoutcast::initSnack

     variable title "Connecting to $server..."
     update
     set sock [socket $server $port]
     fconfigure $sock -blocking 0 -buffering full -buffersize 100000 \
         -translation {binary auto}

     append buff "GET $path HTTP/1.0\n"
     append buff "Host: $server\n"
     append buff "Icy-MetaData:1\n"
     append buff "Accept: */*\n"
     append buff "User-Agent: Tcl/8.4.9\n"
     append buff "\n"
     puts $sock $buff
     flush $sock
     set title "Connected to $server."
     fileevent $sock readable [list shoutcast::readHeader $sock]
 }

 proc shoutcast::init {} {
     package forget snack
     package require snack
     variable header
     array unset header
     set header(icy-metaint) 0
     variable total 0
     variable s {}
     variable sock {}
     variable fd {}
 }

 proc shoutcast::openChannel {} {
     variable fd
     set fd [fifo]
     fconfigure $fd -translation {binary binary} -encoding binary \
         -buffering none -buffersize 100000
 }

 proc shoutcast::closeChannel {} {
     variable fd
     catch {close $fd}
 }

 proc shoutcast::initSnack {} {
     variable s
     set s [snack::sound s]
 }

 proc shoutcast::disconnect {} {
     variable sock
     catch {close $sock}
     shoutcast::closeChannel
 }

 proc shoutcast::play {} {
     variable s
     variable fd
     $s configure -channel $fd -buffersize 100000 -debug 0
     after 3000 [list $s play]
 }

 proc shoutcast::stop {} {
     variable s
     $s stop
     shoutcast::disconnect
     $s destroy
     variable title "<stopped>"
 }

 proc shoutcast::readHeader {sock} {
     variable header
     variable fd

     set count [gets $sock line]
     if {$count == -1 && [eof $sock] == 1} {
         stop
     }
     set h [split $line ":"]
     if {[llength $h] == 2} {
         foreach {key value} $h { set header($key) [string trim $value] }
     }
     # reached end of meta tags; music data henceforth
     if {$count == 1 && $line == "\r"} {
         parray header
         if {[info exist header(icy-name)]} {
             variable title $header(icy-name)
         }
         if {[info exist header(icy-metaint)] && $header(icy-metaint) >= 0} {
             variable metaint $header(icy-metaint)
             variable readSize $metaint
             fileevent $sock readable [list shoutcast::readStreamMetaInt $sock]
         } else {
             fileevent $sock readable [list shoutcast::readStream $sock]
         }
     }
 }

 proc shoutcast::readStream {sock} {
     variable readSize
     variable total
     variable fd
     # stream has just music data, no music information
     fcopy $sock $fd -size 4096
 }

 proc shoutcast::readStreamMetaInt {sock} {
     variable readSize
     variable total
     variable fd
     variable metaint

     set data [read $sock $readSize]
     incr total [string length $data]

     puts -nonewline $fd $data

     if {$total != $metaint} {
         set readSize [expr {$metaint - $total}]
     } else {
         set readSize $metaint
         set total 0
         fileevent $sock readable [list shoutcast::readTitleLength $sock]
     }
 }

 proc shoutcast::readTitleLength {sock} {
     set c 0
     set titleSize [read $sock 1]

     scan $titleSize %c c

     set titleSize [expr {$c * 16}]
     if {$c == 0} {
         fileevent $sock readable [list shoutcast::readStreamMetaInt $sock]
     } else {
         fileevent $sock readable [list shoutcast::readTitle $sock $titleSize]
     }
 }

 proc shoutcast::readTitle {sock size} {
     #Shoutcast song information looks like this:
     #  StreamTitle='<title>';StreamUrl='<url>';

     set t ""
     while {$size > 0} {
         set data [read $sock $size]
         append t $data
         set size [expr {$size - [string length $data]}]
     }
     set t [string trim $t]
     if {[regexp -nocase -- {streamtitle='(.*?)';} $t foo _title] && $_title != ""} {
         variable title $_title
     }
     if {[regexp -nocase -- {streamurl='(.*?)';} $t foo url]} {
         # ignore the URL for now
     }
     fileevent $sock readable [list shoutcast::readStreamMetaInt $sock]
 }

'''player-gui.tcl''' below:

 #//#
 # Shoutcast player interface.  Based on Daniel Zlobec's basic snack
 # stream player (https://wiki.tcl-lang.org/13305).
 #
 # @author Jason Tang ([email protected])
 #//#

 package require Tk

 source player-cmd.tcl

 # change this with other addresses of radio stations
 #set host 206.98.167.99
 #set port 8712

 namespace eval player {
     namespace export *
     set status stop
 }

 proc player::createGui {} {
     variable url "http://64.236.34.67:80/stream/1040"
     label .title -textvariable shoutcast::title -width 50
     pack .title -fill both -expand 1
     button .play -text Play -command player::cmdPlay
     button .stop -text Stop -command player::cmdStop
     button .quit -text Quit -command player::cmdQuit
     pack .quit .stop .play -side right
     label .l -text "URL: "
     entry .entry -textvariable player::url -width 32
     pack .l .entry -side left
 }

 proc player::cmdQuit {} {
     variable status
     if {$status == "play"} {
         shoutcast::stop
         set status stop
     }
     exit
 }

 proc player::cmdPlay {} {
     variable status
     variable url
     if {$status == "play"} {
         return
     }
     if {[regexp {(\Ahttp:\/\/)?([^:/]+)(:(\d+))?(.*)} $url foo foo2 server foo3 port path]} {
         if {$port == ""} {
             set port 80
         }
         if {$path == ""} {
             set path "/"
         }
         puts "server = $server; port = $port; path = $path"
         shoutcast::connect $server $port $path
         set status play
         shoutcast::play
     } else {
          set shoutcast::title "<could not parse url>"
     }
 }

 proc player::cmdStop {} {
     variable status
     if {$status == "play"} {
         shoutcast::stop
         set status stop
     }
 }

 player::createGui

Go back to Jason Tang


TFW Dec 27, 2005. During my break I finally got around to incorporating this code into SnackAmp. I had to increase the memchan buffer size above what it would normally see to prevent the chirps/blips. I used 800000 which works fine. Also, for the non-metadata case the fcopy in shoutcast::readStream needs to be replaced by a read/puts (at least on my windows machine). Otherwise works great!


LV 2007 June 27 Today, at least, jtang.org is not accessible.

tclshout