/* $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/conference.h>
#include <pjmedia/alaw_ulaw.h>
#include <pjmedia/errno.h>
#include <pjmedia/port.h>
#include <pjmedia/silencedet.h>
#include <pjmedia/sound_port.h>
#include <pj/array.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/math.h>
#include <pj/pool.h>
#include <pj/string.h>

#if defined(PJMEDIA_CONF_USE_SWITCH_BOARD) && PJMEDIA_CONF_USE_SWITCH_BOARD!=0

/* CONF_DEBUG enables detailed operation of the conference bridge.
 * Beware that it prints large amounts of logs (several lines per frame).
 */
//#define CONF_DEBUG
#ifdef CONF_DEBUG
#   include <stdio.h>
#   define TRACE_(x)   PJ_LOG(5,x)
#else
#   define TRACE_(x)
#endif

#define THIS_FILE	    "conf_switch.c"

#define SIGNATURE	    PJMEDIA_CONF_SWITCH_SIGNATURE
#define SIGNATURE_PORT	    PJMEDIA_PORT_SIGNATURE('S', 'W', 'T', 'P')
#define NORMAL_LEVEL	    128
#define SLOT_TYPE	    unsigned
#define INVALID_SLOT	    ((SLOT_TYPE)-1)
#define BUFFER_SIZE	    PJMEDIA_CONF_SWITCH_BOARD_BUF_SIZE
#define MAX_LEVEL	    (32767)
#define MIN_LEVEL	    (-32768)

/*
 * DON'T GET CONFUSED WITH TX/RX!!
 *
 * TX and RX directions are always viewed from the conference bridge's point
 * of view, and NOT from the port's point of view. So TX means the bridge
 * is transmitting to the port, RX means the bridge is receiving from the
 * port.
 */


/**
 * This is a port connected to conference bridge.
 */
struct conf_port
{
    SLOT_TYPE		 slot;		/**< Array of listeners.	    */
    pj_str_t		 name;		/**< Port name.			    */
    pjmedia_port	*port;		/**< get_frame() and put_frame()    */
    pjmedia_port_op	 rx_setting;	/**< Can we receive from this port  */
    pjmedia_port_op	 tx_setting;	/**< Can we transmit to this port   */
    unsigned		 listener_cnt;	/**< Number of listeners.	    */
    SLOT_TYPE		*listener_slots;/**< Array of listeners.	    */
    unsigned		 transmitter_cnt;/**<Number of transmitters.	    */

    /* Shortcut for port info. */
    pjmedia_port_info	*info;
    unsigned		 samples_per_frame;

    /* Calculated signal levels: */
    unsigned		 tx_level;	/**< Last tx level to this port.    */
    unsigned		 rx_level;	/**< Last rx level from this port.  */

    /* The normalized signal level adjustment.
     * A value of 128 (NORMAL_LEVEL) means there's no adjustment.
     */
    unsigned		 tx_adj_level;	/**< Adjustment for TX.		    */
    unsigned		 rx_adj_level;	/**< Adjustment for RX.		    */

    pj_timestamp	 ts_clock;
    pj_timestamp	 ts_rx;
    pj_timestamp	 ts_tx;

    /* Tx buffer is a temporary buffer to be used when there's mismatch 
     * between port's ptime with conference's ptime. This buffer is used as 
     * the source to buffer the samples until there are enough samples to 
     * fulfill a complete frame to be transmitted to the port.
     */
    pj_uint8_t		 tx_buf[BUFFER_SIZE]; /**< Tx buffer.		    */
};


/*
 * Conference bridge.
 */
struct pjmedia_conf
{
    unsigned		  options;	/**< Bitmask options.		    */
    unsigned		  max_ports;	/**< Maximum ports.		    */
    unsigned		  port_cnt;	/**< Current number of ports.	    */
    unsigned		  connect_cnt;	/**< Total number of connections    */
    pjmedia_port	 *master_port;	/**< Port zero's port.		    */
    char		  master_name_buf[80]; /**< Port0 name buffer.	    */
    pj_mutex_t		 *mutex;	/**< Conference mutex.		    */
    struct conf_port	**ports;	/**< Array of ports.		    */
    pj_uint8_t		  buf[BUFFER_SIZE];	/**< Common buffer.	    */
};


/* Prototypes */
static pj_status_t put_frame(pjmedia_port *this_port, 
			     pjmedia_frame *frame);
static pj_status_t get_frame(pjmedia_port *this_port, 
			     pjmedia_frame *frame);
static pj_status_t destroy_port(pjmedia_port *this_port);


/*
 * Create port.
 */
static pj_status_t create_conf_port( pj_pool_t *pool,
				     pjmedia_conf *conf,
				     pjmedia_port *port,
				     const pj_str_t *name,
				     struct conf_port **p_conf_port)
{
    struct conf_port *conf_port;
    pjmedia_frame *f;

    PJ_ASSERT_RETURN(pool && conf && port && name && p_conf_port, PJ_EINVAL);

    /* Check port's buffer size */
    if (port->info.fmt.id == PJMEDIA_FORMAT_PCM &&
	PJMEDIA_PIA_SPF(&port->info)*2 > BUFFER_SIZE - sizeof(pjmedia_frame))
    {
	pj_assert(!"Too small buffer size for audio switchboard. "
		   "Try increase PJMEDIA_CONF_SWITCH_BOARD_BUF_SIZE");
	return PJ_ETOOSMALL;
    }

    /* Create port. */
    conf_port = PJ_POOL_ZALLOC_T(pool, struct conf_port);

    /* Set name */
    pj_strdup_with_null(pool, &conf_port->name, name);

    /* Default has tx and rx enabled. */
    conf_port->rx_setting = PJMEDIA_PORT_ENABLE;
    conf_port->tx_setting = PJMEDIA_PORT_ENABLE;

    /* Default level adjustment is 128 (which means no adjustment) */
    conf_port->tx_adj_level = NORMAL_LEVEL;
    conf_port->rx_adj_level = NORMAL_LEVEL;

    /* Create transmit flag array */
    conf_port->listener_slots = (SLOT_TYPE*)
				pj_pool_zalloc(pool, 
					  conf->max_ports * sizeof(SLOT_TYPE));
    PJ_ASSERT_RETURN(conf_port->listener_slots, PJ_ENOMEM);

