最近在做音频相关的内容,接触到音频对讲中的一个需求:回声消除。所谓的回声消除即对应以下模型, 在对讲过程中远端的讲话通过一定方式传输到近端,在近端通过喇叭播放,
这个喇叭播放的声音以及其环境的各种反射,加上近端的语音(包括噪声等)被近端的麦克风采集传送到远端,这样远端就会听到对方的讲话声叠加了自己的讲话声。远端听到自己的讲话声反射回来了,需要消除这部分。这个工作是在近端完成的,近端已知的是A处即远端传过来的语音,以及B处麦克风采集的叠加数据,实际上简单的来说就是要从B中减去A的数据。但是这个减是没办法直接减的,A处的原始数据播放到B处采集,实际上有一个映射关系,我们一般要求其是线性的,这个减法需要一定的算法模型去实现,这就是回声消除算法。
由于A处的原始数据和B处的麦克风采集可能软件上没法比较好的同步,并且A处原始数据经过PA到喇叭播出来之后的效果存在一定映射关系,这个对应线性度不好的话影响也较大,所以有时候硬件上可以在PA之后做一路回声采集(理想是应该要能采集喇叭播放之后的效果但是做不到,只能采集PA之后的),这个采集和B处的麦克风采集使用同一个声卡采集这样可以保持同步,同时也可以减少PA过程的影响。
在找回声消除算法方案时,正好就找到了一个开源的实现speex,见其官网https://www.speex.org/。可以浏览下其官网了解一些背景知识,尤其是其文档可以下载下来先好好看看。
我们这一篇就来分享下speex在pc上的移植,实现一个回声消除测试的pc端的小程序,一方面先体验一下speex,一方面也可以作为后面调试验证的工具,可以dump数据先在pc端进行仿真测试验证,再移植到具体的平台上去。
我这里使用WSL+Ubuntu。
git clone https://gitlab.xiph.org/xiph/speexdsp.git
cd speexdsp
./autogen.sh 生成各种工程,config.h.in等。
添加以下文件
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
对应的还要配置或者指定相应的头文件包含路径。
该文件定义一些基本的数据类型
编译器支持
定义对应类型
否则按照具体平台定义
#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
工程配置宏
HAVE_CONFIG_H
比如gcc中使用”-DHAVE_CONFIG_H”选项。
定义该宏后,每个c文件都会包含config.h文件,通过config.h进行相应的配置。
config.h中
#undef EXPORT
改为
#define EXPORT
config.h中
如果支持浮点则
#undef FLOATING_POINT
改为
#define FLOATING_POINT
如果使用定点则
#undef FIXED_POINT
改为
#define FIXED_POINT
我这里使用浮点
内部有fft实现
kiss_fft.c
kiss_fftr.c
我们先使用内部实现,硬件支持fft的后面再改为硬件实现。
将#undef USE_KISS_FFT
改为
#define USE_KISS_FFT
还可以使用
USE_SMALLFT
以下依赖具体平台
USE_GPL_FFTW3
USE_INTEL_IPP
USE_INTEL_MKL
speexdsp/libspeexdsp/math_approx.h中依赖
sqrt
acos
exp
atan
fabs
log
floor
等接口
这里默认直接使用标准库
链接时-lm指定链接数学库即可。
嵌入式平台则根据自己平台实现对应的接口即可。
speexdsp/libspeexdsp/os_support.h中相关的动态内存管理接口,内存拷贝等接口,打印等接口。
Pc直接使用标准库即可。嵌入式平台则需要根据自己平台去实现对应接口。
我这里是基于WSL的Linux环境。
新建build.sh文件
添加如下内容
#! /bin/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的细节,以及调试,和在平台上的移植等。