/* $Id$ */
/*
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 *
 * 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
 */
#include "systest.h"
#include "gui.h"

#define THIS_FILE "systest.c"

unsigned    test_item_count;
test_item_t test_items[SYSTEST_MAX_TEST];
char	    doc_path[PATH_LENGTH] = {0};
char	    res_path[PATH_LENGTH] = {0};
char	    fpath[PATH_LENGTH];

#define USER_ERROR  "User used said not okay"

static void systest_wizard(void);
static void systest_list_audio_devs(void);
static void systest_display_settings(void);
static void systest_play_tone(void);
static void systest_play_wav1(void);
static void systest_play_wav2(void);
static void systest_rec_audio(void);
static void systest_audio_test(void);
static void systest_latency_test(void);
static void systest_aec_test(void);
static void exit_app(void);

/* Menus */
static gui_menu menu_exit = { "Exit", &exit_app };

static gui_menu menu_wizard =  { "Run test wizard", &systest_wizard };
static gui_menu menu_playtn = { "Play Tone", &systest_play_tone };
static gui_menu menu_playwv1 = { "Play WAV File1", &systest_play_wav1 };
static gui_menu menu_playwv2 = { "Play WAV File2", &systest_play_wav2 };
static gui_menu menu_recaud  = { "Record Audio", &systest_rec_audio };
static gui_menu menu_audtest = { "Device Test", &systest_audio_test };
static gui_menu menu_calclat = { "Latency Test", &systest_latency_test };
static gui_menu menu_sndaec = { "AEC/AES Test", &systest_aec_test };

static gui_menu menu_listdev = { "View Devices", &systest_list_audio_devs };
static gui_menu menu_getsets = { "View Settings", &systest_display_settings };

static gui_menu menu_tests = {
    "Tests", NULL,
    10,
    {
	&menu_wizard,
	&menu_audtest,
	&menu_playtn,
	&menu_playwv1,
	&menu_playwv2,
	&menu_recaud,
	&menu_calclat,
	&menu_sndaec,
	NULL,
	&menu_exit
    }
};

static gui_menu menu_options = {
    "Options", NULL,
    2,
    {
	&menu_listdev,
	&menu_getsets,
    }
};

static gui_menu root_menu = {
    "Root", NULL, 2, {&menu_tests, &menu_options}
};

/*****************************************************************/

#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
PJ_INLINE(char *) add_path(const char *path, const char *fname)
{
    strncpy(fpath, path, PATH_LENGTH);
    strncat(fpath, fname, PATH_LENGTH);
    return fpath;
}
#else
#   define add_path(path, fname) fname
#endif

static void exit_app(void)
{
    systest_save_result(add_path(doc_path, RESULT_OUT_PATH));
    gui_destroy();
}


#include <pjsua-lib/pjsua.h>
#include <pjmedia_audiodev.h>

typedef struct systest_t
{
    pjsua_config	    ua_cfg;
    pjsua_media_config	    media_cfg;
    pjmedia_aud_dev_index   rec_id;
    pjmedia_aud_dev_index   play_id;
} systest_t;

static systest_t systest;
static char textbuf[600];

/* Device ID to test */
int systest_cap_dev_id = PJMEDIA_AUD_DEFAULT_CAPTURE_DEV;
int systest_play_dev_id = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV;

static void systest_perror(const char *title, pj_status_t status)
{
    char errmsg[PJ_ERR_MSG_SIZE];
    char themsg[PJ_ERR_MSG_SIZE + 100];

    if (status != PJ_SUCCESS)
	pj_strerror(status, errmsg, sizeof(errmsg));
    else
	errmsg[0] = '\0';

    strcpy(themsg, title);
    strncat(themsg, errmsg, sizeof(themsg)-1);
    themsg[sizeof(themsg)-1] = '\0';

    gui_msgbox("Error", themsg, WITH_OK);
}

test_item_t *systest_alloc_test_item(const char *title)
{
    test_item_t *ti;

    if (test_item_count == SYSTEST_MAX_TEST) {
	gui_msgbox("Error", "You have done too many tests", WITH_OK);
	return NULL;
    }

    ti = &test_items[test_item_count++];
    pj_bzero(ti, sizeof(*ti));
    pj_ansi_strcpy(ti->title, title);

    return ti;
}

/*****************************************************************************
 * test: play simple ringback tone and hear it
 */