    /* Save some port's infos, for convenience. */
    conf_port->port = port;
    conf_port->info = &port->info;
    conf_port->samples_per_frame = PJMEDIA_PIA_SPF(&port->info);

    /* Init pjmedia_frame structure in the TX buffer. */
    f = (pjmedia_frame*)conf_port->tx_buf;
    f->buf = conf_port->tx_buf + sizeof(pjmedia_frame);

    /* Done */
    *p_conf_port = conf_port;
    return PJ_SUCCESS;
}

/*
 * Create port zero for the sound device.
 */
static pj_status_t create_sound_port( pj_pool_t *pool,
				      pjmedia_conf *conf )
{
    struct conf_port *conf_port;
    pj_str_t name = { "Master/sound", 12 };
    pj_status_t status;

    status = create_conf_port(pool, conf, conf->master_port, &name, &conf_port);
    if (status != PJ_SUCCESS)
	return status;

     /* Add the port to the bridge */
    conf_port->slot = 0;
    conf->ports[0] = conf_port;
    conf->port_cnt++;

    PJ_LOG(5,(THIS_FILE, "Sound device successfully created for port 0"));
    return PJ_SUCCESS;
}

/*
 * Create conference bridge.
 */
PJ_DEF(pj_status_t) pjmedia_conf_create( pj_pool_t *pool,
					 unsigned max_ports,
					 unsigned clock_rate,
					 unsigned channel_count,
					 unsigned samples_per_frame,
					 unsigned bits_per_sample,
					 unsigned options,
					 pjmedia_conf **p_conf )
{
    pjmedia_conf *conf;
    const pj_str_t name = { "Conf", 4 };
    pj_status_t status;

    /* Can only accept 16bits per sample, for now.. */
    PJ_ASSERT_RETURN(bits_per_sample == 16, PJ_EINVAL);

    PJ_LOG(5,(THIS_FILE, "Creating conference bridge with %d ports",
	      max_ports));

    /* Create and init conf structure. */
    conf = PJ_POOL_ZALLOC_T(pool, pjmedia_conf);
    PJ_ASSERT_RETURN(conf, PJ_ENOMEM);

    conf->ports = (struct conf_port**) 
		  pj_pool_zalloc(pool, max_ports*sizeof(void*));
    PJ_ASSERT_RETURN(conf->ports, PJ_ENOMEM);

    conf->options = options;
    conf->max_ports = max_ports;
    
    /* Create and initialize the master port interface. */
    conf->master_port = PJ_POOL_ZALLOC_T(pool, pjmedia_port);
    PJ_ASSERT_RETURN(conf->master_port, PJ_ENOMEM);
    
    pjmedia_port_info_init(&conf->master_port->info, &name, SIGNATURE,
			   clock_rate, channel_count, bits_per_sample,
			   samples_per_frame);

    conf->master_port->port_data.pdata = conf;
    conf->master_port->port_data.ldata = 0;

    conf->master_port->get_frame = &get_frame;
    conf->master_port->put_frame = &put_frame;
    conf->master_port->on_destroy = &destroy_port;


    /* Create port zero for sound device. */
    status = create_sound_port(pool, conf);
    if (status != PJ_SUCCESS)
	return status;

    /* Create mutex. */
    status = pj_mutex_create_recursive(pool, "conf", &conf->mutex);
    if (status != PJ_SUCCESS)
	return status;

    /* Done */

    *p_conf = conf;

    return PJ_SUCCESS;
}


/*
 * Pause sound device.
 */
static pj_status_t pause_sound( pjmedia_conf *conf )
{
    /* Do nothing. */
    PJ_UNUSED_ARG(conf);
    return PJ_SUCCESS;
}

/*
 * Resume sound device.
 */
static pj_status_t resume_sound( pjmedia_conf *conf )
{
    /* Do nothing. */
    PJ_UNUSED_ARG(conf);
    return PJ_SUCCESS;
}


/**
 * Destroy conference bridge.
 */
PJ_DEF(pj_status_t) pjmedia_conf_destroy( pjmedia_conf *conf )
{
    PJ_ASSERT_RETURN(conf != NULL, PJ_EINVAL);

    /* Destroy mutex */
    pj_mutex_destroy(conf->mutex);

    return PJ_SUCCESS;
}


/*
 * Destroy the master port (will destroy the conference)
 */
static pj_status_t destroy_port(pjmedia_port *this_port)
{
    pjmedia_conf *conf = (pjmedia_conf*) this_port->port_data.pdata;
    return pjmedia_conf_destroy(conf);
}

/*
 * Get port zero interface.
 */
PJ_DEF(pjmedia_port*) pjmedia_conf_get_master_port(pjmedia_conf *conf)
{
    /* Sanity check. */
    PJ_ASSERT_RETURN(conf != NULL, NULL);

    /* Can only return port interface when PJMEDIA_CONF_NO_DEVICE was
     * present in the option.
     */
    PJ_ASSERT_RETURN((conf->options & PJMEDIA_CONF_NO_DEVICE) != 0, NULL);
    
    return conf->master_port;
}


/*
 * Set master port name.
 */
PJ_DEF(pj_status_t) pjmedia_conf_set_port0_name(pjmedia_conf *conf,
						const pj_str_t *name)
{
    unsigned len;

    /* Sanity check. */
    PJ_ASSERT_RETURN(conf != NULL && name != NULL, PJ_EINVAL);

    len = name->slen;
    if (len > sizeof(conf->master_name_buf))
	len = sizeof(conf->master_name_buf);
    
    if (len > 0) pj_memcpy(conf->master_name_buf, name->ptr, len);

    conf->ports[0]->name.ptr = conf->master_name_buf;
    conf->ports[0]->name.slen = len;

    conf->master_port->info.name = conf->ports[0]->name;

    return PJ_SUCCESS;
}

/*
 * Add stream port to the conference bridge.
 */
