/* Copyright (c) 1991-2002 Doshita Lab. Speech Group, Kyoto University */
/* Copyright (c) 2000-2002 Speech and Acoustics Processing Lab., NAIST */
/*   All rights reserved   */

/* adin-cut.c --- read audio input from device with skipping silence */

/* $Id: adin-cut.c,v 1.7 2002/11/25 02:23:29 ri Exp $ */

/* use zerocross & level threshold for silence detection */
/* process one speech segment till next silence at a call */

/* for real-time processing, it does not (basically) store speech samples
   to a buffer.  Instead, an external function that directly processes the
   incoming speech segment should be specified.  The incoming speech segments
   are sequencially processed by the function. */

#ifdef HAVE_PTHREAD
/*
 *  threading is enabled.  Two threads share one buffer for sending/receiving
 *  incoming speech:
 *    Thread 1 (created): A/D-in thread with cutting
 *          read in speech data from device, detect sil, and if triggered,
 *          keep appending the segment to buffer.
 *    Thread 2 (original): processing thread
 *          keep watching the buffer, and process the incoming speech
 *          segment as soon as they appear.  Then it shrinks the buffer.
 *
 */
#endif

#include <sent/stddefs.h>
#include <sent/speech.h>
#include <sent/adin.h>

#undef THREAD_DEBUG

/* zero-cross params (local) */
static int c_length  = 5000;	/* samples to hold */
static int c_offset  = 0;	/* data DC offset */
static int wstep   = DEFAULT_WSTEP;	/* data window size */
static int thres;		/* level threshold */
static int noise_zerocross;	/* threshold of zerocross num in a period */
static int nc_max;		/* tail margin */

/* device-dependent configuration */
static boolean adin_cut_on;	/* TRUE if cutting enabled */
static boolean strip_flag;
static boolean silence_cut_default;
static boolean enable_thread = FALSE;	/* invalid if no pthread */

/* device handling function */
static boolean (*ad_resume)();	/* restart input */
static boolean (*ad_pause)();	/* stop input */
static int (*ad_read)(SP16 *, int); /* read in samples */

#ifdef HAVE_PTHREAD
static void adin_thread_create();
#endif

/* store device-dependent configurations to local */
/* (only called from adin_select() via adin_register_func() ) */
void
adin_setup_func(int (*cad_read)(SP16 *, int),
		boolean (*cad_pause)(),
		boolean (*cad_resume)(),
		boolean use_cut_def,
		boolean need_thread)
{
  ad_read = cad_read;
  ad_pause = cad_pause;
  ad_resume = cad_resume;
  silence_cut_default = use_cut_def;
#ifdef HAVE_PTHREAD
  enable_thread = need_thread;
#else
  if (need_thread == TRUE) {
    j_printerr("Warning: thread not supported, input may be corrupted on slow machines\n");
  }
#endif
}

/* setup silence detection parameters (should be called after adin_select()) */
void
adin_setup_param(int silence_cut, boolean strip_zero, int cthres, int czc, int head_margin, int tail_margin, int sample_freq)
{
  float samples_in_msec;
  /* whether to perform silence cutting */
  /* 0...force off  1...force on  2...keep device-specific default */
  if (silence_cut < 2) {
    adin_cut_on = (silence_cut == 1) ? TRUE : FALSE;
  } else {
    adin_cut_on = silence_cut_default;
  }
  /* whether to strip zero samples */
  strip_flag = strip_zero;
  /* set level threshold */
  thres = cthres;
  /* calc & set internal parameter from configuration */
  samples_in_msec = (float) sample_freq / 1000.0;
  c_length = head_margin * samples_in_msec;	/* in msec. */
  noise_zerocross = czc * c_length / sample_freq;
  nc_max = (tail_margin * samples_in_msec / DEFAULT_WSTEP) + 2;
  /*printf("c_length=%d, noise_zerocross=%d nc_max=%d\n", c_length, noise_zerocross, nc_max);*/

#ifdef HAVE_PTHREAD
  if (enable_thread) {
    /* create A/D-in thread */
    adin_thread_create();
  }
#endif
}

/* query functions for the caller to know the resulting configurations */
boolean
query_segment_on()
{
  return adin_cut_on;
}
boolean
query_thread_on()
{
  return enable_thread;
}



#ifdef HAVE_PTHREAD
#include <pthread.h>
/* variables shared among threads */
static pthread_t adin_thread;	/* thread info */
static pthread_mutex_t mutex;	/* lock primitive */
static int dummy;		/* dummy arg */
static boolean transfer_online = FALSE;
#endif

