/* 
 * 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_imp.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/os.h>
#include <pj/string.h>
#include <pjmedia/event.h>

#if PJMEDIA_AUDIO_DEV_HAS_PORTAUDIO

#include <portaudio.h>

#define THIS_FILE       "pa_dev.c"
#define DRIVER_NAME     "PA"

/* Enable call to PaUtil_SetDebugPrintFunction, but this is not always
 * available across all PortAudio versions (?)
 */
/*#define USE_PA_DEBUG_PRINT */

struct pa_aud_factory
{
    pjmedia_aud_dev_factory      base;
    pj_pool_factory             *pf;
    pj_pool_t                   *pool;
};


/* 
 * Sound stream descriptor.
 * This struct may be used for both unidirectional or bidirectional sound
 * streams.
 */
struct pa_aud_stream
{
    pjmedia_aud_stream   base;

    pj_pool_t           *pool;
    pj_str_t             name;
    pjmedia_dir          dir;
    int                  play_id;
    int                  rec_id;
    int                  bytes_per_sample;
    pj_uint32_t          samples_per_sec;
    unsigned             samples_per_frame;
    int                  channel_count;

    PaStream            *rec_strm;
    PaStream            *play_strm;

    void                *user_data;
    pjmedia_aud_rec_cb   rec_cb;
    pjmedia_aud_play_cb  play_cb;

    pj_timestamp         play_timestamp;
    pj_timestamp         rec_timestamp;
    pj_uint32_t          underflow;
    pj_uint32_t          overflow;

    pj_bool_t            quit_flag;

    pj_bool_t            rec_thread_exited;
    pj_bool_t            rec_thread_initialized;
    pj_thread_desc       rec_thread_desc;
    pj_thread_t         *rec_thread;

    pj_bool_t            play_thread_exited;
    pj_bool_t            play_thread_initialized;
    pj_thread_desc       play_thread_desc;
    pj_thread_t         *play_thread;

    /* Sometime the record callback does not return framesize as configured
     * (e.g: in OSS), while this module must guarantee returning framesize
     * as configured in the creation settings. In this case, we need a buffer 
     * for the recorded samples.
     */
    pj_int16_t          *rec_buf;
    unsigned             rec_buf_count;

    /* Sometime the player callback does not request framesize as configured
     * (e.g: in Linux OSS) while sound device will always get samples from 
     * the other component as many as configured samples_per_frame. 
     */
    pj_int16_t          *play_buf;
    unsigned             play_buf_count;
};


/* Factory prototypes */
static pj_status_t  pa_init(pjmedia_aud_dev_factory *f);
static pj_status_t  pa_destroy(pjmedia_aud_dev_factory *f);
static pj_status_t  pa_refresh(pjmedia_aud_dev_factory *f);
static unsigned     pa_get_dev_count(pjmedia_aud_dev_factory *f);
static pj_status_t  pa_get_dev_info(pjmedia_aud_dev_factory *f, 
                                    unsigned index,
                                    pjmedia_aud_dev_info *info);
static pj_status_t  pa_default_param(pjmedia_aud_dev_factory *f,
                                     unsigned index,
                                     pjmedia_aud_param *param);
static pj_status_t  pa_create_stream(pjmedia_aud_dev_factory *f,
                                     const pjmedia_aud_param *param,
                                     pjmedia_aud_rec_cb rec_cb,
                                     pjmedia_aud_play_cb play_cb,
                                     void *user_data,
                                     pjmedia_aud_stream **p_aud_strm);

/* Stream prototypes */
static pj_status_t strm_get_param(pjmedia_aud_stream *strm,
                                  pjmedia_aud_param *param);
static pj_status_t strm_get_cap(pjmedia_aud_stream *strm,
                                pjmedia_aud_dev_cap cap,
                                void *value);
static pj_status_t strm_set_cap(pjmedia_aud_stream *strm,
                                pjmedia_aud_dev_cap cap,
                                const void *value);
static pj_status_t strm_start(pjmedia_aud_stream *strm);
static pj_status_t strm_stop(pjmedia_aud_stream *strm);
static pj_status_t strm_destroy(pjmedia_aud_stream *strm);


static pjmedia_aud_dev_factory_op pa_op = 
{
    &pa_init,
    &pa_destroy,
    &pa_get_dev_count,
    &pa_get_dev_info,
    &pa_default_param,
    &pa_create_stream,
    &pa_refresh    
};

static pjmedia_aud_stream_op pa_strm_op = 
{
    &strm_get_param,
    &strm_get_cap,
    &strm_set_cap,
    &strm_start,
    &strm_stop,
    &strm_destroy
};



