/******************************************************************************
\file asihpirec.c

Commandline play/record application using HPI

Copyright (C) 1997-2017 AudioScience, Inc. All rights reserved.

This software is provided 'as-is', without any express or implied warranty.
In no event will AudioScience Inc. be held liable for any damages arising
from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software
   in a product, an acknowledgment in the product documentation would be
   appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.
3. This copyright notice and list of conditions may not be altered or removed
   from any source distribution.

AudioScience, Inc. <support@audioscience.com>

( This license is GPL compatible see http://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses )

******************************************************************************/
#define SECS_TO_MILLISECS 1000

#ifdef ASIHPIPLAY
	#define PROGNAME "asihpiplay"
	static int isRec = 0;
#elif defined ASIHPIREC
	#define PROGNAME "asihpirec"
	static int isRec = 1;
#else
	#error must define ASIHPIPLAY or ASIHPIREC when building
#endif

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <math.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <getopt.h>
#include <hpi.h>
#include <hpi_version.h>
#include <hpidebug.h>

/*---------------------------------------------------------------------------*/
static struct hpi_hsubsys *hSubSys;
static hpi_handle_t hMixer = 0;
static hpi_handle_t hOutStream;
static hpi_handle_t hInStreams[4];
static struct hpi_format hFormat;
static unsigned int bufsize;

// local protos
static void sigintHandler(int sig);

static void _HandleError(
	uint16_t err,
	const char *filename,
	const int linenum
);

#define HandleError( err ) if((err)) _HandleError( (err), __FILE__, __LINE__ )

#define HandleErrorAndExit( err ) if((err)) { _HandleError( (err), __FILE__, __LINE__ ); sigintHandler(0); }

// global
#define BLOCK_SIZE 32768
static uint8_t abBuffer[BLOCK_SIZE];

#define MAX_LINKED 4
/* Option variables */
static int stream_use_bbm = 1;
static int runtime = -1;		//run forever
static unsigned int stream_num = 0;	// default to stream 0
static uint16_t wAdapterIndex = 0;		// default to adapter 0
static unsigned int samplerate = 48000;
static unsigned int channels = 2;
static unsigned int bitrate = 128000;
static unsigned int format = HPI_FORMAT_PCM16_SIGNED;
static int looping = 0;
static int verbose = 0;
static int setClock = 0;
static int nLinked = 1;

#define MAX_PATH 256
static char szFiles[MAX_LINKED][MAX_PATH] = {"-","-","-","-",};
static FILE *hFiles[MAX_LINKED];

static struct option long_options[] = {
	{"adapter", required_argument, 0, 'a'},
	{"stream-number", required_argument, 0, 'n'},
	{"samplerate", required_argument, 0, 's'},
	{"channels", required_argument, 0, 'c'},
	{"format", required_argument, 0, 'f'},
	{"bitrate", required_argument, 0, 'b'},
	{"set-clock", no_argument, 0, 'g'},
	{"runtime", required_argument, 0, 'r'},
	{"loop", no_argument, 0, 'l'},
	{"verbose", no_argument, 0, 'v'},
	{"disable-bbm", no_argument, 0, 'd'},
	{"version", no_argument, 0, 'V'},
	{"help", no_argument, 0, 'h'},
	{0, 0, 0, 0}
};

static const char *short_options = "a:b:c:df:ghln:r:s:vV?";

static const char *option_help[] = {
	"<adapter number> to test. Default is 0.",
	"<n> stream # to open. Default is 0.",
	"<Hz> samplerate. Default is 48000",
	"<c> channels to play or record. Default is 2.",
	"<f> format index 2=PCM16,4=MP2,5=MP3 14=F32 (see hpi.h). Default is 2.",
	"<r> bitrate for MP2 or MP3",
	"Set adapter local clock to samplerate",
	"<runtime> in seconds.",
	"Loop file (play only).",
	"Verbose info display",
	"Disable use of background bus mastering.",
	"Show the version of this program",
	"Show this text."
};