static void systest_play_tone(void)
{
    /* Ringtones  */
    #define RINGBACK_FREQ1	    440	    /* 400 */
    #define RINGBACK_FREQ2	    480	    /* 450 */
    #define RINGBACK_ON		    3000    /* 400 */
    #define RINGBACK_OFF	    4000    /* 200 */
    #define RINGBACK_CNT	    1	    /* 2   */
    #define RINGBACK_INTERVAL	    4000    /* 2000 */

    unsigned i, samples_per_frame;
    pjmedia_tone_desc tone[RINGBACK_CNT];
    pj_pool_t *pool = NULL;
    pjmedia_port *ringback_port = NULL;
    enum gui_key key;
    int ringback_slot = -1;
    test_item_t *ti;
    pj_str_t name;
    const char *title = "Audio Tone Playback Test";
    pj_status_t status;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    key = gui_msgbox(title,
		     "This test will play simple ringback tone to "
		     "the speaker. Please listen carefully for audio "
		     "impairments such as stutter. You may need "
		     "to let this test running for a while to "
		     "make sure that everything is okay. Press "
		     "OK to start, CANCEL to skip",
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    pool = pjsua_pool_create("ringback", 512, 512);
    samples_per_frame = systest.media_cfg.audio_frame_ptime *
			systest.media_cfg.clock_rate *
			systest.media_cfg.channel_count / 1000;

    /* Ringback tone (call is ringing) */
    name = pj_str("ringback");
    status = pjmedia_tonegen_create2(pool, &name,
				     systest.media_cfg.clock_rate,
				     systest.media_cfg.channel_count,
				     samples_per_frame,
				     16, PJMEDIA_TONEGEN_LOOP,
				     &ringback_port);
    if (status != PJ_SUCCESS)
	goto on_return;

    pj_bzero(&tone, sizeof(tone));
    for (i=0; i<RINGBACK_CNT; ++i) {
	tone[i].freq1 = RINGBACK_FREQ1;
	tone[i].freq2 = RINGBACK_FREQ2;
	tone[i].on_msec = RINGBACK_ON;
	tone[i].off_msec = RINGBACK_OFF;
    }
    tone[RINGBACK_CNT-1].off_msec = RINGBACK_INTERVAL;

    status = pjmedia_tonegen_play(ringback_port, RINGBACK_CNT, tone,
				  PJMEDIA_TONEGEN_LOOP);
    if (status != PJ_SUCCESS)
	goto on_return;

    status = pjsua_conf_add_port(pool, ringback_port, &ringback_slot);
    if (status != PJ_SUCCESS)
	goto on_return;

    status = pjsua_conf_connect(ringback_slot, 0);
    if (status != PJ_SUCCESS)
	goto on_return;

    key = gui_msgbox(title,
		     "Ringback tone should be playing now in the "
		     "speaker. Press OK to stop. ", WITH_OK);

    status = PJ_SUCCESS;

on_return:
    if (ringback_slot != -1)
	pjsua_conf_remove_port(ringback_slot);
    if (ringback_port)
	pjmedia_port_destroy(ringback_port);
    if (pool)
	pj_pool_release(pool);

    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we encounter error when initializing "
		       "the tone generator: ", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
    } else {
	key = gui_msgbox(title, "Is the audio okay?", WITH_YESNO);
	ti->success = (key == KEY_YES);
	if (!ti->success)
	    pj_ansi_strcpy(ti->reason, USER_ERROR);
    }
    return;
}

/* Util: create file player, each time trying different paths until we get
 * the file.
 */
static pj_status_t create_player(unsigned path_cnt, const char *paths[],
				 pjsua_player_id *p_id)
{
    pj_str_t name;
    pj_status_t status = PJ_ENOTFOUND;
    unsigned i;

    for (i=0; i<path_cnt; ++i) {
	status = pjsua_player_create(pj_cstr(&name, paths[i]), 0, p_id);
	if (status == PJ_SUCCESS)
	    return PJ_SUCCESS;
    }
    return status;
}

/*****************************************************************************
 * test: play WAV file
 */
