/* 
 * 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-audiodev/audiodev.h>
#include <pjmedia-audiodev/audiotest.h>
#include <pjmedia.h>
#include <pjlib.h>
#include <pjlib-util.h>

#define THIS_FILE       "auddemo.c"
#define MAX_DEVICES     64
#define WAV_FILE        "auddemo.wav"


static unsigned dev_count;
static unsigned playback_lat = PJMEDIA_SND_DEFAULT_PLAY_LATENCY;
static unsigned capture_lat = PJMEDIA_SND_DEFAULT_REC_LATENCY;

static void app_perror(const char *title, pj_status_t status)
{
    char errmsg[PJ_ERR_MSG_SIZE];

    pj_strerror(status, errmsg, sizeof(errmsg));        
    printf( "%s: %s (err=%d)\n",
            title, errmsg, status);
}

static void list_devices(void)
{
    unsigned i;
    pj_status_t status;
    
    dev_count = pjmedia_aud_dev_count();
    if (dev_count == 0) {
        PJ_LOG(3,(THIS_FILE, "No devices found"));
        return;
    }

    PJ_LOG(3,(THIS_FILE, "Found %d devices:", dev_count));

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

        status = pjmedia_aud_dev_get_info(i, &info);
        if (status != PJ_SUCCESS)
            continue;

        PJ_LOG(3,(THIS_FILE," %2d: %s [%s] (%d/%d)",
                  i, info.driver, info.name, info.input_count, info.output_count));
    }
}

static const char *decode_caps(unsigned caps)
{
    static char text[200];
    unsigned i;

    text[0] = '\0';

    for (i=0; i<31; ++i) {
        if ((1 << i) & caps) {
            const char *capname;
            capname = pjmedia_aud_dev_cap_name((pjmedia_aud_dev_cap)(1 << i), 
                                               NULL);
            pj_ansi_strxcat(text, capname, sizeof(text));
            pj_ansi_strxcat(text, " ", sizeof(text));
        }
    }

    return text;
}

static void show_dev_info(unsigned index)
{
#define H   "%-20s"
    pjmedia_aud_dev_info info;
    char formats[200];
    pj_status_t status;

    if (index >= dev_count) {
        PJ_LOG(1,(THIS_FILE, "Error: invalid index %u", index));
        return;
    }

    status = pjmedia_aud_dev_get_info(index, &info);
    if (status != PJ_SUCCESS) {
        app_perror("pjmedia_aud_dev_get_info() error", status);
        return;
    }

    PJ_LOG(3, (THIS_FILE, "Device at index %u:", index));
    PJ_LOG(3, (THIS_FILE, "-------------------------"));

    PJ_LOG(3, (THIS_FILE, H": %u (0x%x)", "ID", index, index));
    PJ_LOG(3, (THIS_FILE, H": %s", "Name", info.name));
    PJ_LOG(3, (THIS_FILE, H": %s", "Driver", info.driver));
    PJ_LOG(3, (THIS_FILE, H": %u", "Input channels", info.input_count));
    PJ_LOG(3, (THIS_FILE, H": %u", "Output channels", info.output_count));
    PJ_LOG(3, (THIS_FILE, H": %s", "Capabilities", decode_caps(info.caps)));

    formats[0] = '\0';
    if (info.caps & PJMEDIA_AUD_DEV_CAP_EXT_FORMAT) {
        unsigned i;

        for (i=0; i<info.ext_fmt_cnt; ++i) {
            char bitrate[32];

            switch (info.ext_fmt[i].id) {
            case PJMEDIA_FORMAT_L16:
                pj_ansi_strxcat(formats, "L16/", sizeof(formats));
                break;
            case PJMEDIA_FORMAT_PCMA:
                pj_ansi_strxcat(formats, "PCMA/", sizeof(formats));
                break;
            case PJMEDIA_FORMAT_PCMU:
                pj_ansi_strxcat(formats, "PCMU/", sizeof(formats));
                break;
            case PJMEDIA_FORMAT_AMR:
                pj_ansi_strxcat(formats, "AMR/", sizeof(formats));
                break;
            case PJMEDIA_FORMAT_G729:
                pj_ansi_strxcat(formats, "G729/", sizeof(formats));
                break;
            case PJMEDIA_FORMAT_ILBC:
                pj_ansi_strxcat(formats, "ILBC/", sizeof(formats));
                break;
            default:
                pj_ansi_strxcat(formats, "unknown/", sizeof(formats));
                break;
            }
            pj_ansi_snprintf(bitrate, sizeof(bitrate), "%u",
                             info.ext_fmt[i].det.aud.avg_bps);
            pj_ansi_strxcat(formats, bitrate, sizeof(formats));
            pj_ansi_strxcat(formats, " ", sizeof(formats));
        }
    }
    PJ_LOG(3, (THIS_FILE, H": %s", "Extended formats", formats));

#undef H
}

static void test_device(pjmedia_dir dir, unsigned rec_id, unsigned play_id, 
                        unsigned clock_rate, unsigned ptime, 
                        unsigned chnum)
{
    pjmedia_aud_param param;
    pjmedia_aud_test_results result;
    pj_status_t status;

    if (dir & PJMEDIA_DIR_CAPTURE) {
        status = pjmedia_aud_dev_default_param(rec_id, &param);
    } else {
        status = pjmedia_aud_dev_default_param(play_id, &param);
    }

    if (status != PJ_SUCCESS) {
        app_perror("pjmedia_aud_dev_default_param()", status);
        return;
    }

    param.dir = dir;
    param.rec_id = rec_id;
    param.play_id = play_id;
    param.clock_rate = clock_rate;
    param.channel_count = chnum;
    param.samples_per_frame = clock_rate * chnum * ptime / 1000;

    /* Latency settings */
    param.flags |= (PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY | 
                    PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY);
    param.input_latency_ms = capture_lat;
    param.output_latency_ms = playback_lat;

    PJ_LOG(3,(THIS_FILE, "Performing test.."));

    status = pjmedia_aud_test(&param, &result);
    if (status != PJ_SUCCESS) {
        app_perror("Test has completed with error", status);
        return;
    }

    PJ_LOG(3,(THIS_FILE, "Done. Result:"));

    if (dir & PJMEDIA_DIR_CAPTURE) {
        if (result.rec.frame_cnt==0) {
            PJ_LOG(1,(THIS_FILE, "Error: no frames captured!"));
        } else {
            PJ_LOG(3,(THIS_FILE, "  %-20s: interval (min/max/avg/dev)=%u/%u/%u/%u, burst=%u",
                      "Recording result",
                      result.rec.min_interval,
                      result.rec.max_interval,
                      result.rec.avg_interval,
                      result.rec.dev_interval,
                      result.rec.max_burst));
        }
    }

    if (dir & PJMEDIA_DIR_PLAYBACK) {
        if (result.play.frame_cnt==0) {
            PJ_LOG(1,(THIS_FILE, "Error: no playback!"));
        } else {
            PJ_LOG(3,(THIS_FILE, "  %-20s: interval (min/max/avg/dev)=%u/%u/%u/%u, burst=%u",
                      "Playback result",
                      result.play.min_interval,
                      result.play.max_interval,
                      result.play.avg_interval,
                      result.play.dev_interval,
                      result.play.max_burst));
        }
    }

    if (dir==PJMEDIA_DIR_CAPTURE_PLAYBACK) {
        if (result.rec_drift_per_sec == 0) {
            PJ_LOG(3,(THIS_FILE, " No clock drift detected"));
        } else {
            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_LOG(3,(THIS_FILE, " Clock drifts detected. Capture device "
                                 "is running %d samples per second %s "
                                 "than the playback device",
                                 drift, which));
        }
    }
}