/* temporary input data buffer */
static SP16 *buffer = NULL;	/* buffer */
static int bpmax;		/* maximum size of buffer */
static int bp;			/* current point to store data */
static int current_len;		/* current length of stored samples */
static SP16 *cbuf;

/* purge processed samples */
static void
adin_purge(int from)
{
  if (from > 0 && current_len-from > 0) {
    memmove(buffer, &(buffer[from]), (current_len - from) * sizeof(SP16));
  }
  bp = current_len - from;
}

/* main all-in-one adin function */
/* return: -1 on error, 0 on end of stream, >0 paused by external process */
/* When threaded as A/D-in thread, it runs forever.  When an error occured,
   just tell another thread to stop processing */

static int
adin_cut(int (*ad_process)(SP16 *, int), int (*ad_check)())
{
  static int i;
  static boolean is_valid_data;
  int ad_process_ret;
  int imax, len, cnt;
  static boolean end_of_stream;
  static int end_status;
  
  /* variables for zero-cross routines*/
  static int zc;
  static int nc;

  /*
   * there are 3 buffers:
   *   temporary storage queue: buffer[]
   *   cycle buffer for zero-cross counting: (in zc_e)
   *
   * Each samples are first read to buffer[], then passed to count_zc_e()
   * to find trigger.  Samples between trigger and end of speech are 
   * passed to (*ad_process) with pointer to the first sample and its length.
   *
   */
  if (buffer == NULL) {		/* beginning of stream */
    buffer = (SP16 *)mymalloc(sizeof(SP16) * MAXSPEECHLEN);
    bpmax = MAXSPEECHLEN;
    bp = 0;
    /* reset zero-cross status */
    if (adin_cut_on) {
      init_count_zc_e(thres, c_length, c_offset);
      is_valid_data = FALSE;
    }
    end_of_stream = FALSE;
    wstep = DEFAULT_WSTEP;
    nc = 0;
    cbuf = (SP16 *)mymalloc(sizeof(SP16) * c_length);
  }
      
  /* resume input */
  if (ad_resume != NULL) {
    if ((*ad_resume)() == FALSE)  return(-1);
  }

  /* ---------------------------------------------- */
  /* main loop */
  for (;;) {

    /* Read samples as 16bit shorts (SP16) */
    /* bp: pointer to samples left in queue buffer */
    /* all samples in device are read at offset [bp] -> [0..len-1] */
    if (end_of_stream) {		/* already reaches end of stream */
      /* just return */
      current_len = bp;
    } else {
      /* read as much samples as possible */
      /* returns: -2 on error, -1 on end of stream, >=0 num of read samples */
      cnt = (*ad_read)(&(buffer[bp]), bpmax - bp);
      if (cnt < 0) {		/* end of stream or error */
	if (cnt == -2) end_status = -1; /* error */
	else if (cnt == -1) end_status = 0; /* end of stream */
	end_of_stream = TRUE;		/* mark as end of stream */
	cnt = 0;			/* no new input */
	/* in case the first ad_read() fails */
	if (bp == 0) break;
      }
      /* strip off invalid samples */
      if (cnt > 0) {
	if (strip_flag) {
	  len = strip_zero(&(buffer[bp]), cnt);
	  if (len != cnt) cnt = len;
	}
      }
      /* len = current samples in buffer */
      current_len = bp + cnt;
    }
#ifdef THREAD_DEBUG
    if (end_of_stream) {
      printf("stream already ended\n");
    }
    printf("input: get %d samples [%d-%d]\n", current_len - bp, bp, current_len);
#endif

    if (ad_check != NULL
#ifdef HAVE_PTHREAD
	&& !enable_thread
#endif
	) {
      /* call check callback (called for every period) */
      if ((i = (*ad_check)()) < 0) {
	if ((i == -1 && current_len == 0) || i == -2) {
	  end_status = -2;	/* recognition terminated by outer function */
	  goto break_input;
	}
      }
    }

    /* set process step */
    if (end_of_stream && wstep > current_len) wstep = current_len;

    /* set maximum number of processed samples per loop */
#ifdef HAVE_PTHREAD
    if (enable_thread) imax = current_len; /* store as many as possible */
    else imax = (current_len < wstep) ? current_len : wstep;
#else
    imax = (current_len < wstep) ? current_len : wstep;
#endif

#ifdef THREAD_DEBUG
    printf("process %d samples by %d step\n", imax, wstep);
#endif

    /* proceed for each 'wstep' steps */
    i = 0;
    while (i + wstep <= imax) {
      if (adin_cut_on) {
	/* count zero-cross */
	zc = count_zc_e(&(buffer[i]), wstep);
	if (zc > noise_zerocross) { /* triggering */
	  nc = 0;
	  if (is_valid_data == FALSE) {	/* triggered */
	    is_valid_data = TRUE;   /* start to record */
#ifdef THREAD_DEBUG
	    printf("detect on\n");
#endif
	    /* process stored samples in cycle buffer */
	    if ( ad_process != NULL
#ifdef HAVE_PTHREAD
		 && (!enable_thread || transfer_online)
#endif
		 ) {
	      zc_copy_buffer(cbuf, &len);
	      if (len - wstep > 0) {
#ifdef THREAD_DEBUG
		printf("callback for buffered samples (%d bytes)\n", len - wstep);
#endif
		ad_process_ret = (*ad_process)(cbuf, len - wstep);
		switch(ad_process_ret) {
		case 1:		/* segmented */
		  end_status = 1;
		  adin_purge(i);
#ifdef HAVE_PTHREAD
		  if (enable_thread) { /* just stop transfer */
		    pthread_mutex_lock(&mutex);
		    transfer_online = FALSE;
		    pthread_mutex_unlock(&mutex);
		  } else {
		    goto break_input;
		  }
#else
		  goto break_input;
#endif
		case -1:		/* error */
		  end_status = -1;
		  goto break_input;
		}
	      }
	    }
	  }
	} else if (is_valid_data == TRUE) {
	  /* processing tailing silence */
#ifdef THREAD_DEBUG
	  printf("trailing silence\n");
#endif
	  nc++;
	}
      }
      if(
	 (!adin_cut_on || is_valid_data == TRUE)
#ifdef HAVE_PTHREAD
	 && (!enable_thread || transfer_online)
#endif
	 ) {
	/* process data */
	if ( ad_process != NULL ) {
#ifdef THREAD_DEBUG
	  printf("callback for input sample [%d-%d]\n", i, i+wstep);
#endif
	  /* call external function */
	  ad_process_ret = (*ad_process)(&(buffer[i]),  wstep);
	  switch(ad_process_ret) {
	  case 1:		/* segmented by ad_process() */
	    adin_purge(i+wstep);
	    end_status = 1;
#ifdef HAVE_PTHREAD
	    if (enable_thread) { /* just stop transfer */
	      pthread_mutex_lock(&mutex);
	      transfer_online = FALSE;
	      pthread_mutex_unlock(&mutex);
	    } else {
	      goto break_input;
	    }
#else
	    goto break_input;
#endif
	  case -1:		/* error */
	    end_status = -1;
	    goto break_input;
	  }
	}
      }
      if (adin_cut_on && is_valid_data && nc >= nc_max) {
#ifdef THREAD_DEBUG
	printf("detect off\n");
#endif
	/* end input by silence */
	is_valid_data = FALSE;
	adin_purge(i+wstep);
	end_status = 1;
#ifdef HAVE_PTHREAD
	if (enable_thread) { /* just stop transfer */
	  pthread_mutex_lock(&mutex);
	  transfer_online = FALSE;
	  pthread_mutex_unlock(&mutex);
	} else {
	  goto break_input;
	}
#else
	goto break_input;
#endif
      }
	
      i += wstep;
    }
    
    /* purge processed samples and update queue */
    adin_purge(i);

    /* end of input by end of stream */
    if (end_of_stream && bp == 0) break;
  }

break_input:
  /* pause transfer */

  if (ad_pause != NULL) {
    if ((*ad_pause)() == FALSE) {
      j_printerr("Error: failed to pause recording\n");
      end_status = -1;
    }
  }

  if (end_of_stream) {			/* input already ends */
    if (bp == 0) {		/* rest buffer successfully flushed */
      /* reset status */
      if (adin_cut_on) end_count_zc_e();
      free(buffer);
      buffer = NULL;
    }
    end_status = (bp) ? 1 : 0;
  }
  
  return(end_status);
}

