翻看pjproject中的源码,发现它实现了一个回声消除的例子aectest.c,它主要依赖三种算法(1=speex, 2=echo suppress, 3=WebRtc),这是可选的,实际使用时选择其中的一种。
它调用的一个命令行为:aectest -d 100 -a 1 ../bin/orig8.wav ../bin/echo8.wav ../bin/result8.wav
-d选项是延迟,因为远端进来的参考声音信号被扬声器播放出来,再到麦克风拾取后读出来,存在一定的延时。
-a选项是选择算法类型,其中echo suppress是pjproject自己实现的,在源码echo_suppress.c中。而speex和WebRtc是从开源项目中搬过来的,放在了third_party目录下面。
这个例子对于学习和研究回声消除是比较有用的。
这里的echo suppress是回声抑制算法,这个依赖于双端发生检测(这是一个对二者电平进行不断统计的过程),就是将远端传过来的声音的电平与近端录制的声音的电平相比较,如果远端在发声,则将近端录制的声音进行抑制,抑制的过程也是一个平滑过渡的过程。
这种抑制的方法是非线性的,有时候会造成扬声器的播放断断续续,但也简单实用。
一份较老的代码echo_suppress.c实现:
/* $Id: echo_suppress.c 1417 2007-08-16 10:11:44Z bennylp $ */ /* * 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/types.h> #include <pjmedia/errno.h> #include <pjmedia/silencedet.h> #include <pj/assert.h> #include <pj/lock.h> #include <pj/log.h> #include <pj/os.h> #include <pj/pool.h> #include "echo_internal.h" #define THIS_FILE "echo_suppress.c" /* * Simple echo suppresor */ typedef struct echo_supp { pj_bool_t suppressing; pjmedia_silence_det *sd; pj_time_val last_signal; unsigned samples_per_frame; unsigned tail_ms; } echo_supp; /* * Create. */ PJ_DEF(pj_status_t) echo_supp_create( pj_pool_t *pool, unsigned clock_rate, unsigned samples_per_frame, unsigned tail_ms, unsigned latency_ms, unsigned options, void **p_state ) { echo_supp *ec; pj_status_t status; PJ_UNUSED_ARG(clock_rate); PJ_UNUSED_ARG(options); PJ_UNUSED_ARG(latency_ms); ec = PJ_POOL_ZALLOC_T(pool, struct echo_supp); ec->samples_per_frame = samples_per_frame; ec->tail_ms = tail_ms; status = pjmedia_silence_det_create(pool, clock_rate, samples_per_frame, &ec->sd); if (status != PJ_SUCCESS) return status; pjmedia_silence_det_set_name(ec->sd, "ecsu%p"); pjmedia_silence_det_set_adaptive(ec->sd, PJMEDIA_ECHO_SUPPRESS_THRESHOLD); pjmedia_silence_det_set_params(ec->sd, 100, 500, 3000); *p_state = ec; return PJ_SUCCESS; } /* * Destroy. */ PJ_DEF(pj_status_t) echo_supp_destroy(void *state) { PJ_UNUSED_ARG(state); return PJ_SUCCESS; } /* * Let the AEC knows that a frame has been played to the speaker. */ PJ_DEF(pj_status_t) echo_supp_playback( void *state, pj_int16_t *play_frm ) { echo_supp *ec = (echo_supp*) state; pj_bool_t silence; pj_bool_t last_suppressing = ec->suppressing; silence = pjmedia_silence_det_detect(ec->sd, play_frm, ec->samples_per_frame, NULL); ec->suppressing = !silence; if (ec->suppressing) { pj_gettimeofday(&ec->last_signal); } if (ec->suppressing!=0 && last_suppressing==0) { PJ_LOG(5,(THIS_FILE, "Start suppressing..")); } else if (ec->suppressing==0 && last_suppressing!=0) { PJ_LOG(5,(THIS_FILE, "Stop suppressing..")); } return PJ_SUCCESS; } /* * Let the AEC knows that a frame has been captured from the microphone. */ PJ_DEF(pj_status_t) echo_supp_capture( void *state, pj_int16_t *rec_frm, unsigned options ) { echo_supp *ec = (echo_supp*) state; pj_time_val now; unsigned delay_ms; PJ_UNUSED_ARG(options); pj_gettimeofday(&now); PJ_TIME_VAL_SUB(now, ec->last_signal); delay_ms = PJ_TIME_VAL_MSEC(now); if (delay_ms < ec->tail_ms) { #if defined(PJMEDIA_ECHO_SUPPRESS_FACTOR) && PJMEDIA_ECHO_SUPPRESS_FACTOR!=0 unsigned i; for (i=0; i<ec->samples_per_frame; ++i) { rec_frm[i] = (pj_int16_t)(rec_frm[i] >> PJMEDIA_ECHO_SUPPRESS_FACTOR); } #else pjmedia_zero_samples(rec_frm, ec->samples_per_frame); #endif } return PJ_SUCCESS; } /* * Perform echo cancellation. */ PJ_DEF(pj_status_t) echo_supp_cancel_echo( void *state, pj_int16_t *rec_frm, const pj_int16_t *play_frm, unsigned options, void *reserved ) { echo_supp *ec = (echo_supp*) state; pj_bool_t silence; PJ_UNUSED_ARG(options); PJ_UNUSED_ARG(reserved); silence = pjmedia_silence_det_detect(ec->sd, play_frm, ec->samples_per_frame, NULL); if (!silence) { #if defined(PJMEDIA_ECHO_SUPPRESS_FACTOR) && PJMEDIA_ECHO_SUPPRESS_FACTOR!=0 unsigned i; for (i=0; i<ec->samples_per_frame; ++i) { rec_frm[i] = (pj_int16_t)(rec_frm[i] >> PJMEDIA_ECHO_SUPPRESS_FACTOR); } #else pjmedia_zero_samples(rec_frm, ec->samples_per_frame); #endif } return PJ_SUCCESS; }可供初学者一睹其原貌,但最新的pjproject中的实现就比较复杂。