/*
   emacspeak-dt - Interfacing to a DoubleTalk via C

   $Id: emacspeak-dt.c,v 0.15 1997/05/28 23:29:30 jrv Exp jrv $


   This file is not part of GNU Emacs, but the same permissions apply.
  
   GNU Emacs is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
  
   GNU Emacs is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
  
   You should have received a copy of the GNU General Public License
   along with GNU Emacs; see the file COPYING.  If not, write to
   the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include <config.h>
#include <assert.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <syslog.h>

#include <regexp.h>

#ifndef max
#define max(a,b) ((a)>(b)?(a):(b))
#endif
#ifndef min
#define min(a,b) ((a)<(b)?(a):(b))
#endif

#ifdef TIMESTAMPS
#define IN(s) LOG("entering",s)
#define OUT(s) LOG(" leaving",s)
FILE *nfile;
#include <sys/time.h>
#include <unistd.h>
 
void LOG(char *note, char *function)
{
  static struct timeval now, then={0,0};
  double diff;
  static entry=0;

  gettimeofday(&now, NULL);
  diff = now.tv_sec - then.tv_sec + (now.tv_usec - then.tv_usec)/1000000.;
  then = now;
  if (entry++ < 500) 
    {
      fprintf(nfile, " %2d.%06d %9.6f  %s %s\n", 
	      now.tv_sec, now.tv_usec, diff, note, function);
    }
}
#else /* !TIMESTAMPS */
#define IN(s) 0
#define OUT(s) 0
#endif

				/* map rates of 100...450 to indices 0
                                   through 9 */
#define RATEMIN 100
#define RATEMAX 450
#define RATE2INDEX(r) ((max(min(r,RATEMAX),RATEMIN)-RATEMIN)*9/(RATEMAX-RATEMIN))
#define PAUSE2INDEX(p) p

struct settings
{
  unsigned short serial_number;	/* 0-7Fh:0-7Fh */
  unsigned char *rom_version;	/* variable length string */
  unsigned char mode;		/* 0=Character; 1=Phoneme; 2=Text */
  unsigned char punc_level;	/* nB; 0-7 */
  unsigned char formant_freq;	/* nF; 0-9 */
  unsigned char pitch;		/* nP; 0-99 */
  unsigned char speed;		/* nS; 0-9 */
  unsigned char volume;		/* nV; 0-9 */
  unsigned char tone;		/* nX; 0-2 */
  unsigned char expression;	/* nE; 0-9 */
  unsigned char ext_dict_loaded; /* 1=exception dictionary loaded */
  unsigned char ext_dict_status; /* 1=exception dictionary enabled */
  unsigned char free_ram;	/* # pages (truncated) remaining for
                                   text buffer */
  unsigned char articulation;	/* nA; 0-9 */
  unsigned char reverb;		/* nR; 0-9 */
  unsigned char eob;		/* 7Fh value indicating end of
                                   parameter block */
};

int word_pause=0;		/* pause between words, 0-15 */
int character_pause=0;		/* pause between characters, 0-15 */

int ilog = 0;			/* nonzero if input is to be logged */
int olog = 0;			/* nonzero if output is to be logged */

char *ifilename = "emacspeak.ilog";
char *ofilename = "emacspeak.olog";

char **queue;			/* pointers to text fragments to be
                                   spoken.  Each fragment was
                                   allocated from the heap, and will
                                   be free'd after being spoken. */
int q_head, q_tail, q_size;	/* indexes into queue */
enum {ALL, MOST, SOME, NONE} punctuations = SOME;
enum {CHAR, WORD, TEXT} mode = TEXT;
int say_rate;
int split_caps;
int capitalize;
int space_special_chars;
int speech_rate;
double char_factor = 1.2;
int talking;
int fd;				/* file descriptor for reading from or
				   writing to the speech device */

int ofd;			/* file descriptor for output log */
char *Argv0;
static char *buf;		/* input buffer pointer */
static int bufsiz = 0;		/* # bytes in input buffer */

int gsub(char *exp, char *replacement, char **s);

/*
   these are called from Emacs
*/
static void doubletalk_letter(char *text);
static void doubletalk_reset(void);
static void doubletalk_say(char *text);
static void doubletalk_set_character_scale(double new_factor);
static void doubletalk_set_punctuations(char *mode);
static void doubletalk_set_speech_rate(int rate);
static void doubletalk_speak(char *text);
static void doubletalk_stop(void);
static void doubletalk_tone(char *str);
static void queue_speech(char *element);
/*
   these are only used internally
*/
static struct settings *interrogate(void);
static char * doubletalk_get_acknowledgement(int *);
static void doubletalk_gobble_acknowledgements(void);
static void fix_voices(char **t);
static void queue_clear(void); 
static int queue_empty_p(void); 
static char * queue_remove(void);
static int readable (int fd1, int fd2);
static void speech_task(void);
static void writeout(char *s);
static void *xmalloc(unsigned n);
static void *xrealloc(void *p, unsigned n);
static char *xstrdup(char *s);