#ifdef _MSC_VER
static void poll_delay(int millisecs)
{
	Sleep(millisecs);
}
#else
#include <time.h>
/*---------------------------------------------------------------------------*/
#define MILLI_TO_NANO 1000000
static void poll_delay(int millisecs)
{
	struct timespec poll_millisecs;

	poll_millisecs.tv_sec = 0;
	poll_millisecs.tv_nsec = millisecs * MILLI_TO_NANO;
	nanosleep(&poll_millisecs, 0);
}
#endif

/*---------------------------------------------------------------------------*/
static void help(void)
{
	int i = 0;
	fprintf(stderr, "\nUsage: " PROGNAME " [options] [file]...\n");
	fprintf(stderr, "Play audio using the HPI API.\n\n");

	while (long_options[i].name != 0) {
		fprintf(stderr, "  --%s -%c %s\n",
			long_options[i].name,
			(char)(long_options[i].val), option_help[i]);
		i++;
	}
	exit(0);
}

/*---------------------------------------------------------------------------*/
static void parse_options(
	int argc,
	char *argv[]
)
{
	int c;
    /*********** Parse the command line options ***************/
	while (1) {
		//int this_option_optind = optind ? optind : 1;
		int option_index = 0;

		c = getopt_long(argc, argv, short_options,
			long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 0:
			fprintf(stderr, "option %s",
				long_options[option_index].name);
			if (optarg)
				fprintf(stderr, " with arg %s", optarg);
			fprintf(stderr, "\n");
			break;
		case 'a':
			wAdapterIndex = atoi(optarg);
			break;
		case 'b':
			bitrate = atoi(optarg);
			break;
		case 'c':
			channels = atoi(optarg);
			break;
		case 'd':
			stream_use_bbm = 0;
			break;
		case 'f':
			format = atoi(optarg);
			break;
		case 'g':
			setClock = 1;
			break;
		case 'l':
			if (!isRec)
				looping = 1;
			break;
		case 'n':
			stream_num = atoi(optarg);
			break;
		case 'r':
			runtime = atoi(optarg) ;
			break;
		case 's':
			samplerate = atoi(optarg);
			break;
		case 'v':
			verbose = 1;
			break;
		case 'V':
			fprintf(stderr, PROGNAME " version " HPI_VER_STRING "\n");
			exit(0);
			break;
		case '?':
		case 'h':
			help();
			break;

		default:
			fprintf(stderr,
				"?? getopt returned character code 0%o ??\n",
				c);
		}
	}

	if (optind < argc) {
		int i=0;
		while ((optind < argc) && (i < MAX_LINKED)) {
			strncpy(szFiles[i], argv[optind++], MAX_PATH - 1);
			i++;
		}
		nLinked =i;
	}

}

static char *state_names[] = {
	"invalid",
	"stopped",
	"play",
	"rec",
	"drained",
	"sine"
};

/*---------------------------------------------------------------------------*/
static int doRec(
	struct hpi_hsubsys *hSubSys,
	hpi_handle_t hStreams[MAX_LINKED],
	unsigned int bufsize,
	FILE * hOutFile[MAX_LINKED]
)
{
	uint16_t err = 0;
	uint16_t wState = HPI_STATE_STOPPED;
	uint32_t dwDataToPlay, dwSamplesRecorded, dwAuxData;
	int i;

	for (i = 0; i < nLinked; i++) {

		//get state of in stream
		err = HPI_InStreamGetInfoEx(hSubSys,
			hStreams[i],
			&wState, NULL, &dwDataToPlay, &dwSamplesRecorded, &dwAuxData);
		HandleError(err);

		if (verbose) {
			char *sn = state_names[0];
			if (wState <= HPI_STATE_SINEGEN)
				sn = state_names[wState];
			fprintf(stderr, "%d %s %8d %8d %8d\n", i, sn, dwSamplesRecorded,
				dwDataToPlay, dwAuxData);
		}

		if (dwDataToPlay >= bufsize) {
			err = HPI_InStreamReadBuf(hSubSys, hStreams[i], abBuffer,
				bufsize);
			HandleError(err);

			if (!err) {
				fwrite(abBuffer, bufsize, 1, hOutFile[i]);
			}
		} else if ((wState == HPI_STATE_STOPPED) && dwDataToPlay) {
			err = HPI_InStreamReadBuf(hSubSys, hStreams[i], abBuffer,
				dwDataToPlay);
			HandleError(err);

			if (!err) {
				fwrite(abBuffer, dwDataToPlay, 1, hOutFile[i]);
			}
		}
	}

	return (err || ((wState == HPI_STATE_STOPPED) && !dwDataToPlay));
}