#ifdef HAVE_PTHREAD

/* call-back for storing triggered samples in thread 1 */
static SP16 *speech;
static int speechlen;
static int
adin_store_buffer(SP16 *now, int len)
{
  if (speechlen + len > MAXSPEECHLEN) {
    j_printerr("Warning: too long input (> %d samples), segmented now\n", MAXSPEECHLEN);
    return(1);			/* force segment */
  }
  pthread_mutex_lock(&mutex);
  memcpy(&(speech[speechlen]), now, len * sizeof(SP16));
  speechlen += len;
  pthread_mutex_unlock(&mutex);
#ifdef THREAD_DEBUG
  printf("input: stored %d samples, total=%d\n", len, speechlen);
#endif
  /* output progress bar in dots */
  /*if ((++dotcount) % 3 == 1) j_printerr(".");*/
  return(0);			/* continue */
}

/* A/D-in thread main function: only call adin_cut() with storing function */
void
adin_thread_input_main(void *dummy)
{
  adin_cut(adin_store_buffer, NULL);
}

/* create A/D-in thread */
static void
adin_thread_create()
{
  /* init storing buffer */
  speechlen = 0;
  speech = (SP16 *)mymalloc(sizeof(SP16) * MAXSPEECHLEN);

  transfer_online = FALSE; /* tell adin-mic thread to wait at initial */

  if (pthread_mutex_init(&(mutex), NULL) != 0) { /* error */
    j_error("Error: pthread: cannot initialize mutex\n");
  }
  if (pthread_create(&adin_thread, NULL, (void *)adin_thread_input_main, NULL) != 0) {
    j_error("Error: pthread: failed to create AD-in thread\n");
  }
  if (pthread_detach(adin_thread) != 0) { /* not join, run forever */
    j_error("Error: pthread: failed to detach AD-in thread\n");
  }
  j_printerr("AD-in thread created\n");
}