static int PaRecorderCallback(const void *input, 
                              void *output,
                              unsigned long frameCount,
                              const PaStreamCallbackTimeInfo* timeInfo,
                              PaStreamCallbackFlags statusFlags,
                              void *userData )
{
    struct pa_aud_stream *stream = (struct pa_aud_stream*) userData;
    pj_status_t status = 0;
    unsigned nsamples;

    PJ_UNUSED_ARG(output);
    PJ_UNUSED_ARG(timeInfo);

    if (stream->quit_flag)
        goto on_break;

    if (input == NULL)
        return paContinue;

    /* Known cases of callback's thread:
     * - The thread may be changed in the middle of a session, e.g: in MacOS 
     *   it happens when plugging/unplugging headphone.
     * - The same thread may be reused in consecutive sessions. The first
     *   session will leave TLS set, but release the TLS data address,
     *   so the second session must re-register the callback's thread.
     */
    if (stream->rec_thread_initialized == 0 || !pj_thread_is_registered()) 
    {
        pj_bzero(stream->rec_thread_desc, sizeof(pj_thread_desc));
        status = pj_thread_register("pa_rec", stream->rec_thread_desc, 
                                    &stream->rec_thread);
        stream->rec_thread_initialized = 1;
        PJ_LOG(5,(THIS_FILE, "Recorder thread started"));
    }

    if (statusFlags & paInputUnderflow)
        ++stream->underflow;
    if (statusFlags & paInputOverflow)
        ++stream->overflow;

    /* Calculate number of samples we've got */
    nsamples = frameCount * stream->channel_count + stream->rec_buf_count;

    if (nsamples >= stream->samples_per_frame) 
    {
        /* If buffer is not empty, combine the buffer with the just incoming
         * samples, then call put_frame.
         */
        if (stream->rec_buf_count) {
            unsigned chunk_count = 0;
            pjmedia_frame frame;
        
            chunk_count = stream->samples_per_frame - stream->rec_buf_count;
            pjmedia_copy_samples(stream->rec_buf + stream->rec_buf_count,
                                 (pj_int16_t*)input, chunk_count);

            frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
            frame.buf = (void*) stream->rec_buf;
            frame.size = stream->samples_per_frame * stream->bytes_per_sample;
            frame.timestamp.u64 = stream->rec_timestamp.u64;
            frame.bit_info = 0;

            status = (*stream->rec_cb)(stream->user_data, &frame);

            input = (pj_int16_t*) input + chunk_count;
            nsamples -= stream->samples_per_frame;
            stream->rec_buf_count = 0;
            stream->rec_timestamp.u64 += stream->samples_per_frame /
                                         stream->channel_count;
        }

        /* Give all frames we have */
        while (nsamples >= stream->samples_per_frame && status == 0) {
            pjmedia_frame frame;

            frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
            frame.buf = (void*) input;
            frame.size = stream->samples_per_frame * stream->bytes_per_sample;
            frame.timestamp.u64 = stream->rec_timestamp.u64;
            frame.bit_info = 0;

            status = (*stream->rec_cb)(stream->user_data, &frame);

            input = (pj_int16_t*) input + stream->samples_per_frame;
            nsamples -= stream->samples_per_frame;
            stream->rec_timestamp.u64 += stream->samples_per_frame /
                                         stream->channel_count;
        }

        /* Store the remaining samples into the buffer */
        if (nsamples && status == 0) {
            stream->rec_buf_count = nsamples;
            pjmedia_copy_samples(stream->rec_buf, (pj_int16_t*)input, 
                                 nsamples);
        }

    } else {
        /* Not enough samples, let's just store them in the buffer */
        pjmedia_copy_samples(stream->rec_buf + stream->rec_buf_count,
                             (pj_int16_t*)input, 
                             frameCount * stream->channel_count);
        stream->rec_buf_count += frameCount * stream->channel_count;
    }

    if (status==0) 
        return paContinue;

on_break:
    if (status != PJ_SUCCESS && !stream->quit_flag) {
        pjmedia_event e;
        
        PJ_PERROR(3, (THIS_FILE, status, "PortAudio recorder callback stopped due to error"));
        
        /* Broadcast PortAudio recorder error */
        pjmedia_event_init(&e, PJMEDIA_EVENT_AUD_DEV_ERROR,
                          &stream->rec_timestamp, &stream->base);
        e.data.aud_dev_err.dir = PJMEDIA_DIR_CAPTURE;
        e.data.aud_dev_err.status = status;
        e.data.aud_dev_err.id = stream->rec_id;
        
        pjmedia_event_publish(NULL, &stream->base, &e,
                             PJMEDIA_EVENT_PUBLISH_DEFAULT);
    }
    stream->rec_thread_exited = 1;
    return paAbort;
}

static int PaPlayerCallback( const void *input, 
                             void *output,
                             unsigned long frameCount,
                             const PaStreamCallbackTimeInfo* timeInfo,
                             PaStreamCallbackFlags statusFlags,
                             void *userData )
{
    struct pa_aud_stream *stream = (struct pa_aud_stream*) userData;
    pj_status_t status = 0;
    unsigned nsamples_req = frameCount * stream->channel_count;

    PJ_UNUSED_ARG(input);
    PJ_UNUSED_ARG(timeInfo);

    if (stream->quit_flag)
        goto on_break;

    if (output == NULL)
        return paContinue;

    /* Known cases of callback's thread:
     * - The thread may be changed in the middle of a session, e.g: in MacOS 
     *   it happens when plugging/unplugging headphone.
     * - The same thread may be reused in consecutive sessions. The first
     *   session will leave TLS set, but release the TLS data address,
     *   so the second session must re-register the callback's thread.
     */
    if (stream->play_thread_initialized == 0 || !pj_thread_is_registered()) 
    {
        pj_bzero(stream->play_thread_desc, sizeof(pj_thread_desc));
        status = pj_thread_register("portaudio", stream->play_thread_desc,
                                    &stream->play_thread);
        stream->play_thread_initialized = 1;
        PJ_LOG(5,(THIS_FILE, "Player thread started"));
    }

    if (statusFlags & paOutputUnderflow)
        ++stream->underflow;
    if (statusFlags & paOutputOverflow)
        ++stream->overflow;


    /* Check if any buffered samples */
    if (stream->play_buf_count) {
        /* samples buffered >= requested by sound device */
        if (stream->play_buf_count >= nsamples_req) {
            pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, 
                                 nsamples_req);
            stream->play_buf_count -= nsamples_req;
            pjmedia_move_samples(stream->play_buf, 
                                 stream->play_buf + nsamples_req,
                                 stream->play_buf_count);
            nsamples_req = 0;
            
            return paContinue;
        }

        /* samples buffered < requested by sound device */
        pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, 
                             stream->play_buf_count);
        nsamples_req -= stream->play_buf_count;
        output = (pj_int16_t*)output + stream->play_buf_count;
        stream->play_buf_count = 0;
    }

    /* Fill output buffer as requested */
    while (nsamples_req && status == 0) {
        if (nsamples_req >= stream->samples_per_frame) {
            pjmedia_frame frame;

            frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
            frame.buf = output;
            frame.size = stream->samples_per_frame *  stream->bytes_per_sample;
            frame.timestamp.u64 = stream->play_timestamp.u64;
            frame.bit_info = 0;

            status = (*stream->play_cb)(stream->user_data, &frame);
            if (status != PJ_SUCCESS)
                goto on_break;

            if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO)
                pj_bzero(frame.buf, frame.size);

            nsamples_req -= stream->samples_per_frame;
            output = (pj_int16_t*)output + stream->samples_per_frame;
        } else {
            pjmedia_frame frame;

            frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
            frame.buf = stream->play_buf;
            frame.size = stream->samples_per_frame *  stream->bytes_per_sample;
            frame.timestamp.u64 = stream->play_timestamp.u64;
            frame.bit_info = 0;

            status = (*stream->play_cb)(stream->user_data, &frame);
            if (status != PJ_SUCCESS)
                goto on_break;

            if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO)
                pj_bzero(frame.buf, frame.size);

            pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, 
                                 nsamples_req);
            stream->play_buf_count = stream->samples_per_frame - nsamples_req;
            pjmedia_move_samples(stream->play_buf, 
                                 stream->play_buf+nsamples_req,
                                 stream->play_buf_count);
            nsamples_req = 0;
        }

        stream->play_timestamp.u64 += stream->samples_per_frame /
                                      stream->channel_count;
    }
    
    if (status==0) 
        return paContinue;