/*---------------------------------------------------------------------------*/
/** Rewind file, then skip ID3 header if found */
static int rewindPlay(FILE * f)
{
  	uint32_t header;
	uint32_t offset = 0;
	size_t nread;

	rewind(f);

#if 1
	nread = fread(&header, 1, sizeof(header), f);

	if (nread < sizeof(header))
		return -1;

	if ((header & 0x00FFFFFF) == 0x00334449L) // "id3_"
	{	// just do enough to be able to skip the id3_ data
		unsigned char  id3_flags;

		nread = fread(&id3_flags, 1, sizeof(id3_flags), f);	// skip minor version
		nread = fread(&id3_flags, 1, sizeof(id3_flags), f);
		nread = fread(&offset, 1, sizeof(offset), f);
		if (nread < sizeof(offset))
			return -1;

		// convert from bytesafe, MSB first
		offset = ((offset & 0x0000007FL)<< 20) + ((offset & 0x00007F00L)<< 6) +
						((offset & 0x007F0000L) >> 9) + ((offset & 0x7F000000L)>> 24);
		offset += 10; 	// add in size of header itself
		if (id3_flags & 0x10)
			offset += 10;  // footer present

		printf("Skipping %d byte ID3 header\n", offset);
	}
	fseek(f, offset, SEEK_SET);
#endif
	return 0;
}
/*---------------------------------------------------------------------------*/
static int doPlay(
	struct hpi_hsubsys *hSubSys,
	hpi_handle_t hStreams[MAX_LINKED],
	struct hpi_format *hpiFormat,
	unsigned int dwBufSizeW,
	FILE * hInFiles[MAX_LINKED]
)
{
	uint16_t err = 0;
	uint16_t wState = HPI_STATE_DRAINED;
	uint32_t dwDataToPlay;
	uint32_t dwStrBufferSize;
	uint32_t dwSamplesPlayed, dwAuxData;
	size_t bytesRead;
	int i;

	for (i = 0; i < nLinked; i++) {
		//get state of stream
		err = HPI_OutStreamGetInfoEx(hSubSys,
			hStreams[i],
			&wState,
			&dwStrBufferSize,
			&dwDataToPlay, &dwSamplesPlayed, &dwAuxData);
		HandleError(err);

		if (verbose) {
			char *sn = state_names[0];
			if (wState <= HPI_STATE_SINEGEN)
				sn = state_names[wState];
			fprintf(stderr, "%d %s %8d %8d %8d\n", i, sn, dwSamplesPlayed,
				dwDataToPlay, dwAuxData);
		}

		if (feof(hInFiles[i])) {
			if (wState != HPI_STATE_PLAYING) {
				// stopped or drained
				return 1;
			}
		} else if ((dwStrBufferSize - dwDataToPlay) > dwBufSizeW) {
			bytesRead = fread(abBuffer, 1, dwBufSizeW, hInFiles[i]);
			if (bytesRead) {
				err = HPI_OutStreamWriteBuf(hSubSys, hStreams[i],
					abBuffer, bytesRead, hpiFormat);
				HandleError(err);
			}
		}
	}
	return (err || wState == HPI_STATE_STOPPED);
}

