【STM32MP257F-DK测评】speexdsp的NS算法移植和测试
[复制链接]
本帖最后由 latera 于 2026-4-14 22:53 编辑
一、speexdsp库说明
SpeexDSP 是一个开源的音频信号处理库,专注于为语音通信提供实时处理功能。它是经典语音编解码器项目 Speex 的一部分,但独立于编解码器,专门处理原始音频信号。
它的核心功能包括:
-
回声消除 (AEC):消除在语音通话中,自己扬声器播放的声音被麦克风再次采集产生的回声。
-
噪声抑制 (NS):降低背景环境噪声,提升语音清晰度。
-
自动增益控制 (AGC):自动调整录音音量,使声音大小保持稳定。
-
静音检测 (VAD):判断一段音频是否包含人声,常用于节省带宽或触发语音识别。
二、speexdsp库使用
STM32MP2的文件系统默认包含speexdsp的库文件和头文件。
image.png (33.7 KB, 下载次数: 0)
下载附件
保存到相册
5 天前 上传
image.png (40.54 KB, 下载次数: 0)
下载附件
保存到相册
5 天前 上传
在程序中,包含头文件,并按初始化设置 --> 设置参数 --> 周期处理 --> 最后销毁 的步骤来编写程序便可。如我要使用噪声抑制的方法:
1. 初始化预处理器(噪声抑制)
SpeexPreprocessState *preprocess_state;
preprocess_state = speex_preprocess_state_init(frame_size, sample_rate); // frame_size 表示一帧音频数据的长度
2. 配置噪声抑制参数和噪声抑制强度
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled); // denoise_enabled = 1 (1表示使能)
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &noise_suppress_level); // noise_suppress_level = -25 (单位db,)
3. 应用噪声抑制
speex_preprocess(preprocess_state, samples, NULL); // samples 为一帧单声道的数据。
程序参考(在后文有测试说明):
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <speex/speex_preprocess.h>
#include <string.h>
#define DEVICE "hw:1,0"
#define SAMPLE_RATE 48000
#define CHANNELS 2
#define SAMPLE_SIZE 2 /* 16-bit */
#define PERIOD_SIZE 256
#define BUFFER_SIZE (8 * PERIOD_SIZE)
#define RING_BUFFER_SIZE 16 /* 环形缓冲区大小(周期数) */
// 环形缓冲区结构
typedef struct {
char *data;
int size;
int period_size;
int write_pos;
int read_pos;
sem_t empty;
sem_t full;
pthread_mutex_t mutex;
} RingBuffer;
// 全局变量
snd_pcm_t *capture_handle, *playback_handle;
RingBuffer ring_buffer;
SpeexPreprocessState *preprocess_state;
// 初始化环形缓冲区
int init_ring_buffer(RingBuffer *rb, int period_size, int num_periods) {
rb->period_size = period_size * CHANNELS * SAMPLE_SIZE;
rb->size = num_periods;
rb->data = (char *)malloc(rb->period_size * num_periods);
if (!rb->data) {
fprintf(stderr, "无法分配环形缓冲区\n");
return -1;
}
rb->write_pos = 0;
rb->read_pos = 0;
sem_init(&rb->empty, 0, num_periods);
sem_init(&rb->full, 0, 0);
pthread_mutex_init(&rb->mutex, NULL);
return 0;
}
// 写入环形缓冲区
void write_ring_buffer(RingBuffer *rb, char *data) {
sem_wait(&rb->empty);
pthread_mutex_lock(&rb->mutex);
memcpy(rb->data + rb->write_pos * rb->period_size, data, rb->period_size);
rb->write_pos = (rb->write_pos + 1) % rb->size;
pthread_mutex_unlock(&rb->mutex);
sem_post(&rb->full);
}
// 从环形缓冲区读取
void read_ring_buffer(RingBuffer *rb, char *data) {
sem_wait(&rb->full);
pthread_mutex_lock(&rb->mutex);
memcpy(data, rb->data + rb->read_pos * rb->period_size, rb->period_size);
rb->read_pos = (rb->read_pos + 1) % rb->size;
pthread_mutex_unlock(&rb->mutex);
sem_post(&rb->empty);
}
// 清理环形缓冲区
void cleanup_ring_buffer(RingBuffer *rb) {
free(rb->data);
sem_destroy(&rb->empty);
sem_destroy(&rb->full);
pthread_mutex_destroy(&rb->mutex);
}
// 采集线程函数
void *capture_thread(void *arg) {
short *buffer = (short *)malloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE);
if (!buffer) {
fprintf(stderr, "无法分配采集缓冲区\n");
return NULL;
}
short *samples = (short *)malloc(PERIOD_SIZE*SAMPLE_SIZE);
int err;
while (1) {
// 从捕获设备读取数据
if ((err = snd_pcm_readi(capture_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "捕获设备发生溢出,正在恢复...\n");
snd_pcm_prepare(capture_handle);
continue;
} else {
fprintf(stderr, "读取捕获设备失败: %s\n", snd_strerror(err));
break;
}
}
// 使用 speexdsp 进行降噪处理
for(int i=0;i<PERIOD_SIZE;i++){
samples[i] = buffer[CHANNELS*i];
}
speex_preprocess(preprocess_state, samples, NULL);
for(int i=0;i<PERIOD_SIZE;i++){
buffer[CHANNELS*i] = samples[i] ;
}
// 写入环形缓冲区
write_ring_buffer(&ring_buffer, buffer);
}
free(buffer);
return NULL;
}
// 播放线程函数
void *playback_thread(void *arg) {
char *buffer = (char *)malloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE);
if (!buffer) {
fprintf(stderr, "无法分配播放缓冲区\n");
return NULL;
}
int err;
while (1) {
// 从环形缓冲区读取数据
read_ring_buffer(&ring_buffer, buffer);
// 写入播放设备
if ((err = snd_pcm_writei(playback_handle, buffer, PERIOD_SIZE)) != PERIOD_SIZE) {
if (err == -EPIPE) {
fprintf(stderr, "播放设备发生下溢,正在恢复...\n");
snd_pcm_prepare(playback_handle);
continue;
} else {
fprintf(stderr, "写入播放设备失败: %s\n", snd_strerror(err));
break;
}
}
}
free(buffer);
return NULL;
}
int main() {
int err;
snd_pcm_hw_params_t *hw_params;
pthread_t capture_tid, playback_tid;
// 初始化环形缓冲区
if (init_ring_buffer(&ring_buffer, PERIOD_SIZE, RING_BUFFER_SIZE) < 0) {
return EXIT_FAILURE;
}
// 打开捕获设备
if ((err = snd_pcm_open(&capture_handle, DEVICE, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
fprintf(stderr, "无法打开捕获设备: %s\n", snd_strerror(err));
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
// 打开播放设备
if ((err = snd_pcm_open(&playback_handle, DEVICE, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf(stderr, "无法打开播放设备: %s\n", snd_strerror(err));
snd_pcm_close(capture_handle);
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
// 配置捕获设备
if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
fprintf(stderr, "无法分配硬件参数结构: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) {
fprintf(stderr, "无法初始化硬件参数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf(stderr, "无法设置访问类型: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf(stderr, "无法设置格式: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, SAMPLE_RATE, 0)) < 0) {
fprintf(stderr, "无法设置采样率: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, CHANNELS)) < 0) {
fprintf(stderr, "无法设置通道数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_period_size(capture_handle, hw_params, PERIOD_SIZE, 0)) < 0) {
fprintf(stderr, "无法设置周期大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_buffer_size(capture_handle, hw_params, BUFFER_SIZE)) < 0) {
fprintf(stderr, "无法设置缓冲区大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) {
fprintf(stderr, "无法设置捕获设备参数: %s\n", snd_strerror(err));
goto cleanup;
}
// 配置播放设备
if ((err = snd_pcm_hw_params_any(playback_handle, hw_params)) < 0) {
fprintf(stderr, "无法初始化播放设备硬件参数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_access(playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf(stderr, "无法设置播放设备访问类型: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_format(playback_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf(stderr, "无法设置播放设备格式: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_rate(playback_handle, hw_params, SAMPLE_RATE, 0)) < 0) {
fprintf(stderr, "无法设置播放设备采样率: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_channels(playback_handle, hw_params, CHANNELS)) < 0) {
fprintf(stderr, "无法设置播放设备通道数: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_period_size(playback_handle, hw_params, PERIOD_SIZE, 0)) < 0) {
fprintf(stderr, "无法设置播放设备周期大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params_set_buffer_size(playback_handle, hw_params, BUFFER_SIZE)) < 0) {
fprintf(stderr, "无法设置播放设备缓冲区大小: %s\n", snd_strerror(err));
goto cleanup;
}
if ((err = snd_pcm_hw_params(playback_handle, hw_params)) < 0) {
fprintf(stderr, "无法设置播放设备参数: %s\n", snd_strerror(err));
goto cleanup;
}
snd_pcm_hw_params_free(hw_params);
// 初始化 speex 降噪器
preprocess_state = speex_preprocess_state_init(PERIOD_SIZE, SAMPLE_RATE);
if (!preprocess_state) {
fprintf(stderr, "无法初始化 speex 降噪器\n");
goto cleanup;
}
// 设置降噪参数
int denoise_enabled = 1;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled);
int noise_suppression = -40;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &noise_suppression);
// 启动播放设备(填充初始缓冲区)
char *init_buffer = (char *)calloc(PERIOD_SIZE * CHANNELS * SAMPLE_SIZE, 1);
if (init_buffer) {
for (int i = 0; i < 3; i++) {
snd_pcm_writei(playback_handle, init_buffer, PERIOD_SIZE);
}
free(init_buffer);
}
// 创建线程
if (pthread_create(&capture_tid, NULL, capture_thread, NULL) != 0) {
fprintf(stderr, "无法创建采集线程\n");
goto cleanup;
}
if (pthread_create(&playback_tid, NULL, playback_thread, NULL) != 0) {
fprintf(stderr, "无法创建播放线程\n");
goto cleanup;
}
// 主循环
printf("开始音频环回测试...\n");
printf("按Ctrl+C退出\n");
while (1) {
sleep(1);
}
cleanup:
// 清理资源
if (preprocess_state) {
speex_preprocess_state_destroy(preprocess_state);
}
if (hw_params) {
snd_pcm_hw_params_free(hw_params);
}
snd_pcm_close(capture_handle);
snd_pcm_close(playback_handle);
cleanup_ring_buffer(&ring_buffer);
return EXIT_FAILURE;
}
Makefile文件为
# Makefile for ALSA loopback test
# Ensure cross-compilation environment is set up
ifeq ($(CC),)
$(warning Cross-compilation environment not set up!)
$(warning Please run: source ../../SDK/environment-setup-cortexa35-ostl-linux)
$(warning before building this project.)
endif
CFLAGS = -Wall -Wextra -g
LDFLAGS = -lasound -lspeexdsp -pthread
TARGET = loopback
SOURCES = loopback.c
all: $(TARGET)
$(TARGET): $(SOURCES)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f $(TARGET)
help:
@echo "ALSA Loopback Test Makefile"
@echo "============================"
@echo "make - 编译程序"
@echo "make clean - 清理编译产物"
@echo "make help - 显示此帮助信息"
.PHONY: all clean help
三、speexdsp NS 测试
我同样使用 usb声卡来测试处理效果,声卡修改参考:【STM32MP257F-DK测评】usb声卡修改 https://bbs.eeworld.com.cn/thread-1345436-1-1.html
在上面的程序中,我将uac声卡设置成16bit的双声道音频。uac同时播放和录音。其中,读取uac声卡数据后,取出左声道来作为speexdsp ans 处理信号,右声道直接不做处理。最后将这2声道的数据从声卡播放出去。电脑的录音,就能获得处理前后的效果对比文件。
测试音频为一段歌曲和白噪声的混音音频。测试结果如下:
image.png (1.05 MB, 下载次数: 0)
下载附件
保存到相册
5 天前 上传
可以看出speexdsp对平缓噪声的抑制效果还是比较好的。测试音频见附件:
|