Tcl MP3 Alarm Clock

Rationale

I (peterc) hate my city's radio stations. Shock jocks are mind-numbing, public radio runs the risk that they might be broadcasting parliamentary procedings (more likely to put me back to sleep than wake me up), and the other stations are just tedious.

Cron + a sound card + Tcl MP3 selector script + mpg123 + a 5 metre headphone extension cable + external PC speakers + mp3 files = No more accidental sleep-ins from poor breakfast radio!

mp3select.sh script

 #!/usr/local/ActiveTcl-8.5/bin/tclsh8.5
 package require fileutil

 ########
 # Essentials

 proc shuffle {list} {
  set n 1
  set slist {}
  foreach item $list {
   set index [expr {int(rand()*$n)}]
   set slist [linsert $slist $index $item]
   incr n
   }
  return $slist
  }

 proc lvarpop {upVar {index 0}} {
  upvar $upVar list;
  if {![info exists list]} { return "-1" }
  set top [lindex $list $index];
  set list [concat [lrange $list 0 [expr $index - 1]] [lrange $list [expr $index +1] end]]
  return $top;
  }

 ########
 # General

 proc get_dirs {} {
  if {![file exists $::dirs_file]} { puts "Error: $::dirs_file does not exist!" ; exit }
  if {![file readable $::dirs_file]} { puts "Error: $::dirs_file unreadable!" ; exit }
  set DF [open $::dirs_file "r"]
  fconfigure $DF -encoding binary -translation binary -buffering none
  set dirs [read $DF]
  close $DF
  return $dirs
  }

 proc random_from_all {items} {
  # Create list
  set fl [list]
  set dirs [get_dirs]
  foreach d $dirs {
   if {![file exists $d]} { continue } ;# Ignore moved/missing directories.
   set mp3 [fileutil::findByPattern $d *.mp3]
   foreach f $mp3 { lappend fl $f }
   # Extra filetypes.
   foreach ext $::othermedia { 
    set mfiles [lsort [fileutil::findByPattern $d -glob *.${ext}]] 
    foreach f $mfiles { lappend fl $f }
    }
   }
  # puts "Full list:\n$fl\n";
  set fl [shuffle $fl]
  set c 0 ; while {$c < $items} { lappend pl [lvarpop fl 0] ; incr c }
  return $pl
  }

 proc ordered_from_random_dirs {items} {
  # Create list
  set fl [list]
  set dl [list]
  set dq [list]
  set pl [list]
  set dirs [get_dirs]
  foreach d $dirs {
   set found [fileutil::findByPattern $d *]
   foreach f $found {
    if {[file isdirectory $f] && [regexp $d $f] } { lappend dl $f }
    }
   }
  # Shuffle directories
  set dl [shuffle $dl]
  set c 0 ; while {$c < $items} { lappend dq [lvarpop dl 0] ; incr c }
  # Collect files inside directories
  foreach d $dq {
   # Standard MP3
   set mp3 [lsort [fileutil::findByPattern $d -glob *.mp3]]
   foreach f $mp3 { lappend pl $f }
   # Extra filetypes.
   foreach ext $::othermedia { 
    set mfiles [lsort [fileutil::findByPattern $d -glob *.$ext]]
    foreach f $mfiles { lappend pl $f } 
    }
   }
  return $pl
  }

 proc set_directories_file {value} { set ::dirs_file $value }
 proc set_toggle_ext {key} {
  # Toggle on or off.
  if {[lsearch $::othermedia $key] < 0} { 
   lappend ::othermedia $key
   } else { 
   lvarpop ::othermedia [lsearch -exact $::othermedia $key] 
   }
  return ok
  }

 ########
 # Main

 interp alias {} -rdof {} ordered_from_random_dirs ;# Random Dirs, Ordered Files
 interp alias {} -rdrf {} random_from_all          ;# Random Dirs, Random Files
 interp alias {} -cf {} set_directories_file       ;# Sets the root dirs file.
 interp alias {} -togext {} set_toggle_ext         ;# Set using extname; toggles

 set ::dirs_file "~/.mp3s/default.cf"
 set ::othermedia [list]

 foreach {opt value} $argv {
  foreach f [$opt $value] {
   if {[string length $f] > 3} { puts $f ; set didsomething 1 }
   }
  }

 if {![info exists didsomething]} {
  puts "Syntax:\n"
  puts " mp3select.sh opt value opt value \[...\]\n"
  puts "Options:\n"
  puts " -rdof X : Random Dirs, Ordered Files; to play X random audioplays."
  puts " -rdrf X : Random Files; for playing X random tracks."
  puts " -cf /path/file.cf : Sets the path to the file with root directories."
  puts " -togext ext : Toggle the inclusion of *.ext files.\n"
  puts "Example:\n"
  puts " mp3select.sh -rdrf 10 -cf ~/punk.cf -rdof 3\n"
  puts " This selects 10 random songs from the dirs in the default file,"
  puts " changes the dirs file to ~/punk.cf then selects 3 random albums.\n"
  puts "Notes:\n"
  puts " * mp3select.sh will not select a song twice in the same option track"
  puts " selection."
  puts " * The -cf setting applies to the next -rdof/-rdrf file selection/s"
  puts " but not previous ones. Multiple -cf may be used."
  puts " * Default file for listing directories: ~/.mp3s/default.cf\n"
  }

default.cf (and other conf/cf files)

Each file contains a list of root directories for your MP3 files, one per line. eg:

 /home/someuser/Music
 /data/Music

These root directories are searched recursively for playable MP3 files. You can list an unlimited number of root directories in each file, and of course, specify any number of conf/cf files on the command-line.

Use examples

Set up in Cron (using crontab -e):

 0 7 *  *  * * /pathto/mp3select.sh -togext m4a -rdrf 30 >/tmp/pl.txt && mplayer -af volnorm -really-quiet -playlist /tmp/pl.txt >/dev/null 2>&1

I originally used 'mpg123 -q [email protected] /tmp/pl.txt' for reading playlists from a file, but, the range of sound levels in my MP3s made me appreciate mplayer's volume normalisation capability.

Changes

peterc 2008-12-12: Now won't fail if one of the directories in your root dirs conf is missing. Now also supports audio of any type by using an extensions toggle (-togext ext). Call once to include, a second time to remove (personally, I use this as I have a few m4a files). The default root dirs conf is now ~/.mp3s/default.cf.


See also AK's A musicbox.