/*---------------------------------------------------------------------------*/
static void startRec(
	struct hpi_hsubsys *hSubSys,
	uint16_t wAdapterIndex,
	unsigned int streamNum,
	unsigned int format,
	unsigned int channels,
	unsigned int samplerate,
	hpi_handle_t hStreams[MAX_LINKED],
	struct hpi_format *hpiFormat,
	unsigned int *bufSize)
{

	uint16_t err = 0;
	int i;

	err = HPI_FormatCreate(hpiFormat, channels, format, samplerate,
		bitrate, 0);
	HandleErrorAndExit(err);

	*bufSize = BLOCK_SIZE / 2;

	err = HPI_InStreamOpen(hSubSys, wAdapterIndex, streamNum, &hStreams[0]);
	HandleErrorAndExit(err);

	err = HPI_InStreamQueryFormat(hSubSys, hStreams[0], hpiFormat);
	HandleErrorAndExit(err);

	err = HPI_InStreamClose(hSubSys, hStreams[0]);
	HandleErrorAndExit(err);

	for (i=0; i< nLinked; i++) {

		err = HPI_InStreamOpen(hSubSys, wAdapterIndex, streamNum + i, &hStreams[i]);
		HandleErrorAndExit(err);


		if (stream_use_bbm) {
			err = HPI_InStreamHostBufferAllocate(hSubSys, hStreams[i],
				BLOCK_SIZE);
			if (err == HPI_ERROR_INVALID_FUNC)
				stream_use_bbm = 0;
			else
				HandleErrorAndExit(err);
		}

		err = HPI_InStreamReset(hSubSys, hStreams[i]);
		HandleErrorAndExit(err);

		err = HPI_InStreamSetFormat(hSubSys, hStreams[i], hpiFormat);
		HandleErrorAndExit(err);


		if (i > 0) {
			if (verbose)
				printf("Linking stream %d to master stream %d\n",
					streamNum + i, streamNum);
			err = HPI_InStreamGroupAdd(hSubSys, hStreams[0], hStreams[i]);
			HandleError(err);
		}
	}

	err = HPI_InStreamStart(hSubSys, hStreams[0]);

	HandleErrorAndExit(err);
}

/*---------------------------------------------------------------------------*/
static void startPlay(
	struct hpi_hsubsys *hSubSys,
	uint16_t wAdapterIndex,
	unsigned int streamNum,
	unsigned int format,
	unsigned int channels,
	unsigned int samplerate,
	hpi_handle_t hStreams[MAX_LINKED],
	struct hpi_format *hpiFormat,
	unsigned int *bufsize,
	FILE * hOutFiles[MAX_LINKED]
)
{
	uint16_t err = 0;
	int i;

	// setup format and size of data block
	err = HPI_FormatCreate(hpiFormat, channels, format, samplerate,
		bitrate, 0);
	HandleErrorAndExit(err);

	err = HPI_OutStreamOpen(hSubSys, wAdapterIndex, streamNum, &hStreams[0]);
		HandleErrorAndExit(err);

	//check first available open stream to make sure we can play this format
	err = HPI_OutStreamQueryFormat(hSubSys, hStreams[0], hpiFormat);
	HandleErrorAndExit(err);

	err = HPI_OutStreamClose(hSubSys, hStreams[0]);

	for (i = 0; i < nLinked; i++) {

		err = HPI_OutStreamOpen(hSubSys, wAdapterIndex, streamNum + i, &hStreams[i]);
		HandleErrorAndExit(err);

		if (stream_use_bbm) {
			err = HPI_OutStreamHostBufferAllocate(hSubSys, hStreams[i],
				BLOCK_SIZE);
			if (err == HPI_ERROR_INVALID_FUNC)
				stream_use_bbm = 0;
			else
				HandleErrorAndExit(err);
		}

		err = HPI_OutStreamReset(hSubSys, hStreams[i]);
		HandleErrorAndExit(err);

		if (i > 0 ) {
			if (verbose)
				printf("Linking stream %d to master stream %d\n",
					streamNum + i, streamNum);
			err = HPI_OutStreamGroupAdd(hSubSys, hStreams[0], hStreams[i]);
			HandleError(err);
		}

		*bufsize = BLOCK_SIZE / 2;

	}
	//prefetch
	for (i = 0; i < nLinked; i++)
		rewindPlay(hOutFiles[i]);
	doPlay(hSubSys, hStreams, hpiFormat, *bufsize, hOutFiles);
	if (nLinked > 1)
		poll_delay(100); // allow buffer xfers to take place

	err = HPI_OutStreamStart(hSubSys, hStreams[0]);
	HandleErrorAndExit(err);
}