on_break:
    if (status != PJ_SUCCESS && !stream->quit_flag) {
        pjmedia_event e;
        
        PJ_PERROR(3, (THIS_FILE, status, "PortAudio playback callback stopped due to error"));
        
        /* Broadcast PortAudio playback error */
        pjmedia_event_init(&e, PJMEDIA_EVENT_AUD_DEV_ERROR,
                          &stream->play_timestamp, &stream->base);
        e.data.aud_dev_err.dir = PJMEDIA_DIR_PLAYBACK;
        e.data.aud_dev_err.status = status;
        e.data.aud_dev_err.id = stream->play_id;
        
        pjmedia_event_publish(NULL, &stream->base, &e,
                             PJMEDIA_EVENT_PUBLISH_DEFAULT);
    }
    stream->play_thread_exited = 1;
    return paAbort;
}


static int PaRecorderPlayerCallback( const void *input, 
                                     void *output,
                                     unsigned long frameCount,
                                     const PaStreamCallbackTimeInfo* timeInfo,
                                     PaStreamCallbackFlags statusFlags,
                                     void *userData )
{
    int rc;

    rc = PaRecorderCallback(input, output, frameCount, timeInfo,
                            statusFlags, userData);
    if (rc != paContinue)
        return rc;

    rc = PaPlayerCallback(input, output, frameCount, timeInfo,
                          statusFlags, userData);
    return rc;
}

#ifdef USE_PA_DEBUG_PRINT
/* Logging callback from PA */
static void pa_log_cb(const char *log)
{
    PJ_LOG(5,(THIS_FILE, "PA message: %s", log));
}

/* We should include pa_debugprint.h for this, but the header
 * is not available publicly. :(
 */
typedef void (*PaUtilLogCallback ) (const char *log);
void PaUtil_SetDebugPrintFunction(PaUtilLogCallback  cb);
#endif


/*
 * Init PortAudio audio driver.
 */
pjmedia_aud_dev_factory* pjmedia_pa_factory(pj_pool_factory *pf)
{
    struct pa_aud_factory *f;
    pj_pool_t *pool;

    pool = pj_pool_create(pf, "portaudio", 64, 64, NULL);
    f = PJ_POOL_ZALLOC_T(pool, struct pa_aud_factory);
    f->pf = pf;
    f->pool = pool;
    f->base.op = &pa_op;

    return &f->base;
}


/* API: Init factory */
static pj_status_t pa_init(pjmedia_aud_dev_factory *f)
{
    int err;

    PJ_UNUSED_ARG(f);

#ifdef USE_PA_DEBUG_PRINT
    PaUtil_SetDebugPrintFunction(&pa_log_cb);
#endif

    err = Pa_Initialize();

    PJ_LOG(4,(THIS_FILE, 
              "PortAudio sound library initialized, status=%d", err));
    PJ_LOG(4,(THIS_FILE, "PortAudio host api count=%d",
                         Pa_GetHostApiCount()));
    PJ_LOG(4,(THIS_FILE, "Sound device count=%d",
                         pa_get_dev_count(f)));

    return err ? PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err) : PJ_SUCCESS;
}


/* API: Destroy factory */
static pj_status_t pa_destroy(pjmedia_aud_dev_factory *f)
{
    struct pa_aud_factory *pa = (struct pa_aud_factory*)f;
    pj_pool_t *pool;
    int err;

    PJ_LOG(4,(THIS_FILE, "PortAudio sound library shutting down.."));

    err = Pa_Terminate();

    pool = pa->pool;
    pa->pool = NULL;
    pj_pool_release(pool);
    
    return err ? PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err) : PJ_SUCCESS;
}


/* API: Refresh the device list. */
static pj_status_t pa_refresh(pjmedia_aud_dev_factory *f)
{
    PJ_UNUSED_ARG(f);
    return PJ_ENOTSUP;
}


/* API: Get device count. */
static unsigned pa_get_dev_count(pjmedia_aud_dev_factory *f)
{
    int count = Pa_GetDeviceCount();
    PJ_UNUSED_ARG(f);
    return count < 0 ? 0 : count;
}


