基于 SpeeX 的音频处理系列之一-speex在 PC 上的移植与回声消除测试
一.前言
最近在做音频相关的内容,接触到音频对讲中的一个需求:回声消除。
所谓的回声消除即对应以下模型, 在对讲过程中远端(对方)的讲话通过一定方式传输到近端,在近端(本地)通过喇叭播放,

这个喇叭播放的声音以及其环境的各种反射,加上近端的语音(包括噪声等)又被近端(本地)的麦克风采集传送到远端,这样远端就会听到对方的讲话声叠加了自己的讲话声。
远端听到自己的讲话声反射回来了,需要消除这部分,这个工作是在近端完成的,近端已知的是A处即远端传过来的语音,以及B处麦克风采集的叠加数据。
实际上简单的来说就是要从B中减去A的数据。但是这个减是没办法直接减的,A处的原始数据播放到B处采集,实际上有一个映射关系,我们一般要求其是线性的,这个减法需要一定的算法模型去实现,这就是回声消除算法。
在找回声消除算法方案时,正好就找到了一个开源的实现 speex ,见其官网 https://www.speex.org/ 。可以浏览下其官网了解一些背景知识,尤其是其文档可以下载下来先好好看看。

我们这一篇就来分享下 speex 在 pc 上的移植,实现一个回声消除测试的 pc 端的小程序,一方面先体验一下 speex ,一方面也可以作为后面调试验证的工具,可以 dump 数据先在 pc 端进行仿真测试验证,再移植到具体的平台上去。
二. 在 PC 环境移植 SpeeX
我这里使用 WSL+Ubuntu 。
2.1 下载源码
git clone https://gitlab.xiph.org/xiph/speexdsp.git
cd speexdsp
./autogen.sh 生成各种工程 ,config.h.in 等。
2.2 添加文件
添加以下文件
speexdsp\include\speex 下所有h 文件
speexdsp_config_types.h.in 改为
speexdsp_config_types.h
speexdsp\libspeexdsp 下所有 h 和 c 文件
test 开头的不需要添加,可以单独放一个文件夹,在写自己的应用时参考。
config.h.in 改为config.h
最终文件如下
lhj@lhj:~/speex/speexdsp$ tree .
.
|-- config.h
|-- include
| `-- speex
| |-- speex_buffer.h
| |-- speex_echo.h
| |-- speex_jitter.h
| |-- speex_preprocess.h
| |-- speex_resampler.h
| |-- speexdsp_config_types.h
| `-- speexdsp_types.h
|-- libspeexdsp
| |-- _kiss_fft_guts.h
| |-- arch.h
| |-- bfin.h
| |-- buffer.c
| |-- fftwrap.c
| |-- fftwrap.h
| |-- filterbank.c
| |-- filterbank.h
| |-- fixed_arm4.h
| |-- fixed_arm5e.h
| |-- fixed_bfin.h
| |-- fixed_debug.h
| |-- fixed_generic.h
| |-- jitter.c
| |-- kiss_fft.c
| |-- kiss_fft.h
| |-- kiss_fftr.c
| |-- kiss_fftr.h
| |-- math_approx.h
| |-- mdf.c
| |-- misc_bfin.h
| |-- os_support.h
| |-- preprocess.c
| |-- pseudofloat.h
| |-- resample.c
| |-- resample_neon.h
| |-- resample_sse.h
| |-- scal.c
| |-- smallft.c
| |-- smallft.h
| `-- vorbis_psy.h
`-- test
|-- testdenoise.c
|-- testecho.c
|-- testjitter.c
|-- testresample.c
`-- testresample2.c
4 directories, 44 files
2.3 配置 speexdsp_config_types.h
该文件定义一些基本的数据类型
编译器支持 则直接包含
定义对应类型
否则按照具体平台定义
#ifndef __SPEEX_TYPES_H__
#define __SPEEX_TYPES_H__
#include
typedef int16_t spx_int16_t;
typedef uint16_t spx_uint16_t;
typedef int32_t spx_int32_t;
typedef uint32_t spx_uint32_t;
#endif
2.4 配置 config.h
工程配置宏
HAVE_CONFIG_H
比如 gcc 中使用 ” -DHAVE_CONFIG_H ” 选项。
定义该宏后,每个 c 文件都会include config.h 文件,通过 config.h 进行相应的配置。
2.4.1 EXPORT 宏
config.h 中
#undef EXPORT
改为
#define EXPORT
2.4.2 配置使用浮点还是定点
config.h 中
如果支持浮点则
#undef FLOATING_POINT
改为
#define FLOATING_POINT
如果使用定点则
#undef FIXED_POINT
改为
#define FIXED_POINT
我这里使用浮点
2.4.3 指定 FFT 实现
内部有 fft 实现
kiss_fft .c
kiss_fft r.c
我们先使用内部实现,硬件支持 fft 的后面再改为硬件实现。
将 #undef USE_KISS_FFT
改为
#define USE_KISS_FFT
还可以使用
USE_SMALLFT
以下依赖具体平台
USE_GPL_FFTW3
USE_INTEL_IPP
USE_INTEL_MKL
2.4.4 math 依赖
speexdsp/libspeexdsp/math_approx.h 中依赖
sqrt
acos
exp
atan
fabs
log
floor
等接口
这里默认直接使用标准库
链接时 -lm 指定链接数学库即可。
嵌入式平台则根据自己平台实现对应的接口即可。
2.4.5Os 依赖
speexdsp/libspeexdsp/os_support.h 中相关的动态内存管理接口,内存拷贝等接口,打印等接口。
Pc 直接使用标准库即可。嵌入式平台则需要根据自己平台去实现对应接口。
2.5 编译
我这里是基于 WSL 的 Linux 环境。
新建 build.sh 文件
添加如下内容
gcc libspeexdsp/*.c speexecho.c -Iinclude -I. -DHAVE_CONFIG_H -lm -o speexecho
其中 speexecho.c 是用户自己的应用代码。
chmod +x build.sh
编译 ./build.sh
三 . 实现回声消除测试程序
我们基于 testecho.c ,该 demo 读原始 spk(echo) 和 mic 的原始数据流,输出回声消除后的数据。
我们这里在原来的基础上修改下,可以读 wav 文件并且输出也是 wav 文件方便后面直接播放。假设我们这里自己的平台也可以 dump , mic 和当前 spk 的 echo 数据为 wav 格式。
我这里有个抓取的 mic3.wav 和 spk3.wav 的测试数据。
我们这里假设 wav 文件都是单通道, 16 位。
实现代码如下
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "speex/speex_echo.h"
#include "speex/speex_preprocess.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* WAV解析 */
#define CHUNK_RIFF "RIFF"
#define CHUNK_WAVE "WAVE"
#define CHUNK_FMT "fmt "
#define CHUNK_DATA "data"
typedef struct
{
uint32_t off;
uint32_t chunksize;
uint16_t audioformat;
uint16_t numchannels;
uint32_t samplerate;
uint32_t byterate;
uint16_t blockalign;
uint16_t bitspersample;
uint32_t datasize;
}wav_t;
static int wav_decode_head(uint8_t* buffer, wav_t* wav)
{
uint8_t* p = buffer;
uint32_t chunksize;
uint32_t subchunksize;
if(0 != memcmp(p,CHUNK_RIFF,4))
{
return -1;
}
p += 4;
chunksize = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
wav->chunksize = chunksize;
p += 4;
if(0 != memcmp(p,CHUNK_WAVE,4))
{
return -2;
}
p += 4;
do
{
if(0 == memcmp(p,CHUNK_FMT,4))
{
p += 4;
subchunksize = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
p += 4;
/* 解析参数 */
wav->audioformat = (uint16_t)p[0] | ((uint16_t)p[1]<<8);
if((wav->audioformat == 0x0001) || (wav->audioformat == 0xFFFE))
{
p += 2;
wav->numchannels = (uint16_t)p[0] | ((uint16_t)p[1]<<8);
p += 2;
wav->samplerate = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
p += 4;
wav->byterate = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
p += 4;
wav->blockalign = (uint16_t)p[0] | ((uint16_t)p[1]<<8);
p += 2;
wav ->bitspersample = (uint16_t)p[0] | ((uint16_t)p[1]<<8);
p += 2;
if(subchunksize >16)
{
/* 有ext区域 */
uint16_t cbsize = (uint16_t)p[0] | ((uint16_t)p[1]<<8);
p += 2;
if(cbsize > 0)
{
/* ext数据 2字节有效bits wValidBitsPerSample ,4字节dwChannelMask 16字节SubFormat */
p += 2;
p += 4;
/* 比对subformat */
p += 16;
}
}
}
else
{
p += subchunksize;
}
}
else if(0 == memcmp(p,CHUNK_DATA,4))
{
p += 4;
subchunksize = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
wav->datasize = subchunksize;
p += 4;
wav->off = (uint32_t)(p- buffer);
return 0;
}
else
{
p += 4;
subchunksize = (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
p += 4;
p += subchunksize;
}
}while((uint32_t)(p - buffer) < (chunksize + 8));
return -3;
}
/* 填充44字节的wav头 */
static void wav_fill_head(uint8_t* buffer, int samples, int chnum, int freq)
{
/*
* 添加wav头信息
*/
uint32_t chunksize = 44-8+samples*chnum*16/8;
uint8_t* p = (uint8_t*)buffer;
uint32_t bps = freq*chnum*16/8;
uint32_t datalen = samples*chnum*16/8;
p[0] = 'R';
p[1] = 'I';
p[2] = 'F';
p[3] = 'F';
p[4] = chunksize & 0xFF;
p[5] = (chunksize>>8) & 0xFF;
p[6] = (chunksize>>16) & 0xFF;
p[7] = (chunksize>>24) & 0xFF;
p[8] = 'W';
p[9] = 'A';
p[10] = 'V';
p[11] = 'E';
p[12] = 'f';
p[13] = 'm';
p[14] = 't';
p[15] = ' ';
p[16] = 16; /* Subchunk1Size */
p[17] = 0;
p[18] = 0;
p[19] = 0;
p[20] = 1; /* PCM */
p[21] = 0;
p[22] = chnum; /* 通道数 */
p[23] = 0;
p[24] = freq & 0xFF;
p[25] = (freq>>8) & 0xFF;
p[26] = (freq>>16) & 0xFF;
p[27] = (freq>>24) & 0xFF;
p[28] = bps & 0xFF; /* ByteRate */
p[29] = (bps>>8) & 0xFF;
p[30] = (bps>>16) & 0xFF;
p[31] = (bps>>24) & 0xFF;
p[32] = chnum*16/8; /* BlockAlign */
p[33] = 0;
p[34] = 16; /* BitsPerSample */
p[35] = 0;
p[36] = 'd';
p[37] = 'a';
p[38] = 't';
p[39] = 'a';
p[40] = datalen & 0xFF;
p[41] = (datalen>>8) & 0xFF;
p[42] = (datalen>>16) & 0xFF;
p[43] = (datalen>>24) & 0xFF;
}
void wav_print(wav_t* wav)
{
printf("off:%d\r\n",wav->off);
printf("chunksize:%d\r\n",wav->chunksize);
printf("audioformat:%d\r\n",wav->audioformat);
printf("numchannels:%d\r\n",wav->numchannels);
printf("samplerate:%d\r\n",wav->samplerate);
printf("byterate:%d\r\n",wav->byterate);
printf("blockalign:%d\r\n",wav->blockalign);
printf("bitspersample:%d\r\n",wav->bitspersample);
printf("datasize:%d\r\n",wav->datasize);
}
#define NN 128
#define TAIL 1024
int main(int argc, char **argv)
{
FILE *spk_fd, *mic_fd, *out_fd;
short spk_buf[NN], mic_buf[NN], out_buf[NN];
uint8_t spk_wav_buf[44]; /* 输入spk wav文件头缓存 */
uint8_t mic_wav_buf[44]; /* 输入mic wav文件头缓存 */
uint8_t out_wav_buf[44]; /* 输出文件wav头缓存 */
wav_t spk_wav;
wav_t mic_wav;
int samps; /* 采样点数 */
int times; /* 读取次数 */
SpeexEchoState *st;
SpeexPreprocessState *den;
int sampleRate;
char* mic_fname = argv[1];
char* spk_fname = argv[2];
char* out_fname = argv[3];
if (argc != 4)
{
fprintf(stderr, "testecho mic.wav spk.wav out.wav\n");
exit(1);
}
spk_fd = fopen(spk_fname, "rb");
if(spk_fd < 0){
fprintf(stderr, "open file %s err\n",spk_fname);
exit(1);
}
mic_fd = fopen(mic_fname, "rb");
if(mic_fd < 0){
fprintf(stderr, "open file %s err\n",mic_fname);
fclose(spk_fd);
exit(1);
}
out_fd = fopen(out_fname, "wb");
if(out_fd < 0){
fprintf(stderr, "open file %s err\n",out_fname);
fclose(spk_fd);
fclose(mic_fd);
exit(1);
}
if(44 != fread(mic_wav_buf, 1, 44, mic_fd)){
fprintf(stderr, "read file %s err\n",mic_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
if(44 != fread(spk_wav_buf, 1, 44, spk_fd)){
fprintf(stderr, "read file %s err\n",spk_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
if(0 != wav_decode_head(spk_wav_buf, &spk_wav)){
fprintf(stderr, "decode file %s err\n",spk_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
printf("[spk_wav]\r\n");
wav_print(&spk_wav);
if(0 != wav_decode_head(mic_wav_buf, &mic_wav)){
fprintf(stderr, "decode file %s err\n",mic_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
printf("[mic_wav]\r\n");
wav_print(&mic_wav);
samps = spk_wav.datasize > mic_wav.datasize ? mic_wav.datasize : spk_wav.datasize; /* 获取较小的数据大小 */
samps /= spk_wav.blockalign; /* 采样点数 = 数据大小 除以 blockalign */
printf("\r\nsamps:%d\r\n",samps);
sampleRate = spk_wav.samplerate;
wav_fill_head(out_wav_buf, samps, 1, sampleRate); /* 输出文件头 */
if(44 != fwrite(out_wav_buf, 1, 44, out_fd)){
fprintf(stderr, "write file %s err\n",out_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
st = speex_echo_state_init(NN, TAIL);
den = speex_preprocess_state_init(NN, sampleRate);
speex_echo_ctl(st, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
speex_preprocess_ctl(den, SPEEX_PREPROCESS_SET_ECHO_STATE, st);
times = samps / NN; /* 一次读取NN个点,读取times次 */
for(int i=0; i<times; i++)
{
if(NN != fread(mic_buf, sizeof(short), NN, mic_fd)){
fprintf(stderr, "read file %s err\n",mic_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
if(NN != fread(spk_buf, sizeof(short), NN, spk_fd)){
fprintf(stderr, "read file %s err\n",spk_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
speex_echo_cancellation(st, mic_buf, spk_buf, out_buf);
speex_preprocess_run(den, out_buf);
if(NN != fwrite(out_buf, sizeof(short), NN, out_fd)){
fprintf(stderr, "write file %s err\n",out_fname);
fclose(spk_fd);
fclose(mic_fd);
fclose(out_fd);
exit(1);
}
}
speex_echo_state_destroy(st);
speex_preprocess_state_destroy(den);
fclose(out_fd);
fclose(spk_fd);
fclose(mic_fd);
return 0;
}
测试
./speexecho mic3.wav spk3.wav out3.wav
可以看到 out3.wav 相对与 mic3.wav 消除掉了 spk3.wav 的部分 , 但是还是有残留没有消除干净 , 后面再来优化。

四 . 总结
以上分享了 speex 在 pc 上的移植使用,实现了一个简单的回声消除的测试程序,先暂时体验一下,后面再来学习了解 speex 的细节,以及调试,和在平台上的移植等。

浙公网安备 33010602011771号