#ifdef NEVER
/* these are unused */
static void doubletalk_pause(void);
static void doubletalk_resume(void);
static void doubletalk_space_special_chars(int flag); 
static void doubletalk_synchronize(void);
#endif

#define SENTINAL "Q.Q"

char init_string[] = 
"\001@"				/* reset */
"\0010T"			/* text mode, no delay */
"\0010Y"			/* timeout = 0 sec.  possible values are:
					0    Indefinite (wait for CR/Null)
					1    200 milliseconds
					2    400 milliseconds
					...
				       15   3000 milliseconds (3 sec.)
				 */
"\0011B"			/* most punctuation */
"\0015S"			/* default speed */
"This is the DoubleTalk "
"driver emacspeak-dt version " SENTINAL "      " /* leave space for long
						version number */
"speakers report\r"
"\001D. . \0010T\r"		/* phoneme mode,  text mode with no delay */
"\0010O Paul \r"
"\00116*"			/* pause */
"\0011O Vader\r"
"\001D. . \0010T\r"
"\0012O Bob\r"
"\001D. . \0010T\r"
"\0013O Pete\r"
"\001D. . \0010T\r"
"\0014O Larry\r"
"\001D. . \0010T\r"

"\0010O \r\n";			/* back to Perfect Paul */
char reset_string[] = 
"\001@  emacspeak-dt interface version " SENTINAL 
"       Restoring sanity to the DoubleTalk.\n";

char revision_string[]="$Revision: 0.15 $";
char *revision = revision_string;

/* use syslog facility to record an error encountered on program line
   line.  (stderr probably has nobody watching.) */
static void error(int line)
{
  openlog("emacspeak-dt", LOG_PID|LOG_PERROR, LOG_LOCAL0);
  syslog(LOG_ERR, "ver %s line %d: %m", revision, line);
  exit(1);
}

char *
doubletalk_get_acknowledgement(int *num)
{
  char *retval = NULL;
  fd_set rfds, wfds, efds;
  struct timeval tv;
  int numchars = 0, maxchars = 0;
IN("doubletalk_get_acknowledgement");
  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);
  FD_SET(0, &rfds);
  FD_ZERO(&wfds);
  FD_ZERO(&efds);
  tv.tv_sec = 0;
  tv.tv_usec = 0;
  (void)select(fd+1, &rfds, &wfds, &efds, &tv);
  if (FD_ISSET(0, &rfds))
    {
      talking = 0;		/* parent has more to say */
      queue_clear();
    }
  else
    {
      while(readable(fd, fd))
	{
	  if (numchars >= maxchars)
	    {
	      maxchars = maxchars*2 + 32;
	      retval = xrealloc(retval, maxchars);
	    }
	  if (read(fd, retval+numchars++, 1) < 0)
	    error(__LINE__);
	  FD_ZERO(&rfds);
	  FD_SET(fd, &rfds);
	  tv.tv_sec = 0;
	  tv.tv_usec = 1000;
	  (void)select(fd+1, &rfds, &wfds, &efds, &tv);
	}
    }
OUT("doubletalk_get_acknowledgement");
  if (num) *num = numchars;
  return retval;
}

/*  Gobble up any garbage the DoubleTalk has returned */
void
doubletalk_gobble_acknowledgements()
{
  int junk;
IN("doubletalk_gobble_acknowledgements");
  while(readable(fd, fd))
    if (read(fd, &junk, 1) < 0)
      error(__LINE__);
OUT("doubletalk_gobble_acknowledgements");
}

void doubletalk_letter(char *text)
{
  char buf[32];
IN("doubletalk_letter");
  if(talking)
    {
      talking = 0;
      queue_clear();
    }
#ifdef NEVER
  if (mode != CHAR)
    {
      sprintf(buf, "\001%dS\001%dC", RATE2INDEX(say_rate), 
	      PAUSE2INDEX(character_pause));
      writeout(buf); 
      mode = CHAR;
    }
  writeout(text); 
#else
  sprintf(buf, "%c\r", *text);
  writeout(buf);
#endif
OUT("doubletalk_letter");
}

void
doubletalk_reset()
{
  doubletalk_gobble_acknowledgements();
  writeout(reset_string);
}

				/* speak in word mode */