/* API: Get device info. */
static pj_status_t  pa_get_dev_info(pjmedia_aud_dev_factory *f, 
                                    unsigned index,
                                    pjmedia_aud_dev_info *info)
{
    const PaDeviceInfo *pa_info;

    PJ_UNUSED_ARG(f);

    pa_info = Pa_GetDeviceInfo(index);
    if (!pa_info)
        return PJMEDIA_EAUD_INVDEV;

    pj_bzero(info, sizeof(*info));
    pj_ansi_strxcpy(info->name, pa_info->name, sizeof(info->name));
    info->input_count = pa_info->maxInputChannels;
    info->output_count = pa_info->maxOutputChannels;
    info->default_samples_per_sec = (unsigned)pa_info->defaultSampleRate;
    pj_ansi_strxcpy(info->driver, DRIVER_NAME, sizeof(info->driver));
    info->caps = PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY |
                 PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY;

    return PJ_SUCCESS;
}


/* API: fill in with default parameter. */
static pj_status_t  pa_default_param(pjmedia_aud_dev_factory *f,
                                     unsigned index,
                                     pjmedia_aud_param *param)
{
    pjmedia_aud_dev_info adi;
    pj_status_t status;

    PJ_UNUSED_ARG(f);

    status = pa_get_dev_info(f, index, &adi);
    if (status != PJ_SUCCESS)
        return status;

    pj_bzero(param, sizeof(*param));
    if (adi.input_count && adi.output_count) {
        param->dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
        param->rec_id = index;
        param->play_id = index;
    } else if (adi.input_count) {
        param->dir = PJMEDIA_DIR_CAPTURE;
        param->rec_id = index;
        param->play_id = PJMEDIA_AUD_INVALID_DEV;
    } else if (adi.output_count) {
        param->dir = PJMEDIA_DIR_PLAYBACK;
        param->play_id = index;
        param->rec_id = PJMEDIA_AUD_INVALID_DEV;
    } else {
        return PJMEDIA_EAUD_INVDEV;
    }

    param->clock_rate = adi.default_samples_per_sec;
    param->channel_count = 1;
    param->samples_per_frame = adi.default_samples_per_sec * 20 / 1000;
    param->bits_per_sample = 16;
    param->flags = adi.caps;
    param->input_latency_ms = PJMEDIA_SND_DEFAULT_REC_LATENCY;
    param->output_latency_ms = PJMEDIA_SND_DEFAULT_PLAY_LATENCY;

    return PJ_SUCCESS;
}


/* Internal: Get PortAudio default input device ID */
static int pa_get_default_input_dev(int channel_count)
{
    int i, count;

    /* Special for Windows - try to use the DirectSound implementation
     * first since it provides better latency.
     */
#if PJMEDIA_PREFER_DIRECT_SOUND
    if (Pa_HostApiTypeIdToHostApiIndex(paDirectSound) >= 0) {
        const PaHostApiInfo *pHI;
        int index = Pa_HostApiTypeIdToHostApiIndex(paDirectSound);
        pHI = Pa_GetHostApiInfo(index);
        if (pHI) {
            const PaDeviceInfo *paDevInfo = NULL;
            paDevInfo = Pa_GetDeviceInfo(pHI->defaultInputDevice);
            if (paDevInfo && paDevInfo->maxInputChannels >= channel_count)
                return pHI->defaultInputDevice;
        }
    }
#endif

    /* Enumerate the host api's for the default devices, and return
     * the device with suitable channels.
     */
    count = Pa_GetHostApiCount();
    for (i=0; i < count; ++i) {
        const PaHostApiInfo *pHAInfo;

        pHAInfo = Pa_GetHostApiInfo(i);
        if (!pHAInfo)
            continue;

        if (pHAInfo->defaultInputDevice >= 0) {
            const PaDeviceInfo *paDevInfo;

            paDevInfo = Pa_GetDeviceInfo(pHAInfo->defaultInputDevice);

            if (paDevInfo->maxInputChannels >= channel_count)
                return pHAInfo->defaultInputDevice;
        }
    }

    /* If still no device is found, enumerate all devices */
    count = Pa_GetDeviceCount();
    for (i=0; i<count; ++i) {
        const PaDeviceInfo *paDevInfo;

        paDevInfo = Pa_GetDeviceInfo(i);
        if (paDevInfo->maxInputChannels >= channel_count)
            return i;
    }
    
    return -1;
}

/* Internal: Get PortAudio default output device ID */
static int pa_get_default_output_dev(int channel_count)
{
    int i, count;

    /* Special for Windows - try to use the DirectSound implementation
     * first since it provides better latency.
     */
#if PJMEDIA_PREFER_DIRECT_SOUND
    if (Pa_HostApiTypeIdToHostApiIndex(paDirectSound) >= 0) {
        const PaHostApiInfo *pHI;
        int index = Pa_HostApiTypeIdToHostApiIndex(paDirectSound);
        pHI = Pa_GetHostApiInfo(index);
        if (pHI) {
            const PaDeviceInfo *paDevInfo = NULL;
            paDevInfo = Pa_GetDeviceInfo(pHI->defaultOutputDevice);
            if (paDevInfo && paDevInfo->maxOutputChannels >= channel_count)
                return pHI->defaultOutputDevice;
        }
    }
#endif

    /* Enumerate the host api's for the default devices, and return
     * the device with suitable channels.
     */
    count = Pa_GetHostApiCount();
    for (i=0; i < count; ++i) {
        const PaHostApiInfo *pHAInfo;

        pHAInfo = Pa_GetHostApiInfo(i);
        if (!pHAInfo)
            continue;

        if (pHAInfo->defaultOutputDevice >= 0) {
            const PaDeviceInfo *paDevInfo;

            paDevInfo = Pa_GetDeviceInfo(pHAInfo->defaultOutputDevice);

            if (paDevInfo->maxOutputChannels >= channel_count)
                return pHAInfo->defaultOutputDevice;
        }
    }

    /* If still no device is found, enumerate all devices */
    count = Pa_GetDeviceCount();
    for (i=0; i<count; ++i) {
        const PaDeviceInfo *paDevInfo;

        paDevInfo = Pa_GetDeviceInfo(i);
        if (paDevInfo->maxOutputChannels >= channel_count)
            return i;
    }

    return -1;
}


