[xiph-cvs] cvs commit: snatch Snatch esd.c oss.c FAQ README libsnatch.c snatch.pl

Monty xiphmont at xiph.org
Mon Nov 12 14:22:17 PST 2001



xiphmont    01/11/12 14:22:16

  Modified:    .        FAQ README libsnatch.c
  Added:       .        Snatch esd.c oss.c
  Removed:     .        snatch.pl
  Log:
  Added FAQ/README
  Seperated audio code out into cleaner modules

Revision  Changes    Path
1.3       +3 -0      snatch/FAQ

Index: FAQ
===================================================================
RCS file: /usr/local/cvsroot/snatch/FAQ,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- FAQ	2001/11/12 04:16:23	1.2
+++ FAQ	2001/11/12 22:22:14	1.3
@@ -105,3 +105,6 @@
    synchronized multimedia events'.  I personally don't consider that
    a huge loss.
 
+   This also happens for one other common reason: Having EsounD support
+   enabled, but no EsounD daemon running.
+

1.2       +13 -8     snatch/README

Index: README
===================================================================
RCS file: /usr/local/cvsroot/snatch/README,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -r1.1 -r1.2
--- README	2001/11/12 04:16:23	1.1
+++ README	2001/11/12 22:22:14	1.2
@@ -297,20 +297,25 @@
 
 Where to look for the file libsnatch.so on startup.  This field is a glob.
 
-3) audio device
+3) OSS audio device
 
-Which OSS audio device names to watch for.  This is not used for the
-Enlightened Sound Daemon (which needs no configuration).
+Which OSS audio device names to watch for. This is only relevant if
+your Linux is using OSS directly.
 
-4) full debug output
+4) EsounD server socket
 
+Where the Enlightened Sound Daemon keeps its connect socket.  This
+option is only relevant if you're using the EsounD daemon.
+
+5) full debug output
+
 The best first thing to set when something goes wrong, but the error
 messages dialogs from the Snatch Robot aren't being helpful.
 
 This reports to stderr the continuous debugging output from
 libsnatch/RealPlayer.
 
-5) capture output
+6) capture output
 
 Select where to put the captured data.  This field may specify stdout
 (but filling in with a bare -), a directory (in which case Snatch will
@@ -323,17 +328,17 @@
 stop capturing.  If you specify an output directory instead, Snatch
 will close the 2GB file, open a new file, and continue capturing.
 
-6) ok
+7) ok
 
 Pressing OK applies any configuration changes immedaitely, saves the
 changes to the configuration file ~/.snatch/config.txt and closes the
 Configuration panel.
 
-7) apply
+8) apply
 
 Same as 'ok', but doe snot close the Configuration panel.
 
-8) cancel
+9) cancel
 
 Discard any changes and close the Configuration panel.
 

1.24      +81 -187   snatch/libsnatch.c

Index: libsnatch.c
===================================================================
RCS file: /usr/local/cvsroot/snatch/libsnatch.c,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -r1.23 -r1.24
--- libsnatch.c	2001/11/12 04:16:23	1.23
+++ libsnatch.c	2001/11/12 22:22:14	1.24
@@ -23,7 +23,6 @@
 #include <sys/un.h>
 #include <netinet/in.h>
 #include <stdarg.h>
-#include <linux/soundcard.h>
 #include <pthread.h>
 #include <X11/Xlib.h>
 #include <X11/extensions/XShm.h>
@@ -35,8 +34,12 @@
 static pthread_mutex_t display_mutex=PTHREAD_MUTEX_INITIALIZER;
 
 static int      (*libc_open)(const char *,int,mode_t);
+static int      (*libc_connect)(int sockfd, const struct sockaddr *serv_addr,
+				socklen_t addrlen);
 static int      (*libc_close)(int);
+static size_t   (*libc_read)(int,void *,size_t);
 static size_t   (*libc_write)(int,const void *,size_t);
+static int      (*libc_readv)(int,struct iovec *,int);
 static int      (*libc_writev)(int,const struct iovec *,int);
 static int      (*libc_ioctl)(int,int,void *);
 static pid_t    (*libc_fork)(void);
@@ -60,16 +63,12 @@
 
 static int debug;
 static char *outpath;
-static char *audioname;
-
 static FILE *backchannel_fd=NULL;
 
 static int audio_fd=-1;
-static int audio_fd_fakeopen=0;
 static int audio_channels=-1;
 static int audio_rate=-1;
 static int audio_format=-1;
-
 static long long audio_samplepos=0;
 static double audio_timezero=0;
 
@@ -93,32 +92,17 @@
 static void OpenOutputFile();
 
 static char *audio_fmts[]={"unknown format",
-			  "8 bit mu-law",
-			  "8 bit A-law",
-			  "ADPCM",
-			  "unsigned, 8 bit",
-			  "signed, 16 bit, little endian",
-			  "signed, 16 bit, big endian",
-			  "signed, 8 bit",
-			  "unsigned, 16 bit, little endian",
-			  "unsigned, 16 bit, big endian",
-			  "MPEG 2",
-			  "Dolby Digial AC3"};
-
-static char *formatname(int format){
-  int i;
-  for(i=0;i<12;i++)
-    if(format==((1<<i)>>1))return(audio_fmts[i]);
-  return(audio_fmts[0]);
-}
-
-static int fmtbytesps[]={0,1,1,0,1,2,2,1,2,2,0,0};
-static int formatbytes(int format){
-  int i;
-  for(i=0;i<12;i++)
-    if(format==((1<<i)>>1))return(fmtbytesps[i]);
-  return(0);
-}
+			   "8 bit mu-law",
+			   "8 bit A-law",
+			   "ADPCM",
+			   "unsigned, 8 bit",
+			   "signed, 16 bit, little endian",
+			   "signed, 16 bit, big endian",
+			   "signed, 8 bit",
+			   "unsigned, 16 bit, little endian",
+			   "unsigned, 16 bit, big endian"};
+
+static int audio_fmt_bytesps[]={0,1,1,0,1,2,2,1,2,2};
 
 static char *nstrdup(char *s){
   if(s)return strdup(s);
@@ -154,14 +138,11 @@
   return(tp.tv_sec+tp.tv_usec*.000001);
 }
 
-#include "x11.c" /* yeah, ugly, but I don't want to leak symbols. 
-		    Oh and I'm lazy. */
+/* yeah, ugly, but I don't want to leak symbols.  Oh and I'm lazy. */
+#include "x11.c" 
+#include "oss.c"
+#include "esd.c"
 
-
-/* although RealPlayer is both multiprocess and multithreaded, we
-don't lock because we assume only one thread/process will be mucking
-with a specific X drawable or audio device at a time */
-
 void *get_me_symbol(char *symbol){
   void *ret=dlsym(RTLD_NEXT,symbol);
   if(ret==NULL){
@@ -234,6 +215,7 @@
       CloseOutputFile(); /* it will only happen on Robot commands that would
                             be starting a new file */
       break;
+    case 'E':
     case 'U':
     case 'P':
     case 'L':
@@ -262,13 +244,17 @@
             if(openfile)free(openfile);
             openfile=buf;
             break;
+	  case 'E':
+	    if(esdsocket)free(esdsocket);
+	    esdsocket=buf;
+	    break;
           case 'F':
             if(outpath)free(outpath);
             outpath=buf;
             break;
           case 'D':
-	    if(audioname)free(audioname);
-	    audioname=buf;
+	    if(ossname)free(ossname);
+	    ossname=buf;
             break;
           }
       }
@@ -317,8 +303,11 @@
 
     /* get handles to the libc symbols we're subverting */
     libc_open=get_me_symbol("open");
+    libc_connect=get_me_symbol("connect");
     libc_close=get_me_symbol("close");
+    libc_read=get_me_symbol("read");
     libc_write=get_me_symbol("write");
+    libc_readv=get_me_symbol("readv");
     libc_writev=get_me_symbol("writev");
     libc_ioctl=get_me_symbol("ioctl");
     libc_fork=get_me_symbol("fork");
@@ -347,20 +336,8 @@
                 "           set (%s)\n",outpath);
     }
 
-    /* audio device? */
-    audioname=nstrdup(getenv("SNATCH_AUDIO_DEVICE"));
-    if(!audioname){
-      if(debug)
-	fprintf(stderr,
-		"----env: SNATCH_AUDIO_DEVICE\n"
-		"           not set. Using default (/dev/dsp*).\n");
-      audioname=nstrdup("/dev/dsp*");
-    }else{
-      if(debug)
-	fprintf(stderr,
-		"----env: SNATCH_AUDIO_DEVICE\n"
-		"           set (%s)\n",audioname);
-    }
+    oss_hook_init();
+    esd_hook_init();
 
     if(getenv("SNATCH_AUDIO_FAKE")){
       if(debug)
@@ -387,7 +364,7 @@
                 "           not set.\n");
 
     if(debug)