void doubletalk_say(char *text)
{
  int copied = 0;
  char buf[32], *s;
IN("doubletalk_say");

  if(talking)
    talking=0;
  doubletalk_gobble_acknowledgements();
  if (mode != TEXT)
    {
      sprintf(buf, "\001%dS\001%dT", RATE2INDEX(say_rate), 
	      PAUSE2INDEX(word_pause));
      writeout(buf); 
      mode = TEXT;
    }
				/* simulate the version command */
  if ((s = strstr(text, "[:version speak]")))
    {
      char buf[100], *t;
      struct settings *sp = interrogate();
      sprintf(buf, "DoubleTalk driver version %s, ROM version %s, "
	      "serial number %u ", 
	      revision, sp->rom_version, sp->serial_number);
      t = xmalloc(s - text + strlen(buf) + 1);
      *s = 0;
      strcpy(t, text);
      strcat(t, buf);
      text = t;
      copied = 1;
    }
  if ((strstr(text, "[:n")))
    {
      text = xstrdup(text);
      copied = 1;
      fix_voices(&text);

      if (ilog)
	{
	  FILE *dfile = fopen(ifilename,"a");
	  fprintf(dfile, "%s(%d):  after substitutions = \"%s\"\n", 
		  __FILE__, __LINE__, text); 
	  fclose(dfile);
	}

      writeout(text);
    }
  else
    {
     while ((s = strchr(text, ' ')))
	*s = '\r';		/* CR forces DoubleTalk to speak immediately */
    }
  if (s = strstr(text, "[*]")) 
    {
      if (!copied) text = xstrdup(text);
      copied = 1;
      strncpy(strstr(text, "[*]"), "   ", 3);
    }
  writeout(text); 
  if (copied) free(text);
OUT("doubletalk_say");
}

void doubletalk_set_character_scale(double new_factor)
{
  say_rate = (int)(speech_rate*new_factor);
}

void doubletalk_set_punctuations(char *mode)
{
  char buf[32];
				/* most = all but whitespace */
  char *choices[]={"all","most","some","none"};

  int n, i;
  for (i = 0; i < 4; i++)
    if (strncmp(mode, choices[i], strlen(choices[i])) == 0)
      {
	sprintf(buf, "\030\00177I\001%dB", i);
	writeout(buf);
	doubletalk_get_acknowledgement(0);
	punctuations = i;
	break;
      }
}

void doubletalk_set_speech_rate(int rate)
{
  char buf[32];
  sprintf(buf, "\001%dS", RATE2INDEX(rate));
  writeout(buf);
  speech_rate = rate;
  say_rate = (int)(rate*char_factor);
}

void doubletalk_speak(char *text)
{
IN("doubletalk_speak");
  queue_speech(text);
  if(!talking)
    {
      if (mode != TEXT)
	{
	  sprintf(buf, "\001%dT", PAUSE2INDEX(word_pause));
	  writeout(buf);
	  mode = TEXT;
	}
      speech_task();
      writeout("\r\n");
    }
OUT("doubletalk_speak");
}

void doubletalk_stop()
{
  fd_set rfds, wfds, efds;
  struct timeval tv;
  char junk;
IN("doubletalk_stop");
  queue_clear();
  doubletalk_gobble_acknowledgements();
  write(fd, "\030", 1);		/* FIXME this should ignore handshaking */

  /* The DECtalk driver waits up to 1 sec for the file to become
    readable.  The DoubleTalk apparently does not reply, so no wait
    is required.

  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);
  FD_ZERO(&wfds);
  FD_ZERO(&efds);
  tv.tv_sec = 1;
  tv.tv_usec = 0;
  if(select(fd+1, &rfds, &wfds, &efds, &tv)) */

  if(readable(fd, fd))
    if (read(fd, &junk, 1) < 0)
      error(__LINE__);
  talking = 0;
  doubletalk_gobble_acknowledgements();
OUT("doubletalk_stop");
}

				/* str has frequency in Hz and
				   duration in msec */