/* Internal: create capture/recorder stream */
static pj_status_t create_rec_stream( struct pa_aud_factory *pa,
                                      const pjmedia_aud_param *param,
                                      pjmedia_aud_rec_cb rec_cb,
                                      void *user_data,
                                      pjmedia_aud_stream **p_snd_strm)
{
    pj_pool_t *pool;
    pjmedia_aud_dev_index rec_id;
    struct pa_aud_stream *stream;
    PaStreamParameters inputParam;
    int sampleFormat;
    const PaDeviceInfo *paDevInfo = NULL;
    const PaHostApiInfo *paHostApiInfo = NULL;
    unsigned paFrames, paRate, paLatency;
    const PaStreamInfo *paSI;
    PaError err;

    PJ_ASSERT_RETURN(rec_cb && p_snd_strm, PJ_EINVAL);

    rec_id = param->rec_id;
    if (rec_id < 0) {
        rec_id = pa_get_default_input_dev(param->channel_count);
        if (rec_id < 0) {
            /* No such device. */
            return PJMEDIA_EAUD_NODEFDEV;
        }
    }

    paDevInfo = Pa_GetDeviceInfo(rec_id);
    if (!paDevInfo) {
        /* Assumed it is "No such device" error. */
        return PJMEDIA_EAUD_INVDEV;
    }

    if (param->bits_per_sample == 8)
        sampleFormat = paUInt8;
    else if (param->bits_per_sample == 16)
        sampleFormat = paInt16;
    else if (param->bits_per_sample == 32)
        sampleFormat = paInt32;
    else
        return PJMEDIA_EAUD_SAMPFORMAT;
    
    pool = pj_pool_create(pa->pf, "recstrm", 1024, 1024, NULL);
    if (!pool)
        return PJ_ENOMEM;

    stream = PJ_POOL_ZALLOC_T(pool, struct pa_aud_stream);
    stream->pool = pool;
    pj_strdup2_with_null(pool, &stream->name, paDevInfo->name);
    stream->dir = PJMEDIA_DIR_CAPTURE;
    stream->rec_id = rec_id;
    stream->play_id = -1;
    stream->user_data = user_data;
    stream->samples_per_sec = param->clock_rate;
    stream->samples_per_frame = param->samples_per_frame;
    stream->bytes_per_sample = param->bits_per_sample / 8;
    stream->channel_count = param->channel_count;
    stream->rec_cb = rec_cb;

    stream->rec_buf = (pj_int16_t*)pj_pool_alloc(pool, 
                      stream->samples_per_frame * stream->bytes_per_sample);
    stream->rec_buf_count = 0;

    pj_bzero(&inputParam, sizeof(inputParam));
    inputParam.device = rec_id;
    inputParam.channelCount = param->channel_count;
    inputParam.hostApiSpecificStreamInfo = NULL;
    inputParam.sampleFormat = sampleFormat;
    if (param->flags & PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY)
        inputParam.suggestedLatency = param->input_latency_ms / 1000.0;
    else
        inputParam.suggestedLatency = PJMEDIA_SND_DEFAULT_REC_LATENCY / 1000.0;

    paHostApiInfo = Pa_GetHostApiInfo(paDevInfo->hostApi);

    /* Frames in PortAudio is number of samples in a single channel */
    paFrames = param->samples_per_frame / param->channel_count;

    err = Pa_OpenStream( &stream->rec_strm, &inputParam, NULL,
                         param->clock_rate, paFrames, 
                         paClipOff, &PaRecorderCallback, stream );
    if (err != paNoError) {
        pj_pool_release(pool);
        return PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err);
    }

    paSI = Pa_GetStreamInfo(stream->rec_strm);
    paRate = (unsigned)paSI->sampleRate;
    paLatency = (unsigned)(paSI->inputLatency * 1000);

    PJ_LOG(5,(THIS_FILE, "Opened device %s (%s) for recording, sample "
                         "rate=%d, ch=%d, "
                         "bits=%d, %d samples per frame, latency=%d ms",
                         paDevInfo->name, paHostApiInfo->name,
                         paRate, param->channel_count,
                         param->bits_per_sample, param->samples_per_frame,
                         paLatency));

    *p_snd_strm = &stream->base;
    return PJ_SUCCESS;
}