PJ_DEF(pj_status_t) pjmedia_conf_add_port( pjmedia_conf *conf,
					   pj_pool_t *pool,
					   pjmedia_port *strm_port,
					   const pj_str_t *port_name,
					   unsigned *p_port )
{
    struct conf_port *conf_port;
    unsigned index;
    pj_status_t status;

    PJ_ASSERT_RETURN(conf && pool && strm_port, PJ_EINVAL);
    /*
    PJ_ASSERT_RETURN(conf->clock_rate == strm_port->info.clock_rate, 
		     PJMEDIA_ENCCLOCKRATE);
    PJ_ASSERT_RETURN(conf->channel_count == strm_port->info.channel_count, 
		     PJMEDIA_ENCCHANNEL);
    PJ_ASSERT_RETURN(conf->bits_per_sample == strm_port->info.bits_per_sample,
		     PJMEDIA_ENCBITS);
    */

    /* Port's samples per frame should be equal to or multiplication of 
     * conference's samples per frame.
     */
    /*
    Not sure if this is needed!
    PJ_ASSERT_RETURN((conf->samples_per_frame %
		     strm_port->info.samples_per_frame==0) ||
		     (strm_port->info.samples_per_frame %
		     conf->samples_per_frame==0),
		     PJMEDIA_ENCSAMPLESPFRAME);
    */

    /* If port_name is not specified, use the port's name */
    if (!port_name)
	port_name = &strm_port->info.name;

    pj_mutex_lock(conf->mutex);

    if (conf->port_cnt >= conf->max_ports) {
	pj_assert(!"Too many ports");
	pj_mutex_unlock(conf->mutex);
	return PJ_ETOOMANY;
    }

    /* Find empty port in the conference bridge. */
    for (index=0; index < conf->max_ports; ++index) {
	if (conf->ports[index] == NULL)
	    break;
    }

    pj_assert(index != conf->max_ports);

    /* Create conf port structure. */
    status = create_conf_port(pool, conf, strm_port, port_name, &conf_port);
    if (status != PJ_SUCCESS) {
	pj_mutex_unlock(conf->mutex);
	return status;
    }

    /* Put the port. */
    conf_port->slot = index;
    conf->ports[index] = conf_port;
    conf->port_cnt++;

    /* Done. */
    if (p_port) {
	*p_port = index;
    }

    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}


/*
 * Add passive port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_add_passive_port( pjmedia_conf *conf,
						   pj_pool_t *pool,
						   const pj_str_t *name,
						   unsigned clock_rate,
						   unsigned channel_count,
						   unsigned samples_per_frame,
						   unsigned bits_per_sample,
						   unsigned options,
						   unsigned *p_slot,
						   pjmedia_port **p_port )
{
    PJ_UNUSED_ARG(conf);
    PJ_UNUSED_ARG(pool);
    PJ_UNUSED_ARG(name);
    PJ_UNUSED_ARG(clock_rate);
    PJ_UNUSED_ARG(channel_count);
    PJ_UNUSED_ARG(samples_per_frame);
    PJ_UNUSED_ARG(bits_per_sample);
    PJ_UNUSED_ARG(options);
    PJ_UNUSED_ARG(p_slot);
    PJ_UNUSED_ARG(p_port);

    return PJ_ENOTSUP;
}



/*
 * Change TX and RX settings for the port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_configure_port( pjmedia_conf *conf,
						  unsigned slot,
						  pjmedia_port_op tx,
						  pjmedia_port_op rx)
{
    struct conf_port *conf_port;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && slot<conf->max_ports, PJ_EINVAL);

    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[slot];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    if (tx != PJMEDIA_PORT_NO_CHANGE)
	conf_port->tx_setting = tx;

    if (rx != PJMEDIA_PORT_NO_CHANGE)
	conf_port->rx_setting = rx;

    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}


/*
 * Connect port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_connect_port( pjmedia_conf *conf,
					       unsigned src_slot,
					       unsigned sink_slot,
					       int level )
{
    struct conf_port *src_port, *dst_port;
    pj_bool_t start_sound = PJ_FALSE;
    pjmedia_audio_format_detail *src_afd, *dst_afd;
    unsigned i;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && src_slot<conf->max_ports && 
		     sink_slot<conf->max_ports, PJ_EINVAL);

    /* For now, level MUST be zero. */
    PJ_ASSERT_RETURN(level == 0, PJ_EINVAL);

    pj_mutex_lock(conf->mutex);

    /* Ports must be valid. */
    src_port = conf->ports[src_slot];
    dst_port = conf->ports[sink_slot];
    if (!src_port || !dst_port) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    src_afd = pjmedia_format_get_audio_format_detail(&src_port->info->fmt, 1);
    dst_afd = pjmedia_format_get_audio_format_detail(&dst_port->info->fmt, 1);

    /* Format must match. */
    if (src_port->info->fmt.id != dst_port->info->fmt.id ||
	src_afd->avg_bps != dst_afd->avg_bps)
    {
	pj_mutex_unlock(conf->mutex);
	return PJMEDIA_ENOTCOMPATIBLE;
    }

    /* Clock rate must match. */
    if (src_afd->clock_rate != dst_afd->clock_rate) {
	pj_mutex_unlock(conf->mutex);
	return PJMEDIA_ENCCLOCKRATE;
    }

    /* Channel count must match. */
    if (src_afd->channel_count != dst_afd->channel_count) {
	pj_mutex_unlock(conf->mutex);
	return PJMEDIA_ENCCHANNEL;
    }

    /* Source and sink ptime must be equal or a multiplication factor. */
    if ((src_afd->frame_time_usec % dst_afd->frame_time_usec != 0) &&
        (dst_afd->frame_time_usec % src_afd->frame_time_usec != 0))
    {
	pj_mutex_unlock(conf->mutex);
	return PJMEDIA_ENCSAMPLESPFRAME;
    }
    
    /* If sink is currently listening to other ports, it needs to be released
     * first before the new connection made.
     */ 
    if (dst_port->transmitter_cnt > 0) {
	unsigned j;
	pj_bool_t transmitter_found = PJ_FALSE;

	pj_assert(dst_port->transmitter_cnt == 1);
	for (j=0; j<conf->max_ports && !transmitter_found; ++j) {
	    if (conf->ports[j]) {
		unsigned k;

		for (k=0; k < conf->ports[j]->listener_cnt; ++k) {
		    if (conf->ports[j]->listener_slots[k] == sink_slot) {
			PJ_LOG(2,(THIS_FILE, "Connection [%d->%d] is "
				  "disconnected for new connection [%d->%d]",
				  j, sink_slot, src_slot, sink_slot));
			pjmedia_conf_disconnect_port(conf, j, sink_slot);
			transmitter_found = PJ_TRUE;
			break;
		    }
		}
	    }
	}
	pj_assert(dst_port->transmitter_cnt == 0);
    }

    /* Check if connection has been made */
    for (i=0; i<src_port->listener_cnt; ++i) {
	if (src_port->listener_slots[i] == sink_slot)
	    break;
    }

    /* Update master port info shortcut, note that application may update
     * the master port info when the audio device needs to be reopened with
     * a new format to match to ports connection format.
     */
    conf->ports[0]->samples_per_frame = PJMEDIA_PIA_SPF(conf->ports[0]->info);

    if (i == src_port->listener_cnt) {
	src_port->listener_slots[src_port->listener_cnt] = sink_slot;
	++conf->connect_cnt;
	++src_port->listener_cnt;
	++dst_port->transmitter_cnt;

	if (conf->connect_cnt == 1)
	    start_sound = 1;

	PJ_LOG(4,(THIS_FILE,"Port %d (%.*s) transmitting to port %d (%.*s)",
		  src_slot,
		  (int)src_port->name.slen,
		  src_port->name.ptr,
		  sink_slot,
		  (int)dst_port->name.slen,
		  dst_port->name.ptr));
    }

    pj_mutex_unlock(conf->mutex);

    /* Sound device must be started without mutex, otherwise the
     * sound thread will deadlock (?)
     */
    if (start_sound)
	resume_sound(conf);

    return PJ_SUCCESS;
}