/*---------------------------------------------------------------------------*/
static void sigintHandler(int sig)
{
	uint16_t err = 0;

	fprintf(stderr, "Terminating.\n");

	if (!hSubSys)
		return;

	if (!isRec) {

		err = HPI_OutStreamStop(hSubSys, hOutStream);
		HandleError(err);
		//drain the buffer
		while (!doPlay(hSubSys, &hOutStream, &hFormat, bufsize,
				hFiles)) {
		}

		err = HPI_OutStreamClose(hSubSys, hOutStream);
		HandleError(err);

		if (stream_use_bbm)
			err = HPI_OutStreamHostBufferFree(hSubSys,
				hOutStream);

	} else {
		int i;
		for (i=0; i< nLinked; i++) {
			err = HPI_InStreamStop(hSubSys, hInStreams[i]);
			HandleError(err);
		}

		//drain the buffer
		while (!doRec(hSubSys, hInStreams, bufsize, hFiles)) {
		}

		for (i=0; i< nLinked; i++) {
			err = HPI_InStreamClose(hSubSys, hInStreams[i]);
			HandleError(err);

			if (stream_use_bbm)
				err = HPI_InStreamHostBufferFree(hSubSys, hInStreams[i]);
			if (hFiles[i])
				fclose(hFiles[i]);
		}
	}


	if (hMixer) {
		err = HPI_MixerClose(hSubSys, hMixer);
		HandleError(err);
	}

	err = HPI_AdapterClose(hSubSys, wAdapterIndex);

	HPI_SubSysFree(hSubSys);
	exit(0);
}

