/******************************************************************************
 * GYachI-Voiceclient
 * Copyright (C) 2006  Stefan Sikora <hoshy[at]schrauberstube.de>
 *
 * This program 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 of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *****************************************************************************/

/*******************
 * Sound interface *
 ******************/
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

#include "sound.h"
#include "protocol.h"
#include "support.h"

// General sound-functions
int init_ALSA();
void record_ALSA(unsigned char *data, int size);

// Truespeech-functions
int init_tsp();
int init_stream(int recflag);
int uninit_stream();
void uninit_tsp();

int samplechunk;

void uninit_ALSA();
void play_ALSA(unsigned char *data, int size);
void play_incoming();
void record_outgoing();

pthread_t play_tid = 0;				// playback-thread's id
pthread_t rec_tid  = 0;				// record-thread's id

void reset_talk_labels()
{
	GtkWidget *label1 = lookup_widget(gyv_mainwindow, "gyv_label_name");
	GtkWidget *label2 = lookup_widget(gyv_mainwindow, "gyv_label_alias");

	if (recording) {
		gtk_label_set_text(GTK_LABEL(label1), "");
		gtk_label_set_text(GTK_LABEL(label2), "*** TALKING ***");
	} else {
		gtk_label_set_text(GTK_LABEL(label1), "Noone is speaking ?");
		gtk_label_set_text(GTK_LABEL(label2), "Alias of noone ...");
	}
}

/* Basic inits */
int init_sound() {
	memset(&soundbuffer, 0, sizeof(soundbuffer));
	recvchunk = 0;	
	playchunk = 0;
	samplechunk = 0;
	silence   = 0;				// begin to play when enough chunks are decoded
	play_tid = 0;
	rec_tid  = 0;
	playing   = 0;
	recording = 0;
	if (init_ALSA()) return(1);
	if (init_tsp()) return(1);
	if (init_stream(0)) return(1);

	// start playback in own thread
	pthread_cond_init(&data_cond, NULL);
	pthread_mutex_init(&data_mutex, NULL);

	dbg_print("Entering playing-thread\n", NULL);		
	reset_talk_labels();
	pthread_create(&play_tid, NULL, (void *)play_incoming, NULL);
	dbg_print("init_sound: created play_tid: %08x\n", (char *)play_tid);
//	pthread_detach(play_tid);
	return(0);
}

void stop_record_thread()
{
	int status;

	if (rec_tid) {
		dbg_print("stop_record_thread: joining rec_tid: 08x\n", (char *)rec_tid);
		recording = 0;
		pthread_join(rec_tid, (void *)&status);
		rec_tid=0;
	}
}

void stop_play_thread()
{
	int status;

	if (play_tid) {
		dbg_print("stop_play_thread: joining play_tid: %08x\n", (char *)play_tid);
		playing = 0;
		silence = 0;
		pthread_cond_signal(&data_cond);
		pthread_join(play_tid, (void*)&status);
		play_tid=0;
	}
}

void uninit_sound() {
	stop_play_thread();
	stop_record_thread();

	uninit_ALSA();
	uninit_stream();
	uninit_tsp();

	dbg_print("done.\n", NULL);
}


/* turn off playback and begin to record */
void start_recording() {
	// wait for playback-thread
	stop_play_thread();

	// adjust streaming to PCM->TSP
	uninit_stream();
	init_stream(1);

	// start recording in own thread
	pthread_create(&rec_tid, NULL, (void *)record_outgoing, NULL);
	dbg_print("start_recording: created rec_tid: %08x\n", (char *)rec_tid);
//	pthread_detach(rec_tid);
}


/* stop recording and turn back to playback */
void stop_recording() {
	// wait for record-thread
	stop_record_thread();

	// send "I stopped speacking packet"
	ytspstop();
	
	// adjust streaming to TSP->PCM
	uninit_stream();
	init_stream(0);

	// start playback-thread again
	reset_talk_labels();
	pthread_create(&play_tid, NULL, (void *)play_incoming, NULL);
	dbg_print("stop_recording: created play_tid: %08x\n", (char *)play_tid);
//	pthread_detach(play_tid);
	reset_talk_labels();
}


/* record a chunk of speech, convert and send via udp */
void record_outgoing() {
	recording = 1;
	reset_talk_labels();

	while (recording) {
		record_ALSA((unsigned char*)&recordbuffer, 720);
		ytspcraft();		// TODO: this will create lags, just for testing, later using ringbuffer!
	}
	dbg_print("Leaving recording-thread\n", NULL);
	pthread_exit(NULL);
}