/* Internal: create playback stream */
static pj_status_t create_play_stream(struct pa_aud_factory *pa,
                                      const pjmedia_aud_param *param,
                                      pjmedia_aud_play_cb play_cb,
                                      void *user_data,
                                      pjmedia_aud_stream **p_snd_strm)
{
    pj_pool_t *pool;
    pjmedia_aud_dev_index play_id;
    struct pa_aud_stream *stream;
    PaStreamParameters outputParam;
    int sampleFormat;
    const PaDeviceInfo *paDevInfo = NULL;
    const PaHostApiInfo *paHostApiInfo = NULL;
    const PaStreamInfo *paSI;
    unsigned paFrames, paRate, paLatency;
    PaError err;

    PJ_ASSERT_RETURN(play_cb && p_snd_strm, PJ_EINVAL);

    play_id = param->play_id;
    if (play_id < 0) {
        play_id = pa_get_default_output_dev(param->channel_count);
        if (play_id < 0) {
            /* No such device. */
            return PJMEDIA_EAUD_NODEFDEV;
        }
    } 

    paDevInfo = Pa_GetDeviceInfo(play_id);
    if (!paDevInfo) {
        /* Assumed it is "No such device" error. */
        return PJMEDIA_EAUD_INVDEV;
    }

    if (param->bits_per_sample == 8)
        sampleFormat = paUInt8;
    else if (param->bits_per_sample == 16)
        sampleFormat = paInt16;
    else if (param->bits_per_sample == 32)
        sampleFormat = paInt32;
    else
        return PJMEDIA_EAUD_SAMPFORMAT;
    
    pool = pj_pool_create(pa->pf, "playstrm", 1024, 1024, NULL);
    if (!pool)
        return PJ_ENOMEM;

    stream = PJ_POOL_ZALLOC_T(pool, struct pa_aud_stream);
    stream->pool = pool;
    pj_strdup2_with_null(pool, &stream->name, paDevInfo->name);
    stream->dir = PJMEDIA_DIR_PLAYBACK;
    stream->play_id = play_id;
    stream->rec_id = -1;
    stream->user_data = user_data;
    stream->samples_per_sec = param->clock_rate;
    stream->samples_per_frame = param->samples_per_frame;
    stream->bytes_per_sample = param->bits_per_sample / 8;
    stream->channel_count = param->channel_count;
    stream->play_cb = play_cb;

    stream->play_buf = (pj_int16_t*)pj_pool_alloc(pool, 
                                            stream->samples_per_frame * 
                                            stream->bytes_per_sample);
    stream->play_buf_count = 0;

    pj_bzero(&outputParam, sizeof(outputParam));
    outputParam.device = play_id;
    outputParam.channelCount = param->channel_count;
    outputParam.hostApiSpecificStreamInfo = NULL;
    outputParam.sampleFormat = sampleFormat;
    if (param->flags & PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY)
        outputParam.suggestedLatency=param->output_latency_ms / 1000.0;
    else
        outputParam.suggestedLatency=PJMEDIA_SND_DEFAULT_PLAY_LATENCY/1000.0;

    paHostApiInfo = Pa_GetHostApiInfo(paDevInfo->hostApi);

    /* Frames in PortAudio is number of samples in a single channel */
    paFrames = param->samples_per_frame / param->channel_count;

    err = Pa_OpenStream( &stream->play_strm, NULL, &outputParam,
                         param->clock_rate,  paFrames, 
                         paClipOff, &PaPlayerCallback, stream );
    if (err != paNoError) {
        pj_pool_release(pool);
        return PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err);
    }

    paSI = Pa_GetStreamInfo(stream->play_strm);
    paRate = (unsigned)(paSI->sampleRate);
    paLatency = (unsigned)(paSI->outputLatency * 1000);

    PJ_LOG(5,(THIS_FILE, "Opened device %d: %s(%s) for playing, sample rate=%d"
                         ", ch=%d, "
                         "bits=%d, %d samples per frame, latency=%d ms",
                         play_id, paDevInfo->name, paHostApiInfo->name, 
                         paRate, param->channel_count,
                         param->bits_per_sample, param->samples_per_frame, 
                         paLatency));

    *p_snd_strm = &stream->base;

    return PJ_SUCCESS;
}