void
doubletalk_tone(char *str)
{

/* The DoubleTalk tone generator is controlled by the three parameters
   n, Kd, and K1.  The frequency of the tone in Hz is K1*603/(155-n),
   and the duration in seconds is Kd*(155-n)*.256/617.  The three
   parameters are subject to these constraints: 1 <= n <= 99, 1 <= K1
   <= 255, and 1 <= Kd <= 255.  These permit frequencies up to 2746 Hz
   and durations from 23 msec to 16.29 sec.  Here, we use 'tau' to
   stand for (155-n). */

#ifndef max
#define max(a,b) ((a)>(b)?(a):(b))
#endif
#ifndef min
#define min(a,b) ((a)<(b)?(a):(b))
#endif

  double frequency, duration, ratio, term, scale;
  int n, Kd, K1, K2, tau, taumin=56, taumax=154;
  char *buf = xmalloc(32+strlen(str));

  sscanf(str, "%lf %lf", &frequency, &duration);
  duration *= .001;		/* express duration in sec */

  taumin = 56;			/* first priority: hardware limits */
  taumax = 154;

				/* second priority: reach specified
                                   frequency (provided the above
                                   hardware limits are respected) */
  taumin = max(taumin, min(603/frequency, taumax));
  taumax = min(taumax, max(255*603/frequency, taumin));

				/* third priority: reach specified
                                   duration (provided the above limits
                                   are respected) */
  taumin = max(taumin, min(617*duration/.256/255, taumax));
  taumax = min(taumax, max(617*duration/.256, taumin));

  if (taumin == taumax)
    {
      tau = taumin;
      K1 = min(255, max(1, tau*frequency/603));
    }
  else
    {

/* Find good values of tau and K1.  K1/tau should approximate
frequency/603.  We express the latter as a continued fraction, and (if
possible) use one of its approximates.  In other words, we express
frequency/603 in the form t0 + 1/(t1 + 1/(t2 + 1/(t3 + ...))) where ti
is an integer.  To form an approximate of this continued fraction, we
ignore everything after one of the '+' signs, and reduce to a regular
fraction.  The more terms we include, the more accurate the
approximate is.  For example, pi = 3 + 1/(7 + 1/(15 + 1/(1 + 1/(293 +
...)))).  The first four approximates are: 3, 22/7, 333/106, and
355/113.

   An approximate is accurate to about the same number of digits as it
has.  The last approximate shown above for pi is accurate to 7 decimal
places.  Our numerator and denominator can have 8 bits, so we can hope
for about 16 bits of accuracy, or almost 5 decimal digits.  We can't
really do this well, since tau is always restricted to the interval
56...154, and sometimes a much smaller interval.

   Here, we use a recurrence relation which lets us calculate
successive approximates in the forward direction.  */

      int num0, den0, num1, den1, num2, den2;

      ratio = frequency/603;
      if (ratio > 1.)
	{
	  num0 = 1.; den0 = 0.;
	  num1 = floor(ratio);	/* num1/den1 is the first approximate */
	  den1 = 1;
	  ratio -= num1;
	}
      else
	{
	  num0 = 0.; den0 = 1.;
	  ratio = 1./ratio;
	  num1 = 1;		/* num1/den1 is the first approximate */
	  den1 = floor(ratio);
	  ratio -= den1;
	}
      while(ratio > 0.)
	{
	  ratio = 1./ratio;
	  if (ratio > 1000.) break;
	  term = floor(ratio);
	  ratio -= term;
	  num2 = num0 + num1*term;
	  den2 = den0 + den1*term;
	  if (num2 > 255 || den2 > taumax) break;
	  num0 = num1; num1 = num2;
	  den0 = den1; den1 = den2;
	}
      if (den1 < taumin)
	{
	  scale = ceil(taumin/(double)den1);
	  num1 *= scale;
	  den1 *= scale;
	}
      if (den1 > taumax)
	{			/* There was no approximate whose
				   denominator, nor an integer
				   multiple of one, is in the allowed
				   range.  We fall back on a simpler
				   approximation. */
	  tau = (taumin + taumax)/2;
	  K1 = frequency*tau/603 + .5;
	}
      else
	{
	  K1 = max(1, min(255, num1));
	  tau = max(taumin, min(taumax, den1));
	}
    }
  n = 155 - tau;
  Kd = max(1, min(255, (int)(617*duration/tau/.256 + .5)));
  K2 = K1;			/* There can be a second simultaneous
				   sine wave with frequency determined
				   by K2.  K2=0 would disable the
				   second source.  However, we cannot
				   have K2=0 since that would
				   terminate the string.  Instead, we
				   make both the same. */

  sprintf(buf, "\001%dJ%c%c%c", n, Kd, K1, K2);
  writeout(buf);

  free(buf);
}

  /*
    The personalities and their codes:
(dtk-define-voice 'paul "[:np ]")   \0010O Paul
(dtk-define-voice 'harry "[:nh ]")  \0011O Vader
(dtk-define-voice 'dennis "[:nd]")  \0012O Bob
(dtk-define-voice 'frank "[:nf]")   \0013O Pete
(dtk-define-voice 'betty "[:nb]")   \0014O Larry
(dtk-define-voice 'ursula "[:nu]")  \0010O\00175P Paulene
(dtk-define-voice 'rita "[:nr]")    \0011O\00175P Madam Vader
(dtk-define-voice 'wendy "[:nw]")   \0012O\00175P Bobby
(dtk-define-voice 'kit "[:nk]")     \0013O\00175P Petra
    New voice codes for ROM version 5.12 and later:
                                    \0015O
                                    \0016O
                                    \0017O
*/
void fix_voices(char **t)
{
  char c, *s;
  int i;
  struct {char c; char *s;}
  voices[]=
  {
    {'p',"\0010O"},
    {'h',"\0011O"},
    {'d',"\0012O"},
    {'f',"\0013O"},
    {'b',"\0014O"},
    {'u',"\0010O\00175P"},
    {'r',"\0011O\00175P"},
    {'w',"\0012O\00175P"},
    {'k',"\0013O\00175P"},
  };
  s = strstr(*t, "[:n");
  c = s[3];
  for (i = 0; i < 8; i++)
    if (s[3] == voices[i].c)
      gsub("\\[:n.*\\]", voices[i].s, t);
}

int queue_empty_p() {return q_head == q_tail;}

void queue_clear() {q_head = q_tail = 0;}

char *
queue_remove()
{
  if (queue_empty_p())
    return NULL;
  if (q_head == q_size) 
    q_head = 0;
  return queue[q_head++];
}