static void systest_play_wav(unsigned path_cnt, const char *paths[])
{
    pjsua_player_id play_id = PJSUA_INVALID_ID;
    enum gui_key key;
    test_item_t *ti;
    const char *title = "WAV File Playback Test";
    pj_status_t status;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    pj_ansi_snprintf(textbuf, sizeof(textbuf),
		     "This test will play %s file to "
		     "the speaker. Please listen carefully for audio "
		     "impairments such as stutter. Let this test run "
		     "for a while to make sure that everything is okay."
		     " Press OK to start, CANCEL to skip",
		     paths[0]);

    key = gui_msgbox(title, textbuf,
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    /* WAV port */
    status = create_player(path_cnt, paths, &play_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    status = pjsua_conf_connect(pjsua_player_get_conf_port(play_id), 0);
    if (status != PJ_SUCCESS)
	goto on_return;

    key = gui_msgbox(title,
		     "WAV file should be playing now in the "
		     "speaker. Press OK to stop. ", WITH_OK);

    status = PJ_SUCCESS;

on_return:
    if (play_id != -1)
	pjsua_player_destroy(play_id);

    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we've encountered error", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
    } else {
	key = gui_msgbox(title, "Is the audio okay?", WITH_YESNO);
	ti->success = (key == KEY_YES);
	if (!ti->success)
	    pj_ansi_strcpy(ti->reason, USER_ERROR);
    }
    return;
}

static void systest_play_wav1(void)
{
    const char *paths[] = { add_path(res_path, WAV_PLAYBACK_PATH),
			    ALT_PATH1 WAV_PLAYBACK_PATH };
    systest_play_wav(PJ_ARRAY_SIZE(paths), paths);
}

static void systest_play_wav2(void)
{
    const char *paths[] = { add_path(res_path, WAV_TOCK8_PATH),
			    ALT_PATH1 WAV_TOCK8_PATH};
    systest_play_wav(PJ_ARRAY_SIZE(paths), paths);
}


/*****************************************************************************
 * test: record audio
 */
static void systest_rec_audio(void)
{
    const pj_str_t filename = pj_str(add_path(doc_path, WAV_REC_OUT_PATH));
    pj_pool_t *pool = NULL;
    enum gui_key key;
    pjsua_recorder_id rec_id = PJSUA_INVALID_ID;
    pjsua_player_id play_id = PJSUA_INVALID_ID;
    pjsua_conf_port_id rec_slot = PJSUA_INVALID_ID;
    pjsua_conf_port_id play_slot = PJSUA_INVALID_ID;
    pj_status_t status = PJ_SUCCESS;
    const char *title = "Audio Recording";
    test_item_t *ti;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    key = gui_msgbox(title,
		     "This test will allow you to record audio "
		     "from the microphone, and playback the "
		     "audio to the speaker. Press OK to start recording, "
		     "CANCEL to skip.",
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    pool = pjsua_pool_create("rectest", 512, 512);

    status = pjsua_recorder_create(&filename, 0, NULL, -1, 0, &rec_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    rec_slot = pjsua_recorder_get_conf_port(rec_id);

    status = pjsua_conf_connect(0, rec_slot);
    if (status != PJ_SUCCESS)
	goto on_return;

    key = gui_msgbox(title,
		     "Recording is in progress now, please say "
		     "something in the microphone. Press OK "
		     "to stop recording", WITH_OK);

    pjsua_conf_disconnect(0, rec_slot);
    rec_slot = PJSUA_INVALID_ID;
    pjsua_recorder_destroy(rec_id);
    rec_id = PJSUA_INVALID_ID;

    status = pjsua_player_create(&filename, 0, &play_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    play_slot = pjsua_player_get_conf_port(play_id);

    status = pjsua_conf_connect(play_slot, 0);
    if (status != PJ_SUCCESS)
	goto on_return;

    key = gui_msgbox(title,
		     "Recording has been stopped. "
		     "The recorded audio is being played now to "
		     "the speaker device, in a loop. Listen for "
		     "any audio impairments. Press OK to stop.",
		     WITH_OK);

on_return:
    if (rec_slot != PJSUA_INVALID_ID)
	pjsua_conf_disconnect(0, rec_slot);
    if (rec_id != PJSUA_INVALID_ID)
	pjsua_recorder_destroy(rec_id);
    if (play_slot != PJSUA_INVALID_ID)
	pjsua_conf_disconnect(play_slot, 0);
    if (play_id != PJSUA_INVALID_ID)
	pjsua_player_destroy(play_id);
    if (pool)
	pj_pool_release(pool);

    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we encountered an error: ", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
    } else {
	key = gui_msgbox(title, "Is the audio okay?", WITH_YESNO);
	ti->success = (key == KEY_YES);
	if (!ti->success) {
	    pj_ansi_snprintf(textbuf, sizeof(textbuf),
			     "You will probably need to copy the recorded "
			     "WAV file %s to a desktop computer and analyze "
			     "it, to find out whether it's a recording "
			     "or playback problem.",
			     WAV_REC_OUT_PATH);
	    gui_msgbox(title, textbuf, WITH_OK);
	    pj_ansi_strcpy(ti->reason, USER_ERROR);
	}
    }
}


/****************************************************************************
 * test: audio system test
 */
static void systest_audio_test(void)
{
    enum {
	GOOD_MAX_INTERVAL = 5,
    };
    const pjmedia_dir dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
    pjmedia_aud_param param;
    pjmedia_aud_test_results result;
    pj_size_t textbufpos;
    enum gui_key key;
    unsigned problem_count = 0;
    const char *problems[16];
    char drifttext[120];
    test_item_t *ti;
    const char *title = "Audio Device Test";
    pj_status_t status;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    key = gui_msgbox(title,
		     "This will run an automated test for about "
		     "ten seconds or so, and display some "
		     "statistics about your sound device. "
		     "Please don't do anything until the test completes. "
		     "Press OK to start, or CANCEL to skip this test.",
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    /* Disable sound device in pjsua first */
    pjsua_set_no_snd_dev();

    /* Setup parameters */
    status = pjmedia_aud_dev_default_param(systest.play_id, &param);
    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we had error in pjmedia_aud_dev_default_param()", status);
	pjsua_set_snd_dev(systest.rec_id, systest.play_id);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
	ti->reason[sizeof(ti->reason)-1] = '\0';
	return;
    }

    param.dir = dir;
    param.rec_id = systest.rec_id;
    param.play_id = systest.play_id;
    param.clock_rate = systest.media_cfg.snd_clock_rate;
    param.channel_count = systest.media_cfg.channel_count;
    param.samples_per_frame = param.clock_rate * param.channel_count *
			      systest.media_cfg.audio_frame_ptime / 1000;

    /* Latency settings */
    param.flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY |
		    PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY);
    param.input_latency_ms = systest.media_cfg.snd_rec_latency;
    param.output_latency_ms = systest.media_cfg.snd_play_latency;

    /* Run the test */
    status = pjmedia_aud_test(&param, &result);
    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we encountered error with the test", status);
	pjsua_set_snd_dev(systest.rec_id, systest.play_id);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
	ti->reason[sizeof(ti->reason)-1] = '\0';
	return;
    }

    /* Restore pjsua sound device */
    pjsua_set_snd_dev(systest.rec_id, systest.play_id);

    /* Analyze the result! */
    strcpy(textbuf, "Here are the audio statistics:\r\n");
    textbufpos = strlen(textbuf);

    if (result.rec.frame_cnt==0) {
	problems[problem_count++] =
	    "No audio frames were captured from the microphone. "
	    "This means the audio device is not working properly.";
    } else {
	pj_ansi_snprintf(textbuf+textbufpos,
			 sizeof(textbuf)-textbufpos,
			 "Rec : interval (min/max/avg/dev)=\r\n"
			 "         %u/%u/%u/%u (ms)\r\n"
			 "      max burst=%u\r\n",
			 result.rec.min_interval,
			 result.rec.max_interval,
			 result.rec.avg_interval,
			 result.rec.dev_interval,
			 result.rec.max_burst);
	textbufpos = strlen(textbuf);

	if (result.rec.max_burst > GOOD_MAX_INTERVAL) {
	    problems[problem_count++] =
		"Recording max burst is quite high";
	}
    }

    if (result.play.frame_cnt==0) {
	problems[problem_count++] =
	    "No audio frames were played to the speaker. "
	    "This means the audio device is not working properly.";
    } else {
	pj_ansi_snprintf(textbuf+textbufpos,
			 sizeof(textbuf)-textbufpos,
			 "Play: interval (min/max/avg/dev)=\r\n"
			 "         %u/%u/%u/%u (ms)\r\n"
			 "      burst=%u\r\n",
			 result.play.min_interval,
			 result.play.max_interval,
			 result.play.avg_interval,
			 result.play.dev_interval,
			 result.play.max_burst);
	textbufpos = strlen(textbuf);

	if (result.play.max_burst > GOOD_MAX_INTERVAL) {
	    problems[problem_count++] =
		"Playback max burst is quite high";
	}
    }

    if (result.rec_drift_per_sec) {
	const char *which = result.rec_drift_per_sec>=0 ? "faster" : "slower";
	unsigned drift = result.rec_drift_per_sec>=0 ?
			    result.rec_drift_per_sec :
			    -result.rec_drift_per_sec;

	pj_ansi_snprintf(drifttext, sizeof(drifttext),
			"Clock drifts detected. Capture "
			"is %d samples/sec %s "
			"than the playback device",
			drift, which);
	problems[problem_count++] = drifttext;
    }

    if (problem_count == 0) {
	pj_ansi_snprintf(textbuf+textbufpos,
			 sizeof(textbuf)-textbufpos,
			 "\r\nThe sound device seems to be okay!");
	textbufpos = strlen(textbuf);

	key = gui_msgbox("Audio Device Test", textbuf, WITH_OK);
    } else {
	unsigned i;

	pj_ansi_snprintf(textbuf+textbufpos,
			 sizeof(textbuf)-textbufpos,
			 "There could be %d problem(s) with the "
			 "sound device:\r\n",
			 problem_count);
	textbufpos = strlen(textbuf);

	for (i=0; i<problem_count; ++i) {
	    pj_ansi_snprintf(textbuf+textbufpos,
			     sizeof(textbuf)-textbufpos,
			     " %d: %s\r\n", i+1, problems[i]);
	    textbufpos = strlen(textbuf);
	}

	key = gui_msgbox(title, textbuf, WITH_OK);
    }

    ti->success = PJ_TRUE;
    pj_ansi_strncpy(ti->reason, textbuf, sizeof(ti->reason));
    ti->reason[sizeof(ti->reason)-1] = '\0';
}


/****************************************************************************
 * sound latency test
 */
static int calculate_latency(pj_pool_t *pool, pjmedia_port *wav,
			     unsigned *lat_sum, unsigned *lat_cnt,
			     unsigned *lat_min, unsigned *lat_max)
{
    pjmedia_frame frm;
    short *buf;
    unsigned i, clock_rate, samples_per_frame;
    pj_size_t read, len;
    unsigned start_pos;
    pj_bool_t first;
    pj_status_t status;

    *lat_sum = 0;
    *lat_cnt = 0;
    *lat_min = 10000;
    *lat_max = 0;

    samples_per_frame = PJMEDIA_PIA_SPF(&wav->info);
    clock_rate = PJMEDIA_PIA_SRATE(&wav->info);
    frm.buf = pj_pool_alloc(pool, samples_per_frame * 2);
    frm.size = samples_per_frame * 2;
    len = pjmedia_wav_player_get_len(wav);
    buf = pj_pool_alloc(pool, len + samples_per_frame);

    /* Read the whole file */
    read = 0;
    while (read < len/2) {
	status = pjmedia_port_get_frame(wav, &frm);
	if (status != PJ_SUCCESS)
	    break;

	pjmedia_copy_samples(buf+read, (short*)frm.buf, samples_per_frame);
	read += samples_per_frame;
    }

    if (read < 2 * clock_rate) {
	systest_perror("The WAV file is too short", PJ_SUCCESS);
	return -1;
    }

    /* Zero the first 500ms to remove loud click noises
     * (keypad press, etc.)
     */
    pjmedia_zero_samples(buf, clock_rate / 2);

    /* Loop to calculate latency */
    start_pos = 0;
    first = PJ_TRUE;
    while (start_pos < len/2 - clock_rate) {
	int max_signal = 0;
	unsigned max_signal_pos = start_pos;
	unsigned max_echo_pos = 0;
	unsigned pos;
	unsigned lat;

	/* Get the largest signal in the next 0.7s */
	for (i=start_pos; i<start_pos + clock_rate * 700 / 1000; ++i) {
	    if (abs(buf[i]) > max_signal) {
		max_signal = abs(buf[i]);
		max_signal_pos = i;
	    }
	}

	/* Advance 10ms from max_signal_pos */
	pos = max_signal_pos + 10 * clock_rate / 1000;

	/* Get the largest signal in the next 800ms */
	max_signal = 0;
	max_echo_pos = pos;
	for (i=pos; i<pos+clock_rate * 8 / 10; ++i) {
	    if (abs(buf[i]) > max_signal) {
		max_signal = abs(buf[i]);
		max_echo_pos = i;
	    }
	}

	lat = (max_echo_pos - max_signal_pos) * 1000 / clock_rate;

#if 0
	PJ_LOG(4,(THIS_FILE, "Signal at %dms, echo at %d ms, latency %d ms",
		  max_signal_pos * 1000 / clock_rate,
		  max_echo_pos * 1000 / clock_rate,
		  lat));
#endif

	*lat_sum += lat;
	(*lat_cnt)++;
	if (lat < *lat_min)
	    *lat_min = lat;
	if (lat > *lat_max)
	    *lat_max = lat;

	/* Advance next loop */
	if (first) {
	    start_pos = max_signal_pos + clock_rate * 9 / 10;
	    first = PJ_FALSE;
	} else {
	    start_pos += clock_rate;
	}
    }

    return 0;
}


static void systest_latency_test(void)
{
    const char *ref_wav_paths[] = { add_path(res_path, WAV_TOCK8_PATH), ALT_PATH1 WAV_TOCK8_PATH };
    pj_str_t rec_wav_file;
    pjsua_player_id play_id = PJSUA_INVALID_ID;
    pjsua_conf_port_id play_slot = PJSUA_INVALID_ID;
    pjsua_recorder_id rec_id = PJSUA_INVALID_ID;
    pjsua_conf_port_id rec_slot = PJSUA_INVALID_ID;
    pj_pool_t *pool = NULL;
    pjmedia_port *wav_port = NULL;
    unsigned lat_sum=0, lat_cnt=0, lat_min=0, lat_max=0;
    enum gui_key key;
    test_item_t *ti;
    const char *title = "Audio Latency Test";
    pj_status_t status;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    key = gui_msgbox(title,
		     "This test will try to find the audio device's "
		     "latency. We will play a special WAV file to the "
		     "speaker for ten seconds, then at the end "
		     "calculate the latency. Please don't do anything "
		     "until the test is done.", WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }
    key = gui_msgbox(title,
		     "For this test to work, we must be able to capture "
		     "the audio played in the speaker (the echo), and only"
		     " that audio (i.e. you must be in relatively quiet "
		     "place to run this test). "
		     "Press OK to start, or CANCEL to skip.",
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    status = create_player(PJ_ARRAY_SIZE(ref_wav_paths), ref_wav_paths,
			   &play_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    play_slot = pjsua_player_get_conf_port(play_id);

    rec_wav_file = pj_str(add_path(doc_path, WAV_LATENCY_OUT_PATH));
    status = pjsua_recorder_create(&rec_wav_file, 0, NULL, -1, 0, &rec_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    rec_slot = pjsua_recorder_get_conf_port(rec_id);

    /* Setup the test */
    //status = pjsua_conf_connect(0, 0);
    status = pjsua_conf_connect(play_slot, 0);
    status = pjsua_conf_connect(0, rec_slot);
    status = pjsua_conf_connect(play_slot, rec_slot);


    /* We're running */
    PJ_LOG(3,(THIS_FILE, "Please wait while test is running (~10 sec)"));
    gui_sleep(10);

    /* Done with the test */
    //status = pjsua_conf_disconnect(0, 0);
    status = pjsua_conf_disconnect(play_slot, rec_slot);
    status = pjsua_conf_disconnect(0, rec_slot);
    status = pjsua_conf_disconnect(play_slot, 0);

    pjsua_recorder_destroy(rec_id);
    rec_id = PJSUA_INVALID_ID;

    pjsua_player_destroy(play_id);
    play_id = PJSUA_INVALID_ID;

    /* Confirm that echo is heard */
    gui_msgbox(title,
	       "Test is done. Now we need to confirm that we indeed "
	       "captured the echo. We will play the captured audio "
	       "and please confirm that you can hear the 'tock' echo.",
	       WITH_OK);

    status = pjsua_player_create(&rec_wav_file, 0, &play_id);
    if (status != PJ_SUCCESS)
	goto on_return;

    play_slot = pjsua_player_get_conf_port(play_id);

    status = pjsua_conf_connect(play_slot, 0);
    if (status != PJ_SUCCESS)
	goto on_return;

    key = gui_msgbox(title,
		     "The captured audio is being played back now. "
		     "Can you hear the 'tock' echo?",
		     WITH_YESNO);

    pjsua_player_destroy(play_id);
    play_id = PJSUA_INVALID_ID;

    if (key != KEY_YES)
	goto on_return;

    /* Now analyze the latency */
    pool = pjsua_pool_create("latency", 512, 512);

    status = pjmedia_wav_player_port_create(pool, rec_wav_file.ptr, 0, 0, 0, &wav_port);
    if (status != PJ_SUCCESS)
	goto on_return;

    status = calculate_latency(pool, wav_port, &lat_sum, &lat_cnt,
			       &lat_min, &lat_max);
    if (status != PJ_SUCCESS)
	goto on_return;

on_return:
    if (wav_port)
	pjmedia_port_destroy(wav_port);
    if (pool)
	pj_pool_release(pool);
    if (play_id != PJSUA_INVALID_ID)
	pjsua_player_destroy(play_id);
    if (rec_id != PJSUA_INVALID_ID)
	pjsua_recorder_destroy(rec_id);

    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we encountered an error: ", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
    } else if (key != KEY_YES) {
	ti->success = PJ_FALSE;
	if (!ti->success) {
	    pj_ansi_strcpy(ti->reason, USER_ERROR);
	}
    } else {
	char msg[200];
	pj_size_t msglen;

	pj_ansi_snprintf(msg, sizeof(msg),
			 "The sound device latency:\r\n"
			 " Min=%u, Max=%u, Avg=%u\r\n",
			 lat_min, lat_max, lat_sum/lat_cnt);
	msglen = strlen(msg);

	if (lat_sum/lat_cnt > 500) {
	    pj_ansi_snprintf(msg+msglen, sizeof(msg)-msglen,
			     "The latency is huge!\r\n");
	    msglen = strlen(msg);
	} else if (lat_sum/lat_cnt > 200) {
	    pj_ansi_snprintf(msg+msglen, sizeof(msg)-msglen,
			     "The latency is quite high\r\n");
	    msglen = strlen(msg);
	}

	key = gui_msgbox(title, msg, WITH_OK);

	ti->success = PJ_TRUE;
	pj_ansi_strncpy(ti->reason, msg, sizeof(ti->reason));
	ti->reason[sizeof(ti->reason)-1] = '\0';
    }
}


static void systest_aec_test(void)
{
    const char *ref_wav_paths[] = { add_path(res_path, WAV_PLAYBACK_PATH),
				    ALT_PATH1 WAV_PLAYBACK_PATH };
    pjsua_player_id player_id = PJSUA_INVALID_ID;
    pjsua_recorder_id writer_id = PJSUA_INVALID_ID;
    enum gui_key key;
    test_item_t *ti;
    const char *title = "AEC/AES Test";
    unsigned last_ec_tail = 0;
    pj_status_t status;
    pj_str_t tmp;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    key = gui_msgbox(title,
		     "This test will try to find whether the AEC/AES "
		     "works good on this system. Test will play a file "
		     "while recording from mic. The recording will be "
		     "played back later so you can check if echo is there. "
		     "Press OK to start.",
		     WITH_OKCANCEL);
    if (key != KEY_OK) {
	ti->skipped = PJ_TRUE;
	return;
    }

    /* Save current EC tail */
    status = pjsua_get_ec_tail(&last_ec_tail);
    if (status != PJ_SUCCESS)
	goto on_return;

    /* Set EC tail setting to default */
    status = pjsua_set_ec(PJSUA_DEFAULT_EC_TAIL_LEN, 0);
    if (status != PJ_SUCCESS)
	goto on_return;

    /*
     * Create player and recorder
     */
    status = create_player(PJ_ARRAY_SIZE(ref_wav_paths), ref_wav_paths,
			   &player_id);
    if (status != PJ_SUCCESS) {
	PJ_PERROR(1,(THIS_FILE, status, "Error opening WAV file %s",
		     WAV_PLAYBACK_PATH));
	goto on_return;
    }

    status = pjsua_recorder_create(
                 pj_cstr(&tmp, add_path(doc_path, AEC_REC_PATH)), 0, 0, -1,
                 0, &writer_id);
    if (status != PJ_SUCCESS) {
	PJ_PERROR(1,(THIS_FILE, status, "Error writing WAV file %s",
		     AEC_REC_PATH));
	goto on_return;
    }

    /*
     * Start playback and recording.
     */
    pjsua_conf_connect(pjsua_player_get_conf_port(player_id), 0);
    pj_thread_sleep(100);
    pjsua_conf_connect(0, pjsua_recorder_get_conf_port(writer_id));

    /* Wait user signal */
    gui_msgbox(title, "AEC/AES test is running. Press OK to stop this test.",
	       WITH_OK);

    /*
     * Stop and close playback and recorder
     */
    pjsua_conf_disconnect(0, pjsua_recorder_get_conf_port(writer_id));
    pjsua_conf_disconnect(pjsua_player_get_conf_port(player_id), 0);
    pjsua_recorder_destroy(writer_id);
    pjsua_player_destroy(player_id);
    player_id = PJSUA_INVALID_ID;
    writer_id = PJSUA_INVALID_ID;

    /*
     * Play the result.
     */
    status = pjsua_player_create(
                 pj_cstr(&tmp, add_path(doc_path, AEC_REC_PATH)),
                 0, &player_id);
    if (status != PJ_SUCCESS) {
	PJ_PERROR(1,(THIS_FILE, status, "Error opening WAV file %s", AEC_REC_PATH));
	goto on_return;
    }
    pjsua_conf_connect(pjsua_player_get_conf_port(player_id), 0);

    /* Wait user signal */
    gui_msgbox(title, "We are now playing the captured audio from the mic. "
		      "Check if echo (of the audio played back previously) is "
		      "present in the audio. The recording is stored in "
		      AEC_REC_PATH " for offline analysis. "
		      "Press OK to stop.",
		      WITH_OK);

    pjsua_conf_disconnect(pjsua_player_get_conf_port(player_id), 0);

    key = gui_msgbox(title,
		     "Did you notice any echo in the recording?",
		     WITH_YESNO);


on_return:
    if (player_id != PJSUA_INVALID_ID)
	pjsua_player_destroy(player_id);
    if (writer_id != PJSUA_INVALID_ID)
	pjsua_recorder_destroy(writer_id);

    /* Wait until sound device closed before restoring back EC tail setting */
    while (pjsua_snd_is_active())
	pj_thread_sleep(10);
    pjsua_set_ec(last_ec_tail, 0);


    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we encountered an error: ", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
    } else if (key == KEY_YES) {
	ti->success = PJ_FALSE;
	if (!ti->success) {
	    pj_ansi_strcpy(ti->reason, USER_ERROR);
	}
    } else {
	char msg[200];

	pj_ansi_snprintf(msg, sizeof(msg), "Test succeeded.\r\n");

	ti->success = PJ_TRUE;
	pj_ansi_strncpy(ti->reason, msg, sizeof(ti->reason));
	ti->reason[sizeof(ti->reason)-1] = '\0';
    }
}


/****************************************************************************
 * configurations
 */
static void systest_list_audio_devs()
{
    unsigned i, dev_count;
    pj_size_t len=0;
    pj_status_t status;
    test_item_t *ti;
    enum gui_key key;
    const char *title = "Audio Device List";

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    dev_count = pjmedia_aud_dev_count();
    if (dev_count == 0) {
	key = gui_msgbox(title,
			 "No audio devices are found", WITH_OK);
	ti->success = PJ_FALSE;
	pj_ansi_strcpy(ti->reason, "No device found");
	return;
    }

    pj_ansi_snprintf(ti->reason+len, sizeof(ti->reason)-len,
		     "Found %u devices\r\n", dev_count);
    len = strlen(ti->reason);

    for (i=0; i<dev_count; ++i) {
	pjmedia_aud_dev_info info;

	status = pjmedia_aud_dev_get_info(i, &info);
	if (status != PJ_SUCCESS) {
	    systest_perror("Error retrieving device info: ", status);
	    ti->success = PJ_FALSE;
	    pj_strerror(status, ti->reason, sizeof(ti->reason));
	    return;
	}

	pj_ansi_snprintf(ti->reason+len, sizeof(ti->reason)-len,
			 " %2d: %s [%s] (%d/%d)\r\n",
		          i, info.driver, info.name,
			  info.input_count, info.output_count);
	len = strlen(ti->reason);
    }

    ti->reason[len] = '\0';
    key = gui_msgbox(title, ti->reason, WITH_OK);
    PJ_UNUSED_ARG(key);

    ti->success = PJ_TRUE;
}

static void systest_display_settings(void)
{
    pjmedia_aud_dev_info di;
    pj_size_t len = 0;
    enum gui_key key;
    test_item_t *ti;
    const char *title = "Audio Settings";
    pj_status_t status;

    ti = systest_alloc_test_item(title);
    if (!ti)
	return;

    PJ_LOG(3,(THIS_FILE, "Running %s", title));

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Version: %s\r\n",
		     pj_get_version());
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Test clock rate: %d\r\n",
		     systest.media_cfg.clock_rate);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Device clock rate: %d\r\n",
		     systest.media_cfg.snd_clock_rate);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Aud frame ptime: %d\r\n",
		     systest.media_cfg.audio_frame_ptime);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Channel count: %d\r\n",
		     systest.media_cfg.channel_count);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Audio switching: %s\r\n",
	    (PJMEDIA_CONF_USE_SWITCH_BOARD ? "Switchboard" : "Conf bridge"));
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len, "Snd buff count: %d\r\n",
		     PJMEDIA_SOUND_BUFFER_COUNT);
    len = strlen(textbuf);

    /* Capture device */
    status = pjmedia_aud_dev_get_info(systest.rec_id, &di);
    if (status != PJ_SUCCESS) {
	systest_perror("Error querying device info", status);
	ti->success = PJ_FALSE;
	pj_strerror(status, ti->reason, sizeof(ti->reason));
	return;
    }

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len,
		     "Rec dev : %d (%s) [%s]\r\n",
		     systest.rec_id,
		     di.name,
		     di.driver);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len,
		     "Rec  buf : %d msec\r\n",
		     systest.media_cfg.snd_rec_latency);
    len = strlen(textbuf);

    /* Playback device */
    status = pjmedia_aud_dev_get_info(systest.play_id, &di);
    if (status != PJ_SUCCESS) {
	systest_perror("Error querying device info", status);
	return;
    }

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len,
		     "Play dev: %d (%s) [%s]\r\n",
		     systest.play_id,
		     di.name,
		     di.driver);
    len = strlen(textbuf);

    pj_ansi_snprintf(textbuf+len, sizeof(textbuf)-len,
		     "Play buf: %d msec\r\n",
		     systest.media_cfg.snd_play_latency);
    len = strlen(textbuf);

    ti->success = PJ_TRUE;
    pj_ansi_strncpy(ti->reason, textbuf, sizeof(ti->reason));
    ti->reason[sizeof(ti->reason)-1] = '\0';
    key = gui_msgbox(title, textbuf, WITH_OK);
    PJ_UNUSED_ARG(key); /* Warning about unused var */
}