/* Internal: Create both player and recorder stream */
static pj_status_t create_bidir_stream(struct pa_aud_factory *pa,
                                       const pjmedia_aud_param *param,
                                       pjmedia_aud_rec_cb rec_cb,
                                       pjmedia_aud_play_cb play_cb,
                                       void *user_data,
                                       pjmedia_aud_stream **p_snd_strm)
{
    pj_pool_t *pool;
    pjmedia_aud_dev_index rec_id, play_id;
    struct pa_aud_stream *stream;
    PaStream *paStream = NULL;
    PaStreamParameters inputParam;
    PaStreamParameters outputParam;
    int sampleFormat;
    const PaDeviceInfo *paRecDevInfo = NULL;
    const PaDeviceInfo *paPlayDevInfo = NULL;
    const PaHostApiInfo *paRecHostApiInfo = NULL;
    const PaHostApiInfo *paPlayHostApiInfo = NULL;
    const PaStreamInfo *paSI;
    unsigned paFrames, paRate, paInputLatency, paOutputLatency;
    PaError err;

    PJ_ASSERT_RETURN(play_cb && rec_cb && p_snd_strm, PJ_EINVAL);

    rec_id = param->rec_id;
    if (rec_id < 0) {
        rec_id = pa_get_default_input_dev(param->channel_count);
        if (rec_id < 0) {
            /* No such device. */
            return PJMEDIA_EAUD_NODEFDEV;
        }
    }

    paRecDevInfo = Pa_GetDeviceInfo(rec_id);
    if (!paRecDevInfo) {
        /* Assumed it is "No such device" error. */
        return PJMEDIA_EAUD_INVDEV;
    }

    play_id = param->play_id;
    if (play_id < 0) {
        play_id = pa_get_default_output_dev(param->channel_count);
        if (play_id < 0) {
            /* No such device. */
            return PJMEDIA_EAUD_NODEFDEV;
        }
    } 

    paPlayDevInfo = Pa_GetDeviceInfo(play_id);
    if (!paPlayDevInfo) {
        /* Assumed it is "No such device" error. */
        return PJMEDIA_EAUD_INVDEV;
    }


    if (param->bits_per_sample == 8)
        sampleFormat = paUInt8;
    else if (param->bits_per_sample == 16)
        sampleFormat = paInt16;
    else if (param->bits_per_sample == 32)
        sampleFormat = paInt32;
    else
        return PJMEDIA_EAUD_SAMPFORMAT;
    
    pool = pj_pool_create(pa->pf, "sndstream", 1024, 1024, NULL);
    if (!pool)
        return PJ_ENOMEM;

    stream = PJ_POOL_ZALLOC_T(pool, struct pa_aud_stream);
    stream->pool = pool;
    pj_strdup2_with_null(pool, &stream->name, paRecDevInfo->name);
    stream->dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
    stream->play_id = play_id;
    stream->rec_id = rec_id;
    stream->user_data = user_data;
    stream->samples_per_sec = param->clock_rate;
    stream->samples_per_frame = param->samples_per_frame;
    stream->bytes_per_sample = param->bits_per_sample / 8;
    stream->channel_count = param->channel_count;
    stream->rec_cb = rec_cb;
    stream->play_cb = play_cb;

    stream->rec_buf = (pj_int16_t*)pj_pool_alloc(pool, 
                      stream->samples_per_frame * stream->bytes_per_sample);
    stream->rec_buf_count = 0;

    stream->play_buf = (pj_int16_t*)pj_pool_alloc(pool, 
                       stream->samples_per_frame * stream->bytes_per_sample);
    stream->play_buf_count = 0;

    pj_bzero(&inputParam, sizeof(inputParam));
    inputParam.device = rec_id;
    inputParam.channelCount = param->channel_count;
    inputParam.hostApiSpecificStreamInfo = NULL;
    inputParam.sampleFormat = sampleFormat;
    if (param->flags & PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY)
        inputParam.suggestedLatency = param->input_latency_ms / 1000.0;
    else
        inputParam.suggestedLatency = PJMEDIA_SND_DEFAULT_REC_LATENCY / 1000.0;

    paRecHostApiInfo = Pa_GetHostApiInfo(paRecDevInfo->hostApi);

    pj_bzero(&outputParam, sizeof(outputParam));
    outputParam.device = play_id;
    outputParam.channelCount = param->channel_count;
    outputParam.hostApiSpecificStreamInfo = NULL;
    outputParam.sampleFormat = sampleFormat;
    if (param->flags & PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY)
        outputParam.suggestedLatency=param->output_latency_ms / 1000.0;
    else
        outputParam.suggestedLatency=PJMEDIA_SND_DEFAULT_PLAY_LATENCY/1000.0;

    paPlayHostApiInfo = Pa_GetHostApiInfo(paPlayDevInfo->hostApi);

    /* Frames in PortAudio is number of samples in a single channel */
    paFrames = param->samples_per_frame / param->channel_count;

    /* If both input and output are on the same device, open a single stream
     * for both input and output.
     */
    if (rec_id == play_id) {
        err = Pa_OpenStream( &paStream, &inputParam, &outputParam,
                             param->clock_rate, paFrames, 
                             paClipOff, &PaRecorderPlayerCallback, stream );
        if (err == paNoError) {
            /* Set play stream and record stream to the same stream */
            stream->play_strm = stream->rec_strm = paStream;
        }
    } else {
        err = -1;
    }

    /* .. otherwise if input and output are on the same device, OR if we're
     * unable to open a bidirectional stream, then open two separate
     * input and output stream.
     */
    if (paStream == NULL) {
        /* Open input stream */
        err = Pa_OpenStream( &stream->rec_strm, &inputParam, NULL,
                             param->clock_rate, paFrames, 
                             paClipOff, &PaRecorderCallback, stream );
        if (err == paNoError) {
            /* Open output stream */
            err = Pa_OpenStream( &stream->play_strm, NULL, &outputParam,
                                 param->clock_rate, paFrames, 
                                 paClipOff, &PaPlayerCallback, stream );
            if (err != paNoError)
                Pa_CloseStream(stream->rec_strm);
        }
    }

    if (err != paNoError) {
        pj_pool_release(pool);
        return PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err);
    }

    paSI = Pa_GetStreamInfo(stream->rec_strm);
    paRate = (unsigned)(paSI->sampleRate);
    paInputLatency = (unsigned)(paSI->inputLatency * 1000);
    paSI = Pa_GetStreamInfo(stream->play_strm);
    paOutputLatency = (unsigned)(paSI->outputLatency * 1000);

    PJ_LOG(5,(THIS_FILE, "Opened device %s(%s)/%s(%s) for recording and "
                         "playback, sample rate=%d, ch=%d, "
                         "bits=%d, %d samples per frame, input latency=%d ms, "
                         "output latency=%d ms",
                         paRecDevInfo->name, paRecHostApiInfo->name,
                         paPlayDevInfo->name, paPlayHostApiInfo->name,
                         paRate, param->channel_count,
                         param->bits_per_sample, param->samples_per_frame,
                         paInputLatency, paOutputLatency));

    *p_snd_strm = &stream->base;

    return PJ_SUCCESS;
}


/* API: create stream */
static pj_status_t  pa_create_stream(pjmedia_aud_dev_factory *f,
                                     const pjmedia_aud_param *param,
                                     pjmedia_aud_rec_cb rec_cb,
                                     pjmedia_aud_play_cb play_cb,
                                     void *user_data,
                                     pjmedia_aud_stream **p_aud_strm)
{
    struct pa_aud_factory *pa = (struct pa_aud_factory*)f;
    pj_status_t status;

    if (param->dir == PJMEDIA_DIR_CAPTURE) {
        status = create_rec_stream(pa, param, rec_cb, user_data, p_aud_strm);
    } else if (param->dir == PJMEDIA_DIR_PLAYBACK) {
        status = create_play_stream(pa, param, play_cb, user_data, p_aud_strm);
    } else if (param->dir == PJMEDIA_DIR_CAPTURE_PLAYBACK) {
        status = create_bidir_stream(pa, param, rec_cb, play_cb, user_data, 
                                     p_aud_strm);
    } else {
        return PJ_EINVAL;
    }

    if (status != PJ_SUCCESS)
        return status;

    (*p_aud_strm)->op = &pa_strm_op;

    return PJ_SUCCESS;
}


