Speex回声消除

本文介绍了使用Speex库进行回声消除的方法。详细说明了回声消除的过程、Speex的AEC算法及最优步长估计,还阐述了相关函数的使用,如初始化、参数设置、回声消除等。同时提到了单声道和双声道回声消除的注意事项,以及采样率、码率、音频帧等音频知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

传给回声消除器的两个声音信号,必须同步得非常的好。具体过程如下:
1、在B端,接收到A说的话以后,要把这些话音数据传给回声消除器做参考,然后再传给声卡,声卡再通过扬声器放出来,这有一段延时。
2、MIC采集B说话的声音和扬声器的声音,然后传给回声消除器,与A传过来的数据比较,从采集到的数据中把频域和参考数据相同的部分消除掉。
如果传给消除器的两个信号同步得不好,即两个信号找不到频域相同的部分,就没有办法进行消除了。

Speex的AEC是以NLMS(Normalized Least Mean Square)为基础,用MDF(multidelay block frequency domain)频域实现,最终推导出最优步长估计:残余回声与误差之比。最优步长等于残余回声方差与误差信号方差之比。 只有改与泄露系数相关部分的代码,才是对效果影响最大的地方,因为根据泄露系数,最终会估计出滤波器的最优步长。

 命令: ./testecho speaker1.wav micin1.wav out1.wav

  测试结果:
  最新的speex的aec效果非常的好,超出了我的想象,回声消除效果不是一般的好,看来是speex更新了不少,因为自从2007年之后,speex很长一段时间都没有更新过代码。有兴趣的同学可以听一下消回声后的和之前的音频对比。

代码解析:

  初始化中,第一个参数是每次处理的帧长度,这个一般是从10ms(80) 到30ms(240) 的处理长度,太长和太短都不是很好,filter_length 也是一个长度,它实际上就是speaker到rec之间的时间差。这个在不同设备上是不同的,跟产品的使用场景,结构,以及软件耗时有关系,一般的是可以测试出来的。
SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length)

系统默认的消回声采样是8k的,如下所示,假如你想改变采样频率,
/* This is the default sampling rate */
427 st->sampling_rate = 8000;
428 st->spec_average = DIV32_16(SHL32(EXTEND32(st->frame_size), 15), st->sampling_rate);

要使用下面的函数:speex_preprocess_state_init(NN,sampleRate)
接下来是要配置消回声的参数设置,一般是采样率设置。
speex_echo_ctl(st, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
参数都可以以下这些:

46 /** Obtain frame size used by the AEC */
47 #define SPEEX_ECHO_GET_FRAME_SIZE 3
48
49 /** Set sampling rate */
50 #define SPEEX_ECHO_SET_SAMPLING_RATE 24
51 /** Get sampling rate */
52 #define SPEEX_ECHO_GET_SAMPLING_RATE 25
53
54 /* Can't set window sizes */
55 /** Get size of impulse response (int32) */
56 #define SPEEX_ECHO_GET_IMPULSE_RESPONSE_SIZE 27
57
58 /* Can't set window content */
59 /** Get impulse response (int32[]) */
60 #define SPEEX_ECHO_GET_IMPULSE_RESPONSE 29

最重要的函数登场了:这个函数,非常的好用,估计只要看一下入参,你就知道怎么使用了。具体的使用就看上面的例子吧。
void speex_echo_cancellation(SpeexEchoState *st, const spx_int16_t *in, const spx_int16_t *far_end, spx_int16_t *out)

假如在预处理中有些参数设置,需要调用预处理函数再把输出的结果处理一下,假如预处理没有了,那就不需要了。
speex_preprocess_run(den, e_buf);
其实,代码流程就这么简单,但是,想把系统效果调试的很好,还是要花不少功夫的。

注意事项:

1 AEC的线性算法处理不了Non-linear distortion(非线性失真)
2 在其它预处理前 先调用AEC
3 speex的aec并不是很适合音响系统里,音响中要慎用。耳机中效果还挺好

————————————————
应用 Speex 库进行回声消除的主要步骤如下:

(1)首先设置音频基本参数,需要调用以下函数 :

_speex_preprocess=speex_preprocess_state_init(FRAMESIZE,8000);

在使用其他功能之前首先调用 Speex 的预处理函数初始化音频参数,函数第一个参数是每帧的大小,第二个参数是采样率。在语音通话中一般采用的帧长为 20 ms,假如是 16 k的语音数据,帧长 20 ms 等于 320 个采样点。

(2)创建回声消除 AEC,设置相关参数。调用 speex_echo_state_init(m_nFrameSize,m_nFilterLen) 创建一个AEC(Acoustic Echo Chancellor,AEC),该函数的第二个参数是尾音长度,即喇叭到麦克风的延迟时间,该参数直接影响了回声消除的效果。

调 用 speex_echo_ctl(st,SPEEX_ECHO_SET_SAMPLING_RATE,&sampleRate)来设置相关参数。 (3)最后在线程中循环调用函数消除回声。调用 speex_echo_cancellation(st,ref_buf,echo_buf,e_buf)函数将数据传递给 AEC,第二个参数是喇叭播放数据,第三个参数是麦克风采集数据,第四个参数是回声消除后的数据。

调用 speex_preprocess_run(_speex_preprocess,outbuf)启动 Speex 的音频处理进行回声消除。Outbuf 即为处理后的数据。

————————————

Speex中的回声消除默认是按单声道处理的
那么Speex支持直接对双声道的数据进行回声消除吗?
答案是肯定的,10年前,Speex的作者Jean-Marc Valin已经给出了答案。

Two related news here.
First, I've merged in support for stereo (or more channels) acoustic echo cancellation in mainline. The standard AEC remains the same, but I added a speex_echo_state_init_mc() call that takes the number of speakers and microphones.

作者说可以用speex_echo_state_init_mc函数来提供对多声道的支持。

speex_echo_state_init_mc定义如下:

speex_echo_state_init_mc

nb_mic表示麦克风(近端数据)的通道数
nb_speakers 表示扬声器(远端数据)的通道数

查看speex_echo_state_init的源码:

/** Creates a new echo canceller state */
EXPORT SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length)
{
    return speex_echo_state_init_mc(frame_size, filter_length, 1, 1);
}