/************************************** MAIN ***********************/
int main(int argc,	char *argv[])
{
	uint16_t err = 0;		// HPI error
	uint32_t version = 0;

	int numAdapters;
	uint16_t wVersion;
	uint32_t dwSerialNumber;
	uint16_t wType;
	uint16_t wNumOutStreams;
	uint16_t wNumInStreams;
	uint16_t prop1, prop2;
	hpi_handle_t hControl;
	int i;

	int poll_millisecs;
	int bytes_per_sample;
#ifndef _MSC_VER
	time_t start_time;
#endif

	parse_options(argc, argv);

	signal(SIGINT, sigintHandler);

	for (i=0; i<nLinked; i++)
	if (isRec) {
		if (strcmp("-", szFiles[i]) == 0)
			hFiles[i] = fdopen(fileno(stdout), "wb");
		else
			hFiles[i] = fopen(szFiles[i], "w+b");
	} else {
		if (strcmp("-", szFiles[i]) == 0)
			hFiles[i] = fdopen(fileno(stdin), "rb");
		else
			hFiles[i] = fopen(szFiles[i], "rb");
	}

	// open subsystem and find adapters
	hSubSys = HPI_SubSysCreate();
	if (hSubSys == NULL) {
		fprintf(stderr, "hSubSys==NULL\n");
		exit(1);
	}

	err = HPI_SubSysGetVersionEx(hSubSys, &version);
	HandleErrorAndExit(err);
	fprintf(stderr, "SubSys version=%x\n", version);

	err = HPI_SubSysGetNumAdapters(hSubSys, &numAdapters);
	HandleErrorAndExit(err);
	fprintf(stderr, "Found %d adapters\n", numAdapters);


	err = HPI_AdapterOpen(hSubSys, wAdapterIndex);
	HandleErrorAndExit(err);

	err = HPI_AdapterGetInfo(hSubSys,
		wAdapterIndex,
		&wNumOutStreams, &wNumInStreams,
		&wVersion, &dwSerialNumber, &wType);
	HandleErrorAndExit(err);

	err = HPI_AdapterGetProperty(hSubSys, wAdapterIndex, HPI_ADAPTER_PROPERTY_SOFTWARE_VERSION, &prop1, &prop2);
	if (err)
		version = (((wVersion >> 13) * 100) + ((wVersion >> 7) & 0x3f)) * 100;
	else
		version = (prop1 >> 8) * 10000 + (prop1 & 0xFF) * 100 + (prop2 & 0xFF);

	fprintf(stderr, "Using adapter ID=%4X Index=%d NumOutStreams=%d NumInStreams=%d S/N=%d\nHw Version %c%d DSP code version %03d\n",
			wType, wAdapterIndex, wNumOutStreams, wNumInStreams,
			dwSerialNumber,
			((wVersion >> 3) & 0xf) + 'A',	// Hw version major
			wVersion & 0x7,	// Hw version minor
			version
		);
/*
    //HPI doesn't come back with an error if we try to use a non existing stream, it crashes instead
    if ( ( !isRec && stream_num >= wNumOutStreams ) || ( isRec && stream_num >= wNumInStreams ) )
        HandleErrorAndExit( HPI_ERROR_INVALID_STREAM );
*/
	// open the mixer of this adapter
	err = HPI_MixerOpen(hSubSys, wAdapterIndex, &hMixer);

	HandleErrorAndExit(err);

	err = HPI_MixerGetControl(hSubSys, hMixer,
		HPI_SOURCENODE_CLOCK_SOURCE, 0, 0, 0,
		HPI_CONTROL_SAMPLECLOCK, &hControl);

	if (!err && setClock) {
		fprintf(stderr, "Setting local clock rate to %d\n",
			samplerate);
		err = HPI_SampleClock_SetLocalRate(hSubSys, hControl,
			samplerate);
		HandleErrorAndExit(err);
	}

	fprintf(stderr,
		"%s stream %d, format %d, channels %d, samplerate %d\n",
		isRec ? "Recording from" : "Playing to", stream_num, format,
		channels, samplerate);
	if (isRec) {
		startRec(hSubSys, wAdapterIndex, stream_num, format, channels,
			samplerate, hInStreams, &hFormat, &bufsize);
	} else {
		if (looping)
			fprintf(stderr, "Looping forever ctrl-C to exit\n");
		startPlay(hSubSys, wAdapterIndex, stream_num, format,
			channels, samplerate, &hOutStream, &hFormat, &bufsize,
			hFiles);
	}

	switch (format) {
	case HPI_FORMAT_PCM8_UNSIGNED:
		bytes_per_sample = 1;
		break;
	case HPI_FORMAT_PCM16_SIGNED:
	case HPI_FORMAT_PCM16_BIGENDIAN:
		bytes_per_sample = 2;
		break;
	case HPI_FORMAT_PCM24_SIGNED:
		bytes_per_sample = 3;
		break;
	default:		/* very conservative for MPEG, but never mind */
		bytes_per_sample = 4;
		break;
	}

	poll_millisecs =
		(BLOCK_SIZE * 1000) / (samplerate * channels *
		bytes_per_sample);
	poll_millisecs /= 4;	// one quarter of the block size

	// convert from millisecs to number of poll loops
#ifdef _MSC_VER
	runtime = runtime * SECS_TO_MILLISECS / poll_millisecs;
#else
	start_time = time(NULL);
#endif

	while (1) {
		if (runtime != -1)
#ifdef _MSC_VER
			if (!--runtime)
				break;
#else
			if (difftime(time(NULL) , start_time) > runtime)
				break;
#endif
		poll_delay(poll_millisecs);
		if (isRec) {
			if (doRec(hSubSys, hInStreams, bufsize, hFiles))
				break;
		} else {
			if (doPlay(hSubSys, &hOutStream, &hFormat, bufsize,
					hFiles)) {
				if (looping) {
					for (i = 0; i < nLinked; i++)
						rewindPlay(hFiles[i]);
					continue;
				}
				break;
			}
		}
	}

	sigintHandler(0);
	return 0;
}

/****************************** HandleError **********************/
static void _HandleError(
	uint16_t err,
	const char *filename,
	const int linenum
)
{
	char szError[256];

	HPI_GetErrorText(err, szError);
	fprintf(stderr, "%s %d HPI ERROR %d %s\n", filename, linenum, err,
		szError);
}