static pj_status_t wav_rec_cb(void *user_data, pjmedia_frame *frame)
{
    return pjmedia_port_put_frame((pjmedia_port*)user_data, frame);
}

static void record(unsigned rec_index, const char *filename)
{
    pj_pool_t *pool = NULL;
    pjmedia_port *wav = NULL;
    pjmedia_aud_param param;
    pjmedia_aud_stream *strm = NULL;
    char line[10], *dummy;
    pj_status_t status;

    if (filename == NULL)
        filename = WAV_FILE;

    pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "wav",
                          1000, 1000, NULL);

    status = pjmedia_wav_writer_port_create(pool, filename, 16000, 
                                            1, 320, 16, 0, 0, &wav);
    if (status != PJ_SUCCESS) {
        app_perror("Error creating WAV file", status);
        goto on_return;
    }

    status = pjmedia_aud_dev_default_param(rec_index, &param);
    if (status != PJ_SUCCESS) {
        app_perror("pjmedia_aud_dev_default_param()", status);
        goto on_return;
    }

    param.dir = PJMEDIA_DIR_CAPTURE;
    param.clock_rate = PJMEDIA_PIA_SRATE(&wav->info);
    param.samples_per_frame = PJMEDIA_PIA_SPF(&wav->info);
    param.channel_count = PJMEDIA_PIA_CCNT(&wav->info);
    param.bits_per_sample = PJMEDIA_PIA_BITS(&wav->info);

    status = pjmedia_aud_stream_create(&param, &wav_rec_cb, NULL, wav,
                                       &strm);
    if (status != PJ_SUCCESS) {
        app_perror("Error opening the sound device", status);
        goto on_return;
    }

    status = pjmedia_aud_stream_start(strm);
    if (status != PJ_SUCCESS) {
        app_perror("Error starting the sound device", status);
        goto on_return;
    }

    PJ_LOG(3,(THIS_FILE, "Recording started, press ENTER to stop"));
    dummy = fgets(line, sizeof(line), stdin);
    PJ_UNUSED_ARG(dummy);