可以看出speex_echo_state_init中实质就是调用了speex_echo_state_init_mc
其中nb_mic和nb_speakers默认设置为1,也就是按单声道处理。

frame_size的含义

Speex中通过speex_echo_state_init函数来初始化frame_size和滤波器长度filter_length:

speex_echo_state_init

speex_echo_state_init_mcspeex_echo_state_init有一个共同的参数。

frame_size  
Number of samples to process at one time (should correspond to 10-20 ms)

frame_size代表啥? 对于单声道和双声道有什么区别?

单声道

如果单声道,假设采样率48k,这里取10ms的数据:

frame_size = 48000 * 10 / 1000 = 480
双声道

总结成一句话:

Jean-Marc Valin escreveu:
In stereo mode, you need to use the init_mc() call and consider the
number of samples per channel. Also, more than 20 ms frames are a bad idea.

frame_size表示the number of samples per channel,也就是单个声道的采样点数,所以双声道甚至多声道时,frame_size和单声道时一样的。

————————————————————

Speex中的回声消除可以对双声道(aec stereo)或多声道的数据进行直接处理。
以下以双声道进行举例:
主要注意事项有三点:
1. 用speex_echo_state_init_mc来进行初始化

echo_state       = speex_echo_state_init_mc(frame_size, filter_length, 2, 2);

2. frame_size的大小
Speex 双声道回声消除中frame_size的含义中已经详细说明,frame_size就是单声道每次处理的采样点的个数。
以48k,取10ms为例:

    int sampleRate     = 48000;
    int nFrameSizeInMS = 10;
    int frame_size    = (nFrameSizeInMS  * sampleRate  * 1.0) / 1000;

计算结果为480.
3. 每次读取多少数据给speex_echo_cancellation
通过对源码进行分析,如果声道数为N,则应该读取N*frame_size的数据给回声消除的模块。
比如双声道,则每次近端数据和远端数据应该读取2 * frame_size个采样点的数据给回声消除

双声道回声消除的小demo如下:
https://github.com/ZhaoliangGuo/speex/blob/master/testecho_stereo/testecho_stereo.cpp

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>
#include "arch.h"

#ifdef __cplusplus  
extern "C" {
#endif

#include "speex/speex_echo.h"
#include "speex/speex_preprocess.h"

#ifdef __cplusplus
}
#endif

#include <windows.h>