/*
 * Disconnect port
 */
PJ_DEF(pj_status_t) pjmedia_conf_disconnect_port( pjmedia_conf *conf,
						  unsigned src_slot,
						  unsigned sink_slot )
{
    struct conf_port *src_port, *dst_port;
    unsigned i;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && src_slot<conf->max_ports && 
		     sink_slot<conf->max_ports, PJ_EINVAL);

    pj_mutex_lock(conf->mutex);

    /* Ports must be valid. */
    src_port = conf->ports[src_slot];
    dst_port = conf->ports[sink_slot];
    if (!src_port || !dst_port) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    /* Check if connection has been made */
    for (i=0; i<src_port->listener_cnt; ++i) {
	if (src_port->listener_slots[i] == sink_slot)
	    break;
    }

    if (i != src_port->listener_cnt) {
	pjmedia_frame_ext *f;

	pj_assert(src_port->listener_cnt > 0 && 
		  src_port->listener_cnt < conf->max_ports);
	pj_assert(dst_port->transmitter_cnt > 0 && 
		  dst_port->transmitter_cnt < conf->max_ports);
	pj_array_erase(src_port->listener_slots, sizeof(SLOT_TYPE), 
		       src_port->listener_cnt, i);
	--conf->connect_cnt;
	--src_port->listener_cnt;
	--dst_port->transmitter_cnt;
	
	/* Cleanup listener TX buffer. */
	f = (pjmedia_frame_ext*)dst_port->tx_buf;
	f->base.type = PJMEDIA_FRAME_TYPE_NONE;
	f->base.size = 0;
	f->samples_cnt = 0;
	f->subframe_cnt = 0;

	PJ_LOG(4,(THIS_FILE,
		  "Port %d (%.*s) stop transmitting to port %d (%.*s)",
		  src_slot,
		  (int)src_port->name.slen,
		  src_port->name.ptr,
		  sink_slot,
		  (int)dst_port->name.slen,
		  dst_port->name.ptr));
    }

    pj_mutex_unlock(conf->mutex);

    if (conf->connect_cnt == 0) {
	pause_sound(conf);
    }

    return PJ_SUCCESS;
}

/*
 * Get number of ports currently registered to the conference bridge.
 */
PJ_DEF(unsigned) pjmedia_conf_get_port_count(pjmedia_conf *conf)
{
    return conf->port_cnt;
}

/*
 * Get total number of ports connections currently set up in the bridge.
 */
PJ_DEF(unsigned) pjmedia_conf_get_connect_count(pjmedia_conf *conf)
{
    return conf->connect_cnt;
}


/*
 * Remove the specified port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_remove_port( pjmedia_conf *conf,
					      unsigned port )
{
    struct conf_port *conf_port;
    unsigned i;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && port < conf->max_ports, PJ_EINVAL);

    /* Suspend the sound devices.
     * Don't want to remove port while port is being accessed by sound
     * device's threads!
     */

    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[port];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    conf_port->tx_setting = PJMEDIA_PORT_DISABLE;
    conf_port->rx_setting = PJMEDIA_PORT_DISABLE;

    /* Remove this port from transmit array of other ports. */
    for (i=0; i<conf->max_ports; ++i) {
	unsigned j;
	struct conf_port *src_port;

	src_port = conf->ports[i];

	if (!src_port)
	    continue;

	if (src_port->listener_cnt == 0)
	    continue;

	for (j=0; j<src_port->listener_cnt; ++j) {
	    if (src_port->listener_slots[j] == port) {
		pj_array_erase(src_port->listener_slots, sizeof(SLOT_TYPE),
			       src_port->listener_cnt, j);
		pj_assert(conf->connect_cnt > 0);
		--conf->connect_cnt;
		--src_port->listener_cnt;
		break;
	    }
	}
    }

    /* Update transmitter_cnt of ports we're transmitting to */
    while (conf_port->listener_cnt) {
	unsigned dst_slot;
	struct conf_port *dst_port;
	pjmedia_frame_ext *f;

	dst_slot = conf_port->listener_slots[conf_port->listener_cnt-1];
	dst_port = conf->ports[dst_slot];
	--dst_port->transmitter_cnt;
	--conf_port->listener_cnt;
	pj_assert(conf->connect_cnt > 0);
	--conf->connect_cnt;

	/* Cleanup & reinit listener TX buffer. */
	f = (pjmedia_frame_ext*)dst_port->tx_buf;
	f->base.type = PJMEDIA_FRAME_TYPE_NONE;
	f->base.size = 0;
	f->samples_cnt = 0;
	f->subframe_cnt = 0;
    }

    /* Remove the port. */
    conf->ports[port] = NULL;
    --conf->port_cnt;

    pj_mutex_unlock(conf->mutex);


    /* Stop sound if there's no connection. */
    if (conf->connect_cnt == 0) {
	pause_sound(conf);
    }

    return PJ_SUCCESS;
}