/* When threading, this function replaces the original adin_cut in processing thread */
/* used for module mode: return value: -2 = input cancellation forced by control module */
static int
adin_thread_process(int (*ad_process)(SP16 *, int), int (*ad_check)())
{
  int prev_len, nowlen;
  int ad_process_ret;
  int end_status;
  int i;

  /* reset storing buffer --- input while recognition will be ignored */
  pthread_mutex_lock(&mutex);
  if (speechlen == 0) transfer_online = TRUE; /* tell adin-mic thread to start recording */
#ifdef THREAD_DEBUG
  printf("process: reset, speechlen = %d, online=%d\n", speechlen, transfer_online);
#endif
  pthread_mutex_unlock(&mutex);

  j_printerr("<<< please speak >>>");

  /* main processing loop */
  prev_len = 0;
  for(;;) {
    /* get current length (locking) */
    pthread_mutex_lock(&mutex);
    nowlen = speechlen;
    pthread_mutex_unlock(&mutex);
    if (ad_check != NULL) {
      if ((i = (*ad_check)()) < 0) {
	if ((i == -1 && nowlen == 0) || i == -2) {
	  pthread_mutex_lock(&mutex);
	  transfer_online = FALSE;
	  speechlen = 0;
	  pthread_mutex_unlock(&mutex);
	  return(-2);
	}
      }
    }
    if (prev_len < nowlen) {
#ifdef THREAD_DEBUG
      printf("process: proceed [%d-%d]\n",prev_len, nowlen);
#endif
      if (prev_len == 0) {	/* first trigger */
	/* flush prompt */
	j_printerr("\r                    \r");
      }
      /* got new sample, process */
      /* As the speech[] buffer is monotonously increase,
	 content of speech buffer [prev_len..nowlen] would not alter
	 in both threads
	 So locking is not needed while processing.
       */
      /*printf("main: read %d-%d\n", prev_len, nowlen);*/
      if (ad_process != NULL) {
	ad_process_ret = (*ad_process)(&(speech[prev_len]),  nowlen - prev_len);
#ifdef THREAD_DEBUG
	printf("ad_process_ret=%d\n",ad_process_ret);
#endif
	switch(ad_process_ret) {
	case 1:			/* segmented */
	  /* segmented by callback function */
	  /* purge processed samples and keep transfering */
	  pthread_mutex_lock(&mutex);
	  if(speechlen > nowlen) {
	    memmove(buffer, &(buffer[nowlen]), (speechlen - nowlen) * sizeof(SP16));
	    speechlen = speechlen - nowlen;
	  } else {
	    speechlen = 0;
	  }
	  transfer_online = FALSE;
	  pthread_mutex_unlock(&mutex);
	  /* keep transfering */
	  return(1);		/* return with segmented status */
	case -1:		/* error */
	  pthread_mutex_lock(&mutex);
	  transfer_online = FALSE;
	  pthread_mutex_unlock(&mutex);
	  return(-1);		/* return with error */
	}
      }
      prev_len = nowlen;
    } else {
      if (transfer_online == FALSE) {
	/* segmented by zero-cross */
	/* reset storing buffer for next input */
	pthread_mutex_lock(&mutex);
	speechlen = 0;
	pthread_mutex_unlock(&mutex);
        break;
      }
      usleep(100000);   /* wait = 0.1sec*/            
    }
  }

  /* as threading assumes infinite input */
  /* return value should be 1 (segmented) */
  return(1);
}
#endif /* HAVE_PTHREAD */


/* global entry point */
int
adin_go(int (*ad_process)(SP16 *, int), int (*ad_check)())
{
#ifdef HAVE_PTHREAD
  if (enable_thread) {
    return(adin_thread_process(ad_process, ad_check));
  }
#endif
  return(adin_cut(ad_process, ad_check));
}