void
queue_speech(char *element)
{
  char *t = strdup(element), *s;
  if (t == NULL) error(__LINE__);

				/* first protect against DoubleTalk going
                                   into spell mode by separating
                                   certain characters with spaces */
  gsub("\\[\\*\\]", " ", &t);
  if(punctuations == ALL)
    gsub("[%&;()$+=/]", " \0 ", &t);
  else
    gsub("([a-zA-Z])([,.!%&;()$+=/])([a-zA-z])", "\\1 \\2 \\3", &t);
  if (capitalize)
				/* precede capitals with a tone */
				/* DECtalk generates tone 440 for 10
                                   msec -- we use 440 for 23 msec (the
                                   minimum tone duration) instead */
    gsub("([A-Z])", "\00199J\001\050\050 \\1", &t);
  if (split_caps)
    gsub("([^ -_A-Z])([A-Z][a-zA-Z]* )", "\\1\001d \001t\\2\001d \001t ", &t);
				/* \00116* gives a 100 msec pause */
  gsub("([^ -_A-Z])([A-Z])", "\\1\001D,\001T\\2", &t);
  if (punctuations == SOME)
    gsub("--", "\001d \001t", &t);
  if ((s = strstr(t, "[:n")))
    fix_voices(&t);

  if (ilog)
    {
      FILE *dfile = fopen(ifilename,"a");
      fprintf(dfile, "%s(%d):  after substitutions = \"%s\"\n", 
	      __FILE__, __LINE__, t); 
      fclose(dfile);
    }
      
  if (q_tail + 1 == q_head || (q_head == 0 && q_tail == q_size))
    {				/* queue is full - must enlarge */
      unsigned n = q_size - q_head; /* number of pointers to move */
      unsigned newsize = q_size*2; /* number of pointers in enlarged
				      queue */

/*
      if (ilog)
	{
	  int i;
	  FILE *dfile = fopen(ifilename,"a");
	  fprintf(dfile, "%s(%d): enlarging string queue from %d to %d\n"
		  "before:", __FILE__, __LINE__, q_size, newsize);
	  for (i = 0; i < q_size; i++)
	    fprintf(dfile, "%s%u", i==q_head?" [":(i==q_tail?"] ":" "), 
		    (unsigned)queue[i]);
	  if (q_tail == q_size) fprintf(dfile, "]");
	  fprintf (dfile, "\n");
	  fclose(dfile);
	}
*/

      queue = xrealloc(queue, newsize*sizeof(char *));
      if (q_head)
	{
	  memmove(queue + newsize - n, queue + q_head, 
		  n*sizeof(char *));
	  q_head = q_size - n;
	}
      q_size = newsize;

/*
      if (ilog)
	{
	  int i;
	  FILE *dfile = fopen(ifilename,"a");
	  fprintf(dfile, "after (head=%u, tail=%u):", q_head, q_tail);
	  for (i = 0; i < q_size; i++)
	    fprintf(dfile, "%s%u", i==q_head?" [":(i==q_tail?"] ":" "), 
		    (unsigned)queue[i]);
	  if (q_tail == q_size) fprintf(dfile, "]");
	  fprintf (dfile, "\n");
	  fclose(dfile);
	}
*/

    }
  if(q_tail == q_size) q_tail = 0;
  queue[q_tail++] = t;
}

/* return nonzero if either file descriptor fd1 or fd2 could be read
   without blocking. */
int
readable (int fd1, int fd2)
{  
  fd_set rfds, wfds, efds;
  struct timeval tv;

  FD_ZERO(&rfds);
  FD_SET(fd1, &rfds);
  FD_SET(fd2, &rfds);
  FD_ZERO(&wfds);
  FD_ZERO(&efds);
  tv.tv_sec = 0;       	/* do not wait for the files to become readable */
  tv.tv_usec = 0;
  return select(max(fd1, fd2)+1, &rfds, &wfds, &efds, &tv);
}

/* read one line from stdin, and leave it in buf.  Return zero for
   error or EOF.  Enlarge buffer if needed.
*/
int 
readline()
{
  
  char *s;
IN("readline");
  if (bufsiz == 0)
    buf = xmalloc(bufsiz = 64);
  s = buf;
  while((s = fgets(s, bufsiz, stdin)) != NULL)
    {
      s = buf + strlen(buf) - 1;
      if (*s == '\n')
	{
	  *s = 0;
#ifdef TIMESTAMPS
fprintf(nfile, "            %s\n", buf);
#endif
OUT("readline");
	  return 1;
	}
      buf = xrealloc(buf, bufsiz *= 2);
      s = buf + strlen(buf);
    }
OUT("readline");
  return 0;		/* EOF or error */
}

