本帖最后由 xhackerustc 于 2025-5-3 16:33 编辑
#申请原创# @21小跑堂
STM32N6系列集成有MDF(Multi-function digital filter),然后阅读原理图发现STM32N6570-DK又板载了一颗MEMS芯片:MP23DB01HP,
这款MEMS芯片是ST家的传感器产品之一,两者配合就可以做到录音功能了。
事实上STM32Cube_FW_N6中的一个测试用例(Projects/STM32N6570-DK/Examples/MDF/MDF_AudioRecorder)展示了录音的效果。其中MDF的滤波器配置结构体MdfFilterconfig可配置各种参数比如放大系数、各种滤波参数等等,也就说STM32N6硬件上已经做了大部分的降噪处理,可见MDF非常强大,如果对去噪声要求不是极高,当下的代码已经直接能用了。
笔者使用这个测试用例测试语音时发现如果同时有人规则地敲桌子、拍球,或者把朝大马路上的窗户开启汽车鸣笛,这些规则的背景噪声还能听到,如果此时要求更高把这些规则背景噪声去除,这时候就要上一些软件去噪方案了。这块开源的方案不多,以前是用speex中自带去噪功能,现在自从2018年Jean-Marc Valin大牛独创性的基于递归神经网络(RNN)的ML模型来抑制音频源中的噪声并开源了RNNoise方案后,RNNoise方案也得到大量的应用。RNNoise原理请阅读Jean-Marc的论文https://jmvalin.ca/demo/rnnoise/ 以及 https://arxiv.org/pdf/1709.08243.pdf 本文只从工程实践性角度移植RNNoise到STM32N6上并做基本测试。
RNNoise的移植
clone RNNoise的源码
git clone https://gitlab.xiph.org/xiph/rnnoise.git
PC机上编译
注:这一步会下载训练好的数据并自动解压
rnnoise源码拷贝
然后我们需要把如下文件加进STM32N6的测试工程:
src/celt_lpc.c src/kiss_fft.c src/rnn.c
src/denoise.c src/pitch.c src/rnn_data.c
src/arch.h src/opus_types.h
src/celt_lpc.h src/pitch.h
src/common.h src/rnn_data.h
src/_kiss_fft_guts.h src/rnn.h
src/kiss_fft.h src/tansig_table.h
src/denoise.h
rnnoise源码修改
虽然STM32N6的CM55开了双精度浮点支持,但是对rnnoise去噪来说双精度其实没必要,float的精度就够啦,所以在CFLAGS中加入两个参数
-fsingle-precision-constant --Wdouble-promotion -Werror
意思是只用单精度float运算,然后源码中任何float->double promotion的地方都会有编译器警告,再配合-Werror CFLAGS编译器碰上就可以报错,一行一行的改,改动基本上是如下这两大类改动:
改1: 任意浮点数常量加f后缀,比如代码中有大量的.5这样浮点常量,改成.5f即可
改2: 任意math中的函数调用加f后缀,比如sqrt()->sqrtf()
这样把所有rnnoise代码中的双精度浮点的使用就可以全去掉啦。
stm32n6的rnnoise处理能力测试
rnnoise默认是48KHZ采样语音,但Projects/STM32N6570-DK/Examples/MDF/MDF_AudioRecorder是16KHZ采样,我们先看看48KHZ的采样,以STM32N6的CPU能否做到实时线性去燥。这里实时线性去噪意思是采样得到的数据能否及时处理,为此笔者仿rnnoise的example写了个小测试用例,代码片段如下:
float x[FRAME_SIZE];
int i, loops = 100;
DenoiseState* st = rnnoise_create(NULL);
/* Enable the cycle counter */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL = 0;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
int cycles = DWT->CYCCNT;
for (j = 0; j < loops; j++) {
rnnoise_process_frame(st, x, x);
}
cycles = (DWT->CYCCNT - cycles);
printf("%u cycles\n", cycles / loops);
这部分代码在STM32N6上运行结果是2435154 cycles,因cpu是600MHZ,那么就是0.00405859s,FRAME_SIZE沿用rnnoise定义是480个采样数据,那么STM32N6 cpu每秒可做480/0.00405859 ~= 118268个采样>数据的RNN去噪,所以即使48KHZ采样即每秒48000个采样数据,STM32N6的CPU也能cover住。
去噪效果
因从MP23DB01HP全向数字麦克风采集数据并DMA搬运到sram/psram非本文重点,事实上ST官方的Projects/STM32N6570-DK/Examples/MDF/MDF_AudioRecorder照着改改就可以用的,即使调节也无非改改MDF的参数,所以这里着重测试rnnoise的去噪效果,所以笔者测试过程如下:
初始化psram=》openocd加载一段裸pcm数据到psram=》运行rnnoise去噪,并保存到psram中=》openocd把此去噪后的pcm数据dump到PC机,然后用audacity软件比较去噪前和去噪后的pcm数据得到结论。
首先psram初始化
STM32Cube_FW_N6其实唷psram初始化例子,在Projects/STM32N6570-DK/Examples/XSPI/XSPI_PSRAM_MemoryMapped/,我们拿过来改改即可,核心代码片段如下: MX_XSPI1_Init();
Configure_APMemory();
/*Configure Memory Mapped mode*/
sCommand.OperationType = HAL_XSPI_OPTYPE_WRITE_CFG;
sCommand.InstructionMode = HAL_XSPI_INSTRUCTION_8_LINES;
sCommand.InstructionWidth = HAL_XSPI_INSTRUCTION_8_BITS;
sCommand.InstructionDTRMode = HAL_XSPI_INSTRUCTION_DTR_DISABLE;
sCommand.Instruction = WRITE_CMD;
sCommand.AddressMode = HAL_XSPI_ADDRESS_8_LINES;
sCommand.AddressWidth = HAL_XSPI_ADDRESS_32_BITS;
sCommand.AddressDTRMode = HAL_XSPI_ADDRESS_DTR_ENABLE;
sCommand.Address = 0x0;
sCommand.AlternateBytesMode = HAL_XSPI_ALT_BYTES_NONE;
sCommand.DataMode = HAL_XSPI_DATA_16_LINES;
sCommand.DataDTRMode = HAL_XSPI_DATA_DTR_ENABLE;
sCommand.DataLength = BUFFERSIZE;
sCommand.DummyCycles = DUMMY_CLOCK_CYCLES_WRITE;
sCommand.DQSMode = HAL_XSPI_DQS_ENABLE;
if (HAL_XSPI_Command(&hxspi1, &sCommand, HAL_XSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
sCommand.OperationType = HAL_XSPI_OPTYPE_READ_CFG;
sCommand.Instruction = READ_CMD;
sCommand.DummyCycles = DUMMY_CLOCK_CYCLES_READ;
sCommand.DQSMode = HAL_XSPI_DQS_ENABLE;
if (HAL_XSPI_Command(&hxspi1, &sCommand, HAL_XSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
Error_Handler();
}
sMemMappedCfg.TimeOutActivation = HAL_XSPI_TIMEOUT_COUNTER_ENABLE;
sMemMappedCfg.TimeoutPeriodClock = 0x34;
if (HAL_XSPI_MemoryMapped(&hxspi1, &sMemMappedCfg) != HAL_OK)
{
Error_Handler();
}
其次rnnoise处理psram中的原始pcm数据
float x[FRAME_SIZE];
short *srctmp = 0x90000000;
short *dsttmp = 0x90000000 + 4*1024*1024;
__BKPT(0); //此处breakpoint,openocd会收到这个信息,然后openocd即可加载原始PCM数据
while(srctmp < (short *)(0x90000000 + PCM_LEN)) {
for (i=0;i<FRAME_SIZE;i++) x[i] = *srctmp++;
rnnoise_process_frame(st, x, x);
for (i=0;i<FRAME_SIZE;i++) *dsttmp++ = x[i];
}
for (;;) __WFI();
openocd加载pcm数据到psram
load_image /tmp/output.raw 0x90000000
openocd把解噪后的pcm数据dump到pc机
dump_image /tmp/my.raw 0x90400000 3563520
audacity对比rnnoise解噪效果
如图所示上面的是解噪前,下面的解噪后,可以看到rnnoise效果还是很不错的,很适合用于语音降噪。
后续工作:
Cortex-M55 MVE优化rnnoise的代码
能否利用STM32N6自带的NPU来优化rnnoise的代码
|