FRCRN语音降噪实战:集成STM32F103C8T6的嵌入式音频处理方案
FRCRN语音降噪实战:集成STM32F103C8T6的嵌入式音频处理方案
你有没有遇到过这种情况?在嘈杂的工厂车间里,想通过语音指令控制设备,但机器轰鸣声完全盖过了人声;或者家里的智能音箱在客厅开着电视时,总是听不清你的命令。噪声,这个无处不在的干扰,一直是语音交互落地的一大难题。
今天要聊的,就是一个把专业级语音降噪技术“塞进”小小嵌入式板卡里的实战方案。我们不用复杂的DSP芯片,就用大家手边可能都有的STM32F103C8T6最小系统板,配合云端强大的FRCRN降噪模型,搭建一套成本可控、效果不错的实时音频处理系统。这套思路特别适合那些对成本敏感,但又需要一定语音清晰度的场景,比如工业现场的设备语音控制、智能家居的中控节点,或者一些教育玩具里。
1. 为什么要在嵌入式场景做语音降噪?
先说说背景。传统的嵌入式语音处理,要么靠硬件滤波电路,只能对付固定频率的噪声;要么用一些简单的软件算法,比如谱减法,效果有限,还容易损伤语音本身。而像FRCRN这类基于深度学习的模型,在服务器上已经能实现非常干净的降噪效果,但它的计算量对单片机来说是天方夜谭。
所以,我们的思路很直接:让专业的设备干专业的事。让STM32负责它擅长的——实时采集音频、打包发送;让云端服务器负责它擅长的——运行复杂的FRCRN模型进行降噪。两者通过通信链路结合起来,各取所长。STM32F103C8T6这块板子,价格便宜,资源够用(有ADC、DMA、定时器,还有串口和网络接口可选),是扮演前端采集角色的绝佳选择。
这个方案的核心价值在于实用性和可落地性。它不需要你更换昂贵的硬件,而是在现有常见的嵌入式平台上,通过架构设计,接入了先进的AI降噪能力。对于很多产品来说,这是一个从“语音功能不可用”到“语音功能基本可用”的关键跨越。
2. 系统整体设计思路
这套系统可以看成三个部分:前端采集、云端处理、结果回传。听起来简单,但每个环节都有一些细节需要注意。
2.1 硬件连接与选型
首先看前端。核心就是STM32F103C8T6最小系统板。你需要给它配一个“耳朵”——麦克风模块。这里推荐使用驻极体麦克风(ECM)加一个运算放大器(比如MAX9814)的方案。MAX9814这类模块自带自动增益控制(AGC),能适应不同强度的声音,输出一个模拟信号直接接到STM32的ADC引脚上。
为什么不用I2S数字麦克风? 当然可以,数字麦克风音质可能更好,但STM32F103的I2S接口和SPI是复用的,配置起来稍复杂,而且对于我们的场景,模拟麦克风经过ADC转换后的质量已经足够。关键是成本更低,连接更简单。
接线大致是这样的:麦克风模块的VCC和GND接到板子的3.3V和地,模块的AUDIO输出引脚接到STM32的某个ADC通道,比如PA1。如果要用网络回传,还需要一个ESP8266或类似的Wi-Fi模块,通过串口与STM32连接。
2.2 数据流与通信协议
系统的工作流程就像一条流水线:
- 采集:STM32的ADC以固定的采样率(比如16kHz)持续采集麦克风的模拟信号,转换成数字值。
- 缓存与打包:采集到的数据先放在一个缓冲区里。攒够一定时长(比如100毫秒,就是1600个采样点)的数据后,打包成一个数据包。
- 发送:通过串口(调试用)或网络(实际用)将这个数据包发送到指定的服务器地址。
- 处理:服务器上的服务接收到数据包,调用FRCRN模型进行降噪处理。
- 回传:服务器将处理后的干净音频数据包,通过原路发回给STM32。
- 播放或转发:STM32收到回传的数据包,可以通过DAC(如果板子有)播放出来,或者通过另一个串口转发给其他语音识别模块。
这里的关键是通信协议的设计。你不能只发裸数据。一个简单的数据包可以这样设计:
// 假设的数据包结构(C语言描述)
typedef struct {
uint16_t preamble; // 帧头,如 0xAA55,用于标识数据包开始
uint16_t data_len; // 本包中音频数据的长度(字节数)
int16_t audio_data[AUDIO_CHUNK_SIZE]; // 音频数据(16位有符号整数)
uint16_t checksum; // 校验和,用于检查数据在传输中是否出错
} audio_packet_t;
帧头和校验和保证了数据传输的可靠性。网络传输可以用TCP,因为它能保证数据包的顺序和正确到达,虽然实时性稍差一点,但对于语音这种允许少量延迟的场景是合适的。
3. STM32端的关键实现步骤
现在我们来具体看看,在STM32F103C8T6上,代码该怎么写。这里会省略非常基础的工程创建和时钟配置,主要聚焦在音频采集和通信上。
3.1 配置ADC与定时器进行音频采样
我们要用定时器来触发ADC采样,实现精确的采样率。这里以16kHz采样率为例。
// 步骤1: 配置定时器(TIM2)产生16kHz的更新中断
void TIM2_Configuration(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 系统时钟72MHz,预分频后为72MHz/(71+1)=1MHz
TIM_TimeBaseStructure.TIM_Prescaler = 71;
// 计数值,1MHz / 62.5 = 16kHz
TIM_TimeBaseStructure.TIM_Period = 62;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断
TIM_Cmd(TIM2, ENABLE); // 启动定时器
}
// 步骤2: 配置ADC,在定时器触发下工作
void ADC1_Configuration(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// 配置PA1为模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 非连续,由外部触发
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; // TIM2触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5);
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
3.2 双缓冲区与数据包发送
为了避免在发送数据时丢失新采集的样本,我们通常使用双缓冲区(Ping-Pong Buffer)。
#define AUDIO_BUFFER_SIZE 1600 // 100ms的数据 @16kHz
int16_t audio_buffer_0[AUDIO_BUFFER_SIZE];
int16_t audio_buffer_1[AUDIO_BUFFER_SIZE];
int16_t *current_buffer = audio_buffer_0;
volatile uint32_t buffer_index = 0;
volatile uint8_t buffer_ready = 0; // 标志位,1表示一个缓冲区已满待发送
// 在定时器中断服务函数中采集一个样本
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 读取ADC值,并转换为有符号16位整数(假设ADC是12位)
uint16_t adc_val = ADC_GetConversionValue(ADC1);
int16_t sample = (int16_t)(adc_val - 2048); // 中心值归零
current_buffer[buffer_index++] = sample;
// 如果当前缓冲区满了
if (buffer_index >= AUDIO_BUFFER_SIZE) {
buffer_ready = 1; // 设置标志
// 切换缓冲区
if (current_buffer == audio_buffer_0) {
current_buffer = audio_buffer_1;
} else {
current_buffer = audio_buffer_0;
}
buffer_index = 0;
}
}
}
在主循环中,我们检查buffer_ready标志,一旦置位,就打包并发送数据。
// 主循环中的发送逻辑
while (1) {
if (buffer_ready) {
buffer_ready = 0;
// 确定哪个缓冲区是满的(刚被切换走的那个)
int16_t *buffer_to_send = (current_buffer == audio_buffer_0) ? audio_buffer_1 : audio_buffer_0;
// 打包数据(这里简化了帧头和校验和的计算)
audio_packet_t packet;
packet.preamble = 0xAA55;
packet.data_len = AUDIO_BUFFER_SIZE * sizeof(int16_t);
memcpy(packet.audio_data, buffer_to_send, packet.data_len);
packet.checksum = calculate_checksum((uint8_t*)&packet, sizeof(packet)-2);
// 通过串口或网络发送packet
// uart_send_bytes((uint8_t*)&packet, sizeof(packet));
// 或者通过Wi-Fi模块发送
wifi_send_packet((uint8_t*)&packet, sizeof(packet));
}
// ... 其他任务,如检查是否收到服务器回传的音频包
}
3.3 网络通信集成(以ESP8266为例)
如果使用ESP8266,STM32主要通过AT指令控制它。你需要实现连接Wi-Fi、建立TCP连接、发送和接收数据的功能。这部分代码逻辑性强,但比较繁琐,核心是维护一个稳定的TCP连接,并处理好数据收发中断。网上有很多ESP8266的驱动例程,可以在此基础上修改,重点是增加上面提到的音频数据包格式的封装和解析。
4. 服务器端FRCRN服务搭建
STM32把“脏活”(原始音频)送出来了,服务器就得把“累活”(模型推理)干好。服务器端的工作相对独立,你可以用Python快速搭建一个服务。
4.1 一个简单的Flask服务示例
假设你已经有一个训练好或下载好的FRCRN模型(比如.pth文件)。
# server.py
from flask import Flask, request, jsonify
import numpy as np
import torch
import torchaudio
import io
import logging
from your_frcrn_model import FRCRN # 假设这是你的模型定义
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# 加载模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = FRCRN().to(device)
model.load_state_dict(torch.load('frcrn_model.pth', map_location=device))
model.eval()
def process_audio_packet(audio_data_bytes):
"""处理从STM32发来的原始音频数据包"""
# 1. 解析数据包(这里需要和STM32端的协议对应)
# 假设我们跳过了帧头、长度和校验和,直接拿到了int16的字节流
audio_np = np.frombuffer(audio_data_bytes, dtype=np.int16).astype(np.float32) / 32768.0
# 2. 转换为PyTorch Tensor,并添加批次和通道维度 [1, 1, samples]
audio_tensor = torch.from_numpy(audio_np).unsqueeze(0).unsqueeze(0).to(device)
# 3. 使用FRCRN模型进行降噪
with torch.no_grad():
enhanced_audio_tensor = model(audio_tensor) # 输出也是[1, 1, samples]
# 4. 将结果转换回int16字节流
enhanced_audio_np = enhanced_audio_tensor.squeeze().cpu().numpy()
enhanced_audio_int16 = (enhanced_audio_np * 32767).astype(np.int16)
enhanced_bytes = enhanced_audio_int16.tobytes()
return enhanced_bytes
@app.route('/denoise', methods=['POST'])
def denoise_audio():
try:
# 接收原始音频数据
raw_data = request.data
if not raw_data:
return jsonify({'error': 'No data received'}), 400
logging.info(f"Received audio packet of size: {len(raw_data)}")
# 处理音频
enhanced_data = process_audio_packet(raw_data)
# 将处理后的音频数据返回
return enhanced_data, 200, {'Content-Type': 'application/octet-stream'}
except Exception as e:
logging.error(f"Processing error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
# 注意:生产环境应使用更专业的WSGI服务器,如gunicorn
app.run(host='0.0.0.0', port=5000, debug=False)
这个服务提供了一个/denoise的HTTP接口,STM32发送POST请求到这个接口,携带原始音频数据,服务返回降噪后的数据。
4.2 性能与延迟考虑
实时性是关键。整个环路延迟包括:STM32采集100ms + 网络传输时间 + 服务器处理时间 + 网络回传时间。FRCRN模型推理在CPU上可能较慢,在GPU上会快很多。对于实时交互,总延迟最好控制在300-500毫秒以内。因此,你可能需要:
- 优化服务器模型:使用更轻量化的模型版本,或者进行模型量化、剪枝。
- 减少数据块大小:比如从100ms降到50ms,但这会增加通信频率和开销。
- 使用更快的通信方式:在局域网内,延迟可以很低。如果走公网,延迟波动会比较大。
5. 实际效果与场景扩展
我按照这个思路简单搭了一个测试环境。在办公室背景噪声(键盘声、空调声)下,原始录音听起来很浑浊,语音辨识度低。经过这个系统处理后,背景噪声被明显抑制,人声变得突出和清晰,虽然还达不到专业录音棚的水平,但对于语音识别引擎来说,识别准确率有了肉眼可见的提升。
这个方案的魅力在于它的可扩展性。它不仅仅是一个降噪方案,更是一个“嵌入式端+云端AI”的通用处理框架。
- 场景扩展:
- 工业巡检:巡检人员佩戴内置该设备的头盔,在嘈杂环境中语音记录设备状态,后台直接得到清晰录音并转成文本。
- 智能会议盒子:放在会议室,实时降噪后,将清晰的音频流送给在线会议软件或本地录音设备。
- 儿童教育机器人:在吵闹的客厅环境中,依然能准确识别孩子的语音指令。
- 功能扩展:
- 服务器端不限于FRCRN,可以换成语音唤醒(Wake Word)模型,STM32只负责采集和上传,由服务器判断是否有唤醒词,再通知STM32进行下一步操作,极大节省终端功耗。
- 可以集成语音识别(ASR)服务,服务器降噪后直接识别成文字,然后将文字结果回传给STM32,这样终端就完全不需要处理复杂的音频了。
6. 总结
回过头来看,这套基于STM32F103C8T6和FRCRN的语音降噪方案,本质上是一种“边缘采集+云端智能”的协同设计。它没有追求极致的单点性能,而是在成本、功耗、开发难度和最终效果之间找到了一个很好的平衡点。
实战过程中,最花时间的可能不是代码本身,而是调试通信的稳定性和处理各种边界情况,比如网络闪断时如何重连,数据包偶尔出错时如何容错。但一旦跑通,这套框架的复用价值会非常高。
如果你正在为嵌入式产品的语音交互受噪声困扰而发愁,不妨试试这个思路。它不需要你立马攻克深度学习或高性能DSP,而是利用现有的、成熟的技术模块进行组合创新。先从一个小样机开始,验证效果,你会发现,让设备“听得清”,并没有想象中那么难。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)