/* $Id$ */
/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * 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 <pjmedia.h>
#include <pjlib-util.h>	/* pj_getopt */
#include <pjlib.h>

#include <stdlib.h>	/* atoi() */
#include <stdio.h>

#include "util.h"

/**
 * \page page_pjmedia_samples_confsample_c Samples: Using Conference Bridge
 *
 * Sample to mix multiple files in the conference bridge and play the
 * result to sound device.
 *
 * This file is pjsip-apps/src/samples/confsample.c
 *
 * \includelineno confsample.c
 */


/* For logging purpose. */
#define THIS_FILE   "confsample.c"


/* Shall we put recorder in the conference */
#define RECORDER    1


static const char *desc = 
 " FILE:								    \n"
 "									    \n"
 "  confsample.c							    \n"
 "									    \n"
 " PURPOSE:								    \n"
 "									    \n"
 "  Demonstrate how to use conference bridge.				    \n"
 "									    \n"
 " USAGE:								    \n"
 "									    \n"
 "  confsample [options] [file1.wav] [file2.wav] ...			    \n"
 "									    \n"
 " options:								    \n"
 SND_USAGE
 "									    \n"
 "  fileN.wav are optional WAV files to be connected to the conference      \n"
 "  bridge. The WAV files MUST have single channel (mono) and 16 bit PCM    \n"
 "  samples. It can have arbitrary sampling rate.			    \n"
 "									    \n"
 " DESCRIPTION:								    \n"
 "									    \n"
 "  Here we create a conference bridge, with at least one port (port zero   \n"
 "  is always created for the sound device).				    \n"
 "									    \n"
 "  If WAV files are specified, the WAV file player ports will be connected \n"
 "  to slot starting from number one in the bridge. The WAV files can have  \n"
 "  arbitrary sampling rate; the bridge will convert it to its clock rate.  \n"
 "  However, the files MUST have a single audio channel only (i.e. mono).  \n";


 
/* 
 * Prototypes: 
 */

/* List the ports in the conference bridge */
static void conf_list(pjmedia_conf *conf, pj_bool_t detail);

/* Display VU meter */
static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur);


/* Show usage */
static void usage(void)
{
    puts("");
    puts(desc);
}



/* Input simple string */
static pj_bool_t input(const char *title, char *buf, pj_size_t len)
{
    char *p;

    printf("%s (empty to cancel): ", title); fflush(stdout);
    if (fgets(buf, (int)len, stdin) == NULL)
	return PJ_FALSE;

    /* Remove trailing newlines. */
    for (p=buf; ; ++p) {
	if (*p=='\r' || *p=='\n') *p='\0';
	else if (!*p) break;
    }

    if (!*buf)
	return PJ_FALSE;
    
    return PJ_TRUE;
}


/*****************************************************************************
 * main()
 */
int main(int argc, char *argv[])
{
    int dev_id = -1;
    int clock_rate = CLOCK_RATE;
    int channel_count = NCHANNELS;
    int samples_per_frame = NSAMPLES;
    int bits_per_sample = NBITS;

    pj_caching_pool cp;
    pjmedia_endpt *med_endpt;
    pj_pool_t *pool;
    pjmedia_conf *conf;

    int i, port_count, file_count;
    pjmedia_port **file_port;	/* Array of file ports */
    pjmedia_port *rec_port = NULL;  /* Wav writer port */

    char tmp[10];
    pj_status_t status;


    /* Must init PJLIB first: */
    status = pj_init();
    PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1);

    /* Get command line options. */
    if (get_snd_options(THIS_FILE, argc, argv, &dev_id, &clock_rate,
			&channel_count, &samples_per_frame, &bits_per_sample))
    {
	usage();
	return 1;
    }

    /* Must create a pool factory before we can allocate any memory. */
    pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0);

    /* 
     * Initialize media endpoint.
     * This will implicitly initialize PJMEDIA too.
     */
    status = pjmedia_endpt_create(&cp.factory, NULL, 1, &med_endpt);
    PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1);

    /* Create memory pool to allocate memory */
    pool = pj_pool_create( &cp.factory,	    /* pool factory	    */
			   "wav",	    /* pool name.	    */
			   4000,	    /* init size	    */
			   4000,	    /* increment size	    */
			   NULL		    /* callback on error    */
			   );


    file_count = argc - pj_optind;
    port_count = file_count + 1 + RECORDER;

    /* Create the conference bridge. 
     * With default options (zero), the bridge will create an instance of
     * sound capture and playback device and connect them to slot zero.
     */
    status = pjmedia_conf_create( pool,	    /* pool to use	    */
				  port_count,/* number of ports	    */
				  clock_rate,
				  channel_count,
				  samples_per_frame,
				  bits_per_sample,
				  0,	    /* options		    */
				  &conf	    /* result		    */
				  );
    if (status != PJ_SUCCESS) {
	app_perror(THIS_FILE, "Unable to create conference bridge", status);
	return 1;
    }

