diff options
Diffstat (limited to 'pjmedia/src/pjmedia/sound_port.c')
-rw-r--r-- | pjmedia/src/pjmedia/sound_port.c | 742 |
1 files changed, 742 insertions, 0 deletions
diff --git a/pjmedia/src/pjmedia/sound_port.c b/pjmedia/src/pjmedia/sound_port.c new file mode 100644 index 0000000..4a475e7 --- /dev/null +++ b/pjmedia/src/pjmedia/sound_port.c @@ -0,0 +1,742 @@ +/* $Id: sound_port.c 4082 2012-04-24 13:09:14Z bennylp $ */ +/* + * 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/sound_port.h> +#include <pjmedia/alaw_ulaw.h> +#include <pjmedia/delaybuf.h> +#include <pjmedia/echo.h> +#include <pjmedia/errno.h> +#include <pj/assert.h> +#include <pj/log.h> +#include <pj/rand.h> +#include <pj/string.h> /* pj_memset() */ + +#define AEC_TAIL 128 /* default AEC length in ms */ +#define AEC_SUSPEND_LIMIT 5 /* seconds of no activity */ + +#define THIS_FILE "sound_port.c" + +//#define TEST_OVERFLOW_UNDERFLOW + +struct pjmedia_snd_port +{ + int rec_id; + int play_id; + pj_uint32_t aud_caps; + pjmedia_aud_param aud_param; + pjmedia_aud_stream *aud_stream; + pjmedia_dir dir; + pjmedia_port *port; + + pjmedia_clock_src cap_clocksrc, + play_clocksrc; + + unsigned clock_rate; + unsigned channel_count; + unsigned samples_per_frame; + unsigned bits_per_sample; + unsigned options; + unsigned prm_ec_options; + + /* software ec */ + pjmedia_echo_state *ec_state; + unsigned ec_options; + unsigned ec_tail_len; + pj_bool_t ec_suspended; + unsigned ec_suspend_count; + unsigned ec_suspend_limit; +}; + +/* + * The callback called by sound player when it needs more samples to be + * played. + */ +static pj_status_t play_cb(void *user_data, pjmedia_frame *frame) +{ + pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data; + pjmedia_port *port; + const unsigned required_size = frame->size; + pj_status_t status; + + pjmedia_clock_src_update(&snd_port->play_clocksrc, &frame->timestamp); + + port = snd_port->port; + if (port == NULL) + goto no_frame; + + 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 == required_size); + + 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*)frame->buf); + } + + + return PJ_SUCCESS; + +no_frame: + frame->type = PJMEDIA_FRAME_TYPE_AUDIO; + frame->size = required_size; + pj_bzero(frame->buf, frame->size); + + 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*)frame->buf); + } + } + + return PJ_SUCCESS; +} + + +/* + * The callback called by sound recorder when it has finished capturing a + * frame. + */ +static pj_status_t rec_cb(void *user_data, pjmedia_frame *frame) +{ + pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data; + pjmedia_port *port; + + pjmedia_clock_src_update(&snd_port->cap_clocksrc, &frame->timestamp); + + 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*) frame->buf, 0); + } + + pjmedia_port_put_frame(port, frame); + + + return PJ_SUCCESS; +} + +/* + * The callback called by sound player when it needs more samples to be + * played. This version is for non-PCM data. + */ +static pj_status_t play_cb_ext(void *user_data, pjmedia_frame *frame) +{ + pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data; + pjmedia_port *port = snd_port->port; + + if (port == NULL) { + frame->type = PJMEDIA_FRAME_TYPE_NONE; + return PJ_SUCCESS; + } + + pjmedia_port_get_frame(port, frame); + + return PJ_SUCCESS; +} + + +/* + * The callback called by sound recorder when it has finished capturing a + * frame. This version is for non-PCM data. + */ +static pj_status_t rec_cb_ext(void *user_data, pjmedia_frame *frame) +{ + pjmedia_snd_port *snd_port = (pjmedia_snd_port*) user_data; + pjmedia_port *port; + + port = snd_port->port; + if (port == NULL) + return PJ_SUCCESS; + + pjmedia_port_put_frame(port, frame); + + return PJ_SUCCESS; +} + +/* Initialize with default values (zero) */ +PJ_DEF(void) pjmedia_snd_port_param_default(pjmedia_snd_port_param *prm) +{ + pj_bzero(prm, sizeof(*prm)); +} + +/* + * 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 ) +{ + pjmedia_aud_rec_cb snd_rec_cb; + pjmedia_aud_play_cb snd_play_cb; + pjmedia_aud_param param_copy; + pj_status_t status; + + /* Check if sound has been started. */ + if (snd_port->aud_stream != NULL) + return PJ_SUCCESS; + + PJ_ASSERT_RETURN(snd_port->dir == PJMEDIA_DIR_CAPTURE || + snd_port->dir == PJMEDIA_DIR_PLAYBACK || + snd_port->dir == PJMEDIA_DIR_CAPTURE_PLAYBACK, + PJ_EBUG); + + /* Get device caps */ + if (snd_port->aud_param.dir & PJMEDIA_DIR_CAPTURE) { + pjmedia_aud_dev_info dev_info; + + status = pjmedia_aud_dev_get_info(snd_port->aud_param.rec_id, + &dev_info); + if (status != PJ_SUCCESS) + return status; + + snd_port->aud_caps = dev_info.caps; + } else { + snd_port->aud_caps = 0; + } + + /* Process EC settings */ + pj_memcpy(¶m_copy, &snd_port->aud_param, sizeof(param_copy)); + if (param_copy.flags & PJMEDIA_AUD_DEV_CAP_EC) { + /* EC is wanted */ + if ((snd_port->prm_ec_options & PJMEDIA_ECHO_USE_SW_ECHO) == 0 && + snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC) + { + /* Device supports EC */ + /* Nothing to do */ + } else { + /* Application wants to use software EC or device + * doesn't support EC, remove EC settings from + * device parameters + */ + param_copy.flags &= ~(PJMEDIA_AUD_DEV_CAP_EC | + PJMEDIA_AUD_DEV_CAP_EC_TAIL); + } + } + + /* Use different callback if format is not PCM */ + if (snd_port->aud_param.ext_fmt.id == PJMEDIA_FORMAT_L16) { + snd_rec_cb = &rec_cb; + snd_play_cb = &play_cb; + } else { + snd_rec_cb = &rec_cb_ext; + snd_play_cb = &play_cb_ext; + } + + /* Open the device */ + status = pjmedia_aud_stream_create(¶m_copy, + snd_rec_cb, + snd_play_cb, + snd_port, + &snd_port->aud_stream); + + if (status != PJ_SUCCESS) + return status; + + /* Inactivity limit before EC is suspended. */ + snd_port->ec_suspend_limit = AEC_SUSPEND_LIMIT * + (snd_port->clock_rate / + snd_port->samples_per_frame); + + /* Create software EC if parameter specifies EC and + * (app specifically requests software EC or device + * doesn't support EC). Only do this if the format is PCM! + */ + if ((snd_port->aud_param.flags & PJMEDIA_AUD_DEV_CAP_EC) && + ((snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC)==0 || + (snd_port->prm_ec_options & PJMEDIA_ECHO_USE_SW_ECHO) != 0) && + param_copy.ext_fmt.id == PJMEDIA_FORMAT_PCM) + { + if ((snd_port->aud_param.flags & PJMEDIA_AUD_DEV_CAP_EC_TAIL)==0) { + snd_port->aud_param.flags |= PJMEDIA_AUD_DEV_CAP_EC_TAIL; + snd_port->aud_param.ec_tail_ms = AEC_TAIL; + PJ_LOG(4,(THIS_FILE, "AEC tail is set to default %u ms", + snd_port->aud_param.ec_tail_ms)); + } + + status = pjmedia_snd_port_set_ec(snd_port, pool, + snd_port->aud_param.ec_tail_ms, + snd_port->prm_ec_options); + if (status != PJ_SUCCESS) { + pjmedia_aud_stream_destroy(snd_port->aud_stream); + snd_port->aud_stream = NULL; + return status; + } + } + + /* Start sound stream. */ + if (!(snd_port->options & PJMEDIA_SND_PORT_NO_AUTO_START)) { + status = pjmedia_aud_stream_start(snd_port->aud_stream); + } + if (status != PJ_SUCCESS) { + pjmedia_aud_stream_destroy(snd_port->aud_stream); + snd_port->aud_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->aud_stream) { + pjmedia_aud_stream_stop(snd_port->aud_stream); + pjmedia_aud_stream_destroy(snd_port->aud_stream); + snd_port->aud_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_param param; + pj_status_t status; + + pjmedia_snd_port_param_default(¶m); + + status = pjmedia_aud_dev_default_param(rec_id, ¶m.base); + if (status != PJ_SUCCESS) + return status; + + param.base.dir = PJMEDIA_DIR_CAPTURE_PLAYBACK; + param.base.rec_id = rec_id; + param.base.play_id = play_id; + param.base.clock_rate = clock_rate; + param.base.channel_count = channel_count; + param.base.samples_per_frame = samples_per_frame; + param.base.bits_per_sample = bits_per_sample; + param.options = options; + param.ec_options = 0; + + return pjmedia_snd_port_create2(pool, ¶m, p_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_param param; + pj_status_t status; + + pjmedia_snd_port_param_default(¶m); + + status = pjmedia_aud_dev_default_param(dev_id, ¶m.base); + if (status != PJ_SUCCESS) + return status; + + param.base.dir = PJMEDIA_DIR_CAPTURE; + param.base.rec_id = dev_id; + param.base.clock_rate = clock_rate; + param.base.channel_count = channel_count; + param.base.samples_per_frame = samples_per_frame; + param.base.bits_per_sample = bits_per_sample; + param.options = options; + param.ec_options = 0; + + return pjmedia_snd_port_create2(pool, ¶m, p_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_param param; + pj_status_t status; + + pjmedia_snd_port_param_default(¶m); + + status = pjmedia_aud_dev_default_param(dev_id, ¶m.base); + if (status != PJ_SUCCESS) + return status; + + param.base.dir = PJMEDIA_DIR_PLAYBACK; + param.base.play_id = dev_id; + param.base.clock_rate = clock_rate; + param.base.channel_count = channel_count; + param.base.samples_per_frame = samples_per_frame; + param.base.bits_per_sample = bits_per_sample; + param.options = options; + param.ec_options = 0; + + return pjmedia_snd_port_create2(pool, ¶m, p_port); +} + + +/* + * Create sound port. + */ +PJ_DEF(pj_status_t) pjmedia_snd_port_create2(pj_pool_t *pool, + const pjmedia_snd_port_param *prm, + pjmedia_snd_port **p_port) +{ + pjmedia_snd_port *snd_port; + pj_status_t status; + unsigned ptime_usec; + + PJ_ASSERT_RETURN(pool && prm && p_port, PJ_EINVAL); + + snd_port = PJ_POOL_ZALLOC_T(pool, pjmedia_snd_port); + PJ_ASSERT_RETURN(snd_port, PJ_ENOMEM); + + snd_port->dir = prm->base.dir; + snd_port->rec_id = prm->base.rec_id; + snd_port->play_id = prm->base.play_id; + snd_port->clock_rate = prm->base.clock_rate; + snd_port->channel_count = prm->base.channel_count; + snd_port->samples_per_frame = prm->base.samples_per_frame; + snd_port->bits_per_sample = prm->base.bits_per_sample; + pj_memcpy(&snd_port->aud_param, &prm->base, sizeof(snd_port->aud_param)); + snd_port->options = prm->options; + snd_port->prm_ec_options = prm->ec_options; + + ptime_usec = prm->base.samples_per_frame * 1000 / prm->base.channel_count / + prm->base.clock_rate * 1000; + pjmedia_clock_src_init(&snd_port->cap_clocksrc, PJMEDIA_TYPE_AUDIO, + snd_port->clock_rate, ptime_usec); + pjmedia_clock_src_init(&snd_port->play_clocksrc, PJMEDIA_TYPE_AUDIO, + snd_port->clock_rate, ptime_usec); + + /* Start sound device immediately. + * If there's no port connected, the sound callback will return + * empty signal. + */ + status = start_sound_device( pool, snd_port ); + if (status != PJ_SUCCESS) { + pjmedia_snd_port_destroy(snd_port); + return status; + } + + *p_port = snd_port; + return PJ_SUCCESS; +} + + +/* + * 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_aud_stream*) pjmedia_snd_port_get_snd_stream( + pjmedia_snd_port *snd_port) +{ + PJ_ASSERT_RETURN(snd_port, NULL); + return snd_port->aud_stream; +} + + +/* + * Change EC settings. + */ +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_aud_param prm; + 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); + + /* Determine whether we use device or software EC */ + if ((snd_port->prm_ec_options & PJMEDIA_ECHO_USE_SW_ECHO) == 0 && + snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC) + { + /* We use device EC */ + pj_bool_t ec_enabled; + + /* Query EC status */ + status = pjmedia_aud_stream_get_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC, + &ec_enabled); + if (status != PJ_SUCCESS) + return status; + + if (tail_ms != 0) { + /* Change EC setting */ + + if (!ec_enabled) { + /* Enable EC first */ + pj_bool_t value = PJ_TRUE; + status = pjmedia_aud_stream_set_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC, + &value); + if (status != PJ_SUCCESS) + return status; + } + + if ((snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC_TAIL)==0) { + /* Device does not support setting EC tail */ + return PJMEDIA_EAUD_INVCAP; + } + + return pjmedia_aud_stream_set_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC_TAIL, + &tail_ms); + + } else if (ec_enabled) { + /* Disable EC */ + pj_bool_t value = PJ_FALSE; + return pjmedia_aud_stream_set_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC, + &value); + } else { + /* Request to disable EC but EC has been disabled */ + /* Do nothing */ + return PJ_SUCCESS; + } + + } else { + /* We use software EC */ + + /* Check if there is change in parameters */ + if (tail_ms==snd_port->ec_tail_len && options==snd_port->ec_options) { + PJ_LOG(5,(THIS_FILE, "pjmedia_snd_port_set_ec() ignored, no " + "change in settings")); + return PJ_SUCCESS; + } + + status = pjmedia_aud_stream_get_param(snd_port->aud_stream, &prm); + if (status != PJ_SUCCESS) + return status; + + /* Audio stream must be in PCM format */ + PJ_ASSERT_RETURN(prm.ext_fmt.id == PJMEDIA_FORMAT_PCM, + PJ_EINVALIDOP); + + /* Destroy AEC */ + if (snd_port->ec_state) { + pjmedia_echo_destroy(snd_port->ec_state); + snd_port->ec_state = NULL; + } + + if (tail_ms != 0) { + unsigned delay_ms; + + //No need to add input latency in the latency calculation, + //since actual input latency should be zero. + //delay_ms = (si.rec_latency + si.play_latency) * 1000 / + // snd_port->clock_rate; + /* Set EC latency to 3/4 of output latency to reduce the + * possibility of missing/late reference frame. + */ + delay_ms = prm.output_latency_ms * 3/4; + status = pjmedia_echo_create2(pool, snd_port->clock_rate, + snd_port->channel_count, + 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; + } + + snd_port->ec_options = options; + snd_port->ec_tail_len = tail_ms; + } + + 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); + + /* Determine whether we use device or software EC */ + if (snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC) { + /* We use device EC */ + pj_bool_t ec_enabled; + pj_status_t status; + + /* Query EC status */ + status = pjmedia_aud_stream_get_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC, + &ec_enabled); + if (status != PJ_SUCCESS) + return status; + + if (!ec_enabled) { + *p_length = 0; + } else if (snd_port->aud_caps & PJMEDIA_AUD_DEV_CAP_EC_TAIL) { + /* Get device EC tail */ + status = pjmedia_aud_stream_get_cap(snd_port->aud_stream, + PJMEDIA_AUD_DEV_CAP_EC_TAIL, + p_length); + if (status != PJ_SUCCESS) + return status; + } else { + /* Just use default */ + *p_length = AEC_TAIL; + } + + } else { + /* We use software EC */ + *p_length = snd_port->ec_state ? snd_port->ec_tail_len : 0; + } + return PJ_SUCCESS; +} + + +/* + * Get clock source. + */ +PJ_DEF(pjmedia_clock_src *) +pjmedia_snd_port_get_clock_src( pjmedia_snd_port *snd_port, + pjmedia_dir dir ) +{ + return (dir == PJMEDIA_DIR_CAPTURE? &snd_port->cap_clocksrc: + &snd_port->play_clocksrc); +} + + +/* + * Connect a port. + */ +PJ_DEF(pj_status_t) pjmedia_snd_port_connect( pjmedia_snd_port *snd_port, + pjmedia_port *port) +{ + pjmedia_audio_format_detail *afd; + + PJ_ASSERT_RETURN(snd_port && port, PJ_EINVAL); + + afd = pjmedia_format_get_audio_format_detail(&port->info.fmt, PJ_TRUE); + + /* Check that port has the same configuration as the sound device + * port. + */ + if (afd->clock_rate != snd_port->clock_rate) + return PJMEDIA_ENCCLOCKRATE; + + if (PJMEDIA_AFD_SPF(afd) != snd_port->samples_per_frame) + return PJMEDIA_ENCSAMPLESPFRAME; + + if (afd->channel_count != snd_port->channel_count) + return PJMEDIA_ENCCHANNEL; + + if (afd->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; +} + + |