int main(int argc, char **argv)
{
    int sampleRate     = 48000;
    int nFrameSizeInMS = 10;
    int nFilterLenInMS = 120;

    SpeexEchoState       *echo_state       = NULL;
    SpeexPreprocessState *preprocess_state = NULL; 

    SYSTEMTIME st;
    ::GetLocalTime(&st);

    int frame_size    = (nFrameSizeInMS  * sampleRate  * 1.0) / 1000;
    int filter_length = (nFilterLenInMS  * sampleRate  * 1.0) / 1000;

    char szMicFileName[]     = ".\\near48_stereo.pcm";
    char szSpeakerFileName[] = ".\\far48_stereo.pcm";

    char szOutputFileName[512];
    sprintf(szOutputFileName, ".\\output_stereo_samplerate_%d_NN_%4d_TAIL_%4d_%04d%02d%02d_%02d%02d%02d.pcm",
        sampleRate,
        frame_size, filter_length,
        st.wYear, st.wMonth, st.wDay, 
        st.wHour, st.wMinute, st.wSecond);
    printf("%s\n", szOutputFileName);

    LARGE_INTEGER timeStartCount;
    LARGE_INTEGER timeEndCount;
    LARGE_INTEGER timeFreq;
    QueryPerformanceFrequency(&timeFreq);
    QueryPerformanceCounter(&timeStartCount);

    FILE *echo_fd,  *ref_fd, *e_fd;

    ref_fd  = fopen(szMicFileName,     "rb");
    echo_fd = fopen(szSpeakerFileName, "rb"); 
    e_fd    = fopen(szOutputFileName,  "wb");

    echo_state       = speex_echo_state_init_mc(frame_size, filter_length, 2, 2); // for stereo
    frame_size *= 2; // length read each time

    preprocess_state = speex_preprocess_state_init(frame_size, sampleRate);

    speex_echo_ctl(echo_state,             SPEEX_ECHO_SET_SAMPLING_RATE,    &sampleRate);
    speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_ECHO_STATE, echo_state);

    short *echo_buf; 
    short *ref_buf;  
    short *e_buf;  

    echo_buf = new short[frame_size];
    ref_buf  = new short[frame_size];
    e_buf    = new short[frame_size];

    while (!feof(ref_fd) && !feof(echo_fd))
    {
        int nLen = fread(ref_buf,  sizeof(short), frame_size, ref_fd);
        if (nLen < 0)
        {
            break;
        }

        nLen = fread(echo_buf, sizeof(short), frame_size, echo_fd);
        if (nLen < 0)
        {
            break;
        }

        speex_echo_cancellation(echo_state, ref_buf, echo_buf, e_buf);
        speex_preprocess_run(preprocess_state, e_buf);

        fwrite(e_buf, sizeof(short), frame_size, e_fd);
    }

    // Destroys an echo canceller state
    speex_echo_state_destroy(echo_state);
    speex_preprocess_state_destroy(preprocess_state);

    fclose(e_fd);
    fclose(echo_fd);
    fclose(ref_fd);

    if (echo_buf)
    {
        delete [] echo_buf;
        echo_buf = NULL;
    }

    if (ref_buf)
    {
        delete [] ref_buf;
        ref_buf = NULL;
    }

    if (e_buf)
    {
        delete [] e_buf;
        e_buf = NULL;
    }

    QueryPerformanceCounter(&timeEndCount);
    double elapsed = (((double)(timeEndCount.QuadPart - timeStartCount.QuadPart) * 1000/ timeFreq.QuadPart));

    printf("AEC Done. TimeDuration: %.2f ms\n", elapsed);

    getchar();

    return 0;
}




链接:https://www.jianshu.com/p/a40ee4d95f91

————————————————————

采样率:
实际中,人发出的声音信号为模拟信号,想要在实际中处理必须为数字信号,即采用采样、量化、编码的处理方案。处理的第一步为采样,即模数转换。简单地说就是通过波形采样的方法记录1秒钟长度的声音,需要多少个数据。根据奈魁斯特(NYQUIST)采样定理,用两倍于一个正弦波的频繁率进行采样就能完全真实地还原该波形。所以,对于声音信号而言,要想对离散信号进行还原,必须将抽样频率定为40KHz以上。实际中,一般定为44.1KHz。44.1KHz采样率的声音就是要花费44100个数据来描述1秒钟的声音波形。原则上采样率越高,声音的质量越好,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级。22.05 KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则已达到DVD音质了。

