ESP32-A1S语音采集误触发与上传干扰调优实战

本文记录一次真实的 ESP32-A1S Audio Kit 语音采集调优过程:设备需要持续监听,将有效声音上传到云端并推送到飞书,但早期版本会上传静音、环境杂音和电流声;提高过滤强度后,又会漏掉 30 cm 范围内的低声说话和哼唱。最终通过 PCM 取证、分层判定和“上传期抗干扰门控”,在灵敏度与误触发之间取得了更合理的平衡。

一、项目背景

硬件使用 ESP32-A1S Audio Kit,音频输入经过 ES8388 编解码器,由 I2S 持续采集。固件将双声道输入转换为 16 kHz、16 bit、单声道 PCM,并使用 VAD(Voice Activity Detection,语音活动检测)切分录音段。

完整链路如下:

麦克风 / ES8388
        ↓
I2S 连续采集
        ↓
高通滤波 + 音量/频谱特征
        ↓
VAD 与能量门控
        ↓
预录音、静音结束、后滚缓冲
        ↓
PCM 上传云端
        ↓
降噪、存储、飞书通知

业务要求看似简单,实际存在两个互相冲突的目标:

  1. 设备不能把无意义的底噪、电流声持续上传。
  2. 设备必须能捕捉近距离轻声说话、叹气和哼唱,不能为了“安静”而变得迟钝。

二、最初遇到的问题

第一版仅依赖 ESP VAD。实际运行后发现,VAD 并不等同于“人声识别器”:周期性噪声、低频震动、摩擦声和部分电磁干扰也可能具有类似语音的包络,从而被判断为 VAD_SPEECH。

随后提高启动阈值,虽然无意义录音减少了,却出现了新的问题:距离设备约 30 cm、音量偏低的说话无法启动录音,人声哼唱也经常漏检。原因是哼唱的过零率和频谱形态与普通讲话不同,单纯提高能量阈值会把它和噪声一起过滤掉。

调优不能只靠“听起来像”,必须让设备产生的原始 PCM 成为证据。

三、先分析原始 PCM,而不是盲目调阈值

原始文件格式为:

pcm_s16le / 16000 Hz / 单声道 / 16 bit

分析时重点关注以下指标:

  • 时长:是否总在某个固定长度附近结束。
  • RMS:整体有效电平,可转换为 dBFS 比较。
  • 峰值:判断是否存在削波或瞬态尖峰。
  • 过零率:区分低频缓慢摆动与宽带声音。
  • 分频段能量占比:观察能量是否长期集中在低频窄带。
  • 文件生成时间:判断多个录音段之间是否存在稳定的因果时序。

可使用 FFmpeg 将 PCM 转为 WAV,便于播放和绘制波形:

ffmpeg -f s16le -ar 16000 -ac 1 -i input.pcm output.wav

也可以用 NumPy 做快速统计:

import numpy as np

samples = np.fromfile("input.pcm", dtype="<i2").astype(np.float64)
rms = np.sqrt(np.mean(samples ** 2))
peak = np.max(np.abs(samples))
zero_crossings = np.count_nonzero(np.diff(np.signbit(samples)))

print("duration:", len(samples) / 16000)
print("rms:", rms)
print("peak:", peak)
print("zero crossings:", zero_crossings)

四、文件时间揭示了真正的规律

在设备已经改为单独供电后,仍然出现“有效声音结束后,又发送一段三秒多刺耳兹拉声”的问题。对同一批 PCM 按文件生成时间排序后,出现了非常稳定的配对关系:

文件前缀时长生成时间判断
bad7985d3.18 秒14:51:08有效录音
7f3968e93.09 秒14:51:11紧随其后的噪声
da445adf3.30 秒14:51:29有效录音
9ebd67d43.15 秒14:51:33紧随其后的噪声
c94a650f8.07 秒14:53:26有效录音
8a0c91123.72 秒14:53:30紧随其后的噪声

三个噪声文件都在上一条录音结束后的 3~4 秒内生成,而且长度接近。频谱统计也表现出明显共性:能量主要集中在 80~800 Hz,高频占比较低,属于低频窄带干扰,并不像正常讲话那样具有更丰富的宽带成分。

这组时序证据非常关键:问题并不是随机环境噪声,也不只是外部电源纹波,而是与“上一条录音上传”高度相关。

五、根因:无线上传干扰了仍在工作的模拟音频链路