/*****************************************************************/

int systest_init(void)
{
    pjsua_logging_config log_cfg;
    pj_status_t status = PJ_SUCCESS;

    status = pjsua_create();
    if (status != PJ_SUCCESS) {
	systest_perror("Sorry we've had error in pjsua_create(): ", status);
	return status;
    }

    pjsua_logging_config_default(&log_cfg);
    log_cfg.log_filename = pj_str(add_path(doc_path, LOG_OUT_PATH));

    pjsua_config_default(&systest.ua_cfg);
    pjsua_media_config_default(&systest.media_cfg);
    systest.media_cfg.clock_rate = TEST_CLOCK_RATE;
    systest.media_cfg.snd_clock_rate = DEV_CLOCK_RATE;
    if (OVERRIDE_AUD_FRAME_PTIME)
	systest.media_cfg.audio_frame_ptime = OVERRIDE_AUD_FRAME_PTIME;
    systest.media_cfg.channel_count = CHANNEL_COUNT;
    systest.rec_id = REC_DEV_ID;
    systest.play_id = PLAY_DEV_ID;
    systest.media_cfg.ec_tail_len = 0;
    systest.media_cfg.snd_auto_close_time = 0;

#if defined(OVERRIDE_AUDDEV_PLAY_LAT) && OVERRIDE_AUDDEV_PLAY_LAT!=0
    systest.media_cfg.snd_play_latency = OVERRIDE_AUDDEV_PLAY_LAT;
#endif

#if defined(OVERRIDE_AUDDEV_REC_LAT) && OVERRIDE_AUDDEV_REC_LAT!=0
    systest.media_cfg.snd_rec_latency = OVERRIDE_AUDDEV_REC_LAT;
#endif

    status = pjsua_init(&systest.ua_cfg, &log_cfg, &systest.media_cfg);
    if (status != PJ_SUCCESS) {
	pjsua_destroy();
	systest_perror("Sorry we've had error in pjsua_init(): ", status);
	return status;
    }

    status = pjsua_start();
    if (status != PJ_SUCCESS) {
	pjsua_destroy();
	systest_perror("Sorry we've had error in pjsua_start(): ", status);
	return status;
    }

    status = gui_init(&root_menu);
    if (status != 0)
	goto on_return;

    return 0;

on_return:
    gui_destroy();
    return status;
}