/* Check for soundbuffer and playback stream in own thread */
void play_incoming() {
	playing = 1;
	while (playing) {
		pthread_mutex_lock(&data_mutex);
		pthread_cond_wait(&data_cond, &data_mutex);
		if (playchunk >= tspdelay) {
			silence = 1;
		}
		pthread_mutex_unlock(&data_mutex);

		while (silence) {
			play_ALSA((unsigned char *)(&soundbuffer)+1440*samplechunk, 720);
			samplechunk++;
			if (samplechunk >= max_tspchunks) samplechunk = 0;		// wrap around

			pthread_mutex_lock(&data_mutex);
			playchunk --;
			if (playchunk < 0) {
				playchunk = 0; /* really should never happen */
			}
			if (playchunk == 0) {
				silence = 0;
			}
			pthread_mutex_unlock(&data_mutex);

		}
	}
	dbg_print("Leaving play-thread\n", NULL);		
	pthread_exit(NULL);	
}

#include <stdlib.h>
#include <stdio.h>

/***********************
 * ALSA sound-handling *
 **********************/
#include <alsa/asoundlib.h>

snd_pcm_t *pcm_handleplay;								// Handle for playback device
snd_pcm_t *pcm_handlecapt;								// Handle for capture device

int set_hw_ALSA(snd_pcm_t *pcm_handle) {
    int rate = 8000;									// sample rate
    int exact_rate;	
    snd_pcm_hw_params_t *hwparams;						// hardware parameters
	unsigned period_time = 0;
	unsigned buffer_time = 0;
	snd_pcm_uframes_t period_frames = 0;
	snd_pcm_uframes_t buffer_frames = 0;
  
    snd_pcm_hw_params_alloca(&hwparams);

    /* Init hwparams with full configuration space */
    if (snd_pcm_hw_params_any(pcm_handle, hwparams) < 0) {
      fprintf(stderr, "ALSA: Can not configure this PCM device.\n");
      return(-1);
    }

    if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
      fprintf(stderr, "ALSA: Error setting access.\n");
      return(-1);
    }
  
    if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE) < 0) {
      fprintf(stderr, "ALSA: Error setting format.\n");
      return(-1);
    }

    // Set sample rate. If the exact rate is not supported 
    // by the hardware, use nearest possible rate.          
    exact_rate = rate;
    if (snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, &exact_rate, 0) < 0) {
      fprintf(stderr, "ALSA: Error setting rate.\n");
      return(-1);
    }
    if (rate != exact_rate) {
      fprintf(stderr, "ALSA: The rate %d Hz is not supported by your hardware.\nUsing %d Hz instead.\n", rate, exact_rate);
    }

    if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 1) < 0) {
      fprintf(stderr, "ALSA: Error setting channels.\n");
      return(-1);
    }

	snd_pcm_hw_params_get_buffer_time_max(hwparams, &buffer_time, 0);
	if (buffer_time > 500000) buffer_time = 500000;

	if (buffer_time > 0) period_time = buffer_time / 4;
	else period_frames = buffer_frames / 4;

	if (period_time > 0) snd_pcm_hw_params_set_period_time_near(pcm_handle, hwparams, &period_time, 0);
	else snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &period_frames, 0);

	if (buffer_time > 0) snd_pcm_hw_params_set_buffer_time_near(pcm_handle, hwparams, &buffer_time, 0);
	else snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hwparams, &buffer_frames);
						
    if (snd_pcm_hw_params(pcm_handle, hwparams) < 0) {
      fprintf(stderr, "ALSA: Error setting HW params.\n");
      return(-1);
    }
	return(0);
}