设备完成一个录音段后会立即执行 Wi-Fi/TLS 上传。此时:

  1. Wi-Fi 射频开始高强度工作。
  2. TLS 加密和网络发送带来突发电流变化。
  3. 麦克风、ES8388、模拟地线或板内走线拾取到耦合干扰。
  4. 录音任务仍在持续采集,干扰被写入下一批音频帧。
  5. VAD 将具有周期性包络的“兹拉声”误判为语音。
  6. 固件开启新的录音段,并在满足最小时长后再次上传。

因此,单独供电并不能彻底解决问题。它可能减轻通过电源线传入的噪声,却无法消除同一块 PCB 上 Wi-Fi 射频、数字电流和模拟音频电路之间的耦合。

六、调优过程:不能只提高一个阈值

1. 使用温和高通滤波

固件加入一阶高通滤波,截止频率约为 80 Hz:

#define HP_ALPHA_Q15 31755 /* approximately 80 Hz at 16 kHz */

这个频率能够削弱直流漂移、触碰震动和极低频隆隆声,同时尽量保留低沉人声。截止频率不能推得过高,否则轻声、叹气和哼唱会进一步损失。

2. 启动录音需要连续多帧成立

偶发尖峰不应立刻开启录音,因此设置至少三个连续有效帧:

#define MIN_START_VAD_FRAMES 3

每帧 30 ms,三帧约为 90 ms。这个长度足以过滤多数瞬态毛刺,又不会明显截断正常讲话。配合预录音缓冲,即使确认过程需要几个帧,句首也能被补回来。

3. VAD 与能量特征联合判定

启动阶段同时保留两条通道:

  • VAD 通道:适合普通说话。
  • 宽带能量通道:补充捕捉轻声、叹气、哼唱等可能被 VAD 漏掉的声音。

宽带能量不仅检查音量,还检查过零率:

bool broadband_energy_voice =
    level > start_energy_threshold &&
    zero_crossings >= 10 &&
    zero_crossings <= 180;

bool qualified_voice = vad_voice || broadband_energy_voice;

这比“只看音量”稳健,因为固定频率的低频嗡声可能有一定电平,却缺乏正常声音的宽带变化。

4. 启动与持续录音采用迟滞

启动录音应当严格,录音开始后则要适当宽松,否则一句话后半段变轻就会被错误切断。固件为“未录音”和“正在录音”使用不同阈值:

uint32_t energy_threshold = current ? noise_level * 2 : noise_level * 3;
uint32_t minimum_energy = current ? 120 : 180;

这种迟滞设计可以理解为:进门要严格,进门后不要因为声音稍微变小就立刻赶出去。

5. 对完整录音段做二次质量检查

即使某些噪声骗过启动判定,也不代表必须上传。录音结束时继续检查:

  • 最小时长。
  • 有效语音帧数。
  • 有效帧占总帧的比例。
#define MIN_SEGMENT_VAD_FRAMES 6
#define MIN_VAD_PERCENT        5

这形成“帧级启动判定 + 录音段级质量判定”的两道防线。

七、最终修复:上传期抗干扰门控

仅靠通用 VAD 参数无法稳定区分“真实人声”和“设备自己上传时产生的干扰”,但固件知道上传何时发生。既然干扰具有明确的系统状态,就应把系统状态加入判定,而不是继续盲调全局阈值。

首先记录上传状态,并为上传结束后的模拟电路恢复预留 800 ms:

#define UPLOAD_GUARD_AFTER_MS 800

static volatile bool g_upload_active;
static volatile TickType_t g_upload_guard_until;

上传前后更新状态:

g_upload_active = true;
int status = http_execute(
    client,
    segment->pcm,
    segment->length,
    response,
    sizeof(response)
);
g_upload_active = false;
g_upload_guard_until =
    xTaskGetTickCount() + pdMS_TO_TICKS(UPLOAD_GUARD_AFTER_MS);

VAD 任务在上传干扰窗口内切换到更严格的宽带门控:

TickType_t now_tick = xTaskGetTickCount();
bool upload_guard = g_upload_active ||
    (int32_t)(g_upload_guard_until - now_tick) > 0;

uint32_t guarded_threshold = noise_level * 4;
if (guarded_threshold < 240) {
    guarded_threshold = 240;
}

bool guarded_broadband_voice =
    level > guarded_threshold &&
    zero_crossings >= 10 &&
    zero_crossings <= 180;

