/* $Id$ */ /* * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) * Copyright (C) 2003-2008 Benny Prijono * * 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 #include #include #include #include #include #include #include #include /* 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; /* audio frame preview callbacks */ void *user_data; pjmedia_aud_play_cb on_play_frame; pjmedia_aud_rec_cb on_rec_frame; }; /* * 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 = (unsigned)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); } /* Invoke preview callback */ if (snd_port->on_play_frame) (*snd_port->on_play_frame)(snd_port->user_data, frame); 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); } } /* Invoke preview callback */ if (snd_port->on_play_frame) (*snd_port->on_play_frame)(snd_port->user_data, frame); 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); /* Invoke preview callback */ if (snd_port->on_rec_frame) (*snd_port->on_rec_frame)(snd_port->user_data, frame); 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); /* Invoke preview callback */ if (snd_port->on_play_frame) (*snd_port->on_play_frame)(snd_port->user_data, 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; /* Invoke preview callback */ if (snd_port->on_rec_frame) (*snd_port->on_rec_frame)(snd_port->user_data, frame); 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); /* Normalize rec_id & play_id */ if (rec_id < 0) rec_id = PJMEDIA_AUD_DEFAULT_CAPTURE_DEV; if (play_id < 0) play_id = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV; 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); /* Normalize dev_id */ if (dev_id < 0) dev_id = PJMEDIA_AUD_DEFAULT_CAPTURE_DEV; 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); /* Normalize dev_id */ if (dev_id < 0) dev_id = PJMEDIA_AUD_DEFAULT_PLAYBACK_DEV; 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; snd_port->user_data = prm->user_data; snd_port->on_play_frame = prm->on_play_frame; snd_port->on_rec_frame = prm->on_rec_frame; 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 echo canceller statistics. */ PJ_DEF(pj_status_t) pjmedia_snd_port_get_ec_stat( pjmedia_snd_port *snd_port, pjmedia_echo_stat *p_stat) { PJ_ASSERT_RETURN(snd_port && p_stat, PJ_EINVAL); if (snd_port->ec_state) { return pjmedia_echo_get_stat(snd_port->ec_state, p_stat); } else { return PJ_ENOTFOUND; } } /* * 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; }