void
speech_task()
{
  char *s, buf[32];
  int index;
IN("speech_task");

  index = 1;
  talking = 1;
  doubletalk_gobble_acknowledgements();
  while(talking)
    {
      if(queue_empty_p())
	talking = 0;
      else
	{
	  s = queue_remove();
	  writeout(s);
	  free(s);
				/* set index marker with printable
                                   value */
	  sprintf(buf, "\001%dI", index%26 + 'A');
	  writeout(buf);

	  s = doubletalk_get_acknowledgement(0);
	  free(s);		/* ignore what the device sends back */
	  index++;
	  if (readable(0, 0))
	    talking = 0;
	}
    }
OUT("speech_task");
}

void
writeout(char *s)
{
IN("writeout");
  if (write(fd, s, strlen(s)) < 0)
    error(__LINE__);
  if (olog)
    {
      if (write(ofd, s, strlen(s)) < 0)
	error(__LINE__);
      if (write(ofd, "\n", 1) < 0)
	error(__LINE__);
    }
OUT("writeout");
}

void *xmalloc(unsigned n)
{
  void *u = NULL;
  return xrealloc(u, n);
}

void *xrealloc(void *p, unsigned n)
{
  void *u;
  u = realloc(p, n);
  if (u == NULL)
    {
      error(__LINE__);
      exit(1);
    }
  return u;
}

static char *xstrdup(char *s)
{
  char *t = xmalloc(strlen(s)+1);
  strcpy(t, s);
  return t;
}

static struct settings *interrogate(void)
{
  unsigned char *t, *end;
  int total, n, fail=0;
  static struct settings status;
  doubletalk_gobble_acknowledgements();
  write(fd, "\001?", 2);
  usleep(100000L);
  t = doubletalk_get_acknowledgement(&total);
  if (total < 21) return 0;	/* invalid */
  end = t + total;
  status.serial_number = t[0] + t[1]*256; /* serial number is little endian */
  t += 2;
  n = strcspn(t, "\r");
  t[n] = 0;
  status.rom_version = xstrdup(t);
  t += n + 1;

  status.mode = *t++;
  status.punc_level = *t++;
  status.formant_freq = *t++;
  status.pitch = *t++;
  status.speed = *t++;
  status.volume = *t++;
  status.tone = *t++;
  status.expression = *t++;
  status.ext_dict_loaded = *t++;
  status.ext_dict_status = *t++;
  status.free_ram = *t++;
  status.articulation = *t++;
  status.reverb = *t++;
  status.eob = end[-1];		/* any extra parameters between reverb
				   and the end of block marker are
				   ignored. */
  return &status;
}

/* print warning message if `val' is less than `min' or greater than `max'.
   `field' is the name of the element (in a settings struct).
   This is a separate function so the compiler doesn't generate
   warnings about useless tests (i.e. testing whether an unsigned is
   less than zero). */
void 
validate_byte(int val, int min, int max, char *field)
{
  if (val < min || val > max) 
     printf("WARNING: status.%s: %u is outside valid range %u through %u\n", 
	    field, val, min, max);
}