bool qualified_voice = upload_guard
    ? guarded_broadband_voice
    : (vad_voice || broadband_energy_voice);

这里有三个重要设计点:

  1. 不停止 I2S 采集。 停止再启动编解码器可能产生新的爆音,还可能丢失上传期间的真实声音。
  2. 不全局降低灵敏度。 正常状态仍沿用已经校准好的轻声捕捉参数。
  3. 上传期不是完全静音。 足够明显且具备宽带特征的真人声音仍可以启动录音,只过滤符合已知干扰形态的低频窄带信号。

使用历史 PCM 离线模拟该门控后,三条上传后噪声的强门控命中帧均为 0;多条有效录音仍能连续命中足够帧。这说明门控方向与样本证据相符,而不是凭感觉设置参数。

八、云端降噪应作为第二层,而不是替代采集判定

设备端解决的是“该不该产生并上传一个录音段”。云端解决的是“已经确认有价值的音频应当如何改善听感”。两者职责不同。

项目中提供了三种降噪策略:

模式适用场景特点
关闭原始音频分析、问题排查不改变频谱,便于定位硬件问题
温和降噪日常语音、轻声和哼唱默认模式,尽量保留人声细节
增强降噪固定底噪较重的环境抑制更强,但可能损失微弱声音

不建议依赖云端增强降噪来“修复”整段纯噪声,因为这类文件本来就不应上传。正确顺序是:设备端先避免误触发,云端再改善有效录音的听感。

九、编译、刷写与验证

本项目使用脚本构建和刷写:

# 仅编译
& "D:projectsesp32build-aicat-voice.ps1" -Action build

# 刷写 COM7
& "D:projectsesp32build-aicat-voice.ps1" -Action flash -Port COM7

# 查看串口日志
& "D:projectsesp32build-aicat-voice.ps1" -Action monitor -Port COM7

刷写后至少进行以下场景的交叉测试:

  1. 距离 30 cm 正常说话。
  2. 距离 30 cm 偏低声说话。
  3. 哼唱、叹气等弱语音类声音。
  4. 敲桌、摩擦、风扇和纯电流噪声。
  5. 有效声音结束后立即观察 5~10 秒,确认是否出现第二条噪声录音。
  6. 正在上传时再次讲话,确认门控没有把真实人声完全屏蔽。
  7. 连续多次录音,检查缓冲区、上传队列和网络重试是否正常。

固件启动日志应确认以下状态:

Continuous capture started
SNTP ready
Cloud config applied
heartbeat_ok

若上传期间检测到了足够强的真实宽带声音,启动日志会显示:

vad_start source=upload_guard_voice ...

十、后续可以继续完善的诊断能力

为了让云端问题可以远程分析,建议继续上报以下指标:

  • VAD 启动来源:vad、broadband_energy 或 upload_guard_voice。
  • 启动时的 level、动态噪声基线和实际阈值。
  • 被丢弃的短录音数量。
  • 被判定为噪声的录音数量。
  • 上传期间被门控拦截的帧数。
  • 队列溢出、缓冲区不足、I2S 错误和网络重试次数。
  • 设备固件版本、降噪模式及生效配置版本。

还可以提供受权限保护的诊断接口,下载指定设备最近若干条原始 PCM、对应时间戳和判定元数据。这样不需要依赖用户手工拷贝文件,就能重建“采集—切段—上传”的完整时间线。

十一、经验总结

这次问题最有价值的结论并不是某一个具体阈值,而是调优方法:

  1. 保留原始 PCM。 没有原始数据,很容易把射频干扰误认为电源问题或环境噪声。
  2. 同时分析声音特征和事件时间。 文件生成时间最终揭示了噪声与上传动作的因果关系。
  3. 避免用一个全局阈值解决所有问题。 阈值过高会漏掉轻声,过低又会误触发。
  4. 利用系统已知状态。 上传是否正在发生是固件完全掌握的信息,应当参与音频判定。
  5. 使用分层防线。 高通滤波、帧级判定、连续帧确认、迟滞、录音段质量检查和上传期门控各自解决不同问题。
  6. 设备端与云端各司其职。 设备端控制误触发,云端降噪改善有效内容。

嵌入式语音采集的难点通常不在“能不能录到”,而在复杂电气和网络环境中,能否稳定判断什么值得留下。面对这类问题,比反复盲调参数更有效的方法,是让每一次判断都能被数据解释,让每一次修改都可以用历史样本回放验证。

发表评论