码率:
对于音频信号而言,必须进行编码。在这里,编码指信源编码,即数据压缩。如果,未经过数据压缩,直接量化进行传输则被称为PCM(脉冲编码调制)。要算一个PCM音频流的码率是一件很轻松的事情,采样率值×采样大小值×声道数 bps。一个采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的WAV文件,它的数据速率则为 44.1K×16×2 =1411.2 Kbps。我们常说128K的MP3,对应的WAV的参数,就是这个1411.2 Kbps,这个参数也被称为数据带宽,它和ADSL中的带宽是一个概念。将码率除以8,就可以得到这个WAV的数据速率,即176.4KB/s。这表示存储一秒钟采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的音频信号,需要176.4KB的空间,1分钟则约为10.34M,这对大部分用户是不可接受的,尤其是喜欢在电脑上听音乐的朋友,要降低磁盘占用,只有2种方法,降低采样指标或者压缩。降低采样指标是不可取的,因此专家们研发了各种压缩方案。最原始的有DPCM、ADPCM,其中最出名的为MP3。所以,采用了数据压缩以后的码率远小于原始码。

音频帧:
音频的帧的概念没有视频帧那么清晰,几乎所有视频编码格式都可以简单的认为一帧就是编码后的一副图像。但音频帧跟编码格式相关,它是各个编码标准自己实现的。
如果以PCM(未经编码的音频数据)来说,它根本就不需要帧的概念,根据采样率和采样精度就可以播放了。比如采样率为44.1HZ,采样精度为16位的音频,你可以算出bitrate(比特率)是44100*16kbps,每秒的音频数据是固定的44100*16/8 字节。
mp3帧较为复杂一点,包含了更多的信息,比如采样率,比特率,等各种参数。具体如下:
音频数据帧个数由文件大小和帧长决定,每个FRAME的长度可能不固定,也可能固定,由位率bitrate决定,每个FRAME又分为帧头和数据实体两部分,帧头记录了mp3的位率,采样率,版本等信息,每个帧之间相互独立。
每帧持续时间(秒) = 每帧采样数 / 采样频率(HZ)
可以这么理解:每帧采用数就是要采取的总数,采样率就是采取的速度,相除就得到时间。

函数名称 speex_echo_state_init
头文件 #include “speex_echo.h”
函数功能 初始化Speex声学回音消除器句柄。
Speex声学回音消除器句柄用于对16位有符号整型单声道原始音频数据进行回音消除。
函数声明 SpeexEchoState * speex_echo_state_init (int frame_size, int filter_length);
函数参数
frame_size,[输入]:存放一帧16位有符号整型单声道原始音频数据的采样数量,单位个,一般为10毫秒到20毫秒。
例如:8000Hz采样频率20毫秒本参数就是160。
filter_length,[输入]:存放回音消除过滤器的采样数量,单位个,一般为100毫秒到500毫秒。
如果采样频率是8000Hz,选择300毫秒,本参数就是8000÷1000×300。
本参数具体大小要慢慢调,调的好不好直接影响回音消除的效果。
返回值 Speex声学回音消除器句柄。
其他说明
如果是对一条音频流回音消除,那么回音消除从开始到结束都应该用一个Speex声学回音消除器句柄,中途不要更换Speex声学回音消除器句柄,也不要用一个Speex声学回音消除器句柄给多条音频流回音消除,否则会导致回音消除后的音频数据不正确。当Speex声学回音消除器句柄不再使用时,必须调用speex_echo_state_destroy()函数销毁Speex声学回音消除器句柄,否则会内存泄漏。

函数名称 speex_echo_state_reset
头文件 #include “speex_echo.h”
函数功能 重置Speex声学回音消除器句柄为初始状态。
函数声明 void speex_echo_state_reset (SpeexEchoState * st)
函数参数
st,[输入]:存放Speex声学回音消除器句柄。
返回值 无