on_return:
    if (strm) {
        pjmedia_aud_stream_stop(strm);
        pjmedia_aud_stream_destroy(strm);
    }
    if (wav)
        pjmedia_port_destroy(wav);
    if (pool)
        pj_pool_release(pool);
}


static pj_status_t wav_play_cb(void *user_data, pjmedia_frame *frame)
{
    return pjmedia_port_get_frame((pjmedia_port*)user_data, frame);
}


static void play_file(unsigned play_index, const char *filename)
{
    pj_pool_t *pool = NULL;
    pjmedia_port *wav = NULL;
    pjmedia_aud_param param;
    pjmedia_aud_stream *strm = NULL;
    char line[10], *dummy;
    pj_status_t status;

    if (filename == NULL)
        filename = WAV_FILE;

    pool = pj_pool_create(pjmedia_aud_subsys_get_pool_factory(), "wav",
                          1000, 1000, NULL);

    status = pjmedia_wav_player_port_create(pool, filename, 20, 0, 0, &wav);
    if (status != PJ_SUCCESS) {
        app_perror("Error opening WAV file", status);
        goto on_return;
    }

    status = pjmedia_aud_dev_default_param(play_index, &param);
    if (status != PJ_SUCCESS) {
        app_perror("pjmedia_aud_dev_default_param()", status);
        goto on_return;
    }

    param.dir = PJMEDIA_DIR_PLAYBACK;
    param.clock_rate = PJMEDIA_PIA_SRATE(&wav->info);
    param.samples_per_frame = PJMEDIA_PIA_SPF(&wav->info);
    param.channel_count = PJMEDIA_PIA_CCNT(&wav->info);
    param.bits_per_sample = PJMEDIA_PIA_BITS(&wav->info);

    status = pjmedia_aud_stream_create(&param, NULL, &wav_play_cb, wav,
                                       &strm);
    if (status != PJ_SUCCESS) {
        app_perror("Error opening the sound device", status);
        goto on_return;
    }

    status = pjmedia_aud_stream_start(strm);
    if (status != PJ_SUCCESS) {
        app_perror("Error starting the sound device", status);
        goto on_return;
    }

    PJ_LOG(3,(THIS_FILE, "Playback started, press ENTER to stop"));
    dummy = fgets(line, sizeof(line), stdin);
    PJ_UNUSED_ARG(dummy);

on_return:
    if (strm) {
        pjmedia_aud_stream_stop(strm);
        pjmedia_aud_stream_destroy(strm);
    }
    if (wav)
        pjmedia_port_destroy(wav);
    if (pool)
        pj_pool_release(pool);
}


static void print_menu(void)
{
    puts("");
    puts("Audio demo menu:");
    puts("-------------------------------");
    puts("  l                        List devices");
    puts("  R                        Refresh devices");
    puts("  i ID                     Show device info for device ID");
    puts("  t RID PID CR PTIM [CH]   Perform test on the device:");
    puts("                             RID:  record device ID (-1 for no)");
    puts("                             PID:  playback device ID (-1 for no)");
    puts("                             CR:   clock rate");
    puts("                             PTIM: ptime in ms");
    puts("                             CH:   # of channels");
    puts("  r RID [FILE]             Record capture device RID to WAV file");
    puts("  p PID [FILE]             Playback WAV file to device ID PID");
    puts("  d [RLAT PLAT]            Get/set sound device latencies (in ms):");
    puts("                             Specify no param to get current latencies setting");
    puts("                             RLAT: record latency (-1 for default)");
    puts("                             PLAT: playback latency (-1 for default)");
    puts("  v                        Toggle log verbosity");
    puts("  q                        Quit");
    puts("");
    printf("Enter selection: ");
    fflush(stdout);
}