int init_ALSA() {
    char *pcm_name;				// name of PCM-device
    pcm_name = strdup("default");
  
    if (snd_pcm_open(&pcm_handleplay, pcm_name, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
      fprintf(stderr, "ALSA: Error opening PCM-playback device %s\n", pcm_name);
      return(-1);
    }
	if (set_hw_ALSA(pcm_handleplay)) return(-1);
	  
    if (snd_pcm_open(&pcm_handlecapt, pcm_name, SND_PCM_STREAM_CAPTURE, 0) < 0) {
      fprintf(stderr, "ALSA: Error opening PCM-capture device %s\n", pcm_name);
      return(-1);
    }
	if (set_hw_ALSA(pcm_handlecapt)) return(-1);
	return(0);
}


void uninit_ALSA() {
    snd_pcm_close(pcm_handleplay);
    snd_pcm_close(pcm_handlecapt);
}


void play_ALSA(unsigned char *data, int size) {
	int pcmreturn;
	while ((pcmreturn = snd_pcm_writei(pcm_handleplay, data, size)) < 0) {
        snd_pcm_prepare(pcm_handleplay);
//        fprintf(stderr, "ALSA: Buffer Underrun\n");
    }
}


void record_ALSA(unsigned char *data, int size) {
	int pcmreturn;
	while ((pcmreturn = snd_pcm_readi(pcm_handlecapt, data, size)) < 0) {
        snd_pcm_prepare(pcm_handlecapt);
//        fprintf(stderr, "ALSA: Buffer overrun\n");
    }
}

/****************************************************************
* Part for TrueSpeech-Encoding/-Decoding in sounds.c for GYachI *
*                                                               *
* Taken and modified from pY!Voice (Erica Andrews)              *
* 2006 Stefan Sikora <hoshy@schrauberstube.de>                  *
****************************************************************/

#include "loader.h"
#include "driver.h"
#include "wine/windef.h"
#include "wineacm.h"
#include "string.h"

char* get_path(char* x){  return strdup(x);}		// needed by registry.c

/* added: Erica Andrews, the 90 byte header Rob talks about */
/* This was much easier to extract in readable form using Python instead of a hex editor */
/* Will be useful when we send truspeech data back to the server, it might not like our 
     raw format */ 

/* a file containing the 90-byte UTF-8 binary header junk */
#define TSP_HEADER  "tsp.header"
// Looks like entire .wav header for a TrueSpeech
// file is 90 bytes long.  This means if we assume
// we have a TrueSpeech input file we can just skip
// the first 90 bytes instead of deleting them first
// with a hex editor to create a .raw file.

#define TRUESPEECH_FORMAT 0x22
#define TRUESPEECH_CBSIZE 32

PWINE_ACMDRIVERID acmDriver1 = 0;	// Truespeech
PWINE_ACMDRIVERID acmDriver2 = 0;	// PCM

WAVEFORMATEX *ts_wf = 0;			// Truespeech
WAVEFORMATEX pcm_wf;				// PCM
WAVEFORMATEX *i_wf = 0;				// Pointer to Input-Format
WAVEFORMATEX *o_wf = 0;				// Pointer to Output-Format

HACMSTREAM handle;					// Stream-handle
MMRESULT ret;
ACMSTREAMHEADER header;				// structure for conversion buffer
DWORD srcsize = 0;					// sizes
DWORD destsize = 0;

void* xmalloc(size_t s) {
	void* p = malloc(s);
	if (!p) {
		printf("out of memory\n");
		exit(1);
	}
  return p;
}


/* register drivers, set format-conventions */
int init_tsp() {
	acmDriver1 = MSACM_RegisterDriver("tssoft32.acm", TRUESPEECH_FORMAT, 0);
	if (!acmDriver1) {
    	printf("error registering TrueSpeech ACM driver for TrueSpeech\n");
    	return 1;
	}

	acmDriver2 = MSACM_RegisterDriver("tssoft32.acm", WAVE_FORMAT_PCM, 0);
  	if (!acmDriver2) {
    	printf("error registering TrueSpeech ACM driver for PCM\n");
    	return 1;
	}

	return(0);
}


/* unregister drivers */
void uninit_tsp() {
	MSACM_UnregisterDriver(acmDriver1);
	MSACM_UnregisterDriver(acmDriver2);
}


/* setup streaming and conversion buffer *
* recflag = 0: TSP->PCM                  *
* recflag = 1: PCM->TSP                  */
int init_stream(int recflag) {
	long* iptr = 0;

	// TrueSpeech format
	ts_wf = xmalloc(sizeof(WAVEFORMATEX) + TRUESPEECH_CBSIZE);
	memset(ts_wf, 0, sizeof(*ts_wf) + TRUESPEECH_CBSIZE);

	ts_wf->wFormatTag      = TRUESPEECH_FORMAT;
	ts_wf->nChannels       = 1;
	ts_wf->nSamplesPerSec  = 8000;
	ts_wf->wBitsPerSample  = 1;
	ts_wf->nBlockAlign     = 32;
	ts_wf->nAvgBytesPerSec = 1067;
	ts_wf->cbSize          = TRUESPEECH_CBSIZE;

	// write extra data needed by TrueSpeech codec found
	// from examining a TrueSpeech .wav file header
	iptr = (long*)(ts_wf + 1);
	*iptr = 0x00f00001;

	// Typical PCM format, 16-bit signed samples at 8 KHz.
	memset(&pcm_wf, 0, sizeof(pcm_wf));
  
	pcm_wf.wFormatTag      = WAVE_FORMAT_PCM;
	pcm_wf.nChannels       = 1;
	pcm_wf.nSamplesPerSec  = 8000;
	pcm_wf.wBitsPerSample  = 16;
	pcm_wf.nBlockAlign     = pcm_wf.nChannels * pcm_wf.wBitsPerSample / 8;
	pcm_wf.nAvgBytesPerSec = pcm_wf.nSamplesPerSec * pcm_wf.nBlockAlign;

	// decide which way to perform the conversion
	if (recflag) {				// PCM->TSP
		i_wf = &pcm_wf;
		o_wf = ts_wf;
	} 
	else {
		i_wf = ts_wf;			// TSP->PCM
		o_wf = &pcm_wf;
	}
	

	// TrueSpeech isn't confident it can do realtime
	// compression.  Tell it we don't care and it's happy.
	ret = acmStreamOpen(&handle, 0, i_wf, o_wf, 0, 0, 0, ACM_STREAMOPENF_NONREALTIME);
	if (ret) {
    	if (ret == ACMERR_NOTPOSSIBLE) printf("invalid codec\n");
    	else printf("acmStreamOpen error %d\n", ret);
		return(1);
    }


	// we assume that the format with the largest block alignment is
	// the most picky.  And it basically turns out that TrueSpeech has
	// the largest block alignment.  Anyway, we use that block alignment
	// to ask for the preferred size of the "other" format's buffer.
	// Then we turn around and ask for the first one's preferred buffer
	// size based on the other's buffer size, which seems to turn out
	// to be its block alignment.  Or something like that.
	if (i_wf->nBlockAlign > o_wf->nBlockAlign) {
    	ret = acmStreamSize(handle, i_wf->nBlockAlign, &destsize, ACM_STREAMSIZEF_SOURCE);
    	if (ret) {
      		printf("acmStreamSize error %d\n", ret);
      		return(1);
    	}
    	if (!destsize) {
			printf("ACM codec reports destsize=0\n");
			return(1);
	    }

    	ret = acmStreamSize(handle, destsize, &srcsize, ACM_STREAMSIZEF_DESTINATION);
		if (ret) {
			printf("acmStreamSize error\n");
			return(1);
		}
	}
	else {
    	ret = acmStreamSize(handle, o_wf->nBlockAlign, &srcsize, ACM_STREAMSIZEF_DESTINATION);
    	if (ret) {
			printf("acmStreamSize error %d\n", ret);
			return(1);
		}
	    if (!srcsize) {
			printf("ACM codec reports srcsize=0\n");
			return(1);
		}

    	ret = acmStreamSize(handle, srcsize, &destsize, ACM_STREAMSIZEF_SOURCE);
		if (ret) {
			printf("acmStreamSize error\n");
			return(1);
		}
	}

	// set up conversion buffers

	memset(&header, 0, sizeof(header));
	header.cbStruct    = sizeof(header);
	header.cbSrcLength = srcsize;
	header.pbSrc       = xmalloc(header.cbSrcLength);
	header.cbDstLength = destsize;
	header.pbDst       = xmalloc(header.cbDstLength);
  
	ret = acmStreamPrepareHeader(handle, &header, 0);
	if (ret) {
		printf("error preparing header\n");
		return(1);
	}
	
	return(0);
}


/* close streaming */
int uninit_stream() {
	ret = acmStreamUnprepareHeader(handle, &header, 0);
	if (ret) {
		printf("error unpreparing header\n");
		return(1);
	}

	free(header.pbSrc);
	free(header.pbDst);
  
    
	ret = acmStreamClose(handle, 0);
	if (ret) {
		printf("error closing acm stream\n");
		return(1);
	}

	free(ts_wf);
	return(0);
}


/* does the conversion in memory for us               */
/* returns length of destination buffer, 0 on failure */
int tsp_conversion(char *src, char *dst, int srclen, int dstlen) {
	int complen = 0;
	
	while (srclen >= srcsize) {
		memcpy(header.pbSrc, src, srcsize);
		src += srcsize;
		srclen -= srcsize;

		ret = acmStreamConvert(handle, &header, ACM_STREAMCONVERTF_BLOCKALIGN);
		if (ret) {
			printf("conversion error\n");
			return(0);
		}

		if (!(header.fdwStatus & ACMSTREAMHEADER_STATUSF_DONE)) {
			printf("header not marked done %lx\n", (long unsigned int)header.fdwStatus);
			return(0);
		}

		if (header.cbSrcLengthUsed != header.cbSrcLength) {
			printf("didn't use all of source data\n");
			return(0);
		}

		// everything ok, copy data
		if (dstlen >= header.cbDstLengthUsed) {
			memcpy(dst, header.pbDst, header.cbDstLengthUsed);
			dst += header.cbDstLengthUsed;
			dstlen -= header.cbDstLengthUsed;
			complen += header.cbDstLengthUsed;
		}
		else {
			printf("Destination buffer for conversion is too small! %i\n", header.cbDstLengthUsed);
			return(0);
		}
	}

	// now convert the remaining bit of the file
  	// and ask for any leftover stuff
/*
	header.cbSrcLength = srclen;
  	while (1) {
		if (header.cbSrcLength) {
			// not a full block, but not quite the "end" either
			memcpy(header.pbSrc, src, header.cbSrcLength);
			ret = acmStreamConvert(handle, &header, 0);
		}
		else {
			// now we're at the end, let's see if anything is left
			ret = acmStreamConvert(handle, &header, ACM_STREAMCONVERTF_END);
		}
    
		if (ret) {
			printf("conversion error\n");
			return(0);
		}
    
		if (!(header.fdwStatus & ACMSTREAMHEADER_STATUSF_DONE)) {
			printf("header not marked done %lx\n", header.fdwStatus);
			return(0);
		}
    
		if (header.cbSrcLengthUsed != header.cbSrcLength) {
			printf("didn't use all of source data\n");
			return(0);
		}

		if (!header.cbDstLengthUsed) {
			printf("nothing given to output, must be done\n");
			break;
		}
    
		if (dstlen >= header.cbDstLengthUsed) {
			memcpy(dst, header.pbDst, header.cbDstLengthUsed);
			dst += header.cbDstLengthUsed;
			dstlen -= header.cbDstLengthUsed;
			complen += header.cbDstLengthUsed;
		}
		else {
			printf("Destination buffer for conversion is too small!\n");
			return(0);
		}

		// ensure we are getting the remaining stuff on the next time around
		header.cbSrcLength = 0;
	}
*/
	return(complen);
}


/* simple test */
int test() {
	FILE *fi;
	FILE *fo;
	char *srcbuf;
	char *dstbuf;
	int dstlen;
	
	srcbuf = malloc(6880);
	dstbuf = malloc(6880*20);
	fi = fopen("truespeechout.raw", "rb");
	fread(srcbuf, 1, 6880, fi);
	fclose(fi);
	
	//----
	// init_stream(0): TSP->PCM, init_stream(1): PCM->TSP
	if (init_tsp()) return(0);			// nur beim voiceinit
	if (init_stream(0)) return(0);		// beim wechsel von record<->play nötig !
	
	dstlen = tsp_conversion(srcbuf, dstbuf, 6880, 6880*20);	
	
	uninit_stream();
//	uninit_tsp();
	//----
	
	if (dstlen) {
		fo = fopen("hdho_test1.raw", "wb");
		fwrite(dstbuf, 1, dstlen, fo);
		fclose(fo);
	}
		
	free(srcbuf);
	free(dstbuf);


	srcbuf = malloc(103540);
	dstbuf = malloc(7000);
	fi = fopen("pcmin.raw", "rb");
	fread(srcbuf, 1, 103540, fi);
	fclose(fi);
	
	//----
	// init_stream(0): TSP->PCM, init_stream(1): PCM->TSP
//	if (init_tsp()) return(0);			// nur beim voiceinit
	if (init_stream(1)) return(0);		// beim wechsel von record<->play nötig !
	
	dstlen = tsp_conversion(srcbuf, dstbuf, 103540, 7000);	
	
	uninit_stream();
	uninit_tsp();
	//----
	
	if (dstlen) {
		fo = fopen("hdho_test2.raw", "wb");
		fwrite(dstbuf, 1, dstlen, fo);
		fclose(fo);
	}
		
	free(srcbuf);
	free(dstbuf);
	return(0);
}
