600 lines
15 KiB
C
600 lines
15 KiB
C
/* $Id$ */
|
|
/*
|
|
* Copyright (C) 2003-2007 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/sound_port.h>
|
|
#include <pjmedia/delaybuf.h>
|
|
#include <pjmedia/echo.h>
|
|
#include <pjmedia/errno.h>
|
|
#include <pjmedia/plc.h>
|
|
#include <pj/assert.h>
|
|
#include <pj/log.h>
|
|
#include <pj/rand.h>
|
|
#include <pj/string.h> /* pj_memset() */
|
|
|
|
//#define SIMULATE_LOST_PCT 20
|
|
#define AEC_TAIL 128 /* default AEC length in ms */
|
|
#define AEC_SUSPEND_LIMIT 5 /* seconds of no activity */
|
|
|
|
#define THIS_FILE "sound_port.c"
|
|
|
|
enum
|
|
{
|
|
PJMEDIA_PLC_ENABLED = 1,
|
|
};
|
|
|
|
//#define DEFAULT_OPTIONS PJMEDIA_PLC_ENABLED
|
|
#define DEFAULT_OPTIONS 0
|
|
|
|
|
|
struct pjmedia_snd_port
|
|
{
|
|
int rec_id;
|
|
int play_id;
|
|
pjmedia_snd_stream *snd_stream;
|
|
pjmedia_dir dir;
|
|
pjmedia_port *port;
|
|
unsigned options;
|
|
|
|
pjmedia_echo_state *ec_state;
|
|
unsigned aec_tail_len;
|
|
|
|
pj_bool_t ec_suspended;
|
|
unsigned ec_suspend_count;
|
|
unsigned ec_suspend_limit;
|
|
|
|
pjmedia_plc *plc;
|
|
|
|
unsigned clock_rate;
|
|
unsigned channel_count;
|
|
unsigned samples_per_frame;
|
|
unsigned bits_per_sample;
|
|
|
|
#if PJMEDIA_SOUND_USE_DELAYBUF
|
|
pjmedia_delay_buf *delay_buf;
|
|
#endif
|
|
};
|
|
|
|
/*
|
|
* The callback called by sound player when it needs more samples to be
|
|
* played.
|
|
*/
|
|
static pj_status_t play_cb(/* in */ void *user_data,
|
|
/* in */ pj_uint32_t timestamp,
|
|
/* out */ void *output,
|
|
/* out */ unsigned size)
|
|
{
|
|
pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data;
|
|
pjmedia_port *port;
|
|
pjmedia_frame frame;
|
|
pj_status_t status;
|
|
|
|
/* We're risking accessing the port without holding any mutex.
|
|
* It's possible that port is disconnected then destroyed while
|
|
* we're trying to access it.
|
|
* But in the name of performance, we'll try this approach until
|
|
* someone complains when it crashes.
|
|
*/
|
|
port = snd_port->port;
|
|
if (port == NULL)
|
|
goto no_frame;
|
|
|
|
frame.buf = output;
|
|
frame.size = size;
|
|
frame.timestamp.u32.hi = 0;
|
|
frame.timestamp.u32.lo = timestamp;
|
|
|
|
#if PJMEDIA_SOUND_USE_DELAYBUF
|
|
status = pjmedia_delay_buf_get(snd_port->delay_buf, (pj_int16_t*)output);
|
|
if (status != PJ_SUCCESS) {
|
|
pj_bzero(output, size);
|
|
}
|
|
|
|
pjmedia_port_put_frame(port, &frame);
|
|
#endif
|
|
|
|
status = pjmedia_port_get_frame(port, &frame);
|
|
if (status != PJ_SUCCESS)
|
|
goto no_frame;
|
|
|
|
if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO)
|
|
goto no_frame;
|
|
|
|
/* Must supply the required samples */
|
|
pj_assert(frame.size == size);
|
|
|
|
#ifdef SIMULATE_LOST_PCT
|
|
/* Simulate packet lost */
|
|
if (pj_rand() % 100 < SIMULATE_LOST_PCT) {
|
|
PJ_LOG(4,(THIS_FILE, "Frame dropped"));
|
|
goto no_frame;
|
|
}
|
|
#endif
|
|
|
|
if (snd_port->plc)
|
|
pjmedia_plc_save(snd_port->plc, (pj_int16_t*) output);
|
|
|
|
if (snd_port->ec_state) {
|
|
if (snd_port->ec_suspended) {
|
|
snd_port->ec_suspended = PJ_FALSE;
|
|
//pjmedia_echo_state_reset(snd_port->ec_state);
|
|
PJ_LOG(4,(THIS_FILE, "EC activated"));
|
|
}
|
|
snd_port->ec_suspend_count = 0;
|
|
pjmedia_echo_playback(snd_port->ec_state, (pj_int16_t*)output);
|
|
}
|
|
|
|
|
|
return PJ_SUCCESS;
|
|
|
|
no_frame:
|
|
|
|
if (snd_port->ec_state && !snd_port->ec_suspended) {
|
|
++snd_port->ec_suspend_count;
|
|
if (snd_port->ec_suspend_count > snd_port->ec_suspend_limit) {
|
|
snd_port->ec_suspended = PJ_TRUE;
|
|
PJ_LOG(4,(THIS_FILE, "EC suspended because of inactivity"));
|
|
}
|
|
if (snd_port->ec_state) {
|
|
/* To maintain correct delay in EC */
|
|
pjmedia_echo_playback(snd_port->ec_state, (pj_int16_t*)output);
|
|
}
|
|
}
|
|
|
|
/* Apply PLC */
|
|
if (snd_port->plc) {
|
|
|
|
pjmedia_plc_generate(snd_port->plc, (pj_int16_t*) output);
|
|
#ifdef SIMULATE_LOST_PCT
|
|
PJ_LOG(4,(THIS_FILE, "Lost frame generated"));
|
|
#endif
|
|
} else {
|
|
pj_bzero(output, size);
|
|
}
|
|
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
/*
|
|
* The callback called by sound recorder when it has finished capturing a
|
|
* frame.
|
|
*/
|
|
static pj_status_t rec_cb(/* in */ void *user_data,
|
|
/* in */ pj_uint32_t timestamp,
|
|
/* in */ void *input,
|
|
/* in*/ unsigned size)
|
|
{
|
|
pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data;
|
|
pjmedia_port *port;
|
|
pjmedia_frame frame;
|
|
|
|
/* We're risking accessing the port without holding any mutex.
|
|
* It's possible that port is disconnected then destroyed while
|
|
* we're trying to access it.
|
|
* But in the name of performance, we'll try this approach until
|
|
* someone complains when it crashes.
|
|
*/
|
|
port = snd_port->port;
|
|
if (port == NULL)
|
|
return PJ_SUCCESS;
|
|
|
|
/* Cancel echo */
|
|
if (snd_port->ec_state && !snd_port->ec_suspended) {
|
|
pjmedia_echo_capture(snd_port->ec_state, (pj_int16_t*) input, 0);
|
|
}
|
|
|
|
#if PJMEDIA_SOUND_USE_DELAYBUF
|
|
PJ_UNUSED_ARG(size);
|
|
PJ_UNUSED_ARG(timestamp);
|
|
PJ_UNUSED_ARG(frame);
|
|
pjmedia_delay_buf_put(snd_port->delay_buf, (pj_int16_t*)input);
|
|
#else
|
|
frame.buf = (void*)input;
|
|
frame.size = size;
|
|
frame.type = PJMEDIA_FRAME_TYPE_AUDIO;
|
|
frame.timestamp.u32.lo = timestamp;
|
|
|
|
pjmedia_port_put_frame(port, &frame);
|
|
#endif
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Start the sound stream.
|
|
* This may be called even when the sound stream has already been started.
|
|
*/
|
|
static pj_status_t start_sound_device( pj_pool_t *pool,
|
|
pjmedia_snd_port *snd_port )
|
|
{
|
|
pj_status_t status;
|
|
|
|
/* Check if sound has been started. */
|
|
if (snd_port->snd_stream != NULL)
|
|
return PJ_SUCCESS;
|
|
|
|
/* Open sound stream. */
|
|
if (snd_port->dir == PJMEDIA_DIR_CAPTURE) {
|
|
status = pjmedia_snd_open_rec( snd_port->rec_id,
|
|
snd_port->clock_rate,
|
|
snd_port->channel_count,
|
|
snd_port->samples_per_frame,
|
|
snd_port->bits_per_sample,
|
|
&rec_cb,
|
|
snd_port,
|
|
&snd_port->snd_stream);
|
|
|
|
} else if (snd_port->dir == PJMEDIA_DIR_PLAYBACK) {
|
|
status = pjmedia_snd_open_player( snd_port->play_id,
|
|
snd_port->clock_rate,
|
|
snd_port->channel_count,
|
|
snd_port->samples_per_frame,
|
|
snd_port->bits_per_sample,
|
|
&play_cb,
|
|
snd_port,
|
|
&snd_port->snd_stream);
|
|
|
|
} else if (snd_port->dir == PJMEDIA_DIR_CAPTURE_PLAYBACK) {
|
|
status = pjmedia_snd_open( snd_port->rec_id,
|
|
snd_port->play_id,
|
|
snd_port->clock_rate,
|
|
snd_port->channel_count,
|
|
snd_port->samples_per_frame,
|
|
snd_port->bits_per_sample,
|
|
&rec_cb,
|
|
&play_cb,
|
|
snd_port,
|
|
&snd_port->snd_stream);
|
|
} else {
|
|
pj_assert(!"Invalid dir");
|
|
status = PJ_EBUG;
|
|
}
|
|
|
|
if (status != PJ_SUCCESS)
|
|
return status;
|
|
|
|
|
|
#ifdef SIMULATE_LOST_PCT
|
|
snd_port->options |= PJMEDIA_PLC_ENABLED;
|
|
#endif
|
|
|
|
/* If we have player components, allocate buffer to save the last
|
|
* frame played to the speaker. The last frame is used for packet
|
|
* lost concealment (PLC) algorithm.
|
|
*/
|
|
if ((snd_port->dir & PJMEDIA_DIR_PLAYBACK) &&
|
|
(snd_port->options & PJMEDIA_PLC_ENABLED))
|
|
{
|
|
status = pjmedia_plc_create(pool, snd_port->clock_rate,
|
|
snd_port->samples_per_frame *
|
|
snd_port->channel_count,
|
|
0, &snd_port->plc);
|
|
if (status != PJ_SUCCESS) {
|
|
PJ_LOG(4,(THIS_FILE, "Unable to create PLC"));
|
|
snd_port->plc = NULL;
|
|
}
|
|
}
|
|
|
|
/* Inactivity limit before EC is suspended. */
|
|
snd_port->ec_suspend_limit = AEC_SUSPEND_LIMIT *
|
|
(snd_port->clock_rate /
|
|
snd_port->samples_per_frame);
|
|
|
|
/* Start sound stream. */
|
|
status = pjmedia_snd_stream_start(snd_port->snd_stream);
|
|
if (status != PJ_SUCCESS) {
|
|
pjmedia_snd_stream_close(snd_port->snd_stream);
|
|
snd_port->snd_stream = NULL;
|
|
return status;
|
|
}
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
/*
|
|
* Stop the sound device.
|
|
* This may be called even when there's no sound device in the port.
|
|
*/
|
|
static pj_status_t stop_sound_device( pjmedia_snd_port *snd_port )
|
|
{
|
|
/* Check if we have sound stream device. */
|
|
if (snd_port->snd_stream) {
|
|
pjmedia_snd_stream_stop(snd_port->snd_stream);
|
|
pjmedia_snd_stream_close(snd_port->snd_stream);
|
|
snd_port->snd_stream = NULL;
|
|
}
|
|
|
|
/* Destroy AEC */
|
|
if (snd_port->ec_state) {
|
|
pjmedia_echo_destroy(snd_port->ec_state);
|
|
snd_port->ec_state = NULL;
|
|
}
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
/*
|
|
* Create bidirectional port.
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_create( pj_pool_t *pool,
|
|
int rec_id,
|
|
int play_id,
|
|
unsigned clock_rate,
|
|
unsigned channel_count,
|
|
unsigned samples_per_frame,
|
|
unsigned bits_per_sample,
|
|
unsigned options,
|
|
pjmedia_snd_port **p_port)
|
|
{
|
|
pjmedia_snd_port *snd_port;
|
|
pj_status_t status;
|
|
|
|
PJ_ASSERT_RETURN(pool && p_port, PJ_EINVAL);
|
|
|
|
snd_port = PJ_POOL_ZALLOC_T(pool, pjmedia_snd_port);
|
|
PJ_ASSERT_RETURN(snd_port, PJ_ENOMEM);
|
|
|
|
snd_port->rec_id = rec_id;
|
|
snd_port->play_id = play_id;
|
|
snd_port->dir = PJMEDIA_DIR_CAPTURE_PLAYBACK;
|
|
snd_port->options = options | DEFAULT_OPTIONS;
|
|
snd_port->clock_rate = clock_rate;
|
|
snd_port->channel_count = channel_count;
|
|
snd_port->samples_per_frame = samples_per_frame;
|
|
snd_port->bits_per_sample = bits_per_sample;
|
|
|
|
#if PJMEDIA_SOUND_USE_DELAYBUF
|
|
status = pjmedia_delay_buf_create(pool, "snd_buff", samples_per_frame,
|
|
16, &snd_port->delay_buf);
|
|
PJ_ASSERT_RETURN(status == PJ_SUCCESS, status);
|
|
#else
|
|
PJ_UNUSED_ARG(status);
|
|
#endif
|
|
|
|
*p_port = snd_port;
|
|
|
|
|
|
/* Start sound device immediately.
|
|
* If there's no port connected, the sound callback will return
|
|
* empty signal.
|
|
*/
|
|
return start_sound_device( pool, snd_port );
|
|
|
|
}
|
|
|
|
/*
|
|
* Create sound recorder AEC.
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_create_rec( pj_pool_t *pool,
|
|
int dev_id,
|
|
unsigned clock_rate,
|
|
unsigned channel_count,
|
|
unsigned samples_per_frame,
|
|
unsigned bits_per_sample,
|
|
unsigned options,
|
|
pjmedia_snd_port **p_port)
|
|
{
|
|
pjmedia_snd_port *snd_port;
|
|
|
|
PJ_ASSERT_RETURN(pool && p_port, PJ_EINVAL);
|
|
|
|
snd_port = PJ_POOL_ZALLOC_T(pool, pjmedia_snd_port);
|
|
PJ_ASSERT_RETURN(snd_port, PJ_ENOMEM);
|
|
|
|
snd_port->rec_id = dev_id;
|
|
snd_port->dir = PJMEDIA_DIR_CAPTURE;
|
|
snd_port->options = options | DEFAULT_OPTIONS;
|
|
snd_port->clock_rate = clock_rate;
|
|
snd_port->channel_count = channel_count;
|
|
snd_port->samples_per_frame = samples_per_frame;
|
|
snd_port->bits_per_sample = bits_per_sample;
|
|
|
|
*p_port = snd_port;
|
|
|
|
/* Start sound device immediately.
|
|
* If there's no port connected, the sound callback will return
|
|
* empty signal.
|
|
*/
|
|
return start_sound_device( pool, snd_port );
|
|
}
|
|
|
|
|
|
/*
|
|
* Create sound player port.
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_create_player( pj_pool_t *pool,
|
|
int dev_id,
|
|
unsigned clock_rate,
|
|
unsigned channel_count,
|
|
unsigned samples_per_frame,
|
|
unsigned bits_per_sample,
|
|
unsigned options,
|
|
pjmedia_snd_port **p_port)
|
|
{
|
|
pjmedia_snd_port *snd_port;
|
|
|
|
PJ_ASSERT_RETURN(pool && p_port, PJ_EINVAL);
|
|
|
|
snd_port = PJ_POOL_ZALLOC_T(pool, pjmedia_snd_port);
|
|
PJ_ASSERT_RETURN(snd_port, PJ_ENOMEM);
|
|
|
|
snd_port->play_id = dev_id;
|
|
snd_port->dir = PJMEDIA_DIR_PLAYBACK;
|
|
snd_port->options = options | DEFAULT_OPTIONS;
|
|
snd_port->clock_rate = clock_rate;
|
|
snd_port->channel_count = channel_count;
|
|
snd_port->samples_per_frame = samples_per_frame;
|
|
snd_port->bits_per_sample = bits_per_sample;
|
|
|
|
*p_port = snd_port;
|
|
|
|
/* Start sound device immediately.
|
|
* If there's no port connected, the sound callback will return
|
|
* empty signal.
|
|
*/
|
|
return start_sound_device( pool, snd_port );
|
|
}
|
|
|
|
|
|
/*
|
|
* Destroy port (also destroys the sound device).
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_destroy(pjmedia_snd_port *snd_port)
|
|
{
|
|
PJ_ASSERT_RETURN(snd_port, PJ_EINVAL);
|
|
|
|
return stop_sound_device(snd_port);
|
|
}
|
|
|
|
|
|
/*
|
|
* Retrieve the sound stream associated by this sound device port.
|
|
*/
|
|
PJ_DEF(pjmedia_snd_stream*) pjmedia_snd_port_get_snd_stream(
|
|
pjmedia_snd_port *snd_port)
|
|
{
|
|
PJ_ASSERT_RETURN(snd_port, NULL);
|
|
return snd_port->snd_stream;
|
|
}
|
|
|
|
|
|
/*
|
|
* Enable AEC
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_set_ec( pjmedia_snd_port *snd_port,
|
|
pj_pool_t *pool,
|
|
unsigned tail_ms,
|
|
unsigned options)
|
|
{
|
|
pjmedia_snd_stream_info si;
|
|
pj_status_t status;
|
|
|
|
/* Sound must be opened in full-duplex mode */
|
|
PJ_ASSERT_RETURN(snd_port &&
|
|
snd_port->dir == PJMEDIA_DIR_CAPTURE_PLAYBACK,
|
|
PJ_EINVALIDOP);
|
|
|
|
/* Sound port must have 16bits per sample */
|
|
PJ_ASSERT_RETURN(snd_port->bits_per_sample == 16,
|
|
PJ_EINVALIDOP);
|
|
|
|
/* Destroy AEC */
|
|
if (snd_port->ec_state) {
|
|
pjmedia_echo_destroy(snd_port->ec_state);
|
|
snd_port->ec_state = NULL;
|
|
}
|
|
|
|
snd_port->aec_tail_len = tail_ms;
|
|
|
|
if (tail_ms != 0) {
|
|
unsigned delay_ms;
|
|
|
|
status = pjmedia_snd_stream_get_info(snd_port->snd_stream, &si);
|
|
if (status != PJ_SUCCESS)
|
|
si.rec_latency = si.play_latency = 0;
|
|
|
|
delay_ms = (si.rec_latency + si.play_latency) * 1000 /
|
|
snd_port->clock_rate;
|
|
status = pjmedia_echo_create(pool, snd_port->clock_rate,
|
|
snd_port->samples_per_frame,
|
|
tail_ms, delay_ms,
|
|
options, &snd_port->ec_state);
|
|
if (status != PJ_SUCCESS)
|
|
snd_port->ec_state = NULL;
|
|
else
|
|
snd_port->ec_suspended = PJ_FALSE;
|
|
} else {
|
|
PJ_LOG(4,(THIS_FILE, "Echo canceller is now disabled in the "
|
|
"sound port"));
|
|
status = PJ_SUCCESS;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
/* Get AEC tail length */
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_get_ec_tail( pjmedia_snd_port *snd_port,
|
|
unsigned *p_length)
|
|
{
|
|
PJ_ASSERT_RETURN(snd_port && p_length, PJ_EINVAL);
|
|
*p_length = snd_port->ec_state ? snd_port->aec_tail_len : 0;
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Connect a port.
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_connect( pjmedia_snd_port *snd_port,
|
|
pjmedia_port *port)
|
|
{
|
|
pjmedia_port_info *pinfo;
|
|
|
|
PJ_ASSERT_RETURN(snd_port && port, PJ_EINVAL);
|
|
|
|
/* Check that port has the same configuration as the sound device
|
|
* port.
|
|
*/
|
|
pinfo = &port->info;
|
|
if (pinfo->clock_rate != snd_port->clock_rate)
|
|
return PJMEDIA_ENCCLOCKRATE;
|
|
|
|
if (pinfo->samples_per_frame != snd_port->samples_per_frame)
|
|
return PJMEDIA_ENCSAMPLESPFRAME;
|
|
|
|
if (pinfo->channel_count != snd_port->channel_count)
|
|
return PJMEDIA_ENCCHANNEL;
|
|
|
|
if (pinfo->bits_per_sample != snd_port->bits_per_sample)
|
|
return PJMEDIA_ENCBITS;
|
|
|
|
/* Port is okay. */
|
|
snd_port->port = port;
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|
|
/*
|
|
* Get the connected port.
|
|
*/
|
|
PJ_DEF(pjmedia_port*) pjmedia_snd_port_get_port(pjmedia_snd_port *snd_port)
|
|
{
|
|
PJ_ASSERT_RETURN(snd_port, NULL);
|
|
return snd_port->port;
|
|
}
|
|
|
|
|
|
/*
|
|
* Disconnect port.
|
|
*/
|
|
PJ_DEF(pj_status_t) pjmedia_snd_port_disconnect(pjmedia_snd_port *snd_port)
|
|
{
|
|
PJ_ASSERT_RETURN(snd_port, PJ_EINVAL);
|
|
|
|
snd_port->port = NULL;
|
|
|
|
return PJ_SUCCESS;
|
|
}
|
|
|
|
|