基于 SpeeX 的音频处理系列之一-speex在 PC 上的移植与回声消除测试

.前言

最近在做音频相关的内容,接触到音频对讲中的一个需求:回声消除。

所谓的回声消除即对应以下模型, 在对讲过程中远端(对方)的讲话通过一定方式传输到近端,在近端(本地)通过喇叭播放,

image

这个喇叭播放的声音以及其环境的各种反射,加上近端的语音(包括噪声等)又被近端(本地)的麦克风采集传送到远端,这样远端就会听到对方的讲话声叠加了自己的讲话声。

远端听到自己的讲话声反射回来了,需要消除这部分,这个工作是在近端完成的,近端已知的是A处即远端传过来的语音,以及B处麦克风采集的叠加数据。

实际上简单的来说就是要从B中减去A的数据。但是这个减是没办法直接减的,A处的原始数据播放到B处采集,实际上有一个映射关系,我们一般要求其是线性的,这个减法需要一定的算法模型去实现,这就是回声消除算法。

在找回声消除算法方案时,正好就找到了一个开源的实现 speex ,见其官网 https://www.speex.org/ 。可以浏览下其官网了解一些背景知识,尤其是其文档可以下载下来先好好看看。

image

我们这一篇就来分享下 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 下所有文件

speexdsp_config_types.h.in 改为

speexdsp_config_types.h

speexdsp\libspeexdsp 下所有 和 文件

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 ” 选项。

定义该宏后,每个 文件都会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 的部分 但是还是有残留没有消除干净 后面再来优化。

image

四 总结

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

 
posted @ 2026-01-09 23:40  FBshark  阅读(9)  评论(0)    收藏  举报