int main()
{
    pj_caching_pool cp;
    pj_bool_t done = PJ_FALSE;
    pj_status_t status;

    /* Init pjlib */
    status = pj_init();
    PJ_ASSERT_RETURN(status==PJ_SUCCESS, 1);
    
    pj_log_set_decor(PJ_LOG_HAS_NEWLINE);

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

    status = pjmedia_aud_subsys_init(&cp.factory);
    if (status != PJ_SUCCESS) {
        app_perror("pjmedia_aud_subsys_init()", status);
        pj_caching_pool_destroy(&cp);
        pj_shutdown();
        return 1;
    }

    list_devices();

    while (!done) {
        char line[80];

        print_menu();

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

        switch (line[0]) {
        case 'l':
            list_devices();
            break;

        case 'R':
            pjmedia_aud_dev_refresh();
            puts("Audio device list refreshed.");
            break;

        case 'i':
            {
                unsigned dev_index;
                if (sscanf(line+2, "%u", &dev_index) != 1) {
                    puts("error: device ID required");
                    break;
                }
                show_dev_info(dev_index);
            }
            break;

        case 't':
            {
                pjmedia_dir dir;
                int rec_id, play_id;
                unsigned clock_rate, ptime, chnum;
                int cnt;

                cnt = sscanf(line+2, "%d %d %u %u %u", &rec_id, &play_id, 
                             &clock_rate, &ptime, &chnum);
                if (cnt < 4) {
                    puts("error: not enough parameters");
                    break;
                }
                if (clock_rate < 8000 || clock_rate > 128000) {
                    puts("error: invalid clock rate");
                    break;
                }
                if (ptime < 10 || ptime > 500) {
                    puts("error: invalid ptime");
                    break;
                }
                if (cnt==5) {
                    if (chnum < 1 || chnum > 4) {
                        puts("error: invalid number of channels");
                        break;
                    }
                } else {
                    chnum = 1;
                }

                if (rec_id >= 0 && rec_id < (int)dev_count) {
                    if (play_id >= 0 && play_id < (int)dev_count)
                        dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
                    else
                        dir = PJMEDIA_DIR_CAPTURE;
                } else if (play_id >= 0 && play_id < (int)dev_count) {
                    dir = PJMEDIA_DIR_PLAYBACK;
                } else {
                    puts("error: at least one valid device index required");
                    break;
                }

                test_device(dir, rec_id, play_id, clock_rate, ptime, chnum);
                
            }
            break;

        case 'r':
            /* record */
            {
                int index;
                char filename[80];
                int count;

                count = sscanf(line+2, "%d %s", &index, filename);
                if (count==1)
                    record(index, NULL);
                else if (count==2)
                    record(index, filename);
                else
                    puts("error: invalid command syntax");
            }
            break;

        case 'p':
            /* playback */
            {
                int index;
                char filename[80];
                int count;

                count = sscanf(line+2, "%d %s", &index, filename);
                if (count==1)
                    play_file(index, NULL);
                else if (count==2)
                    play_file(index, filename);
                else
                    puts("error: invalid command syntax");
            }
            break;

        case 'd':
            /* latencies */
            {
                int rec_lat, play_lat;

                if (sscanf(line+2, "%d %d", &rec_lat, &play_lat) == 2) {
                    capture_lat = (unsigned) 
                         (rec_lat>=0? rec_lat:PJMEDIA_SND_DEFAULT_REC_LATENCY);
                    playback_lat = (unsigned)
                         (play_lat >= 0? play_lat : PJMEDIA_SND_DEFAULT_PLAY_LATENCY);
                    printf("Recording latency=%ums, playback latency=%ums",
                           capture_lat, playback_lat);
                } else {
                    printf("Current latencies: record=%ums, playback=%ums",
                           capture_lat, playback_lat);
                }
                puts("");
            }
            break;

        case 'v':
            if (pj_log_get_level() <= 3) {
                pj_log_set_level(5);
                puts("Logging set to detail");
            } else {
                pj_log_set_level(3);
                puts("Logging set to quiet");
            }
            break;

        case 'q':
            done = PJ_TRUE;
            break;
        }
    }

    pj_caching_pool_destroy(&cp);
    pj_shutdown();
    return 0;
}