int
main(int argc, char **argv)
{
  int pid, pip[2];
  char *s, *port, c, *filename = "DoubleTalk", *statement=0;
  extern char *optarg;
  int val;
  int doubletalk_version = 0;	/* nonzero if we are only fetching the
				   ROM version from the DoubleTalk and
				   printing it */
  int testing = 0;		/* nonzero if writes are to actually
				   go into a file instead of the
				   speech device */


  {
    char *s;
				/* isolate the version number */
    revision += strlen("$Revision: ");
    if ((s = strchr(revision, ' '))) *s = 0;
				/* insert version number into messages */
    if ((s = strstr(init_string, SENTINAL)))
      memcpy(s, revision, strlen(revision));
    if ((s = strstr(reset_string, SENTINAL)))
      memcpy(s, revision, strlen(revision));
  }

  Argv0 = argv[0];

  while ((c = getopt(argc, argv, "t:s:vV")) != -1)
    {
      switch(c)
	{
	case 's':
	  statement = optarg;
	  break;
	case 't':
	  testing = 1;
	  filename = optarg;
	  break;
	case 'v':
	  printf("emacspeak-dt DoubleTalk driver for Emacspeak, version %s\n", 
		 revision);
	  return(0);
	case 'V':
	  doubletalk_version = 1;
	  break;
	case '?':
	default:
	  printf("emacspeak-dt - Interfacing Emacspeak to a DoubleTalk via C\n"
		 "usage: emacspeak-dt  [-v]  [-V]  [-t logfile]  [-s statement]\n"
		 "(but usually called by emacs directly)\n");
	  exit(1);
	}
    }

  s = getenv("EMACSPEAK");
  if (s && strcmp(s, "ilog") == 0) ilog = 1;
  if (s && strcmp(s, "olog") == 0) olog = 1;
  if (s && strcmp(s, "log") == 0) ilog = olog = 1;

  if (ilog)
    {
      FILE *dfile = fopen(ifilename,"a");
      time_t now;
      time(&now);
      fprintf(dfile, "%s(%d): starting at %s", 
	      __FILE__, __LINE__, ctime(&now)); 
      fclose(dfile);
    }

  buf = xmalloc(bufsiz = 256);
  queue = xmalloc((q_size = 1)*sizeof(char *));

  /* execute "uname" to find out the operating system */

  if (pipe(pip) < 0)
    error(__LINE__);
  if ((pid = fork()) < 0)
    error(__LINE__);	/* pipe error */
  else if (pid > 0)
    {				/* parent */
      if (close(pip[1]))
	error(__LINE__);
      (void)wait(NULL);
    }
  else
    {				/* child */
      if (close(pip[0]))
	error(__LINE__);
      if (pip[1] != 1)
	if (dup2(pip[1], 1) != 1)
	  error(__LINE__);
      if (execlp("uname","uname",NULL))
	error(__LINE__);
    }
  if (read(pip[0], buf, bufsiz) <= 0)
    strcpy(buf, "Linux");

/* both reads and writes are unbuffered */

  if (strstr(buf, "ULTRIX") || strstr(buf, "OSF1"))
    {
      if ((s = getenv("DTK_PORT")) != NULL)
	port = strdup(s);
      else 
	port = strdup("/dev/tty00");
      fd = open(port, O_RDWR);
      if (fd < 0)
	error(__LINE__);
      sprintf(buf, "stty sane 9600 raw  -echo  <  %.32s", port);
      val = system(buf);
      if (val == 127 || val == -1)
	error(__LINE__);
      sprintf(buf, "stty ixon ixoff  <  %.32s", port);
      val = system(buf);
      if (val == 127 || val == -1)
	error(__LINE__);
    }
  else if (strstr(buf, "SunOS"))
    {
      if ((s = getenv("DTK_PORT")) != NULL)
	port = strdup(s);
      else 
	port = strdup("/dev/ttya");
      fd = open(port, O_RDWR);
      if (fd < 0)
	error(__LINE__);
      sprintf(buf, "stty sane 9600 raw  -echo > %.32s", port);
      val = system(buf);
      if (val == 127 || val == -1)
	error(__LINE__);
      sprintf(buf, "stty ixon ixoff  >  %.32s", port);
      val = system(buf);
      if (val == 127 || val == -1)
	error(__LINE__);
    }      
  else
    {
      /*
	   DoubleTalk Requirements -  
			   One serial port: 9600 baud, 8 data bits,
                           1 stop bit, no parity, RTS/CTS handshaking
			   */
      if ((s = getenv("DTK_PORT")) != NULL)
	port = strdup(s);
      else 
	port = strdup("/dev/ttyS0");
      fd = open(port, O_RDWR);
      if (fd < 0) error(__LINE__);
      sprintf(buf, "stty sane 9600 raw  -echo crtscts < %.32s", port);
      val = system(buf); if (val == 127 || val == -1) error(__LINE__);
				/* linux wants the -echo done separately */
      sprintf(buf, "stty -echo < %.32s", port);
      val = system(buf); if (val == 127 || val == -1) error(__LINE__);
#ifdef NEVER
      sprintf(buf, "stty -a < %.32s > port-settings", port);
      val = system(buf); if (val == 127 || val == -1) error(__LINE__);
      sprintf(buf, " stty ixon ixoff < %.32s", port);
      val = system(buf); if (val == 127 || val == -1) error(__LINE__);
#endif
    }

  if (olog)
    {
      ofd = open(ofilename, O_WRONLY|O_CREAT, 0644);
      if (ofd == -1)
	{
	  error(__LINE__);
	  exit(1);
	}
    }

  /* initialize the speech parameters */

  split_caps = 1;
  capitalize = 0;
  space_special_chars = 1;
  talking = 0;
  speech_rate = 425;
  char_factor = 1.2;
  say_rate = speech_rate*char_factor;
  q_head = q_tail = 0;
  punctuations = SOME;

  signal(SIGINT, SIG_IGN);
  doubletalk_gobble_acknowledgements();

  if (doubletalk_version)
    {

#define VALIDATE_BYTE(field, min, max) \
      validate_byte(sp->field, min, max, #field)

      struct settings *sp;
      int fail=0;

      sp = interrogate();
      if (!sp)
	{
	  printf("emacspeak-dt DoubleTalk driver for Emacspeak, version %s\n"
		 "No DoubleTalk is responding on %s\n",
		 revision, port);
	  return 1;
	}

      sprintf(buf, "emacspeak-dt DoubleTalk driver for Emacspeak, version %s\n"
	      "ROM version: %s\nserial number: %u\nfree ram: %d pages\n",
	      revision, sp->rom_version, sp->serial_number, sp->free_ram);

      printf("%s", buf);
      doubletalk_speak(buf);

      VALIDATE_BYTE(mode, 0, 2);
      VALIDATE_BYTE(punc_level, 0, 7);
      VALIDATE_BYTE(formant_freq, 0, 9);
      VALIDATE_BYTE(pitch, 0, 99);
      VALIDATE_BYTE(speed, 0, 9);
      VALIDATE_BYTE(volume, 0, 9);
      VALIDATE_BYTE(tone, 0, 2);
      VALIDATE_BYTE(expression, 0, 9);
      VALIDATE_BYTE(ext_dict_loaded, 0, 1);
      VALIDATE_BYTE(ext_dict_status, 0, 1);
      VALIDATE_BYTE(free_ram, 0, 255);
      VALIDATE_BYTE(articulation, 0, 9);
      VALIDATE_BYTE(reverb, 0, 9);
      VALIDATE_BYTE(eob, 0x7f, 0x7f);

      return 0;
    }
  if (statement)
    {
      doubletalk_speak(statement);
      return 0;
    }
      
  if (testing)
    {		     /* open regular file INSTEAD of the DoubleTalk */
      close(fd);
      fd = open(filename, O_RDWR|O_CREAT, 0644);
    }

  writeout(init_string);

#ifdef TIMESTAMPS
  nfile = fopen("notes.log", "w");
#endif

  /* the main loop */

  while(readline())
    {
      char *s, *t, *cmd;
				/* eliminate leading whitespace and
                                   surrounding braces */
      s = strrchr(buf, '}');
      if (s != NULL) *s = 0;
      s = buf;
      while (*s == ' '||*s == '\t') s++;
      t = strchr(s, '{');
      if (t != NULL) *t = ' ';

      if (ilog)
	{
	  FILE *dfile = fopen(ifilename,"a");
	  fprintf(dfile, "%s(%d): command = \"%s\"\n", __FILE__, __LINE__, s); 
	  fclose(dfile);
	}

				/* parse the command */
      cmd = s;
      s += strcspn(s, " \t");
      if (*s) *s++ = 0;
      while (*s == ' ') s++;

      if (strcmp(cmd, "dectalk_capitalize") == 0)
	capitalize = atoi(s);

      else if (strcmp(cmd, "l") == 0)
	doubletalk_letter(s);

      else if (strcmp(cmd, "dectalk_reset") == 0)
	doubletalk_reset();

      else if (strcmp(cmd, "dectalk_say") == 0)
	doubletalk_say(s);

      else if (strcmp(cmd, "dectalk_set_character_scale") == 0)
	doubletalk_set_character_scale(atof(s));

      else if (strcmp(cmd, "dectalk_set_punctuations") == 0)
	doubletalk_set_punctuations(s);

      else if (strcmp(cmd, "dectalk_set_speech_rate") == 0)
	doubletalk_set_speech_rate(atoi(s));

      else if (strcmp(cmd, "dectalk_speak") == 0)
	doubletalk_speak(s);

      else if (strcmp(cmd, "dectalk_split_caps") == 0)
	split_caps = atoi(s);

      else if (strcmp(cmd, "s") == 0)
	doubletalk_stop();

      else if (strcmp(cmd, "t") == 0)
	doubletalk_tone(s);

      else if (strcmp(cmd, "q") == 0)
	queue_speech(s);

      else if (strcmp(cmd, "exit") == 0)
	exit(0);
				/* old style commands (Emacspeak 5.0
                                   and earlier) */
      else if (strcmp(cmd, "dectalk_letter") == 0)
	doubletalk_letter(s);
     
      else if (strcmp(cmd, "dectalk_stop") == 0)
	doubletalk_stop();
     
      else if (strcmp(cmd, "dectalk_tone") == 0)
	doubletalk_tone(s);
     
      else if (strcmp(cmd, "queue_speech") == 0)
	queue_speech(s);

      else 
	{
	  if (ilog)
	    {
	      FILE *dfile = fopen(ifilename,"a");
	      fprintf(dfile, "emacspeak-dt: unrecognized command \"%s\"\n", s);
	      fclose(dfile);
	    }
	  fprintf(stderr, "emacspeak-dt: unrecognized command \"%s\"\n", s);
	  exit(1);
	}
    }
  return 0;
}

#ifdef NEVER
/* spare parts warehouse */
void
doubletalk_pause()
{
  writeout("[:pause ]");
  talking=0;
}

void
doubletalk_resume()
{
  writeout("[:resume ]");
  speech_task();
}

void doubletalk_space_special_chars(int flag) {space_special_chars = flag;}

void
doubletalk_synchronize()
{
  queue_speech("[:sync]");
}
#endif /* NEVER */


/* replacement for defective libraries */

#ifndef HAVE_STRSTR
	/* returns a pointer to the beginning of the first occurrence
	   of the substring needle in the string haystack. */
char *strstr(const char *haystack, const char *needle)
{				/* slow but simple implementation */
  char c;
  int hlen=strlen(haystack), nlen = strlen(needle), i, j;

  for (j = 0; j <= hlen-nlen; j++)
    {
      for (i = 0; (c = needle[i]); i++)
	if (haystack[j+i] != needle[i])
	  break;
      if (c == 0)
	return (char *)haystack + j;
    }
  return NULL;
}
#endif