函数名称 speex_echo_ctl
头文件 #include “speex_echo.h”
函数功能 控制Speex声学回音消除器句柄的相关参数。
函数声明 int speex_echo_ctl (SpeexEchoState * st, int request, void * ptr);
函数参数
st,[输入]:存放Speex声学回音消除器句柄。
request,[输入]:存放需要控制的参数,可以为(选一至一个):
SPEEX_ECHO_GET_FRAME_SIZE宏(0x0003):获取Speex声学回音消除器句柄处理一帧时的采样数量,ptr参数为int型变量的内存指针。
SPEEX_ECHO_SET_SAMPLING_RATE宏(0x0018):设置Speex声学回音消除器句柄处理一帧时的采样频率,单位Hz,ptr参数为int型变量的内存指针,默认为8000。
SPEEX_ECHO_GET_SAMPLING_RATE宏(0x0019):获取Speex声学回音消除器句柄处理一帧时的采样频率,ptr参数为int型变量的内存指针。
ptr,[输入&输出]:存放控制参数。本参数是根据request参数来定义的。
返回值
0:成功。
-1:request参数无法识别。

speex_echo_cancel(废弃的)
本函数已经废弃,使用无效。

函数名称 speex_echo_cancellation
头文件 #include “speex_echo.h”
函数功能 根据Speex声学回音消除器句柄,对一帧16位有符号整型单声道原始音频数据进行预处理。
函数声明
void speex_echo_cancellation ( SpeexEchoState * st,
const spx_int16_t * rec,
const spx_int16_t * play,
spx_int16_t * out
);
函数参数
st,[输入]:存放Speex声学回音消除器句柄。
rec,[输入]:存放由音频输入设备录音的一帧16位有符号整型单声道原始音频数据,不能为NULL。
play,[输入]:存放由音频输出设备播放的一帧16位有符号整型单声道原始音频数据,不能为NULL。
out,[输出]:存放经过回音消除后的一帧16位有符号整型单声道原始音频数据,不能为NULL。

返回值 无
其他说明
回音消除的原理就是将音频输入数据中所采集到的音频输出数据清除掉,所以要求音频输入数据和音频输出数据必须是同步的,且音频输入数据中所采集到的回音数据肯定在实际的音频输出数据之后出现。目前还没有找到不同步就可以做回音消除的方法。做回音消除后,最好再使用Speex预处理器对音频输入数据进行噪音抑制、混响消除、自动增益控制、残余回音消除等预处理,不要在回音消除前做预处理操作,否则回音消除效果将降低。如果感觉回音消除效果不好,就把rec、play、out这些参数打印日志出来看看,然后调整speex_echo_state_init()函数的filter_length参数,再测试。

已知问题:
当录音里的回音的音量大于播放的音量,则本函数认为这个不是回音,就不会消除掉。这种情况主要在开了麦克风增益、或者喇叭离麦克风特别近时才会产生。
当录音里的回音和播放的声音区别较大时,则本函数认为这个不是回音,就不会消除掉。这种情况主要是麦克风或音响的音质不好造成的,大多出现在台式机,笔记本一般不会。音频流的刚开始几秒钟内产生的回音可能消除不掉。具体原因未知。

函数名称 speex_echo_state_destroy
头文件 #include “speex_echo.h”
函数功能
销毁Speex声学回音消除器句柄。
函数声明 void speex_echo_state_destroy (SpeexEchoState * st);
函数参数
st,[输入]:存放Speex声学回音消除器句柄。
返回值 无

原文链接:https://blog.csdn.net/Vincentywj/article/details/78269604

The echo canceller is based on the MDF algorithm described in: J. S. Soo, K. K. Pang Multidelay block frequency adaptive filter, IEEE Trans. Acoust. Speech Signal Process., Vol. ASSP-38, No. 2, February 1990. We use the Alternatively Updated MDF (AUMDF) variant. Robustness to double-talk is achieved using a variable learning rate as described in: Valin, J.-M., On Adjusting the Learning Rate in Frequency Domain Echo Cancellation With Double-Talk. IEEE Transactions on Audio, Speech and Language Processing, Vol. 15, No. 3, pp. 1030-1034, 2007. http://people.xiph.org/~jm/papers/valin_taslp2006.pdf There is no explicit double-talk detection, but a continuous variation in the learning rate based on residual echo, double-talk and background noise. About the fixed-point version: All the signals are represented with 16-bit words. The filter weights are represented with 32-bit words, but only the top 16 bits are used in most cases. The lower 16 bits are completely unreliable (due to the fact that the update is done only on the top bits), but help in the adaptation -- probably by removing a "threshold effect" due to quantization (rounding going to zero) when the gradient is small. Another kludge that seems to work good: when performing the weight update, we only move half the way toward the "goal" this seems to reduce the effect of quantization noise in the update phase. This can be seen as applying a gradient descent on a "soft constraint" instead of having a hard constraint.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值