/* 
 * 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_snprintf(title, sizeof(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_snprintf(s, sizeof(s), "#%d ", 
                             port_info->listener_slots[j]);
            pj_ansi_strxcat(txlist, s, sizeof(txlist));

        }

        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("");
}