#if RECORDER
    status = pjmedia_wav_writer_port_create(  pool, "confrecord.wav",
					      clock_rate, channel_count,
					      samples_per_frame, 
					      bits_per_sample, 0, 0, 
					      &rec_port);
    if (status != PJ_SUCCESS) {
	app_perror(THIS_FILE, "Unable to create WAV writer", status);
	return 1;
    }

    pjmedia_conf_add_port(conf, pool, rec_port, NULL, NULL);
#endif


    /* Create file ports. */
    file_port = pj_pool_alloc(pool, file_count * sizeof(pjmedia_port*));

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

	/* Load the WAV file to file port. */
	status = pjmedia_wav_player_port_create( 
			pool,		    /* pool.	    */
			argv[i+pj_optind],  /* filename	    */
			0,		    /* use default ptime */
			0,		    /* flags	    */
			0,		    /* buf size	    */
			&file_port[i]	    /* result	    */
			);
	if (status != PJ_SUCCESS) {
	    char title[80];
	    pj_ansi_sprintf(title, "Unable to use %s", argv[i+pj_optind]);
	    app_perror(THIS_FILE, title, status);
	    usage();
	    return 1;
	}

	/* Add the file port to conference bridge */
	status = pjmedia_conf_add_port( conf,		/* The bridge	    */
					pool,		/* pool		    */
					file_port[i],	/* port to connect  */
					NULL,		/* Use port's name  */
					NULL		/* ptr for slot #   */
					);
	if (status != PJ_SUCCESS) {
	    app_perror(THIS_FILE, "Unable to add conference port", status);
	    return 1;
	}
    }


    /* 
     * All ports are set up in the conference bridge.
     * But at this point, no media will be flowing since no ports are
     * "connected". User must connect the port manually.
     */


    /* Dump memory usage */
    dump_pool_usage(THIS_FILE, &cp);

    /* Sleep to allow log messages to flush */
    pj_thread_sleep(100);


    /*
     * UI Menu: 
     */
    for (;;) {
	char tmp1[10];
	char tmp2[10];
	char *err;
	int src, dst, level, dur;

	puts("");
	conf_list(conf, 0);
	puts("");
	puts("Menu:");
	puts("  s    Show ports details");
	puts("  c    Connect one port to another");
	puts("  d    Disconnect port connection");
	puts("  t    Adjust signal level transmitted (tx) to a port");
	puts("  r    Adjust signal level received (rx) from a port");
	puts("  v    Display VU meter for a particular port");
	puts("  q    Quit");
	puts("");
	
	printf("Enter selection: "); fflush(stdout);

	if (fgets(tmp, sizeof(tmp), stdin) == NULL)
	    break;

	switch (tmp[0]) {
	case 's':
	    puts("");
	    conf_list(conf, 1);
	    break;

	case 'c':
	    puts("");
	    puts("Connect source port to destination port");
	    if (!input("Enter source port number", tmp1, sizeof(tmp1)) )
		continue;
	    src = strtol(tmp1, &err, 10);
	    if (*err || src < 0 || src >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    if (!input("Enter destination port number", tmp2, sizeof(tmp2)) )
		continue;
	    dst = strtol(tmp2, &err, 10);
	    if (*err || dst < 0 || dst >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    status = pjmedia_conf_connect_port(conf, src, dst, 0);
	    if (status != PJ_SUCCESS)
		app_perror(THIS_FILE, "Error connecting port", status);
	    
	    break;

	case 'd':
	    puts("");
	    puts("Disconnect port connection");
	    if (!input("Enter source port number", tmp1, sizeof(tmp1)) )
		continue;
	    src = strtol(tmp1, &err, 10);
	    if (*err || src < 0 || src >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    if (!input("Enter destination port number", tmp2, sizeof(tmp2)) )
		continue;
	    dst = strtol(tmp2, &err, 10);
	    if (*err || dst < 0 || dst >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    status = pjmedia_conf_disconnect_port(conf, src, dst);
	    if (status != PJ_SUCCESS)
		app_perror(THIS_FILE, "Error connecting port", status);
	    

	    break;

	case 't':
	    puts("");
	    puts("Adjust transmit level of a port");
	    if (!input("Enter port number", tmp1, sizeof(tmp1)) )
		continue;
	    src = strtol(tmp1, &err, 10);
	    if (*err || src < 0 || src >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    if (!input("Enter level (-128 to >127, 0 for normal)", 
			      tmp2, sizeof(tmp2)) )
		continue;
	    level = strtol(tmp2, &err, 10);
	    if (*err || level < -128) {
		puts("Invalid level");
		continue;
	    }

	    status = pjmedia_conf_adjust_tx_level( conf, src, level);
	    if (status != PJ_SUCCESS)
		app_perror(THIS_FILE, "Error adjusting level", status);
	    break;


	case 'r':
	    puts("");
	    puts("Adjust receive level of a port");
	    if (!input("Enter port number", tmp1, sizeof(tmp1)) )
		continue;
	    src = strtol(tmp1, &err, 10);
	    if (*err || src < 0 || src >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    if (!input("Enter level (-128 to >127, 0 for normal)", 
			      tmp2, sizeof(tmp2)) )
		continue;
	    level = strtol(tmp2, &err, 10);
	    if (*err || level < -128) {
		puts("Invalid level");
		continue;
	    }

	    status = pjmedia_conf_adjust_rx_level( conf, src, level);
	    if (status != PJ_SUCCESS)
		app_perror(THIS_FILE, "Error adjusting level", status);
	    break;

	case 'v':
	    puts("");
	    puts("Display VU meter");
	    if (!input("Enter port number to monitor", tmp1, sizeof(tmp1)) )
		continue;
	    src = strtol(tmp1, &err, 10);
	    if (*err || src < 0 || src >= port_count) {
		puts("Invalid slot number");
		continue;
	    }

	    if (!input("Enter r for rx level or t for tx level", tmp2, sizeof(tmp2)))
		continue;
	    if (tmp2[0] != 'r' && tmp2[0] != 't') {
		puts("Invalid option");
		continue;
	    }

	    if (!input("Duration to monitor (in seconds)", tmp1, sizeof(tmp1)) )
		continue;
	    dur = strtol(tmp1, &err, 10);
	    if (*err) {
		puts("Invalid duration number");
		continue;
	    }

	    monitor_level(conf, src, tmp2[0], dur);
	    break;

	case 'q':
	    goto on_quit;

	default:
	    printf("Invalid input character '%c'\n", tmp[0]);
	    break;
	}
    }

on_quit:
    
    /* Start deinitialization: */

    /* Destroy conference bridge */
    status = pjmedia_conf_destroy( conf );
    PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1);


    /* Destroy file ports */
    for (i=0; i<file_count; ++i) {
	status = pjmedia_port_destroy( file_port[i]);
	PJ_ASSERT_RETURN(status == PJ_SUCCESS, 1);
    }

    /* Destroy recorder port */
    if (rec_port)
	pjmedia_port_destroy(rec_port);

    /* Release application pool */
    pj_pool_release( pool );

    /* Destroy media endpoint. */
    pjmedia_endpt_destroy( med_endpt );

    /* Destroy pool factory */
    pj_caching_pool_destroy( &cp );

    /* Shutdown PJLIB */
    pj_shutdown();

    /* Done. */
    return 0;
}


/*
 * List the ports in conference bridge
 */
static void conf_list(pjmedia_conf *conf, int detail)
{
    enum { MAX_PORTS = 32 };
    unsigned i, count;
    pjmedia_conf_port_info info[MAX_PORTS];

    printf("Conference ports:\n");

    count = PJ_ARRAY_SIZE(info);
    pjmedia_conf_get_ports_info(conf, &count, info);

    for (i=0; i<count; ++i) {
	char txlist[4*MAX_PORTS];
	unsigned j;
	pjmedia_conf_port_info *port_info = &info[i];	
	
	txlist[0] = '\0';
	for (j=0; j<port_info->listener_cnt; ++j) {
	    char s[10];
	    pj_ansi_sprintf(s, "#%d ", port_info->listener_slots[j]);
	    pj_ansi_strcat(txlist, s);

	}

	if (txlist[0] == '\0') {
	    txlist[0] = '-';
	    txlist[1] = '\0';
	}

	if (!detail) {
	    printf("Port #%02d %-25.*s  transmitting to: %s\n", 
		   port_info->slot, 
		   (int)port_info->name.slen, 
		   port_info->name.ptr,
		   txlist);
	} else {
	    unsigned tx_level, rx_level;

	    pjmedia_conf_get_signal_level(conf, port_info->slot,
					  &tx_level, &rx_level);

	    printf("Port #%02d:\n"
		   "  Name                    : %.*s\n"
		   "  Sampling rate           : %d Hz\n"
		   "  Samples per frame       : %d\n"
		   "  Frame time              : %d ms\n"
		   "  Signal level adjustment : tx=%d, rx=%d\n"
		   "  Current signal level    : tx=%u, rx=%u\n"
		   "  Transmitting to ports   : %s\n\n",
		   port_info->slot,
		   (int)port_info->name.slen,
		   port_info->name.ptr,
		   port_info->clock_rate,
		   port_info->samples_per_frame,
		   port_info->samples_per_frame*1000/port_info->clock_rate,
		   port_info->tx_adj_level,
		   port_info->rx_adj_level,
		   tx_level,
		   rx_level,
		   txlist);
	}

    }
    puts("");
}


/*
 * Display VU meter
 */
static void monitor_level(pjmedia_conf *conf, int slot, int dir, int dur)
{
    enum { SLEEP = 20, SAMP_CNT = 2};
    pj_status_t status;
    int i, total_count;
    unsigned level, samp_cnt;


    puts("");
    printf("Displaying VU meter for port %d for about %d seconds\n",
	   slot, dur);

    total_count = dur * 1000 / SLEEP;

    level = 0;
    samp_cnt = 0;

    for (i=0; i<total_count; ++i) {
	unsigned tx_level, rx_level;
	int j, length;
	char meter[21];

	/* Poll the volume every 20 msec */
	status = pjmedia_conf_get_signal_level(conf, slot, 
					       &tx_level, &rx_level);
	if (status != PJ_SUCCESS) {
	    app_perror(THIS_FILE, "Unable to read level", status);
	    return;
	}

	level += (dir=='r' ? rx_level : tx_level);
	++samp_cnt;

	/* Accumulate until we have enough samples */
	if (samp_cnt < SAMP_CNT) {
	    pj_thread_sleep(SLEEP);
	    continue;
	}

	/* Get average */
	level = level / samp_cnt;

	/* Draw bar */
	length = 20 * level / 255;
	for (j=0; j<length; ++j)
	    meter[j] = '#';
	for (; j<20; ++j)
	    meter[j] = ' ';
	meter[20] = '\0';

	printf("Port #%02d %cx level: [%s] %d  \r",
	       slot, dir, meter, level);

	/* Next.. */
	samp_cnt = 0;
	level = 0;

	pj_thread_sleep(SLEEP);
    }

    puts("");
}