int systest_set_dev(int cap_dev, int play_dev)
{
    systest.rec_id = systest_cap_dev_id = cap_dev;
    systest.play_id = systest_play_dev_id = play_dev;
    return pjsua_set_snd_dev(cap_dev, play_dev);
}

static void systest_wizard(void)
{
    PJ_LOG(3,(THIS_FILE, "Running test wizard"));
    systest_list_audio_devs();
    systest_display_settings();
    systest_play_tone();
    systest_play_wav1();
    systest_rec_audio();
    systest_audio_test();
    systest_latency_test();
    systest_aec_test();
    gui_msgbox("Test wizard", "Test wizard complete.", WITH_OK);
}


int systest_run(void)
{
    gui_start(&root_menu);
    return 0;
}

void systest_save_result(const char *filename)
{
    unsigned i;
    pj_oshandle_t fd;
    pj_time_val tv;
    pj_parsed_time pt;
    pj_ssize_t size;
    const char *text;
    pj_status_t status;

    status = pj_file_open(NULL, filename, PJ_O_WRONLY | PJ_O_APPEND, &fd);
    if (status != PJ_SUCCESS) {
	pj_ansi_snprintf(textbuf, sizeof(textbuf),
			 "Error opening file %s",
			 filename);
	systest_perror(textbuf, status);
	return;
    }

    text = "\r\n\r\nPJSYSTEST Report\r\n";
    size = strlen(text);
    pj_file_write(fd, text, &size);

    /* Put timestamp */
    pj_gettimeofday(&tv);
    if (pj_time_decode(&tv, &pt) == PJ_SUCCESS) {
	pj_ansi_snprintf(textbuf, sizeof(textbuf),
			 "Time: %04d/%02d/%02d %02d:%02d:%02d\r\n",
			 pt.year, pt.mon+1, pt.day,
			 pt.hour, pt.min, pt.sec);
	size = strlen(textbuf);
	pj_file_write(fd, textbuf, &size);
    }

    pj_ansi_snprintf(textbuf, sizeof(textbuf),
		     "Tests invoked: %u\r\n"
		     "-----------------------------------------------\r\n",
		     test_item_count);
    size = strlen(textbuf);
    pj_file_write(fd, textbuf, &size);

    for (i=0; i<test_item_count; ++i) {
        int len;
	test_item_t *ti = &test_items[i];

	len = pj_ansi_snprintf(textbuf, sizeof(textbuf),
			 "\r\nTEST %d: %s %s\r\n",
			 i, ti->title,
			 (ti->skipped? "Skipped" : (ti->success ? "Success" : "Failed")));
	PJ_CHECK_TRUNC_STR(len, textbuf, sizeof(textbuf));
	size = strlen(textbuf);
	pj_file_write(fd, textbuf, &size);

	size = strlen(ti->reason);
	pj_file_write(fd, ti->reason, &size);

	size = 2;
	pj_file_write(fd, "\r\n", &size);
    }


    pj_file_close(fd);

    pj_ansi_snprintf(textbuf, sizeof(textbuf),
		     "Test result successfully appended to file %s",
		     filename);
    gui_msgbox("Test result saved", textbuf, WITH_OK);
}

void systest_deinit(void)
{
    gui_destroy();
    pjsua_destroy();
}