/*
 * Enum ports.
 */
PJ_DEF(pj_status_t) pjmedia_conf_enum_ports( pjmedia_conf *conf,
					     unsigned ports[],
					     unsigned *p_count )
{
    unsigned i, count=0;

    PJ_ASSERT_RETURN(conf && p_count && ports, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    for (i=0; i<conf->max_ports && count<*p_count; ++i) {
	if (!conf->ports[i])
	    continue;

	ports[count++] = i;
    }

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    *p_count = count;
    return PJ_SUCCESS;
}

/*
 * Get port info
 */
PJ_DEF(pj_status_t) pjmedia_conf_get_port_info( pjmedia_conf *conf,
						unsigned slot,
						pjmedia_conf_port_info *info)
{
    struct conf_port *conf_port;
    const pjmedia_audio_format_detail *afd;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && slot<conf->max_ports, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[slot];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    afd = pjmedia_format_get_audio_format_detail(&conf_port->info->fmt, 1);

    pj_bzero(info, sizeof(pjmedia_conf_port_info));

    info->slot = slot;
    info->name = conf_port->name;
    info->tx_setting = conf_port->tx_setting;
    info->rx_setting = conf_port->rx_setting;
    info->listener_cnt = conf_port->listener_cnt;
    info->listener_slots = conf_port->listener_slots;
    info->transmitter_cnt = conf_port->transmitter_cnt;
    info->clock_rate = afd->clock_rate;
    info->channel_count = afd->channel_count;
    info->samples_per_frame = conf_port->samples_per_frame;
    info->bits_per_sample = afd->bits_per_sample;
    info->format = conf_port->port->info.fmt;
    info->tx_adj_level = conf_port->tx_adj_level - NORMAL_LEVEL;
    info->rx_adj_level = conf_port->rx_adj_level - NORMAL_LEVEL;

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pjmedia_conf_get_ports_info(pjmedia_conf *conf,
						unsigned *size,
						pjmedia_conf_port_info info[])
{
    unsigned i, count=0;

    PJ_ASSERT_RETURN(conf && size && info, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    for (i=0; i<conf->max_ports && count<*size; ++i) {
	if (!conf->ports[i])
	    continue;

	pjmedia_conf_get_port_info(conf, i, &info[count]);
	++count;
    }

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    *size = count;
    return PJ_SUCCESS;
}


/*
 * Get signal level.
 */
PJ_DEF(pj_status_t) pjmedia_conf_get_signal_level( pjmedia_conf *conf,
						   unsigned slot,
						   unsigned *tx_level,
						   unsigned *rx_level)
{
    struct conf_port *conf_port;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && slot<conf->max_ports, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[slot];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    if (tx_level != NULL) {
	*tx_level = conf_port->tx_level;
    }

    if (rx_level != NULL) 
	*rx_level = conf_port->rx_level;

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}


/*
 * Adjust RX level of individual port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_adjust_rx_level( pjmedia_conf *conf,
						  unsigned slot,
						  int adj_level )
{
    struct conf_port *conf_port;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && slot<conf->max_ports, PJ_EINVAL);

    /* Value must be from -128 to +127 */
    /* Disabled, you can put more than +127, at your own risk: 
     PJ_ASSERT_RETURN(adj_level >= -128 && adj_level <= 127, PJ_EINVAL);
     */
    PJ_ASSERT_RETURN(adj_level >= -128, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[slot];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    /* Level adjustment is applicable only for ports that work with raw PCM. */
    PJ_ASSERT_RETURN(conf_port->info->fmt.id == PJMEDIA_FORMAT_L16,
		     PJ_EIGNORED);

    /* Set normalized adjustment level. */
    conf_port->rx_adj_level = adj_level + NORMAL_LEVEL;

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}


/*
 * Adjust TX level of individual port.
 */
PJ_DEF(pj_status_t) pjmedia_conf_adjust_tx_level( pjmedia_conf *conf,
						  unsigned slot,
						  int adj_level )
{
    struct conf_port *conf_port;

    /* Check arguments */
    PJ_ASSERT_RETURN(conf && slot<conf->max_ports, PJ_EINVAL);

    /* Value must be from -128 to +127 */
    /* Disabled, you can put more than +127,, at your own risk:
     PJ_ASSERT_RETURN(adj_level >= -128 && adj_level <= 127, PJ_EINVAL);
     */
    PJ_ASSERT_RETURN(adj_level >= -128, PJ_EINVAL);

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Port must be valid. */
    conf_port = conf->ports[slot];
    if (conf_port == NULL) {
	pj_mutex_unlock(conf->mutex);
	return PJ_EINVAL;
    }

    /* Level adjustment is applicable only for ports that work with raw PCM. */
    PJ_ASSERT_RETURN(conf_port->info->fmt.id == PJMEDIA_FORMAT_L16,
		     PJ_EIGNORED);

    /* Set normalized adjustment level. */
    conf_port->tx_adj_level = adj_level + NORMAL_LEVEL;

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}

/* Deliver frm_src to a listener port, eventually call  port's put_frame() 
 * when samples count in the frm_dst are equal to port's samples_per_frame.
 */
static pj_status_t write_frame(struct conf_port *cport_dst,
			       const pjmedia_frame *frm_src)
{
    pjmedia_frame *frm_dst = (pjmedia_frame*)cport_dst->tx_buf;
    
    PJ_TODO(MAKE_SURE_DEST_FRAME_HAS_ENOUGH_SPACE);

    frm_dst->type = frm_src->type;
    frm_dst->timestamp = cport_dst->ts_tx;

    if (frm_src->type == PJMEDIA_FRAME_TYPE_EXTENDED) {

	pjmedia_frame_ext *f_src = (pjmedia_frame_ext*)frm_src;
	pjmedia_frame_ext *f_dst = (pjmedia_frame_ext*)frm_dst;
	unsigned i;

	for (i = 0; i < f_src->subframe_cnt; ++i) {
	    pjmedia_frame_ext_subframe *sf;
	    
	    /* Copy frame to listener's TX buffer. */
	    sf = pjmedia_frame_ext_get_subframe(f_src, i);
	    pjmedia_frame_ext_append_subframe(f_dst, sf->data, sf->bitlen, 
					      f_src->samples_cnt / 
					      f_src->subframe_cnt);

	    /* Check if it's time to deliver the TX buffer to listener, 
	     * i.e: samples count in TX buffer equal to listener's
	     * samples per frame.
	     */
	    if (f_dst->samples_cnt >= cport_dst->samples_per_frame)
	    {
		if (cport_dst->slot) {
		    pjmedia_port_put_frame(cport_dst->port, 
					   (pjmedia_frame*)f_dst);

		    /* Reset TX buffer. */
		    f_dst->subframe_cnt = 0;
		    f_dst->samples_cnt = 0;
		}

		/* Update TX timestamp. */
		pj_add_timestamp32(&cport_dst->ts_tx,
				   cport_dst->samples_per_frame);
	    }
	}

    } else if (frm_src->type == PJMEDIA_FRAME_TYPE_AUDIO) {

	pj_int16_t *f_start, *f_end;

	f_start = (pj_int16_t*)frm_src->buf;
	f_end   = f_start + (frm_src->size >> 1);

	while (f_start < f_end) {
	    unsigned nsamples_to_copy, nsamples_req;

	    /* Copy frame to listener's TX buffer.
	     * Note that if the destination is port 0, just copy the whole
	     * available samples.
	     */
	    nsamples_to_copy = f_end - f_start;
	    nsamples_req = cport_dst->samples_per_frame -
			  (frm_dst->size>>1);
	    if (cport_dst->slot && nsamples_to_copy > nsamples_req)
		nsamples_to_copy = nsamples_req;

	    /* Adjust TX level. */
	    if (cport_dst->tx_adj_level != NORMAL_LEVEL) {
		pj_int16_t *p, *p_end;

		p = f_start;
		p_end = p + nsamples_to_copy;
		while (p < p_end) {
		    pj_int32_t itemp = *p;

		    /* Adjust the level */
		    itemp = (itemp * cport_dst->tx_adj_level) >> 7;

		    /* Clip the signal if it's too loud */
		    if (itemp > MAX_LEVEL) itemp = MAX_LEVEL;
		    else if (itemp < MIN_LEVEL) itemp = MIN_LEVEL;

		    /* Put back in the buffer. */
		    *p = (pj_int16_t)itemp;
		    ++p;
		}
	    }

	    pjmedia_copy_samples((pj_int16_t*)frm_dst->buf + (frm_dst->size>>1),
				 f_start, 
				 nsamples_to_copy);
	    frm_dst->size += nsamples_to_copy << 1;
	    f_start += nsamples_to_copy;

	    /* Check if it's time to deliver the TX buffer to listener, 
	     * i.e: samples count in TX buffer equal to listener's
	     * samples per frame. Note that for destination port 0 this
	     * function will just populate all samples in the TX buffer.
	     */
	    if (cport_dst->slot == 0) {
		/* Update TX timestamp. */
		pj_add_timestamp32(&cport_dst->ts_tx, nsamples_to_copy);
	    } else if ((frm_dst->size >> 1) == 
		       cport_dst->samples_per_frame)
	    {
		pjmedia_port_put_frame(cport_dst->port, frm_dst);

		/* Reset TX buffer. */
		frm_dst->size = 0;

		/* Update TX timestamp. */
		pj_add_timestamp32(&cport_dst->ts_tx, 
				   cport_dst->samples_per_frame);
	    }
	}

    } else if (frm_src->type == PJMEDIA_FRAME_TYPE_NONE) {

	/* Check port format. */
	if (cport_dst->port &&
	    cport_dst->port->info.fmt.id == PJMEDIA_FORMAT_L16)
	{
	    /* When there is already some samples in listener's TX buffer, 
	     * pad the buffer with "zero samples".
	     */
	    if (frm_dst->size != 0) {
		pjmedia_zero_samples((pj_int16_t*)frm_dst->buf,
				     cport_dst->samples_per_frame -
				     (frm_dst->size>>1));

		frm_dst->type = PJMEDIA_FRAME_TYPE_AUDIO;
		frm_dst->size = cport_dst->samples_per_frame << 1;
		if (cport_dst->slot) {
		    pjmedia_port_put_frame(cport_dst->port, frm_dst);

		    /* Reset TX buffer. */
		    frm_dst->size = 0;
		}

		/* Update TX timestamp. */
		pj_add_timestamp32(&cport_dst->ts_tx, 
				   cport_dst->samples_per_frame);
	    }
	} else {
	    pjmedia_frame_ext *f_dst = (pjmedia_frame_ext*)frm_dst;

	    if (f_dst->samples_cnt != 0) {
		frm_dst->type = PJMEDIA_FRAME_TYPE_EXTENDED;
		pjmedia_frame_ext_append_subframe(f_dst, NULL, 0, (pj_uint16_t)
		    (cport_dst->samples_per_frame - f_dst->samples_cnt));
		if (cport_dst->slot) {
		    pjmedia_port_put_frame(cport_dst->port, frm_dst);

		    /* Reset TX buffer. */
		    f_dst->subframe_cnt = 0;
		    f_dst->samples_cnt = 0;
		}

		/* Update TX timestamp. */
		pj_add_timestamp32(&cport_dst->ts_tx, 
				   cport_dst->samples_per_frame);
	    }
	}

	/* Synchronize clock. */
	while (pj_cmp_timestamp(&cport_dst->ts_clock, 
				&cport_dst->ts_tx) > 0)
	{
	    frm_dst->type = PJMEDIA_FRAME_TYPE_NONE;
	    frm_dst->timestamp = cport_dst->ts_tx;
	    if (cport_dst->slot)
		pjmedia_port_put_frame(cport_dst->port, frm_dst);

	    /* Update TX timestamp. */
	    pj_add_timestamp32(&cport_dst->ts_tx, cport_dst->samples_per_frame);
	}
    }

    return PJ_SUCCESS;
}

/*
 * Player callback.
 */
static pj_status_t get_frame(pjmedia_port *this_port, 
			     pjmedia_frame *frame)
{
    pjmedia_conf *conf = (pjmedia_conf*) this_port->port_data.pdata;
    unsigned ci, i;
    
    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Call get_frame() from all ports (except port 0) that has 
     * receiver and distribute the frame (put the frame to the destination 
     * port's buffer to accommodate different ptime, and ultimately call 
     * put_frame() of that port) to ports that are receiving from this port.
     */
    for (i=1, ci=1; i<conf->max_ports && ci<conf->port_cnt; ++i) {
	struct conf_port *cport = conf->ports[i];
	unsigned master_samples_per_frame;

	/* Skip empty port. */
	if (!cport)
	    continue;

	/* Var "ci" is to count how many ports have been visited so far. */
	++ci;

	master_samples_per_frame = PJMEDIA_PIA_SPF(&conf->master_port->info);

	/* Update clock of the port. */
	pj_add_timestamp32(&cport->ts_clock, master_samples_per_frame);

	/* Skip if we're not allowed to receive from this port or 
	 * the port doesn't have listeners.
	 */
	if (cport->rx_setting == PJMEDIA_PORT_DISABLE || 
	    cport->listener_cnt == 0)
	{
	    cport->rx_level = 0;
	    pj_add_timestamp32(&cport->ts_rx, master_samples_per_frame);
	    continue;
	}

	/* Get frame from each port, put it to the listener TX buffer,
	 * and eventually call put_frame() of the listener. This loop 
	 * will also make sure the ptime between conf & port synchronized.
	 */
	while (pj_cmp_timestamp(&cport->ts_clock, &cport->ts_rx) > 0) {
	    pjmedia_frame *f = (pjmedia_frame*)conf->buf;
	    pj_status_t status;
	    unsigned j;
	    pj_int32_t level = 0;

	    pj_add_timestamp32(&cport->ts_rx, cport->samples_per_frame);
	    
	    f->buf = &conf->buf[sizeof(pjmedia_frame)];
	    f->size = cport->samples_per_frame<<1;

	    /* Get frame from port. */
	    status = pjmedia_port_get_frame(cport->port, f);
	    if (status != PJ_SUCCESS)
		continue;

	    /* Calculate & adjust RX level. */
	    if (f->type == PJMEDIA_FRAME_TYPE_AUDIO) {
		if (cport->rx_adj_level != NORMAL_LEVEL) {
		    pj_int16_t *p = (pj_int16_t*)f->buf;
		    pj_int16_t *end;

		    end = p + (f->size >> 1);
		    while (p < end) {
			pj_int32_t itemp = *p;

			/* Adjust the level */
			itemp = (itemp * cport->rx_adj_level) >> 7;

			/* Clip the signal if it's too loud */
			if (itemp > MAX_LEVEL) itemp = MAX_LEVEL;
			else if (itemp < MIN_LEVEL) itemp = MIN_LEVEL;

			level += PJ_ABS(itemp);

			/* Put back in the buffer. */
			*p = (pj_int16_t)itemp;
			++p;
		    }
		    level /= (f->size >> 1);
		} else {
		    level = pjmedia_calc_avg_signal((const pj_int16_t*)f->buf,
						    f->size >> 1);
		}
	    } else if (f->type == PJMEDIA_FRAME_TYPE_EXTENDED) {
		/* For extended frame, level is unknown, so we just set 
		 * it to NORMAL_LEVEL. 
		 */
		level = NORMAL_LEVEL;
	    }

	    cport->rx_level = pjmedia_linear2ulaw(level) ^ 0xff;

	    /* Put the frame to all listeners. */
	    for (j=0; j < cport->listener_cnt; ++j) 
	    {
		struct conf_port *listener;

		listener = conf->ports[cport->listener_slots[j]];

		/* Skip if this listener doesn't want to receive audio */
		if (listener->tx_setting == PJMEDIA_PORT_DISABLE) {
		    pj_add_timestamp32(&listener->ts_tx, 
				       listener->samples_per_frame);
		    listener->tx_level = 0;
		    continue;
		}
    	    
		status = write_frame(listener, f);
		if (status != PJ_SUCCESS) {
		    listener->tx_level = 0;
		    continue;
		}

		/* Set listener TX level based on transmitter RX level & 
		 * listener TX level.
		 */
		listener->tx_level = (cport->rx_level * listener->tx_adj_level)
				     >> 8;
	    }
	}
    }

    /* Keep alive. Update TX timestamp and send frame type NONE to all 
     * underflow ports at their own clock.
     */
    for (i=1, ci=1; i<conf->max_ports && ci<conf->port_cnt; ++i) {
	struct conf_port *cport = conf->ports[i];

	/* Skip empty port. */
	if (!cport)
	    continue;

	/* Var "ci" is to count how many ports have been visited so far. */
	++ci;

	if (cport->tx_setting==PJMEDIA_PORT_MUTE || cport->transmitter_cnt==0)
	{
	    pjmedia_frame_ext *f;
	    
	    /* Clear left-over samples in tx_buffer, if any, so that it won't
	     * be transmitted next time we have audio signal.
	     */
	    f = (pjmedia_frame_ext*)cport->tx_buf;
	    f->base.type = PJMEDIA_FRAME_TYPE_NONE;
	    f->base.size = 0;
	    f->samples_cnt = 0;
	    f->subframe_cnt = 0;
	    
	    cport->tx_level = 0;

	    while (pj_cmp_timestamp(&cport->ts_clock, &cport->ts_tx) > 0)
	    {
		if (cport->tx_setting == PJMEDIA_PORT_ENABLE) {
		    pjmedia_frame tmp_f;

		    tmp_f.timestamp = cport->ts_tx;
		    tmp_f.type = PJMEDIA_FRAME_TYPE_NONE;
		    tmp_f.buf = NULL;
		    tmp_f.size = 0;

		    pjmedia_port_put_frame(cport->port, &tmp_f);
		    pj_add_timestamp32(&cport->ts_tx, cport->samples_per_frame);
		}
	    }
	}
    }

    /* Return sound playback frame. */
    do {
	struct conf_port *this_cport = conf->ports[this_port->port_data.ldata];
	pjmedia_frame *f_src = (pjmedia_frame*) this_cport->tx_buf;

	frame->type = f_src->type;

	if (f_src->type == PJMEDIA_FRAME_TYPE_EXTENDED) {
	    pjmedia_frame_ext *f_src_ = (pjmedia_frame_ext*)f_src;
	    pjmedia_frame_ext *f_dst = (pjmedia_frame_ext*)frame;
	    pjmedia_frame_ext_subframe *sf;
	    unsigned samples_per_subframe;
	    
	    if (f_src_->samples_cnt < this_cport->samples_per_frame) {
		f_dst->base.type = PJMEDIA_FRAME_TYPE_NONE;
		f_dst->samples_cnt = 0;
		f_dst->subframe_cnt = 0;
		break;
	    }

	    f_dst->samples_cnt = 0;
	    f_dst->subframe_cnt = 0;
	    i = 0;
	    samples_per_subframe = f_src_->samples_cnt / f_src_->subframe_cnt;


	    while (f_dst->samples_cnt < this_cport->samples_per_frame) {
		sf = pjmedia_frame_ext_get_subframe(f_src_, i++);
		pj_assert(sf);
		pjmedia_frame_ext_append_subframe(f_dst, sf->data, sf->bitlen,
						  samples_per_subframe);
	    }

	    /* Shift left TX buffer. */
	    pjmedia_frame_ext_pop_subframes(f_src_, i);

	} else if (f_src->type == PJMEDIA_FRAME_TYPE_AUDIO) {
	    if ((f_src->size>>1) < this_cport->samples_per_frame) {
		frame->type = PJMEDIA_FRAME_TYPE_NONE;
		frame->size = 0;
		break;
	    }

	    pjmedia_copy_samples((pj_int16_t*)frame->buf, 
				 (pj_int16_t*)f_src->buf, 
				 this_cport->samples_per_frame);
	    frame->size = this_cport->samples_per_frame << 1;

	    /* Shift left TX buffer. */
	    f_src->size -= frame->size;
	    if (f_src->size)
		pjmedia_move_samples((pj_int16_t*)f_src->buf,
				     (pj_int16_t*)f_src->buf + 
				     this_cport->samples_per_frame,
				     f_src->size >> 1);
	} else { /* PJMEDIA_FRAME_TYPE_NONE */
	    pjmedia_frame_ext *f_src_ = (pjmedia_frame_ext*)f_src;

	    /* Reset source/TX buffer */
	    f_src_->base.size = 0;
	    f_src_->samples_cnt = 0;
	    f_src_->subframe_cnt = 0;
	}
    } while (0);

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}

/*
 * Recorder callback.
 */
static pj_status_t put_frame(pjmedia_port *this_port, 
			     pjmedia_frame *f)
{
    pjmedia_conf *conf = (pjmedia_conf*) this_port->port_data.pdata;
    struct conf_port *cport;
    unsigned j;
    pj_int32_t level = 0;

    /* Lock mutex */
    pj_mutex_lock(conf->mutex);

    /* Get conf port of this port */
    cport = conf->ports[this_port->port_data.ldata];
    if (cport == NULL) {
	/* Unlock mutex */
	pj_mutex_unlock(conf->mutex);
	return PJ_SUCCESS;
    }

    pj_add_timestamp32(&cport->ts_rx, cport->samples_per_frame);
    
    /* Skip if this port is muted/disabled. */
    if (cport->rx_setting == PJMEDIA_PORT_DISABLE) {
	cport->rx_level = 0;
	/* Unlock mutex */
	pj_mutex_unlock(conf->mutex);
	return PJ_SUCCESS;
    }

    /* Skip if no port is listening to the microphone */
    if (cport->listener_cnt == 0) {
	cport->rx_level = 0;
	/* Unlock mutex */
	pj_mutex_unlock(conf->mutex);
	return PJ_SUCCESS;
    }

    /* Calculate & adjust RX level. */
    if (f->type == PJMEDIA_FRAME_TYPE_AUDIO) {
	if (cport->rx_adj_level != NORMAL_LEVEL) {
	    pj_int16_t *p = (pj_int16_t*)f->buf;
	    pj_int16_t *end;

	    end = p + (f->size >> 1);
	    while (p < end) {
		pj_int32_t itemp = *p;

		/* Adjust the level */
		itemp = (itemp * cport->rx_adj_level) >> 7;

		/* Clip the signal if it's too loud */
		if (itemp > MAX_LEVEL) itemp = MAX_LEVEL;
		else if (itemp < MIN_LEVEL) itemp = MIN_LEVEL;

		level += PJ_ABS(itemp);

		/* Put back in the buffer. */
		*p = (pj_int16_t)itemp;
		++p;
	    }
	    level /= (f->size >> 1);
	} else {
	    level = pjmedia_calc_avg_signal((const pj_int16_t*)f->buf,
					    f->size >> 1);
	}
    } else if (f->type == PJMEDIA_FRAME_TYPE_EXTENDED) {
	/* For extended frame, level is unknown, so we just set 
	 * it to NORMAL_LEVEL. 
	 */
	level = NORMAL_LEVEL;
    }

    cport->rx_level = pjmedia_linear2ulaw(level) ^ 0xff;

    /* Put the frame to all listeners. */
    for (j=0; j < cport->listener_cnt; ++j) 
    {
	struct conf_port *listener;
	pj_status_t status;

	listener = conf->ports[cport->listener_slots[j]];

	/* Skip if this listener doesn't want to receive audio */
	if (listener->tx_setting == PJMEDIA_PORT_DISABLE) {
	    pj_add_timestamp32(&listener->ts_tx, 
			       listener->samples_per_frame);
	    listener->tx_level = 0;
	    continue;
	}

	/* Skip loopback for now. */
	if (listener == cport) {
	    pj_add_timestamp32(&listener->ts_tx, 
			       listener->samples_per_frame);
	    listener->tx_level = 0;
	    continue;
	}
	    
	status = write_frame(listener, f);
	if (status != PJ_SUCCESS) {
	    listener->tx_level = 0;
	    continue;
	}

	/* Set listener TX level based on transmitter RX level & listener
	 * TX level.
	 */
	listener->tx_level = (cport->rx_level * listener->tx_adj_level) >> 8;
    }

    /* Unlock mutex */
    pj_mutex_unlock(conf->mutex);

    return PJ_SUCCESS;
}

#endif