/* API: Get stream parameters */
static pj_status_t strm_get_param(pjmedia_aud_stream *s,
                                  pjmedia_aud_param *pi)
{
    struct pa_aud_stream *strm = (struct pa_aud_stream*)s;
    const PaStreamInfo *paPlaySI = NULL, *paRecSI = NULL;

    PJ_ASSERT_RETURN(strm && pi, PJ_EINVAL);
    PJ_ASSERT_RETURN(strm->play_strm || strm->rec_strm, PJ_EINVALIDOP);

    if (strm->play_strm) {
        paPlaySI = Pa_GetStreamInfo(strm->play_strm);
    }
    if (strm->rec_strm) {
        paRecSI = Pa_GetStreamInfo(strm->rec_strm);
    }

    pj_bzero(pi, sizeof(*pi));
    pi->dir = strm->dir;
    pi->play_id = strm->play_id;
    pi->rec_id = strm->rec_id;
    pi->clock_rate = (unsigned)(paPlaySI ? paPlaySI->sampleRate : 
                                paRecSI->sampleRate);
    pi->channel_count = strm->channel_count;
    pi->samples_per_frame = strm->samples_per_frame;
    pi->bits_per_sample = strm->bytes_per_sample * 8;
    if (paRecSI) {
        pi->flags |= PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY;
        pi->input_latency_ms = (unsigned)(paRecSI ? paRecSI->inputLatency * 
                                                    1000 : 0);
    }
    if (paPlaySI) {
        pi->flags |= PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY;
        pi->output_latency_ms = (unsigned)(paPlaySI? paPlaySI->outputLatency * 
                                                     1000 : 0);
    }

    return PJ_SUCCESS;
}


/* API: get capability */
static pj_status_t strm_get_cap(pjmedia_aud_stream *s,
                                pjmedia_aud_dev_cap cap,
                                void *pval)
{
    struct pa_aud_stream *strm = (struct pa_aud_stream*)s;

    PJ_ASSERT_RETURN(strm && pval, PJ_EINVAL);

    if (cap==PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY && strm->rec_strm) {
        const PaStreamInfo *si = Pa_GetStreamInfo(strm->rec_strm);
        if (!si)
            return PJMEDIA_EAUD_SYSERR;

        *(unsigned*)pval = (unsigned)(si->inputLatency * 1000);
        return PJ_SUCCESS;
    } else if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY && strm->play_strm) {
        const PaStreamInfo *si = Pa_GetStreamInfo(strm->play_strm);
        if (!si)
            return PJMEDIA_EAUD_SYSERR;

        *(unsigned*)pval = (unsigned)(si->outputLatency * 1000);
        return PJ_SUCCESS;
    } else {
        return PJMEDIA_EAUD_INVCAP;
    }
}


/* API: set capability */
static pj_status_t strm_set_cap(pjmedia_aud_stream *strm,
                                pjmedia_aud_dev_cap cap,
                                const void *value)
{
    PJ_UNUSED_ARG(strm);
    PJ_UNUSED_ARG(cap);
    PJ_UNUSED_ARG(value);

    /* Nothing is supported */
    return PJMEDIA_EAUD_INVCAP;
}


/* API: start stream. */
static pj_status_t strm_start(pjmedia_aud_stream *s)
{
    struct pa_aud_stream *stream = (struct pa_aud_stream*)s;
    int err = 0;

    PJ_LOG(5,(THIS_FILE, "Starting %s stream..", stream->name.ptr));

    if (stream->play_strm)
        err = Pa_StartStream(stream->play_strm);

    if (err==0 && stream->rec_strm && stream->rec_strm != stream->play_strm) {
        err = Pa_StartStream(stream->rec_strm);
        if (err != 0)
            Pa_StopStream(stream->play_strm);
    }

    PJ_LOG(5,(THIS_FILE, "Done, status=%d", err));

    return err ? PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err) : PJ_SUCCESS;
}


/* API: stop stream. */
static pj_status_t strm_stop(pjmedia_aud_stream *s)
{
    struct pa_aud_stream *stream = (struct pa_aud_stream*)s;
    int i, err = 0;

    stream->quit_flag = 1;
    for (i=0; !stream->rec_thread_exited && i<100; ++i)
        pj_thread_sleep(10);
    for (i=0; !stream->play_thread_exited && i<100; ++i)
        pj_thread_sleep(10);

    pj_thread_sleep(1);

    PJ_LOG(5,(THIS_FILE, "Stopping stream.."));

    if (stream->play_strm)
        err = Pa_StopStream(stream->play_strm);

    if (stream->rec_strm && stream->rec_strm != stream->play_strm)
        err = Pa_StopStream(stream->rec_strm);

    stream->play_thread_initialized = 0;
    stream->rec_thread_initialized = 0;

    PJ_LOG(5,(THIS_FILE, "Done, status=%d", err));

    return err ? PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err) : PJ_SUCCESS;
}


/* API: destroy stream. */
static pj_status_t strm_destroy(pjmedia_aud_stream *s)
{
    struct pa_aud_stream *stream = (struct pa_aud_stream*)s;
    int i, err = 0;

    stream->quit_flag = 1;
    for (i=0; !stream->rec_thread_exited && i<100; ++i) {
        pj_thread_sleep(1);
    }
    for (i=0; !stream->play_thread_exited && i<100; ++i) {
        pj_thread_sleep(1);
    }

    PJ_LOG(5,(THIS_FILE, "Closing %.*s: %lu underflow, %lu overflow",
                         (int)stream->name.slen,
                         stream->name.ptr,
                         stream->underflow, stream->overflow));

    if (stream->play_strm)
        err = Pa_CloseStream(stream->play_strm);

    if (stream->rec_strm && stream->rec_strm != stream->play_strm)
        err = Pa_CloseStream(stream->rec_strm);

    pj_pool_release(stream->pool);

    return err ? PJMEDIA_AUDIODEV_ERRNO_FROM_PORTAUDIO(err) : PJ_SUCCESS;
}

#endif  /* PJMEDIA_AUDIO_DEV_HAS_PORTAUDIO */