-      fprintf(stderr,"    ...: Now watching for RealPlayer audio output.\n");
+      fprintf(stderr,"    ...: Now watching for RealPlayer audio/video output.\n");
 
 
     {
@@ -420,7 +397,7 @@
         addr.sun_family=AF_UNIX;
         strcpy(addr.sun_path,backchannel_socket);
 
-	if(connect(temp_fd,(struct sockaddr *)&addr,sizeof(addr))<0){
+	if((*libc_connect)(temp_fd,(struct sockaddr *)&addr,sizeof(addr))<0){
           fprintf(stderr,
                   "**ERROR: connect() call for backchannel failed.\n"
                   "         returned error %d: %s\n"
@@ -477,65 +454,13 @@
   return((*libc_fork)());
 }
 
-/* The audio device is subverted through open().  If we didn't care
-   about allowing a fake audio open() to 'succeed' even when the real
-   device is busy, then we could just watch for the ioctl(), grab the
-   fd() then, and not need to bother with any silly string matching.
-   However, we *do* care, so we do this the more complex, slightly
-   more error prone way. */
-
 int open(const char *pathname,int flags,...){
   va_list ap;
-  int ret;
   mode_t mode;
 
   initialize();
-
-  /* open needs only watch for the audio device. */
-  if( (audioname[strlen(audioname)-1]=='*' &&
-       !strncmp(pathname,audioname,strlen(audioname)-1)) ||
-      (audioname[strlen(audioname)-1]!='*' &&
-       !strcmp(pathname,audioname))){
-
-      /* a match! */
-      if(audio_fd>-1){
-	/* umm... an audio fd is already open.  report the problem and
-	   continue */
-	fprintf(stderr,
-		"\n"
-		"WARNING: RealPlayer is attempting to open more than one\n"
-		"         audio device (in this case, %s).\n"
-		"         This behavior is unexpected; ignoring this open()\n"
-		"         request.\n",pathname);
-      }else{
-
-	/* are we faking the audio? */
-	if(fake_audiop){
-	  ret=(*libc_open)("/dev/null",O_RDWR,mode);
-	  audio_fd_fakeopen=1;
-	}else{
-	  ret=(*libc_open)(pathname,flags,mode);
-	  audio_fd_fakeopen=0;
-	}
-
-	audio_fd=ret;
-	audio_channels=-1;
-	audio_rate=-1;
-	audio_format=-1;
-	audio_samplepos=0;
-	audio_timezero=bigtime(NULL,NULL);
 
-	if(debug)
-	  fprintf(stderr,
-		  "    ...: Caught RealPlayer opening audio device "
-		  "%s (fd %d).\n",
-		  pathname,ret);
-	if(debug && fake_audiop)
-	  fprintf(stderr,
-		  "    ...: Faking the audio open and writes as requested.\n");
-      }
-      return(ret);
-  }
+  if(oss_identify(pathname))return oss_open_hook(pathname);
 
   if(flags|O_CREAT){
     va_start(ap,flags);
@@ -543,8 +468,16 @@
     va_end(ap);
   }
 
-  ret=(*libc_open)(pathname,flags,mode);
-  return(ret);
+  return ((*libc_open)(pathname,flags,mode));
+}
+
+int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen){
+  initialize();
+
+  if(esd_identify(serv_addr,addrlen)) 
+    return esd_connect_hook(sockfd,serv_addr,addrlen);
+  
+  return ((*libc_connect)(sockfd,serv_addr,addrlen));
 }
 
 int close(int fd){
@@ -552,8 +485,11 @@
 
   initialize();
 
-  ret=(*libc_close)(fd);
+  oss_close_hook(fd);
+  esd_close_hook(fd);
   
+  ret=(*libc_close)(fd);
+
   if(fd==audio_fd){
     audio_fd=-1;
     audio_samplepos=0;
@@ -563,11 +499,17 @@
               "    ...: RealPlayer closed audio playback fd %d\n",fd);
     CloseOutputFile();
   }
-  
+    
   return(ret);
 }
 
+ssize_t read(int fd, void *buf,size_t count){
+  if(esd_rw_hook_p(fd))return(esd_read_hook(fd,buf,count));
+  return((*libc_read)(fd,buf,count));
+}
+
 ssize_t write(int fd, const void *buf,size_t count){
+
   if(fd==audio_fd){
     /* track audio sync regardless of record activity or fake setting;
        we could have record suddenly activated */
@@ -586,6 +528,7 @@
        the current sample buffer will begin playing, not when it
        is queued */
     
+    fprintf(stderr,"Queue is at T%+f\n",stime-now);
     if(stime<now){
       /* queue starved; player will need to skip or stretch.  Advance
          the absolute position to maintain sync.  Note that this is
@@ -611,99 +554,51 @@
         
       }
     }
-    audio_samplepos+=(count/(audio_channels*formatbytes(audio_format)));
+    audio_samplepos+=(count/(audio_channels*audio_fmt_bytesps[audio_format]));
 
     if(fake_audiop)return(count);
   }
 
+  if(esd_rw_hook_p(fd))return(esd_write_hook(fd,buf,count));
+
   return((*libc_write)(fd,buf,count));
 }
 
 int writev(int fd,const struct iovec *v,int n){
-  if(fd==audio_fd){
-    int i,ret,count=0;
-    for(i=0;i<n;i++){
-      ret=write(fd,v[i].iov_base,v[i].iov_len);
-      if(ret<0 && count==0)return(ret);
-      if(ret<0)return(count);
-      count+=ret;
-      if(ret<v[i].iov_len)return(count);
-    }
-    return(count);
-  }else
-    return((*libc_writev)(fd,v,n));
+  int i,ret,count=0;
+  for(i=0;i<n;i++){
+    ret=write(fd,v[i].iov_base,v[i].iov_len);
+    if(ret<0 && count==0)return(ret);
+    if(ret<0)return(count);
+    count+=ret;
+    if(ret<v[i].iov_len)return(count);
+  }
+  return(count);
+}
+
+int readv(int fd,const struct iovec *v,int n){
+  int i,ret,count=0;
+  for(i=0;i<n;i++){
+    ret=read(fd,v[i].iov_base,v[i].iov_len);
+    if(ret<0 && count==0)return(ret);
+    if(ret<0)return(count);
+    count+=ret;
+    if(ret<v[i].iov_len)return(count);
+  }
+  return(count);
 }
 
-/* watch audio ioctl()s to track playback rate/channels/depth */
-/* stdargs are here only to force the clean compile */
 int ioctl(int fd,unsigned long int rq, ...){
   va_list optional;
   void *arg;
-  int ret=0;
   initialize();
   
   va_start(optional,rq);
   arg=va_arg(optional,void *);
   va_end(optional);
-  
-  if(fd==audio_fd){
 
-    if(!fake_audiop && !audio_fd_fakeopen)
-      ret=(*libc_ioctl)(fd,rq,arg);
-    
-    switch(rq){
-    case SNDCTL_DSP_SPEED:
-      audio_rate=*(int *)arg;
-      if(debug)
-	fprintf(stderr,
-		"    ...: Audio output sampling rate set to %dHz.\n",
-		audio_rate);
-      CloseOutputFile();
-      break;
-    case SNDCTL_DSP_CHANNELS:
-      audio_channels=*(int *)arg;
-      if(debug)
-	fprintf(stderr,
-		"    ...: Audio output set to %d channels.\n",
-		  audio_channels);
-      CloseOutputFile();
-      break;
-    case SNDCTL_DSP_SETFMT:
-      audio_format=*(int *)arg;
-      if(debug)
-	fprintf(stderr,
-		"    ...: Audio output format set to %s.\n",
-		formatname(audio_format));
-      CloseOutputFile();
-      break;
-    case SNDCTL_DSP_GETOSPACE:
-      if(fake_audiop){
-	audio_buf_info *temp=arg;
-	temp->fragments=32;
-	temp->fragstotal=32;
-	temp->fragsize=2048;
-	temp->bytes=64*1024;
-	
-	if(debug)
-	  fprintf(stderr,"    ...: Audio output buffer size requested; faking 64k\n");
-	ret=0;
-      }
-      break;
-    case SNDCTL_DSP_GETODELAY: /* Must reject the ODELAY if we're not going to track 
-				  audio bytes and timing! */
-      if(fake_audiop){
-	if(debug)
-	  fprintf(stderr,
-		  "    ...: Rejecting SNDCTL_DSP_GETODELAY ioctl()\n");
-	*(int *)arg=0;
-	ret=-1;
-      }
-      break;
-    }
-    
-    return(ret);
+  if(oss_ioctl_hook_p(fd))return(oss_ioctl_hook(fd,rq,arg));
 
-  }
   return((*libc_ioctl)(fd,rq,arg));
 }
 
@@ -789,7 +684,6 @@
   pthread_mutex_lock(&open_mutex);
   if(outfile_fd>=0){
     videocount=0;
-
     if(debug)fprintf(stderr,"    ...: Capture stopped.\n");
     if(outfile_fd!=STDOUT_FILENO)
       close(outfile_fd);

1.1                  snatch/Snatch

Index: Snatch
===================================================================
#!/usr/bin/perl

use Socket;
use Sys::Hostname;
use Time::Local;
use IPC::Open3;
use File::Glob ':glob';
use Tk;
use Tk::Xrm;
use Tk qw(exit); 

my $HOME=$ENV{"HOME"};
if(!defined($HOME)){
    print "HOME environment variable not set.  Exiting.\n";
    exit 1;
}

$version="Snatch 20011112";
$configdir=$HOME."/.snatch";
$configfile=$configdir."/config.txt";
$historyfile=$configdir."/history.txt";
$logofile=$configdir."/logo.xpm";

my $backchannel_socket="/tmp/snatch.$$";
my $uaddr=sockaddr_un($backchannel_socket);
my $proto=getprotobyname('tcp');
my $comm_ready=0;
my $mode='active';

# default config
$CONFIG{'REALPLAYER'}='{realplay,~/RealPlayer8/realplay,/usr/bin/realplay,/usr/local/bin/realplay}';
$CONFIG{'LIBSNATCH'}='{/usr/local/lib/libsnatch.so,/usr/lib/libsnatch.so,~/snatch/libsnatch.so,~/.snatch/libsnatch.so}';
$CONFIG{'OUTPUT_PATH'}=$HOME;
$CONFIG{'OSS_DEVICE'}="/dev/dsp*";
$CONFIG{'ESD_SOCK'}="/var/run/esound/socket";
$CONFIG{'AUDIO_MUTE'}='no';
$CONFIG{'VIDEO_MUTE'}='no';
$CONFIG{'DEBUG'}='no';

if(! -e $configdir){
    die $! unless mkdir $configdir, 0770;
}

$snatchxpm= <<'EOF';
/* XPM */
static char * snatch_xpm[] = {
"36 27 25 1",
" 	c None",
".	c #060405",
"+	c #8A8787",
"@	c #8D1D27",
"#	c #515052",
"S	c #4F1314",
"%	c #69272B",
"&	c #AE5664",
"*	c #D73138",
"=	c #252524",
"-	c #761922",
";	c #2D0505",
">	c #A53149",
",	c #C8C8C8",
"'	c #676768",
")	c #F3F3F3",
"!	c #A9A9A9",
"~	c #7C5254",
"{	c #F62F36",
"]	c #9A9A99",
"^	c #B32225",
"/	c #261E24",
"(	c #373736",
"_	c #D7D7D7",
":	c #797978",
"   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%   ",
" %S%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ",
"%->%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%>-%",
"S>;S----------------------------;@@%",
"S>;*{{{{{{{{{{{******{{*{{{{{{{{-@>%",
"S>;*{{{*^@--SSSSSSSSSSSSSS---@^{-->%",
"S>;*{{-......................(.SS@>%",
"S>;^{^...............(!/....:)..;->%",
"S>;^{S..+!]=++!:.:!]#,)#'!]=!)!]/->%",
"S>;^{;.:)'_+)]!)'_')+_,+)'_]_,:)#->%",
"S>;@*..#))]')=!_#,,)#)],,./()'')/->%",
"S>;@*;/:#])!).,,_!+)#)'_,(,:)(]_.->%",
"S>;S at S.]__:++.!'!_!!(_]'__:#!.++.->%",
"S>;-{^;........................S;->%",
"S>;S{{{-......................@*;->%",
"S>;S{{{{^;..................;^{*;->%",
"S>;S{{{{{*S................S{{{^.->%",
"S>;S{{{{{{{@..............@{{{{^.->%",
"S>;S{{{{{{{{^;..........;^{{{{{^.->%",
"S>;;{{{{{{{{{*S........-*{{{{{{^.->%",
"S>;;***********^-S..S-^********@.->%",
"S>;.;;;;;;;;;;;;;;;;;;;;;;;;;;;;.->%",
"S>;..............................->%",
"S>;..............................@&~",
"%-@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%>~+",
" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%S% ",
"   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%   "};
EOF

if(! -e $logofile){
    die $! unless open LFILE, ">$logofile";
    print LFILE $snatchxpm;
    close LFILE;
}

# load the config/history
if(-e $configfile){
    die $! unless open CFILE, $configfile;
    while(<CFILE>){
        /^\s*([^=]+)=([^\n]*)/;
        $CONFIG{$1}=$2;
    }
    close CFILE;
}

if(-e $historyfile){
    die $! unless open HFILE, $historyfile;
    while(<HFILE>){
        chomp;
        if(length){
            push @TIMER, $_;
        }
    }
    close HFILE;
}
TimerSort();

# build the UI
my $toplevel=new MainWindow(-class=>'Snatch');
my $Xname=$toplevel->Class;

$toplevel->optionAdd("$Xname.background",  "#8e3740",20);
$toplevel->optionAdd("$Xname*highlightBackground",  "#d38080",20);
$toplevel->optionAdd("$Xname.Panel.background",  "#8e3740",20);
$toplevel->optionAdd("$Xname.Panel.foreground",  "#d0d0d0",20);
$toplevel->optionAdd("$Xname.Panel.font",
                     '-*-helvetica-bold-o-*-*-18-*-*-*-*-*-*-*',20);
$toplevel->optionAdd("$Xname*Statuslabel.font",
                     '-*-helvetica-bold-r-*-*-18-*-*-*-*-*-*-*',20);
$toplevel->optionAdd("$Xname*Statuslabel.foreground", "#606060");
$toplevel->optionAdd("$Xname*Status.font",
                     '-*-helvetica-bold-r-*-*-18-*-*-*-*-*-*-*',20);

$toplevel->optionAdd("$Xname*AlertDetail.font",
                     '-*-helvetica-medium-r-*-*-10-*-*-*-*-*-*-*',20);

$toplevel->optionAdd("$Xname*background",  "#d0d0d0",20);
$toplevel->optionAdd("$Xname*foreground",  '#000000',20);

$toplevel->optionAdd("$Xname*Tab*background",  "#a0a0a0",20);
$toplevel->optionAdd("$Xname*Tab*disabledForeground",  "#ffffff",20);
$toplevel->optionAdd("$Xname*Tab*relief",  "raised",20);
$toplevel->optionAdd("$Xname*Tab*borderWidth",  1,20);

$toplevel->optionAdd("$Xname*Button*background",  "#f0d0b0",20);
$toplevel->optionAdd("$Xname*Button*foreground",  '#000000',20);
$toplevel->optionAdd("$Xname*Button*borderWidth",  '2',20);
$toplevel->optionAdd("$Xname*Button*relief",  'groove',20);

$toplevel->optionAdd("$Xname*activeBackground",  "#ffffff",20);
$toplevel->optionAdd("$Xname*activeForeground",  '#0000a0',20);
$toplevel->optionAdd("$Xname*borderWidth",         0,20);
$toplevel->optionAdd("$Xname*relief",         'flat',20);
$toplevel->optionAdd("$Xname*activeBorderWidth",         1,20);
$toplevel->optionAdd("$Xname*highlightThickness",         0,20);
$toplevel->optionAdd("$Xname*padX",         2,20);
$toplevel->optionAdd("$Xname*padY",         2,20);
$toplevel->optionAdd("$Xname*font",    
                     '-*-helvetica-bold-r-*-*-12-*-*-*-*-*-*-*',20);
$toplevel->optionAdd("$Xname*Entry.font",    
                     '-*-helvetica-medium-r-*-*-12-*-*-*-*-*-*-*',20);

$toplevel->optionAdd("$Xname*Exit.font",    
                     '-*-helvetica-bold-r-*-*-10-*-*-*-*-*-*-*',20);
$toplevel->optionAdd("$Xname*Exit.relief",          'groove',20);
$toplevel->optionAdd("$Xname*Exit.padX",          1,20);
$toplevel->optionAdd("$Xname*Exit.padY",          1,20);
$toplevel->optionAdd("$Xname*Exit.borderWidth",          2,20);
$toplevel->optionAdd("$Xname*Exit*background",  "#a0a0a0",20);
$toplevel->optionAdd("$Xname*Exit*disabledForeground",  "#ffffff",20);

$toplevel->optionAdd("$Xname*Entry.background",  "#ffffff",20);
$toplevel->optionAdd("$Xname*Entry.disabledForeground",  "#c0c0c0",20);
$toplevel->optionAdd("$Xname*Entry.relief",  "sunken",20);
$toplevel->optionAdd("$Xname*Entry.borderWidth",  2,20);

$toplevel->optionAdd("$Xname*ListBox.background",  "#ffffff",20);
$toplevel->optionAdd("$Xname*ListBox.relief",  "sunken",20);
$toplevel->optionAdd("$Xname*ListBox.borderWidth",  1,20);
$toplevel->optionAdd("$Xname*ListFrame.background",  "#ffffff",20);

$toplevel->optionAdd("$Xname*ListRowOdd.background",  "#dfffe7",20);
$toplevel->optionAdd("$Xname*ListRowEven.background",  "#ffffff",20);
$toplevel->optionAdd("$Xname*OldListRowOdd.background",  "#dfffe7",20);
$toplevel->optionAdd("$Xname*OldListRowEven.background",  "#ffffff",20);
$toplevel->optionAdd("$Xname*OldListRowOdd.foreground",  "#aaa0a0",20);
$toplevel->optionAdd("$Xname*OldListRowEven.foreground",  "#aaa0a0",20);

$toplevel->optionAdd("$Xname*Scrollbar*background",  "#f0d0b0",20);
$toplevel->optionAdd("$Xname*Scrollbar*foreground",  '#000000',20);
$toplevel->optionAdd("$Xname*Scrollbar*borderWidth",  '2',20);
$toplevel->optionAdd("$Xname*Scrollbar*relief",  'sunken',20);

$toplevel->optionAdd("$Xname*ClickList*background",  "#f0d0b0",20);
$toplevel->optionAdd("$Xname*ClickList*foreground",  '#000000',20);
$toplevel->optionAdd("$Xname*ClickList*borderWidth",  '1',20);
$toplevel->optionAdd("$Xname*ClickList*relief",  'raised',20);

$toplevel->optionAdd("$Xname*ClickListButton*background",  "#f0d0b0",20);
$toplevel->optionAdd("$Xname*ClickListButton*foreground",  '#000000',20);
$toplevel->optionAdd("$Xname*ClickListButton*borderWidth",  '1',20);
$toplevel->optionAdd("$Xname*ClickListButton*relief",  'raised',20);

$toplevel->optionAdd("$Xname*ClickList.Item*background",  "#f0d0b0",20);
$toplevel->optionAdd("$Xname*ClickList.Item*foreground",  '#000000',20);
$toplevel->optionAdd("$Xname*ClickList.Item*borderWidth",  '0',20);
$toplevel->optionAdd("$Xname*ClickList.Item*relief",  'flat',20);

$toplevel->configure(-background=>$toplevel->optionGet("background",""));

#$toplevel->resizable(FALSE,FALSE);
my $xpm_snatch=$toplevel->Pixmap("_snatchlogo_xpm",-file=>$logofile);

$window_shell=$toplevel->Label(Name=>"shell",borderwidth=>1,relief=>raised)->
    place(-x=>10,-y=>36,-relwidth=>1.0,-relheight=>1.0,
          -width=>-20,-height=>-46,-anchor=>'nw');

$window_setupbar=$toplevel->Button(-class=>Tab,Name=>"setup",text=>"configuration")->
    place(-relx=>1.0,-anchor=>'se',-in=>$window_shell,-bordermode=>outside);
$window_timerbar=$toplevel->Button(-class=>Tab,Name=>"timer",text=>"timer setup")->
    place(-bordermode=>outside,-anchor=>'ne',-in=>$window_setupbar);

$window_quit=$window_shell->Button(-class=>"Exit",text=>"quit")->
    place(-x=>-1,-y=>-1,-relx=>1.0,-rely=>1.0,-anchor=>'se');

$window_logo=$toplevel->
    Label(Name=>"logo",-class=>"Panel",image=>$xpm_snatch)->
    place(-x=>5,-y=>5,-anchor=>'nw');

$window_version=$toplevel->
    Label(Name=>"logo text",-class=>"Panel",text=>$version)->
    place(-x=>5,-relx=>1.0,-rely=>1.0,-anchor=>'sw',-in=>$window_logo);

$window_statuslabel=$window_shell->
    Label(Name=>"statuslabel",-class=>"Statuslabel",text=>"Status: ")->
    place(-x=>5,-y=>0,-rely=>.2,-relheight=>.4,-anchor=>'w');

$window_status=$window_shell->
    Label(Name=>"status",-class=>"Status",text=>"Starting...",-anchor=>'w')->
    place(-x=>5+$window_statuslabel->reqwidth,-rely=>.2,-anchor=>'w',
          -relheight=>.4,-relwidth=>1.0,-width=>-10-$window_statuslabel->reqwidth);

$window_active=$window_shell->Button(Name=>"active",text=>"capture all",
                                    state=>disabled)->
    place(-x=>5,-y=>0,-relx=>0.,-rely=>.55,-relwidth=>.33,
          -width=>-5,-anchor=>'w',-in=>$window_shell);

$window_timer=$window_shell->Button(Name=>"timer",text=>"timed record",
                                    state=>disabled)->
    place(-x=>0,-y=>0,-relx=>.333,-rely=>.55,-relwidth=>.33,
          -width=>-0,-anchor=>'w',-in=>$window_shell);

$window_inactive=$window_shell->Button(Name=>"inactive",text=>"off",
                                    state=>disabled)->
    place(-x=>0,-y=>0,-relx=>.667,-rely=>.55,-relwidth=>.33,
          -width=>-5,-anchor=>'w',-in=>$window_shell);

$window_mute=$window_shell->Label(Name=>"mute",text=>"silent capture: ")->
    place(-x=>5,-y=>0,-relx=>0.,-rely=>.85,
          -anchor=>'w',-in=>$window_shell);

$window_amute=$window_shell->Button(Name=>"audio",text=>"audio",
                                    state=>disabled)->
    place(-x=>2,-relx=>1.0,-relheight=>1.0,-anchor=>'nw',-in=>$window_mute,
          -bordermode=>outside);

$window_vmute=$window_shell->Button(Name=>"video",text=>"video",
                                    state=>disabled)->
    place(-x=>2,-relx=>1.0,-relheight=>1.0,-anchor=>'nw',-in=>$window_amute,
          -bordermode=>outside);

$minwidth=
    $window_logo->reqwidth()+
    $window_version->reqwidth()+
    $window_setupbar->reqwidth()+
    $window_timerbar->reqwidth()+
    30;
$minheight=
    $window_logo->reqheight()+
    $window_statuslabel->reqheight()+
    $window_active->reqheight()+
    max($window_mute->reqheight(),$window_quit->reqheight())+
    30;

$toplevel->minsize($minwidth,$minheight);

my$geometry=$toplevel->optionGet("geometry","");
if(defined($geometry)){
    $toplevel->geometry($geometry);
}else{
    $toplevel->geometry(($minwidth+20).'x'.$minheight);
}
$toplevel->update();

$window_quit->configure(-command=>[sub{Shutdown();}]);
$window_amute->configure(-command=>[sub{Robot_Audio();}]);
$window_vmute->configure(-command=>[sub{Robot_Video();}]);
$window_active->configure(-command=>[sub{Robot_Active();}]);
$window_timer->configure(-command=>[sub{Robot_Timer();}]);
$window_inactive->configure(-command=>[sub{Robot_Inactive();}]);
$window_setupbar->configure(-command=>[sub{Setup();}]);
$window_timerbar->configure(-command=>[sub{Timer();}]);

# bind socket
BindSocket();

# throw a realplayer process and 
ThrowRealPlayer();

# main loop 
Tk::MainLoop();

ub trim_glob{
    # the bsd glob routine deals poorly with some whitespace...
    my$pattern=shift;
    if($pattern eq ""){
        $pattern=".";
    }
    if($pattern ne '-'){

        $pattern=~s/^(\s+).*//;
        $pattern=~s/(\s+)$//;
        
        my at result=bsd_glob($pattern,GLOB_TILDE|GLOB_BRACE);

        if(!defined($result[0])){
            @result=bsd_glob($pattern,GLOB_TILDE|GLOB_BRACE|GLOB_NOCHECK);
        }
        $result[0];

    }else{
        '-';
    }
}

ub ThrowRealPlayer{
    $recording_active=0;
    $recording_pending=0;
    $SIG{CHLD}='IGNORE';

    Status("Starting RealPlayer...");
    # set up the environment
    my$glob=trim_glob("$CONFIG{'LIBSNATCH'}");

    if(GLOB_ERROR || !defined($glob)){
        Status("Failed to find libsnatch.so!");
        Alert("Failed to find libsnatch.so!",
              "Please verify that libsnatch.so is built,".
              " installed, and its location is set correctly ".
              "on the configuration panel.\n");
        return;
    }
    
    $ENV{"SNATCH_DEBUG"}=1;
    $ENV{"LD_PRELOAD"}=$glob;
    $ENV{"SNATCH_COMM_SOCKET"}=$backchannel_socket;

    $glob=trim_glob("$CONFIG{'REALPLAYER'}");
    if(GLOB_ERROR || !defined($glob)){
        Status("Failed to find RealPlayer!");
        Alert("Failed to find RealPlayer!",
              "Please verify that RealPlayer is installed,".
              " executable, and its location is set correctly".
              "on the configuration panel.\n");
        return;
    }

    die "pipe call failed unexpectedly: $!" unless pipe REAL_STDERR,WRITEH;
    $realpid=open3("STDIN",">&STDOUT",">&WRITEH",$glob);
    close WRITEH;

    # a select loop until we have the socket accepted
    my $rin = $win = $ein = '';
    my $rout,$wout,$eout;
    vec($rin,fileno(REAL_STDERR),1)=1;
    vec($rin,fileno(LISTEN_SOCK),1)=1;
    $ein=$rin | $win;

    my $time=20;
    my $stderr_output;

    Status("Waiting for rendevous... [$time]");
    while($time>0){
        my($nfound,$timeleft)=select($rout=$rin, $wout=$win, $eout=$ein, 1);
        if($nfound==0){
            $time--;
            Status("Waiting for rendevous... [$time]");
        }else{
            $toplevel->update();
            if(vec($rout,fileno(REAL_STDERR),1)){
                $bytes=sysread REAL_STDERR, my$scalar, 4096;
                $stderr_output.=$scalar;
                
                if($bytes==0){
                    Status("Rendevous failed.");

                    Alert("RealPlayer didn't start successfully.  ".
                          "Here's the complete debugging output of the ".
                          "attempt:",
                          $stderr_output);

                    return;
                }
            }
            if(vec($rout,fileno(LISTEN_SOCK),1)){
                # socket has a request
                $status=accept(COMM_SOCK,LISTEN_SOCK);
                $comm_ready=1;
                $time=-1;
            }
        }
    }

    if($time==0){
        Alert("Rendevous failed!",
              "RealPlayer appears to have started, but the Snatch ".
              "robot could not connect to it via libsnatch.  Most likely,".
              " this is a result of having a blank or mis-set ".
              "'libsntach.so location' setting on the configuration ".
              "menu.  Please verify this setting before continuing.\n");
        Status("Rendevous failed!");
        return;
    }

    # configure
    Status("RealPlayer started...");
    $toplevel->fileevent(REAL_STDERR,'readable'=>[sub{ReadStderr();}]);
    ButtonConfig();
    SendConfig();
    Robot_Active() if($mode eq 'active');
    Robot_Timer() if($mode eq 'timer');
    Robot_Inactive() if($mode eq 'inactive');
    TestOutpath($CONFIG{OUTPUT_PATH});

}

ub SendConfig{
    send_string("F",$CONFIG{'OUTPUT_PATH'});
    send_string("E",$CONFIG{'ESD_SOCK'});
    send_string("D",$CONFIG{'OSS_DEVICE'});
}

ub BindSocket{
    Status("Binding socket..");
    die $! unless socket(LISTEN_SOCK, PF_UNIX, SOCK_STREAM,0);
    unlink($backchannel_socket);
    die $! unless bind(LISTEN_SOCK,$uaddr);
    die $! unless listen(LISTEN_SOCK,SOMAXCONN);
}

ub Disconnect{
    $comm_ready=0;
    ButtonConfig();
    close COMM_SOCKET if($comm_ready);
    close REAL_STDERR if($comm_ready);
}

sub ReleaseSocket{
    $comm_ready=0;
    unlink($backchannel_socket);
    close(LISTEN_SOCK);
}

ub AcceptSocket{
    Status("Waiting for rendevous...");

    eval{
        local $SIG{ALRM} = sub { Status("Failed to rendevous"); };
        alarm 15;
        $status=accept(COMM_SOCK,LISTEN_SOCK);
        alarm 0;
        if($status){
            #enable the panel
            Status("RealPlayer contacted");
            $comm_ready=1;
            ButtonConfig();
        }
    }
}

ub SaveConfig{
    die $! unless open CFILE, ">$configfile".".tmp";
    foreach $key (keys %CONFIG){
        print CFILE "$key=$CONFIG{$key}\n";
    }
    close CFILE;
    die $! unless rename "$configfile".".tmp", $configfile;
}

ub SaveHistory{
    die $! unless open HFILE, ">$historyfile".".tmp";
    foreach $line (@TIMER){
        print HFILE "$line\n";
    }
    close HFILE;
    die $! unless rename "$historyfile".".tmp", $historyfile;
}

ub Shutdown{
# save the config/history
    Robot_Exit();
    ReleaseSocket();
    SaveConfig();
    SaveHistory();

  Tk::exit(0);
}

sub send_string{
    my($op,$code)=@_;
    syswrite COMM_SOCK,$op;
    syswrite COMM_SOCK, (pack 'S', length $code);
    syswrite COMM_SOCK, $code;
}

ub Robot_PlayLoc{
    my($loc,$username,$password)=@_;
    my $stopcode=join "",("Ks",pack ("S",4));
    my $loccode=join "",("Kl",pack ("S",4));
    
    syswrite COMM_SOCK,$stopcode;

    send_string("P",$password);
    send_string("U",$username);
    send_string("L",$loc);

    syswrite COMM_SOCK,$loccode;
 
    # watch for bad password
}

ub Robot_PlayFile{
    my($openfile)=@_;
    my $stopcode=join "",("Ks",pack ("S",4));
    my $opencode=join "",("Ko",pack ("S",4));

    syswrite COMM_SOCK,$stopcode;
    send_string("O",$openfile);
    syswrite COMM_SOCK,$opencode;
}

ub Robot_Stop{
    $last_timer_event=time();
    $recording_restart=0;
    my $stopcode=join "",("Ks",pack ("S",4));
    syswrite COMM_SOCK,$stopcode;
}

ub Robot_Exit{
    my $exitcode=join "",("Kx",pack ("S",4));
    syswrite COMM_SOCK,$playcode;
    Disconnect();
}

ub Robot_Active{
    $recording_restart=0;
    $last_timer_event=0;
    $next_timer_event=0;
    if(defined($timer_callback) && !recording_active){
        $timer_callback->cancel();
        undef $timer_callback;
    }

    # clear out robot settings to avoid hopelessly confusing the user
    send_string("U","");
    send_string("P","");
    send_string("O","");
    send_string("L","");
    send_string("F",$CONFIG{'OUTPUT_PATH'});
    syswrite COMM_SOCK,'A';
    Robot_Audio($CONFIG{"AUDIO_MUTE"});
    Robot_Video($CONFIG{"VIDEO_MUTE"});
    Status("Ready/waiting to record") if (!$recording_active);
    $mode='active';
    ButtonPressConfig();
}

ub Robot_Inactive{
    $recording_restart=0;
    $last_timer_event=0;
    $next_timer_event=0;
    if(defined($timer_callback)){
        $timer_callback->cancel();
        undef $timer_callback;
    }

    send_string("U","");
    send_string("P","");
    send_string("O","");
    send_string("L","");
    send_string("F",$CONFIG{'OUTPUT_PATH'});
    syswrite COMM_SOCK,'I';
    Robot_Audio($CONFIG{"AUDIO_MUTE"});
    Robot_Video($CONFIG{"VIDEO_MUTE"});
    Status("Recording off");
    $mode='inactive';
    ButtonPressConfig();
}

ub Robot_Timer{
    $last_timer_event=0;
    $next_timer_event=0;
    send_string("O","");
    send_string("L","");
    syswrite COMM_SOCK,'T';
    Status("Timer wait");
    $mode='timer';
    ButtonPressConfig();
    SetupTimerDispatch();
    if(!defined($timer_callback)){
        $timer_callback=$toplevel->repeat(1000,[main::TimerWatch]);
    }
}

ub DoTimedEntry{
    my($start,$line)=@_;
    my($year,$month,$day,$dayofweek,$hour,$minute,$duration,$audio,$video,$username,
       $password,$outfile,$url)=SplitTimerEntry($line);

    $recording_pending=1;
    $recording_restart=1;
    $last_timer_event=$start;
    $next_timer_event=$start+$duration-1; # the -1 is important; makes sure contiguous
                                          # but nonoverlapping events don't interfere

    syswrite COMM_SOCK,'A';    
    send_string("F",$outfile);
    Robot_Audio($audio);
    Robot_Video($video);
    if($url=~/^file:(.*)/){
        #file, through the file dialog
        Robot_PlayFile($1);
    }else{
        #network stream/URL, through location dialog
        Robot_PlayLoc($url,$username,$password);
    }
}

ub SetupTimerDispatch{
    TimerSort();
    my$now=time();
    my at TIMETEMP=@TIMER_TIMES;
    my at ENDTIMETEMP=@TIMER_ENDTIMES;
    $next_timer_event=0;

    foreach my$line (@TIMER){
        my$start=shift @TIMETEMP;
        my$end=shift @ENDTIMETEMP;

        if($start>$last_timer_event && $start<=$now && $end>$now){    
            Robot_Stop();
            DoTimedEntry($start,$line);
            return;
        }else{
            $next_timer_event=$start if(($next_timer_event==0 || 
                                        $start<$next_timer_event) && $start>$now);
        }
    }
    # nothing happening now
    Robot_Stop();
    syswrite COMM_SOCK,'T';    
}
    
sub TimerWatch{
    if($mode=~/timer/){
        my$now=time();

        if($TIMER_ENDTIMES[$TIMER_SORTED[$#TIMER]]<$now){
            Robot_Stop();
            Robot_Inactive();
            ButtonConfig();
        }else{
            
            if($now>=$next_timer_event){
                SetupTimerDispatch();
            }else{
                my$waiting_seconds=$next_timer_event-$now;
                
                my$waiting_minutes=int($waiting_seconds/60);
                $waiting_seconds-=$waiting_minutes*60;
                
                my$waiting_hours=int($waiting_minutes/60);
                $waiting_minutes-=$waiting_hours*60;
                
                my$waiting_days=int($waiting_hours/24);
                $waiting_hours-=$waiting_days*24;
                my$prompt;
                
                if($waiting_days){
                    $prompt=$waiting_days."d ".$waiting_hours."h ".
                        $waiting_minutes."m";
                }elsif($waiting_hours){
                    $prompt=$waiting_hours."h ".$waiting_minutes."m";
                }else{
                    $prompt=$waiting_minutes."m ".$waiting_seconds."s";
                }
                
                if($recording_active){
                    Status("Timer recording [$prompt]");
                }else{
                    if($recording_pending){
                        Status("Starting record...");
                    }else{
                        Status("Timer wait [$prompt]");
                    }
                }
            }
        }
    }else{
        if($recording_active){

            my$now=time();
            my$seconds=$now-$recording_active;
            
            my$minutes=int($seconds/60);
            $seconds-=$minutes*60;
        
            my$hours=int($minutes/60);
            $minutes-=$hours*60;
            
            my$prompt;
            
            if($hours){
                $prompt=$hours."h ".$minutes."m";
            }else{
                $prompt=$minutes."m ".$seconds."s";
            }
            
            
            Status("Recording [$prompt]");
        }else{
            if($recording_pending){
                Status("Starting record...");		
            }else{
                Status("Ready/waiting to record");
            }
            $timer_callback->cancel();
            undef $timer_callback;
        }
    }	
}

ub Robot_Audio{
    my($onoff)=@_;

    if($onoff=~m/yes/){
        syswrite COMM_SOCK,'s';
        $CONFIG{"AUDIO_MUTE"}='yes';
    }elsif($onoff=~m/no/){
        syswrite COMM_SOCK,'S';
        $CONFIG{"AUDIO_MUTE"}='no';
    }else{
        if($CONFIG{"AUDIO_MUTE"}=~/yes/){
            syswrite COMM_SOCK,'S';
            $CONFIG{"AUDIO_MUTE"}='no';
        }else{
            syswrite COMM_SOCK,'s';
            $CONFIG{"AUDIO_MUTE"}='yes';
        }
    }
    ButtonPressConfig();

}

ub Robot_Video{
    my($onoff)=@_;

    if($onoff=~m/yes/){
        syswrite COMM_SOCK,'v';
        $CONFIG{"VIDEO_MUTE"}='yes';
    }elsif($onoff=~m/no/){
        syswrite COMM_SOCK,'V';
        $CONFIG{"VIDEO_MUTE"}='no';
    }else{
        if($CONFIG{"VIDEO_MUTE"}=~/yes/){
            syswrite COMM_SOCK,'V';
            $CONFIG{"VIDEO_MUTE"}='no';
        }else{
            syswrite COMM_SOCK,'v';
            $CONFIG{"VIDEO_MUTE"}='yes';
        }
    }
    ButtonPressConfig();
}

ub SplitTimerEntry{
    my($line)=@_;

    if($line=~/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+):(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)/){
        my $year=$1;
        my $month=$2;
        my $day=$3;
        my $dayofweek=$4;
        my $hour=$5;
        my $minute=$6;
        my $duration=$7;
        
        my $audio=$8;
        my $video=$9;
        
        my $fields=$10;
        
        my $username;
        my $password;
        my $outfile;
        my $url;
        
        ($username,$fields)=LengthParse($fields);
        ($password,$fields)=LengthParse($fields);
        ($outfile,$fields)=LengthParse($fields);
        ($url,$fields)=LengthParse($fields);

        ($year,$month,$day,$dayofweek,$hour,$minute,$duration,$audio,$video,$username,
         $password,$outfile,$url);
    }else{
        undef;
    }
}

ub LengthParse{
    my($line)=@_;
    $line=~/(\d+):(.*)/;
    $length=$1;
    $rest=$2;

    (substr($rest,0,$length),substr($rest,$length));
}	

ub MonthDays{
    my($month,$year)=@_;
    
    if($month==2){
        if($year % 4 !=0){
            28;
        }elsif ($year % 400 == 0){
            29;
        }elsif ($year % 100 == 0){
            28;
        }else{
            29;
        }
    }else{
        my @trans=(0,31,0,31,30,31,30,31,31,30,31,30,31);
        $trans[$month];
    }
}

ub TimerStart{
    my($try,$etry)=TimerWhen(@_);
    $try;
}
               
sub TimerWhen{
    my($try,$etry,$year,$month,$day,$dayofweek,$hour,$minute,$duration)=@_;

    #overguard 
    if($minute ne '*'){while($minute>=60){$minute-=60;
                                          $hour++if($hour ne '*');}};
    if($hour ne '*'){while($hour>=24){$hour-=24;
                                      $day++ if($day ne '*');}};
    if($day ne '*' && $month ne '*' && $year ne '*'){
        while($month>12){$month-=12;$year++;};
        while($day>MonthDays($month,$year)){
            $day-=MonthDays($month,$year);$month++;
            while($month>12){$month-=12;$year++;};
        }
    }
    if($month ne '*'){while($month>12){$month-=12;
                                       $year++ if ($year ne '*')}};
    
    my $now=time();
    my($nowsec,$nowmin,$nowhour,$nowday,$nowmonth,$nowyear)=localtime($now);    
    $nowmon++;
    $nowyear+=1900;

    
    # boundary cases in each...  rather than solving it exactly, we'll
    # solve it empirically. Laziness as a virtue.
    if($year eq '*'){
        ($try,$etry)=TimerWhen($try,$etry,$nowyear-1,$month,$day,$dayofweek,
                       $hour,$minute,$duration);
        return ($try,$etry) if($try>$now);
        ($try,$etry)=TimerWhen($try,$etry,$nowyear,$month,$day,$dayofweek,
                       $hour,$minute,$duration);
        return ($try,$etry) if($try>$now);
        ($try,$etry)=TimerWhen($try,$etry,$nowyear+1,$month,$day,$dayofweek,
                       $hour,$minute,$duration);
        return ($try,$etry) if($try>$now);
    }elsif($month eq '*'){
        for(my$i=1;$i<13;$i++){
            ($try,$etry)=TimerWhen($try,$etry,$year,$i,$day,$dayofweek,
                           $hour,$minute,$duration);
            return ($try,$etry) if($try>$now);
        }
    }elsif($day eq '*'){
        # important to go for a weekday match */
        for(my$i=1;$i<32;$i++){
            ($try,$etry)=TimerWhen($try,$etry,$year,$month,$i,$dayofweek,
                           $hour,$minute,$duration);
            return ($try,$etry) if($try>$now);
        }
    }elsif($hour eq "*"){
        return ($try,$etry);
    }elsif($minute eq "*"){
        return ($try,$etry);
    }elsif($duration eq "*"){
        return ($try,$etry);
    }else{
        if($month==0){
            # oops; we got a bad line in the history file
            return ($try,$etry);
        }
            
        my $start=timelocal(0,$minute,$hour,$day,$month-1,$year);
        my $end=$start+$duration;
        
        # make sure day-of-month and day-of-week agree
        if($dayofweek ne '*'){
            my($tsec,$tmin,$thour,$tday,$tmon,$tyear,$twday)=localtime($start);
            if($twday != $dayofweek){return ($try,$etry)};
        }
        
        if($try==-1){
            return($start,$end);
        }

        if($try<$now && $etry>$now){
            # current best guess straddles now 
            if($start<$now && $start>$try){
                #shouldn't allow this case but eh
                return ($start,$end);
            }
        }

        if($etry<$now){
            # current guess entirely preceeds now; prefer any guess in the future
            return ($start,$end) if($start>$try);
        }

        if($try>$now){
            # current guess in the future.  prefer any guess earlier in time that is not entirely past.
            return ($start,$end) if($start<$try && $end>$now);
        }

    }
    ($try,$etry);
}

ub max{
    my$val=shift @_;
    while (defined(my$n=shift @_)){
        $val=$n if($n>$val);
    }
    $val;
}

ub Status{
    $window_status->configure(text=>shift @_);
    $toplevel->update();
}

ub Alert{
    my($message,$detail,$window)=@_;

    $window=$toplevel if(!defined($window));
    $modal->destroy() if(defined($modal));

    $modal=new MainWindow(-class=>"$Xname");
    $modal->configure(-background=>$modal->optionGet("background",""));
    
    $modal_shell=$modal->Label(-class=>Alert,Name=>"shell",
                               borderwidth=>1,relief=>raised)->
                                   place(-x=>4,-y=>4,-relwidth=>1.0,-relheight=>1.0,
                                         -width=>-8,-height=>-8,-anchor=>'nw');

    $modal_exit=$modal_shell->
        Button(-class=>"Exit",text=>"X")->
                   place(-x=>-1,-y=>-1,-relx=>1.0,-rely=>1.0,-anchor=>'se');
    
    $modal_message=$modal_shell->
        Label(text=>$message,-class=>"AlertText")->
            place(-x=>5,-y=>10);

    $width=$modal_message->reqwidth();
    $width=300 if($width<300);

    $modal_detail=$modal_shell->
        Message(text=>$detail,-class=>"AlertDetail",
                -width=>($width-$modal_exit->reqwidth()))->
            place(-relx=>0,-y=>5,-rely=>1.0,-anchor=>'nw',
                  -in=>$modal_message);

    $width+=20;
    $height=$modal_message->reqheight()+$modal_detail->reqheight()+30;

    my$xx=$window->rootx();
    my$yy=$window->rooty();
    my$ww=$window->width();
    my$hh=$window->height();

    $x=$xx+$ww/2-$width/2;
    $y=$yy+$hh/2-$height/2;

    $modal->geometry($width."x".$height."+".int($x)."+".int($y));
    $modal->resizable(FALSE,FALSE);
    $modal->transient($window);
    $modal_exit->configure(-command=>[sub{$modal->destroy();undef $modal}]);
}

ub ReadStderr{
    my$saveflag=0;

    $bytes=sysread REAL_STDERR, my$scalar, 4096;
    if($bytes==0){
        Disconnect();
        Alert("RealPlayer unexpectedly exited!","Attempting to start a new copy...\n");
        $toplevel->fileevent(REAL_STDERR,'readable' => ''); 
        ThrowRealPlayer();
    }

    print STDERR $scalar if($CONFIG{'DEBUG'} eq 'yes');

    push my at lines, split /\n/, $saved_stderr.$scalar;
    if((chomp $scalar)==0){
        $saved_stderr=$lines[$#lines];
    }else{
        $saved_stderr="";
    }

    foreach my$line (@lines){

        if($line=~/X display closed/){
            Disconnect();
            $toplevel->fileevent(REAL_STDERR,'readable' => ''); 
          Tk::exit(0);
        }	

        if($line=~/ERROR: Could not stat[^\n]+\n\s+([^:]*): (.+)*/){
            Alert("Unable to open output file!",
                  "Libsnatch reported $1: $2\n");
        }elsif($line=~/Password not/){
            Alert("Password not accepted!",
                  "Hopefully self explanatory...\n");
        }elsif($line=~/\*\*ERROR: /){
            Alert("Libsnatch reported an error!",
                  $saved_stderr.$scalar."\n");
        }
        

        if($line=~/bit ZPixmap/){
            Alert("ERROR: This X server is not using 24/32 bit visuals!",
                  "Right now, Snatch is still new ad as such only supports the highest".
                  " bitdepth visuals.  These visuals give the best quality and are thus".
                  " recommended strongly for capture.  Other visuals will eventually be".
                  " supported as well, but they won't work for now.\n");
        }
        
        if($line=~/Capture stopped/){
            $recording_active=0;
            $recording_pending=0;

            if($recording_restart){
                SetupTimerDispatch();
            }
        }

        if($line=~/Capturing/){
            $recording_active=time();
            $recording_pending=0;
            if(!defined($timer_callback)){
                $timer_callback=$toplevel->repeat(1000,[sub{main::TimerWatch();}]);
            }
        }
    }
}

ub ButtonPressConfig(){
    $window_timer->configure(-relief=>'groove') if ($mode ne 'timer');
    $window_timer->configure(-relief=>'sunken') if ($mode eq 'timer');
    $window_active->configure(-relief=>'groove') if ($mode ne 'active');
    $window_active->configure(-relief=>'sunken') if ($mode eq 'active');
    $window_inactive->configure(-relief=>'groove') if ($mode ne 'inactive');
    $window_inactive->configure(-relief=>'sunken') if ($mode eq 'inactive');

    $window_amute->configure(-relief=>'groove') if ($CONFIG{AUDIO_MUTE} eq 'no');
    $window_amute->configure(-relief=>'sunken') if ($CONFIG{AUDIO_MUTE} eq 'yes');
    $window_vmute->configure(-relief=>'groove') if ($CONFIG{VIDEO_MUTE} eq 'no');
    $window_vmute->configure(-relief=>'sunken') if ($CONFIG{VIDEO_MUTE} eq 'yes');

    if(defined($tentry)){

        if($mode=~/^active/ || ($mode=~/timer/ && ($recording_active ||
                                                   $recording_pending))){
            $tentry_test->configure(-state=>disabled);
        }else{
            $tentry_test->configure(-state=>normal);
        }

    }

}

ub ButtonConfig{
    my$now=time();
    if ($#TIMER<0 || $TIMER_ENDTIMES[$TIMER_SORTED[$#TIMER]]<$now || 
        !$comm_ready){
        $window_timer->configure(state=>disabled);
    }else{ 
        $window_timer->configure(state=>normal);
    }
    if (!$comm_ready){
        $window_active->configure(state=>disabled);
        $window_inactive->configure(state=>disabled);
        $window_amute->configure(state=>disabled);
        $window_vmute->configure(state=>disabled);
    }else{ 
        $window_active->configure(state=>normal);
        $window_inactive->configure(state=>normal);
        $window_amute->configure(state=>normal);
        $window_vmute->configure(state=>normal);
    }
    ButtonPressConfig();
}

ub Setup{
    %TEMPCONF=%CONFIG;

    $window_setupbar->configure(-state=>'disabled');
    $window_setupbar->configure(-relief=>'flat');
    $setup=new MainWindow(-class=>'Snatch');

    $setup_title=$setup->
        Label(Name=>"setup text",-class=>"Panel",text=>"Configuration")->
                  place(-x=>10,-y=>5);

    $setup_shell=$setup->Label(Name=>"shell",borderwidth=>1,relief=>raised)->
        place(-x=>10,-y=>$setup_title->reqheight()+10,-relwidth=>1.0,-relheight=>1.0,
              -width=>-20,-height=>-$setup_title->reqheight()-20,-anchor=>'nw');
    
    $setup_quit=$setup_shell->
        Button(-class=>"Exit",text=>"ok")->
                   place(-x=>-1,-y=>-1,-relx=>1.0,-rely=>1.0,-anchor=>'se');
    $setup_apply=$setup_shell->
        Button(-class=>"Exit",text=>"apply")->
                   place(-x=>0,-y=>0,-anchor=>'ne',-in=>$setup_quit,
                         -bordermode=>outside);
    $setup_cancel=$setup_shell->
        Button(-class=>"Exit",text=>"cancel")->
                   place(-x=>1,-y=>-1,-rely=>1.0,-anchor=>'sw');
    

    # Real location
    $nexty=5;
    $temp=$setup_shell->
        Label(text=>"RealPlayer location:")->
            place(-x=>5,-y=>$nexty);
    $setup_shell->
        Entry(-textvariable=>\$TEMPCONF{'REALPLAYER'},-width=>"256")->
            place(-y=>$nexty,-x=>$temp->reqwidth()+10,
                  -anchor=>'nw',-relwidth=>1.0,
                  -height=>$temp->reqheight(),
                  -width=>-$temp->reqwidth()-18);
    $nexty=8+$temp->reqheight();

    #libsnatch location
    $temp=$setup_shell->
        Label(text=>"libsnatch.so location:")->
            place(-x=>5,-y=>$nexty);
    $setup_shell->
        Entry(-textvariable=>\$TEMPCONF{'LIBSNATCH'},-width=>"256")->
            place(-y=>$nexty,-x=>$temp->reqwidth()+10,
                  -anchor=>'nw',-relwidth=>1.0,
                  -height=>$temp->reqheight(),
                  -width=>-$temp->reqwidth()-18);
    $nexty+=8+$temp->reqheight();

    #audio device
    $temp=$setup_shell->
        Label(text=>"OSS audio device:")->
            place(-x=>5,-y=>$nexty);
    $setup_shell->
        Entry(-textvariable=>\$TEMPCONF{'OSS_DEVICE'},-width=>"256")->
            place(-y=>$nexty,-x=>$temp->reqwidth()+10,
                  -anchor=>'nw',-relwidth=>1.0,
                  -height=>$temp->reqheight(),
                  -width=>-$temp->reqwidth()-18);
    $nexty+=8+$temp->reqheight();
    
    #esd port
    $temp=$setup_shell->
        Label(text=>"EsounD server socket:")->
            place(-x=>5,-y=>$nexty);
    $setup_shell->
        Entry(-textvariable=>\$TEMPCONF{'ESD_SOCK'},-width=>"256")->
            place(-y=>$nexty,-x=>$temp->reqwidth()+10,
                  -anchor=>'nw',-relwidth=>1.0,
                  -height=>$temp->reqheight(),
                  -width=>-$temp->reqwidth()-18);
    $nexty+=15+$temp->reqheight();
    
    #debug
    if($TEMPCONF{'DEBUG'} eq 'yes'){
        $temp=$setup_debug=$setup_shell->
            Button(text=>"full debug output",-relief=>'sunken',-pady=>1)->
                place(-x=>5,-y=>$nexty);
    }else{
        $temp=$setup_debug=$setup_shell->
            Button(text=>"full debug output",-pady=>1)->
                   place(-x=>5,-y=>$nexty);
    }
    $setup_debug->configure(-command=>[sub{Setup_Debug();}]);
    $nexty+=15+$temp->reqheight();

    #output path
    $temp=$setup_shell->
        Label(text=>"capture output:")->
            place(-x=>5,-y=>$nexty);

    $setup_path=$setup_shell->
        Entry(-textvariable=>\$TEMPCONF{'OUTPUT_PATH'},-width=>256)->
            place(-x=>$temp->reqwidth()+10,
                  -y=>$nexty,-height=>$temp->reqheight(),
                  -width=>-$temp->reqwidth()-18,
                  -relwidth=>1.0);

    $nexty+=15+$temp->reqheight();

    $minwidth=400;
    $minheight=$nexty+28+$setup_title->reqheight()+$setup_cancel->reqheight();
    
    $setup->minsize($minwidth,$minheight);
    $setup->geometry(($minwidth+20)."x".$minheight);

    $setup_quit->configure(-command=>[sub{
        my $temppath=$TEMPCONF{"OUTPUT_PATH"};
        if(TestOutpath($temppath,$setup)){
            %CONFIG=%TEMPCONF;
            $setup->destroy();undef $setup;
            $CONFIG{OUTPUT_PATH}=trim_glob($temppath);
            $window_setupbar->configure(state=>'normal');
            $window_setupbar->configure(relief=>'raised');
            SaveConfig();
            SendConfig();
            ThrowRealPlayer() if(!$comm_ready);
            Status("Configuration successful");
        }
    }]);

    $setup_apply->configure(-command=>[sub{
        my $temppath=$TEMPCONF{"OUTPUT_PATH"};
        
        if(TestOutpath($temppath,$setup)){
            %CONFIG=%TEMPCONF;
            $CONFIG{OUTPUT_PATH}=trim_glob($temppath);
            SaveConfig();
            SendConfig();	    
            ThrowRealPlayer() if(!$comm_ready);
            Status("Configuration successful");
        }
    }]);
    
    $setup_cancel->configure(-command=>[sub{
        $setup->destroy();undef $setup;
        $window_setupbar->configure(state=>'normal');
        $window_setupbar->configure(relief=>'raised');
    }]);

}

ub TestOutpath{
    my $path=shift;
    my $window=shift;
    
    if($path ne '-'){
        $path=trim_glob($path);
        if(!-W $path){
            # in the event this is a file spec in a writable directory, try touching it
            if(open (TEST,">$path")){
                # oh, ok...
                close(TEST);
                unlink($path);
                return 1;
            }

            Status("Bad output path") if($window==$toplevel);
            Alert("Selected output path isn't writable!",
                  "The output path currently set on the configuration panel either does not exist,".
                  " or is inaccessible due to permissions.  Please set a usable path else ".
                  "recording will fail.\n",$window);
            return 0;
        }
    }
    1;
}

ub Setup_Debug{
    if($TEMPCONF{'DEBUG'} eq 'yes'){
        $TEMPCONF{'DEBUG'}='no';
        $setup_debug->configure(-relief=>groove);
    }else{
        $TEMPCONF{'DEBUG'}='yes';
        $setup_debug->configure(-relief=>sunken);
    }
}    

ub Timer{

    $window_timerbar->configure(-state=>'disabled');
    $window_timerbar->configure(-relief=>'flat');
    $timerw=new MainWindow(-class=>'Snatch');

    $timerw_title=$timerw->
        Label(Name=>"timer text",-class=>"Panel",text=>"Timer Setup")->
            place(-x=>10,-y=>5);
    
    $timerw_shell=$timerw->Label(Name=>"shell",borderwidth=>1,relief=>raised)->
        place(-x=>10,-y=>$timerw_title->reqheight()+10,-relwidth=>1.0,-relheight=>1.0,
              -width=>-20,-height=>-$timerw_title->reqheight()-20,-anchor=>'nw');

    $timerw_quit=$timerw_shell->
        Button(-class=>"Exit",text=>"X")->
                   place(-x=>-1,-y=>-1,-relx=>1.0,-rely=>1.0,-anchor=>'se');
    
    $timerw_quit->configure(-command=>[sub{
        $timerw->destroy();
        undef $listbox;
        undef $timerw;
        $window_timerbar->configure(state=>'normal');
        $window_timerbar->configure(relief=>'raised');
    }]);

    $timerw_delete=$timerw_shell->
        Button(Name=>"delete",text=>"delete",-state=>disabled)->
            place(-x=>-5,-relx=>1.0,-y=>-$timerw_quit->reqheight()-25,
                  -rely=>1.0,-anchor=>'se');

    $timerw_duplicate=$timerw_shell->
        Button(Name=>"edit",text=>"copy",-state=>disabled)->
            place(-x=>0,-y=>-25,-relwidth=>1.0,-anchor=>'sw',
                  -in=>$timerw_delete,-bordermode=>outside);
    $timerw_edit=$timerw_shell->
        Button(Name=>"edit",text=>"edit",-state=>disabled)->
            place(-x=>0,-y=>-5,-relwidth=>1.0,-anchor=>'sw',
                  -in=>$timerw_duplicate,-bordermode=>outside);
    $timerw_add=$timerw_shell->
        Button(Name=>"add",text=>"add")->
            place(-x=>0,-y=>-5,-relwidth=>1.0,-anchor=>'sw',
                  -in=>$timerw_edit,-bordermode=>outside);

    $minwidth=500;
    $minheight=$timerw_add->reqheight()*4+$timerw_quit->reqheight()+$timerw_title->reqheight()+95;
    
    $timerw->minsize($minwidth,$minheight);
    $timerw->geometry(($minwidth+20)."x".$minheight);

    $timerw_add->configure(-command,[sub{Timer_Add();}]);
    $timerw_edit->configure(-command,[sub{Timer_Edit();}]);
    $timerw_delete->configure(-command,[sub{Timer_Delete();}]);
    $timerw_duplicate->configure(-command,[sub{Timer_Copy();}]);

    $listbox=BuildListBox();
    
}

ub BuildListBox(){
    $listbox->destroy() if(defined($listbox));

    # assemble the sorted timer elements we're actually interested into an array for listbox
    my$n=$#TIMER;
    my at listarray;
    
    $daytrans={
        '*','*',
        '0',"Sunday ",
        '1',"Monday ",
        '2',"Tuesday ",
        '3',"Wednesday ",
        '4',"Thursday ",
        '5',"Friday ",
        '6',"Saturday "};

    $monthtrans={
        '*','*',
        '1',"January ",
        '2',"February ",
        '3',"March ",
        '4',"April ",
        '5',"May ",
        '6',"June ",
        '7',"July ",
        '8',"August ",
        '9',"September ",
        '10',"October ",
        "11","November ",
        "12","December "};
    
    for(my$i=0;$i<=$n;$i++){
        my($year,$month,$day,$dayofweek,$hour,$minute,$duration,$audio,$video,$username,
           $password,$outfile,$url)=SplitTimerEntry($TIMER[$TIMER_SORTED[$i]]);
        if(defined($url)){
            my$start=$TIMER_TIMES[$TIMER_SORTED[$i]];
            #also need the end...
            my$end=$TIMER_TIMES[$TIMER_SORTED[$i]]+$duration;
            my$now=time();
            my$emph='';
            if($end<$now){
                $emph='Old';
            }
            
            my$dur_hours=int($duration/3600);
            $duration-=$dur_hours*3600;
            my$dur_minutes=int($duration/60);
            $duration-=$dur_minutes*60;
            
            if($dur_hours==0){
                $dur_hours='00:';
            }else{
                $dur_hours.=":";
            }

            $dur_minutes='0'.int($dur_minutes) if($dur_minutes <10);
            $minute='0'.int($minute) if($minute <10);
            
            push @listarray, "$emph","$year ",$monthtrans->{$month},"$day ",
            $daytrans->{$dayofweek},"$hour:$minute ","$dur_hours$dur_minutes ",$url;
        }else{
            # bad entry; prevent death
            push @listarray, "Old","X ","X ","X ","X ","XXX ","XXX ","Bad Entry ";

        }
    }
    $listbox=Snatch::ListBox::new($timerw_shell,7, at listarray)->
        place(-x=>5,-y=>5,-relheight=>1.0,-relwidth=>1.0,
              -width=>-$timerw_delete->reqwidth()-15,
              -height=>-10,
              -bordermode=>outside);
    
    $listbox->callback(\&Timer_Highlight);
    undef $timer_row;

    $timerw->update();
    CheckTimerOverlap();

    $listbox;
}

ub CheckTimerOverlap{
    my$rows=($#TIMER)+1;

    foreach my$line (@TIMER){
        $rows++;
    }

    for(my$i=0;$i<$rows;$i++){
        for(my$j=$i+1;$j<$rows;$j++){
            my $start1=$TIMER_TIMES[$i];
            my $end1=$TIMER_ENDTIMES[$i];
            my $start2=$TIMER_TIMES[$j];
            my $end2=$TIMER_ENDTIMES[$j];

            if($start1>0 && $start2>0){
                if(($start1>=$start2 && $start1<$end2)||
                   ($start2>=$start1 && $start2<$end1)){
                    Alert("Some timer entries currently overlap!",
                          "When multiple entries overlap, start and stop events are processed ".
                          "in order; that is, if program B is scheduled to begin during program ".
                          "A, program B will interrupt the recording of program A.  Should A ".
                          "continue past the ending point of B, recording program A will resume.\n",
                          $timerw);
                    return;
                }
            }
        }
    }
}

ub TimerSort{
    $count=0;
    my at TIMER_FULL=(map {TimerWhen(-1,-1,(SplitTimerEntry($_)))} @TIMER);
    undef @TIMER_TIMES;
    undef @TIMER_ENDTIMES;
    while(1){
        my$temp=shift @TIMER_FULL;
        if(defined($temp)){
            push @TIMER_TIMES, $temp;
            push @TIMER_ENDTIMES, shift @TIMER_FULL;
        }else{
            last;
        }
    }
    @TIMER_SORTED=sort {$TIMER_TIMES[$a]-$TIMER_TIMES[$b]} (map {$count++} @TIMER);
}    

ub Timer_Highlight{
    if(!defined($tentry)){
        if(defined($highlightnow) && $highlightnow+2>time){
            
            if($timer_row==$_[0]){
                # doubleclick hack.  Edit this entry
                Timer_Edit();
                return;
            }
        }
        
        
        $timerw_edit->configure(-state=>normal);
        $timerw_delete->configure(-state=>normal);
        $timerw_duplicate->configure(-state=>normal);
        $timer_row=shift;
        
        $highlightnow=time;
    }
}

ub Timer_Delete{
    # which real (not sorted) row of the timer array is this?
    my$actual_row=$TIMER_SORTED[$timer_row];

    splice @TIMER,$actual_row,1;
    TimerSort();
    SaveHistory();
    ButtonConfig();
    $timerw_edit->configure(-state=>disabled);
    $timerw_delete->configure(-state=>disabled);
    $timerw_duplicate->configure(-state=>disabled);
    BuildListBox();
}

ub Timer_Add{
    my($nowsec,$nowmin,$nowhour,$nowday,$nowmonth,$nowyear)=localtime time;    
    $nowmonth+=1;
    $nowyear+=1900;
    Timer_Entry(-1,"$nowyear $nowmonth $nowday * $nowhour:$nowmin 3600 yes yes 0: 0: ".
               length($CONFIG{OUTPUT_PATH}).":$CONFIG{OUTPUT_PATH} 0:");
}

ub Timer_Edit{
    Timer_Entry($TIMER_SORTED[$timer_row],$TIMER[$TIMER_SORTED[$timer_row]]);
}
    
sub Timer_Copy{
    Timer_Entry(-1,$TIMER[$TIMER_SORTED[$timer_row]]);
}
    
sub Timer_Entry{
    my$row=shift;

    my($year,$month,$day,$dayofweek,$hour,$minute,$duration,$audio,$video,$username,
       $password,$outfile,$url)=SplitTimerEntry(shift);

    my$duration_hour=int($duration/3600);
    my$duration_minute=int(($duration-$duration_hour*3600+59)/60);
    $duration_minute='0'.int($duration_minute) if($duration_minute <10);
    $minute='0'.int($minute) if($minute <10);

    my($nowsec,$nowmin,$nowhour,$nowday,$nowmonth,$nowyear)=localtime time;    

    $nowmonth++;
    $nowyear+=1900;

    $timerw_add->configure(-state=>disabled);
    $timerw_edit->configure(-state=>disabled);
    $timerw_duplicate->configure(-state=>disabled);
    $timerw_delete->configure(-state=>disabled);

    $tentry=new MainWindow(-class=>'Snatch');

    $tentry_title=$tentry->
        Label(Name=>"timer text",-class=>"Panel",text=>"Add/Edit Timer Entry")->
            place(-x=>10,-y=>5);
    
    $tentry_shell=$tentry->Label(Name=>"shell",borderwidth=>1,relief=>raised)->
        place(-x=>10,-y=>$timerw_title->reqheight()+10,-relwidth=>1.0,-relheight=>1.0,
              -width=>-20,-height=>-$timerw_title->reqheight()-20,-anchor=>'nw');
    
    $tentry_quit=$tentry_shell->
        Button(-class=>"Exit",text=>"ok")->
            place(-x=>-1,-y=>-1,-relx=>1.0,-rely=>1.0,-anchor=>'se');
    $tentry_cancel=$tentry_shell->
        Button(-class=>"Exit",text=>"cancel")->
            place(-x=>1,-y=>-1,-rely=>1.0,-anchor=>'sw');
   
    $tentry_quit->configure(-command=>[sub{

        # check the entry out
        $duration=$duration_hour*3600+$duration_minute*60;
        my$time=TimerStart(-1,-1,$year,$month,$day,$dayofweek,$hour,$minute,$duration);
        if($time<0){
            Alert("Impossible date setting!",
                  "The date checking routines believe the entered date doesn't exist (or is".
                  " far enough in the past it will never trigger anyway).  Please correct the".
                  ' date specification before proceeding, or file a bug report'.
                  " if the date is correct and the code is wrong.\n",$tentry);
        }elsif(TestOutpath($outfile,$tentry)){
            $outfile=trim_glob($outfile);
            my$entry="$year $month $day $dayofweek $hour:$minute $duration $audio $video ".
                length($username).":$username ".length($password).":$password ".
                    length($outfile).":$outfile ".length($url).":$url";
            
            if($row<0){
                push @TIMER, $entry;
                TimerSort();
                ButtonConfig();
            }else{
                splice @TIMER,$row,1,$entry;
                TimerSort();
                ButtonConfig();
            }
            
            SaveHistory();

            if(defined($timerw)){
                BuildListBox();
                
                $timerw_add->configure(-state=>normal);
                if(defined($timer_row)){
                    $timerw_edit->configure(-state=>normal);
                    $timerw_duplicate->configure(-state=>normal);
                    $timerw_delete->configure(-state=>normal);
                }
            }

            $tentry->destroy();
            undef $tentry;
        }
    }]);
    $tentry_cancel->configure(-command=>[sub{
        if(defined($timerw)){
            $timerw_add->configure(-state=>normal);
            if(defined($timer_row)){
                $timerw_edit->configure(-state=>normal);
                $timerw_duplicate->configure(-state=>normal);
                $timerw_delete->configure(-state=>normal);
            }
        }
        $tentry->destroy();
        undef $tentry;
    }]);

    # bwah ha ha.  The bitter end.  
    my $x=5;
    my $y=10;
    my$reqheight=0;

    my$t=$tentry_shell->Label(-text=>"Date:")->
        place(-x=>$x, -y=>$y, -bordermode=>outside);
    $x+=$t->reqwidth()+5;

    # Year
    my$tt=Snatch::ClickList::new($tentry_shell,\$year,
                                 "* (any)",'*',
                                 "$nowyear",$nowyear,
                                 $nowyear+1,$nowyear+1,
                                 $nowyear+2,$nowyear+2)->
                                     place(-x=>$x,-y=>$y,-bordermode=>outside);
    $x+=$tt->reqwidth+5;
    $reqheight=$tt->maxheight()if($tt->maxheight()>$reqheight);
    
    $t->place(-height=>$tt->reqheight());

    # month
    my$t=Snatch::ClickList::new($tentry_shell,\$month,
                                 "* (any)",'*',
                                 "January","1",
                                 "February","2",
                                 "March","3",
                                 "April","4",
                                 "May","5",
                                 "June","6",
                                 "July","7",
                                 "August","8",
                                 "September","9",
                                 "October","10",
                                 "November","11",
                                 "December","12")->
                                     place(-x=>$x,-y=>$y,-bordermode=>outside);
    $x+=$t->reqwidth+5;
    $reqheight=$t->maxheight()if($t->maxheight()>$reqheight);

    # day
    my$t=$tentry_shell->Entry(-width=>2,-textvariable=>\$day,-justify=>right)->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth+5;

    # day of week
    my$t=Snatch::ClickList::new($tentry_shell,\$dayofweek,
                                 "* (any)",'*',
                                 "Sunday","0",
                                 "Monday","1",
                                 "Tuesday","2",
                                 "Wednesday","3",
                                 "Thursday","4",
                                 "Friday","5",
                                 "Saturday","6")->
                                     place(-x=>$x,-y=>$y,-bordermode=>outside);
    $x+=$t->reqwidth+15;
    $reqheight=$t->maxheight()if($t->maxheight()>$reqheight);

    my$t=$tentry_shell->Label(-text=>"Time:")->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth()+5;

    # hour
    my$t=$tentry_shell->Entry(-width=>2,-textvariable=>\$hour,-justify=>right)->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth();

    my$t=$tentry_shell->Label(-text=>":")->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth();

    # minute
    my$t=$tentry_shell->Entry(-width=>2,-textvariable=>\$minute,-justify=>right)->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth+15;

    my$t=$tentry_shell->Label(-text=>"Duration:")->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth()+5;

    # duration hour
    my$t=$tentry_shell->Entry(-width=>2,-textvariable=>\$duration_hour,-justify=>right)->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth();

    my$t=$tentry_shell->Label(-text=>":")->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth();

    # duration minute
    my$t=$tentry_shell->Entry(-width=>2,-textvariable=>\$duration_minute,-justify=>right)->
        place(-x=>$x, -y=>$y, -height=>$tt->reqheight(),-bordermode=>outside);
    $x+=$t->reqwidth+5;

    my$reqwidth=$x+25;
    $reqheight+=$tentry_title->reqheight()+35;  # this is just for the pulldown menus
    $y+=$tt->reqheight()+20;

    
    my$t=$tentry_urllabel=$tentry_shell->Label(-text=>"URL:")->place(-y=>$y,-x=>5,-bordermode=>outside);
    my$tentry_url=$tentry_shell->Entry(-textvariable=>\$url,-width=>2048)->
        place(-y=>$y,-x=>10+$t->reqwidth,-height=>$t->reqheight,-bordermode=>outside,
              -relwidth=>1.0,-width=>-20-$t->reqwidth());
    $y+=$t->reqheight()+5;
    $t=$tentry_usernamelabel=$tentry_shell->Label(-text=>"username:")->place(-y=>$y,-x=>5);
    my$tentry_username=$tentry_shell->Entry(-textvariable=>\$username,-width=>2048)->
        place(-y=>$y,-x=>10+$t->reqwidth,-height=>$t->reqheight,-bordermode=>outside,
              -relwidth=>1.0,-width=>-20-$t->reqwidth());
    $y+=$t->reqheight()+5;
    $t=$tentry_passwordlabel=$tentry_shell->Label(-text=>"password:")->place(-y=>$y,-x=>5);
    my$tentry_password=$tentry_shell->Entry(-textvariable=>\$password,-width=>2048)->
        place(-y=>$y,-x=>10+$t->reqwidth,-height=>$t->reqheight,-bordermode=>outside,
              -relwidth=>1.0,-width=>-20-$t->reqwidth());
    $y+=$tentry_passwordlabel->reqheight()+10;

    my$tentry_silent=$tentry_shell->Label(-text=>"silent record:")->place(-y=>$y,-x=>5,-bordermode=>outside);
    my$tentry_audio=$tentry_shell->Button(-text=>"audio")->
        place(-in=>$tentry_silent,-relx=>1.0,-x=>5,-relheight=>1.0,-bordermode=>outside);
    $tentry_audio->configure(-command=>[main::nonmomentary,\$tentry_audio,\$audio]);
    my$tentry_video=$tentry_shell->Button(-text=>"video")->
        place(-in=>$tentry_audio,-relx=>1.0,-x=>5,-relheight=>1.0,-bordermode=>outside);
    $tentry_video->configure(-command=>[main::nonmomentary,\$tentry_video,\$video]);

    $tentry_test=$tentry_shell->Button(-text=>"test connect now")->
        place(-relx=>1.0,-x=>-10,-y=>$y,-height=>$tentry_silent->reqheight,-anchor=>'ne',
              -bordermode=>outside);

    if($mode=~/^active/ || ($mode=~/timer/ && ($recording_active || $recording_pending))){
        $tentry_test->configure(-state=>disabled);
    }

    $tentry_test->configure(-command=>[sub{
        
        Robot_Audio($audio);
        Robot_Video($video);
        if($url=~/^file:(.*)/){
            #file, through the file dialog
            Robot_PlayFile($1);
        }else{
            #network stream/URL, through location dialog
            Robot_PlayLoc($url,$username,$password);
        }
    }]);
                                       
    #laziness
    nonmomentary(\$tentry_audio,\$audio);
    nonmomentary(\$tentry_audio,\$audio);
    nonmomentary(\$tentry_video,\$video);
    nonmomentary(\$tentry_video,\$video);
    $y+=$tentry_video->reqheight()+5;

    $t=$tentry_outlabel=$tentry_shell->Label(-text=>"output path")->
        place(-x=>5,-y=>$y,-bordermode=>outside);
    my$tentry_out=$tentry_shell->Entry(-textvariable=>\$outfile,-width=>2048)->
        place(-y=>$y,-x=>10+$t->reqwidth,-height=>$t->reqheight,-bordermode=>outside,
              -relwidth=>1.0,-width=>-20-$t->reqwidth());

    $y+=$tentry_outlabel->reqheight()+10;

    $tentry_message=$tentry_shell->
        Message(-text=>"Any field in the date specification may be set to the wildcard * (asterisk); ".
                "recording will happen on all dates in the future matching the provided ".
                "fields.  Time and ".
                "duration are specified in hours and minutes, time uses a 24 hour clock.".
                "\n\n\'Silent record\' indicates that ".
                "during the record operation, no attempt should be made to open the audio device, ".
                "play audio ".
                "or display video.  This is useful both to increase performance and eliminate ".
                "the possibility timed record will fail due to audio device conflicts with other ".
                "applications.\n\nOutput path may be a directory [Snatch create a new ".
                "file for each record request], ".
                "a filename [record will append to that file], or - (dash) indicating standard out.",
                -width=>$reqwidth-30-$tentry_shell->cget(borderwidth)*2,
                -anchor=>w,-class=>AlertDetail)->
                    place(-x=>5,-y=>$y,-relwidth=>1.0,-width=>-10,-bordermode=>outside);
    $y+=$tentry_message->reqheight()+5;

    $reqheight=max($reqheight,$y+$tentry_quit->reqheight+$tentry_title->reqheight()+35);

    $tentry->minsize($reqwidth,$reqheight);
    $tentry->geometry($reqwidth."x".$reqheight);
                            
}

ub nonmomentary{
    my($buttonref,$valref)=@_;

    if($$valref eq 'yes'){
        $$valref='no';
        $$buttonref->configure(-relief=>groove);
    }else{
        $$valref='yes';
        $$buttonref->configure(-relief=>sunken);
    }
}
package Snatch::ListBox;

ub new{
    my%listbox;
    my$this=bless \%listbox;

    my$parent=shift @_;
    my$cols=$listbox{cols}=shift @_;
    $listbox{rows}=0;
    my at textrows;
    my at widgetrows;

    $listbox{textrows}=\@textrows;
    $listbox{widgetrows}=\@widgetrows;

    my$frame=$listbox{frame}=$parent->Frame(-class=>'ListBoxFrame');
    my$scrollbar=$listbox{scrollbar}=$frame->Scrollbar(-orient=>"vertical")->
        place(-relx=>1.0,-relheight=>1.0,-anchor=>'ne',-bordermode=>outside);
    my$pane=$listbox{pane}=$frame->Frame(-class=>'ListBox')->
        place(-relwidth=>1.0,-relheight=>1.0,-width=>-$scrollbar->reqwidth());

    $listbox{window}=$pane->Frame(-class=>'ListFrame')->place(-relwidth=>1.0);

    my$maxheight=0;

    # row by row
    my$done=0;
    for($listbox{rows}=0;!$done;$listbox{rows}++){
        my @textrow=();
        my @widgetrow=();
        $textrows[$listbox{rows}]=\@textrow;
        $widgetrows[$listbox{rows}]=\@widgetrow;
        my$emphasis=shift;

        for(my$j=0;$j<$cols;$j++){
            my$temp=shift;
            if(defined($temp)){
                $textrow[$j]=$temp;
                if($listbox{rows} % 2){
                    my$w=$widgetrow[$j]=$listbox{window}->
                        Label(-class=>$emphasis.'ListRowEven',text=>$temp);
                    $w->bind('<ButtonPress>',[$this=>highlight,$listbox{rows}]);
                }else{
                    my$w=$widgetrow[$j]=$listbox{window}->
                        Label(-class=>$emphasis.'ListRowOdd',text=>$temp);
                    $w->bind('<ButtonPress>',[$this=>highlight,$listbox{rows}]);
                }
            }else{
                $done=1;
                last;
            }
        }
    }
    $listbox{rows}--;
        
    my at maxwidth;
    my$x=0;
    my$y=0;

    # find widths col by col
    for(my$j=0;$j<$listbox{cols};$j++){
        $y=0;
        $maxwidth[$j]=0;
        for(my$i=0;$i<$listbox{rows};$i++){
            my$width=$widgetrows[$i][$j]->reqwidth();
            my$height=$widgetrows[$i][$j]->reqheight();
            $maxwidth[$j]=$width if($width>$maxwidth[$j]);
            $maxheight=$height if($height>$maxheight);
        }

        if($j+1<$listbox{cols}){
            for(my$i=0;$i<$listbox{rows};$i++){
                $widgetrows[$i][$j]->
                    place(-height=>$maxheight,-width=>$maxwidth[$j],
                          -x=>$x,-y=>$y);
                $y+=$maxheight+3;
            }
            $x+=$maxwidth[$j];
        }else{
            for(my$i=0;$i<$listbox{rows};$i++){
                $widgetrows[$i][$j]->configure(-anchor=>w);
                $widgetrows[$i][$j]->
                    place(-height=>$maxheight,-relwidth=>1.0,
                          -width=>-$x,-x=>$x,-y=>$y);
                $y+=$maxheight+3;
            }
            $x+=$maxwidth[$j];
        }
    }

    $pane->bind('<Configure>',[sub{$this->resize();}]);
    $listbox{window}->configure(-height=>$y);
    $scrollbar->configure(-command=>[yview=>$this]);

    $this;
}
   
sub place{
    my$this=shift;
    $this->{frame}->place(@_);
    $this;
}

ub destroy{
    my$this=shift;
    $this->{frame}->destroy();
}

ub yview{
    my$this=shift;
    my$moveto_p=shift;
    my$moveto=shift;

    my$paneheight=$this->{pane}->height();
    my$listheight=$this->{window}->height();

    if($moveto_p=~/moveto/){
        my$y=int($moveto*$listheight);
        $y=$listheight-$paneheight if($y+$paneheight>$listheight);
        $y=0 if($y<0);

        $this->{window}->place(-y=>-$y);
        $this->{scrollbar}->set($this->yview());
    }else{
        my$first=-$this->{window}->y()/$listheight;
        my$second=(-$this->{window}->y()+$paneheight)/$listheight;
    
        ($first,$second);
    }
}

ub resize{
    my$this=shift;
    $this->{scrollbar}->set($this->yview());
}

ub highlight{
    my$this=shift;
    my$row=shift;
    
    if(defined($this->{'highlight'})){
        for(my$i=0;$i<$this->{'cols'};$i++){
            my$b=$this->{'widgetrows'}[$this->{'highlight'}][$i]->optionGet("background","");
            $this->{'widgetrows'}[$this->{'highlight'}][$i]->configure(-background=>$b);
        }
    }
    
    $this->{'highlight'}=$row;
    for(my$i=0;$i<$this->{'cols'};$i++){
        my$b=$this->{'widgetrows'}[$row][$i]->optionGet("highlightBackground","");
        $this->{'widgetrows'}[$row][$i]->configure(-background=>$b);
    }
    
    if(defined($this->{'callback'})){
        $this->{'callback'}($row);
    }
    
}

ub callback{
    $this=shift;
    $this->{'callback'}=shift;
}

# these are a hack that doesn't quite work because Tk doesn't give
# arbitrary control over toplevel, and I don't want to use menu
# widgets for various reasons.

package Snatch::ClickList;

ub new{
    my%clicklist;
    my$this=bless \%clicklist;

    my$parent=$clicklist{parent}=shift @_;
    my$var=$clicklist{variable}=shift @_;
    my$rows=00;
    my at textrows;
    my at valrows;
    my at widgetrows;

    $clicklist{textrows}=\@textrows;
    $clicklist{valrows}=\@valrows;
    $clicklist{widgetrows}=\@widgetrows;

    my$button=$clicklist{button}=$parent->Button(-command=>[$this=>poplist],-class=>'ClickListButton');
    my$list=$clicklist{list}=$parent->Frame(-class=>'ClickList');

    my$maxheight=0;
    my$maxwidth=0;

    # row by row
    for($rows=0;;$rows++){
        my $text=shift;
        my $value=shift;
        if(defined($value)){
            $textrows[$rows]=$text;
            $valrows[$rows]=$value;

            my$w=$widgetrows[$rows]=$list->Button(-class=>'Item',-text=>$text,
                                                  -command=>[$this=>setrow,$rows]);
            $maxheight=$w->reqheight() if($w->reqheight()>$maxheight);
            $maxwidth=$w->reqwidth() if($w->reqwidth()>$maxwidth);
            
        }else{
            last;
        }
    }

    $clicklist{rows}=$rows;
    $clicklist{reqwidth}=$maxwidth+=$list->optionGet(borderWidth,"")*2;
    $clicklist{reqheight}=$maxheight+=$list->optionGet(borderWidth,"")*2;

    my$y=0;
    for(my$i=0;$i<$rows;$i++){
        $widgetrows[$i]->place(-y=>$y,-relwidth=>1.0,-height=>$maxheight);
        $y+=$maxheight;
    }
    $y+=$list->optionGet(borderWidth,"")*2;

    $button->place(-height=>$maxheight,-width=>$maxwidth);
    $list->configure(-width=>$maxwidth,-height=>$y);
    $clicklist{maxheight}=$y+$list->optionGet(borderWidth,"")*2;

    $this->setval($$var);
    $this;
}

ub reqheight{
    my$this=shift;
    $this->{reqheight};
}
sub maxheight{
    my$this=shift;
    $this->{maxheight}+$this->{reqheight};
}

ub reqwidth{
    my$this=shift;
    $this->{reqwidth};
}

ub place{
    my$this=shift;
    $this->{button}->place(@_);
    $this;
}

sub setrow{
    my$this=shift;
    my$row=shift;
    my$val=$this->{valrows}[$row];

    $this->{'set'}=$row;
    ${$this->{'variable'}}=$val;
    $this->{'list'}->placeForget;
    $this->{'button'}->configure(-text=>$this->{textrows}[$this->{'set'}]);
    $this;
}

ub setval{
    my$this=shift;
    my$val=shift;

    my$rows=$this->{rows};
    for(my$i;$i<$rows;$i++){
        if("$this->{valrows}[$i]" eq "$val"){
            $this->setrow($i);
            last;
        }
    }
    $this;
}

ub poplist{
    my$this=shift;
    my$row=$this->{'set'};
    my$list=$this->{'list'};
    my$button=$this->{'button'};

    if(defined($this->{pop})){
        $list->placeForget();
        delete $this->{pop};
    }else{
        $list->raise();
        $list->place(-in=>$button,-relwidth=>1.0,-rely=>1.0,-bordermode=>outside);
        $this->{'pop'}='';
    }
    $this;
}

ub button{
    my$this=shift;
    $this->{'button'};
}

1.1                  snatch/esd.c

Index: esd.c
===================================================================
/* The code for dealing with the EsounD daemon */
#include <endian.h>
#include <pthread.h>

typedef struct {
  int fd;
  int rq;
  int count;

  char fakebuf[1024];
  int fakecount;

  int fake;
} esd_connection;

tatic void esd_add_int_to_fake(esd_connection *e,int x){
  int *ptr=(int *)(e->fakebuf+e->fakecount);
  if(e->fakecount+4>1024){
    fprintf(stderr,
            "**ERROR: The EsounD server faking code has overflowed\n"
            "         its storage buffer.  This *should* be impossible\n"
            "         and seeing this message imlies an internal fault.\n"
            "         if things haven't gone sour yet, they're about to...\n");
    return;
  }
  *ptr=x;
  e->fakecount+=4;
}

tatic void esd_add_char_to_fake(esd_connection *e,char c){
  if(e->fakecount>=1024){
    fprintf(stderr,
            "**ERROR: The EsounD server faking code has overflowed\n"
            "         its storage buffer.  This *should* be impossible\n"
            "         and seeing this message imlies an internal fault.\n"
            "         if things haven't gone sour yet, they're about to...\n");
    return;
  }
  e->fakebuf[e->fakecount]=c;
  e->fakecount++;
}

tatic void esd_add_string_to_fake(esd_connection *e,char *c,int n){
  fprintf(stderr,"string=%s\n",c);
  while(n--)
    esd_add_char_to_fake(e,*c++);
}

tatic void esd_add_nil_to_fake(esd_connection *e,int n){
  while(n--)
    esd_add_char_to_fake(e,0);
}

tatic unsigned int esd_format;
static char *esdsocket;
static char playername[128];
static pthread_mutex_t esd_mutex=PTHREAD_MUTEX_INITIALIZER;

/* RealPlayer opens two EsounD connections.  One for daemon control
   requests, the other for streaming audio */

tatic esd_connection esdconn[2]={{-1},{-1}};
static int esdconmax=2;

tatic esd_connection *getcon(int fd){
  int i;
  pthread_mutex_lock(&esd_mutex);
  for(i=0;i<esdconmax;i++)
    if(esdconn[i].fd==-1){
      memset(&esdconn[i],0,sizeof(esdconn[1]));
      esdconn[i].fd=fd;
      if(fake_audiop)esdconn[i].fake=1;
      esdconn[i].count=20; /* 5 more bytes for auth header */
      pthread_mutex_unlock(&esd_mutex);
      return &esdconn[i];
    }
  fprintf(stderr,
          "**ERROR: RealPlayer tried to open more than two connections\n"
          "         to the EsounD server, something not expected.\n"
          "         Not capturing this one; things may go south quickly.\n");

  pthread_mutex_unlock(&esd_mutex);
  return(NULL);
}

tatic int esd_translate_format(int e){
  switch(e&0xf){
  case 0:
    /* unsigned 8 bit */
    return(4);
  case 1:
    /* signed 16 bit, host endian */
#ifdef __BIG_ENDIAN__
    return(6);
#else
    return(5);
#endif

  default:
    fprintf(stderr,
            "**ERROR: RealPlayer requested an unknown audio format for\n"
            "         playback.  Winging it.\n");
    break;
  }
  return(0);
}

tatic int esd_translate_channels(int e){
  return((e>>4)&0xf);
}

tatic void esd_hook_init(void ){
  /* esd socket? */
  esdsocket=nstrdup(getenv("SNATCH_ESD_SOCKET"));
  if(!esdsocket){
    esdsocket=nstrdup(getenv("ESPEAKER"));
    if(debug){
      if(esdsocket){
        fprintf(stderr,
                "----env: SNATCH_ESD_PORT\n"
                "           not set. Using ESPEAKER variable.\n");
      }else{
        fprintf(stderr,
                "----env: SNATCH_ESD_PORT\n"
                "           not set. Using default (/var/run/esound/socket).\n");
        
        esdsocket=nstrdup("/var/run/esound/socket");
      }
    }
  }else{
    if(debug)
      fprintf(stderr,
              "----env: SNATCH_ESD_SOCKET\n"
              "           set (%s)\n",esdsocket);
  }
}

/* EsounD is socket based.  Watch for a connection */
static int esd_identify(const struct sockaddr *serv_addr,socklen_t addrlen){
  if(serv_addr->sa_family==AF_UNIX){
    struct sockaddr_un *addr=(struct sockaddr_un *)serv_addr;
    if(!strcmp(esdsocket,addr->sun_path))return(1);
    return(0);
  }
  if(serv_addr->sa_family==AF_INET){
    struct sockaddr_in *addr=(struct sockaddr_in *)serv_addr;
    unsigned int port=ntohs(addr->sin_port);
    char *colonpos=strchr(esdsocket,':');
    if(colonpos && port==atoi(colonpos+1))return(1);
  }
  return(0);
}

tatic int esd_connect_hook(int sockfd,const struct sockaddr *serv_addr,
                            socklen_t addrlen){

  esd_connection *esd=getcon(sockfd);
  if(!esd)return(-1);

  if(esd->fake){
    return(0);
  }else{
    int ret=(*libc_connect)(sockfd,serv_addr,addrlen);
    if(ret>-1){
      if(serv_addr->sa_family==AF_UNIX){
        if(debug)
          fprintf(stderr,
                  "    ...: Caught RealPlayer connecting to EsounD server\n"
                  "         local socket %s (fd %d).\n",
                  esdsocket,sockfd);
        
      }else if(serv_addr->sa_family==AF_INET){
        struct sockaddr_in *addr=(struct sockaddr_in *)serv_addr;
        unsigned int port=ntohs(addr->sin_port);
        unsigned long host=ntohl(addr->sin_addr.s_addr);
        if(debug)
          fprintf(stderr,
                  "    ...: Caught RealPlayer connecting to EsounD server\n"
                  "         on host %ld.%ld.%ld.%ld port %d (fd %d).\n",
                  (host>>24)&0xff,(host>>16)&0xff,(host>>8)&0xff,       host&0xff,
                  port,ret);
        
        /* unfortunately there's far too little documentation on how
           the Hell ESD handles mixed-endianness connections (if at
           all; some of the endianness code I've found in it varied
           from frightening to outright broken).  Take no chance of
           blowing out someone's ears. */
        
        fprintf(stderr,
                "**ERROR: Sorry, but Snatch doesn't currently do remote\n"
                "         EsounD connections.  This connection will not be\n"
                "         captured.\n");

        pthread_mutex_lock(&esd_mutex);
        esd->fd=-1;
        pthread_mutex_unlock(&esd_mutex);
        
      }
    }else{
      pthread_mutex_lock(&esd_mutex);
      esd->fd=-1;
      pthread_mutex_unlock(&esd_mutex);
    }
    return(ret);
  }
}

tatic void esd_close_hook(int fd){
  int i;
  for(i=0;i<esdconmax;i++)
    if(fd==esdconn[i].fd)
      esdconn[i].fd=-1;
}

tatic int esd_rw_hook_p(int fd){
  int i;
  if(fd==audio_fd)return(0);
  for(i=0;i<esdconmax;i++)
    if(fd==esdconn[i].fd)return(1);
  return(0);
}

tatic int esd_read_hook(int fd,void *buf,int count){
  int i;
  for(i=0;i<esdconmax;i++)
    if(fd==esdconn[i].fd){
      if(esdconn[i].fake){

        /* read from the fake buffer */
        if(esdconn[i].fakecount<count)
          count=esdconn[i].fakecount;
        
        if(count)memcpy(buf,esdconn[i].fakebuf,count);
        esdconn[i].fakecount-=count;
        if(esdconn[i].fakecount)
          memmove(esdconn[i].fakebuf,
                  esdconn[i].fakebuf+count,
                  esdconn[i].fakecount);

        fprintf(stderr,"esdfd %d FAKE read %d\n",fd,count);
        for(i=0;i<count;i++)
          fprintf(stderr,"0x%x,",(int)(((unsigned char *)buf)[i]));
        fprintf(stderr,"\n");
        return(count);
      }else{
        int ret=((*libc_read)(fd,buf,count));
        int i;
        fprintf(stderr,"esdfd %d read %d\n",fd,ret);
        for(i=0;i<count;i++)
          fprintf(stderr,"0x%x,",(int)(((unsigned char *)buf)[i]));
        fprintf(stderr,"\n");
        return(ret);
      }
    }

  return((*libc_read)(fd,buf,count));
}

tatic int esd_write_hook(int fd, const void *buf,int count){
  int *ptr=(int *)buf;
  char *cptr=(char *)buf;
  int i,n,ret;

  for(i=0;i<esdconmax;i++)
    if(fd==esdconn[i].fd){
      
      if(!esdconn[i].fake){
        ret=(*libc_write)(fd,buf,count);
        if(ret>0)
          n=ret/4;
        else
          return(ret);
      }else{
        ret=count;
        n=count/4;
      }
      
      fprintf(stderr,"esdfd %d write %d [%d]\n",fd,count,*ptr);
      
      while(n-->0){
        if(esdconn[i].count<=0){
          /* handle new request */
          fprintf(stderr,"ESD proto requested=%d fd=%d\n",*ptr,fd);
          esdconn[i].rq=*ptr++;
          switch(esdconn[i].rq){
          case 3:
            /* we're the audio stream! */
            esdconn[i].count=136;
            break;
          case 17:
            /* get complete server info */
            esdconn[i].count=4;
            break;
          case 20:
            /* set volume/balance */
            esdconn[i].count=12;
            break;
          }
        }else{
          /* read or ignore request fields */
          switch(esdconn[i].rq){
          case 3:
            switch(esdconn[i].count){
            case 136:
              esd_format=*ptr;
              audio_format=esd_translate_format(*ptr);
              audio_channels=esd_translate_channels(*ptr);
              
              if(debug)
                fprintf(stderr,"    ...: Audio output set to %d channels.\n",
                        audio_channels);
              if(debug)
                fprintf(stderr,"    ...: Audio output format set to %s.\n",
                        audio_fmts[audio_format]);
              break;
            case 132:
              audio_rate=*ptr;
              if(debug)
                fprintf(stderr,
                        "    ...: Audio output sampling rate set to %dHz.\n",
                        audio_rate);
              break;
            default:
              /* need this for later */
              {
                char *p=playername+128-esdconn[i].count;
                *p++=cptr[0];
                *p++=cptr[1];
                *p++=cptr[2];
                *p++=cptr[3];
              }
              break;
            }
            break;
          case 17:
            /* ignore */
            break;
          case 20:
            /* ignore */
            break;
          }
          ++ptr;
          cptr+=4;
          esdconn[i].count-=4;

          if(esdconn[i].count<=0){
            /* cleanup from request if it's done */
            switch(esdconn[i].rq){
            case 3:
              /* ok, audio stream init all handled; cut it loose */
              audio_fd=fd;
              fprintf(stderr,"ESD audio stream (fd %d) cut loose\n",fd);
              /* no response; just go */
              break;
            case 17:
              /* place the server info in the fake buffer if we're faking */
              if(esdconn[i].fake){
                esd_connection *e=&esdconn[i];

                /* server info */
                esd_add_int_to_fake(e,0);
                esd_add_int_to_fake(e,44100);
                esd_add_int_to_fake(e,0x21); /* stereo 16 bit */

                /* realplayer entry */
                esd_add_int_to_fake(e,0x7);
                esd_add_string_to_fake(e,playername,128);
                esd_add_int_to_fake(e,audio_rate);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,esd_format);

                /* nil player entry */
                esd_add_int_to_fake(e,0);
                esd_add_nil_to_fake(e,128);
                esd_add_int_to_fake(e,0);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,0);

                /* nil sample entry */
                esd_add_int_to_fake(e,0);
                esd_add_nil_to_fake(e,128);
                esd_add_int_to_fake(e,0);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,0x100);
                esd_add_int_to_fake(e,0);
                esd_add_int_to_fake(e,0);

                /* 460 bytes */
              }
              break;
            case 20:case 0:
              /* place the ok response in the fake buffer if we're faking */
              if(esdconn[i].fake){
                esd_add_int_to_fake(&esdconn[i],0x1);
              }
              break;
            }
          }
        }
      }
      return(ret);
    }
  return(-1);
}

1.1                  snatch/oss.c

Index: oss.c
===================================================================
/* All the code for dealing with OSS */

#include <linux/soundcard.h>

tatic char *ossname;
static int oss_fd=-1;
static int oss_fd_fakeopen=0;

tatic int oss_fmt_translate(int format){
  int i;
  for(i=0;i<10;i++)
    if(format==((1<<i)>>1))return(i);
  return(0);
}

tatic void oss_hook_init(void){

  /* oss device? */
  ossname=nstrdup(getenv("SNATCH_OSS_DEVICE"));
  if(!ossname){
    if(debug)
      fprintf(stderr,
              "----env: SNATCH_OSS_DEVICE\n"
              "           not set. Using default (/dev/dsp*).\n");
    ossname=nstrdup("/dev/dsp*");
  }else{
    if(debug)
      fprintf(stderr,
              "----env: SNATCH_OSS_DEVICE\n"
              "           set (%s)\n",ossname);
  }
}

/* The OSS audio device is subverted through open().  If we didn't
   care about allowing a fake audio open() to 'succeed' even when the
   real device is busy, then we could just watch for the ioctl(), grab
   the fd() then, and not need to bother with any silly string
   matching.  However, we *do* care, so we do this the more complex,
   slightly more error prone way. */

tatic int oss_identify(const char *pathname){
  if( (ossname[strlen(ossname)-1]=='*' &&
       !strncmp(pathname,ossname,strlen(ossname)-1)) ||
      (ossname[strlen(ossname)-1]!='*' &&
       !strcmp(pathname,ossname)))return(1);
  return(0);
}

tatic int oss_open_hook(const char *pathname){
  int ret;
  if(oss_fd>-1){
    /* umm... an oss fd is already open.  report the problem and
       continue */
    fprintf(stderr,
            "\n"
            "WARNING: RealPlayer is attempting to open more than one\n"
            "         OSS audio device (in this case, %s).\n"
            "         This behavior is unexpected; ignoring this open()\n"
            "         request.\n",pathname);
    return(-1);
  }else{

    /* are we faking the audio? */
    if(fake_audiop){
      ret=(*libc_open)("/dev/null",O_RDWR,0);
      oss_fd_fakeopen=1;
    }else{
      ret=(*libc_open)(pathname,O_RDWR,0);
      oss_fd_fakeopen=0;
    }

    oss_fd=ret;
    audio_fd=ret;
    audio_channels=-1;
    audio_rate=-1;
    audio_format=-1;
    audio_samplepos=0;
    audio_timezero=bigtime(NULL,NULL);
    
    if(debug)
      fprintf(stderr,
              "    ...: Caught RealPlayer opening OSS audio device "
              "%s (fd %d).\n",
              pathname,ret);
    if(debug && fake_audiop)
      fprintf(stderr,
              "    ...: Faking the audio open and writes as requested.\n");
  }
  return(ret);
}

tatic void oss_close_hook(int fd){
  if(fd==oss_fd)oss_fd=-1;
}

/* watch OSS audio ioctl()s to track playback rate/channels/depth */

tatic int oss_ioctl_hook_p(int fd){
  if(fd==oss_fd)return(1);
  return(0);
}

tatic int oss_ioctl_hook(int fd,int rq,void *arg){
  int ret=0;
  if(!fake_audiop && !oss_fd_fakeopen)
    ret=(*libc_ioctl)(fd,rq,arg);
    
  switch(rq){
  case SNDCTL_DSP_SPEED:
    audio_rate=*(int *)arg;
    if(debug)
      fprintf(stderr,
              "    ...: Audio output sampling rate set to %dHz.\n",
              audio_rate);
    CloseOutputFile();
    break;
  case SNDCTL_DSP_CHANNELS:
    audio_channels=*(int *)arg;
    if(debug)
      fprintf(stderr,
              "    ...: Audio output set to %d channels.\n",
              audio_channels);
    CloseOutputFile();
    break;
  case SNDCTL_DSP_SETFMT:
    audio_format=oss_fmt_translate(*(int *)arg);
    if(debug)
      fprintf(stderr,
              "    ...: Audio output format set to %s.\n",
              audio_fmts[audio_format]);
    CloseOutputFile();
    break;
  case SNDCTL_DSP_GETOSPACE:
    if(fake_audiop){
      audio_buf_info *temp=arg;
      temp->fragments=32;
      temp->fragstotal=32;
      temp->fragsize=2048;
      temp->bytes=64*1024;
      
      if(debug)
        fprintf(stderr,"    ...: OSS output buffer size requested; faking 64k\n");
      ret=0;
    }
    break;
  case SNDCTL_DSP_GETODELAY: /* Must reject the ODELAY if we're not going to 
                                properly simulate it! */
    if(fake_audiop){
      if(debug)
        fprintf(stderr,
                "    ...: Rejecting SNDCTL_DSP_GETODELAY ioctl()\n");
      *(int *)arg=0;
      ret=-1;
    }
    break;
  }
  
  return(ret);
}

--- >8 ----
List archives:  http://www.xiph.org/archives/
Ogg project homepage: http://www.xiph.org/ogg/
To unsubscribe from this list, send a message to 'cvs-request at xiph.org'
containing only the word 'unsubscribe' in the body.  No subject is needed.
Unsubscribe messages sent to the list will be ignored/filtered.



More information about the commits mailing list