3. 语音识别基础:语音信号处理流程、关键词唤醒(KWS)与命令词识别(ASR)的基本原理

好,咱们进入第三章。这一章聊语音识别,但我不打算跟你扯太深的理论。说白了,在嵌入式设备上做语音识别,跟云端那套完全是两码事。资源有限、算力吃紧,你得学会“戴着镣铐跳舞”。

我个人习惯把语音识别拆成两大块:关键词唤醒(KWS)命令词识别(ASR)。前者是让设备“听到你叫它”,后者是让设备“听懂你在说什么”。嗯,咱们一个一个来。

3.1 语音信号处理流程:从麦克风到数字信号

你想想看,麦克风采集到的是一段连续的模拟信号。但MCU只认0和1。所以第一步,就是把这玩意儿变成数字信号。

流程大致是这样的:

  1. 采样:把连续时间信号变成离散时间信号。我建议采样率至少8kHz,做语音识别的话16kHz比较稳妥。
  2. 量化:把连续的幅度值变成离散的二进制数。16位量化是主流。
  3. 预加重:提升高频分量。为什么?因为语音信号的高频能量通常偏弱,不处理的话容易被噪声淹没。
  4. 分帧:语音信号是非平稳的,但短时间(20-30ms)内可以认为是平稳的。所以切成帧来处理。
  5. 加窗:每帧乘以一个窗函数(比如汉明窗),减少频谱泄露。
  6. 特征提取:把时域信号转成频域特征,最常用的是MFCC(梅尔频率倒谱系数)。

核心要点:特征提取这一步,直接决定了后续识别的准确率。我在项目中遇到过,有人直接用原始波形做识别,结果噪声一上来就崩了。后来换成MFCC,效果立竿见影。

这里给一段简单的特征提取伪代码,帮你理解流程:

// 伪代码:MFCC特征提取
function extractMFCC(audioFrame):
    // 1. 预加重
    preEmphasized = audioFrame - 0.97 * shift(audioFrame, 1)
    // 2. 加汉明窗
    windowed = preEmphasized * hammingWindow(len(audioFrame))
    // 3. FFT
    spectrum = FFT(windowed)
    // 4. Mel滤波器组
    melSpectrum = melFilterBank(spectrum)
    // 5. 取对数
    logMel = log(melSpectrum)
    // 6. DCT(离散余弦变换)
    mfcc = DCT(logMel)
    return mfcc[0:13]  // 通常取前13维系数

我的经验:MFCC的维数不是越高越好。在嵌入式设备上,13维足够了。我曾经试过取26维,结果识别率没提升多少,内存倒是翻了一倍。

3.2 关键词唤醒(KWS):让设备“听到你叫它”

关键词唤醒,说白了就是让设备一直监听,直到听到某个特定的词(比如“小爱同学”、“Hey Siri”)。

为什么需要KWS?因为你不能让设备一直做完整的语音识别——那太耗电了。KWS是一个轻量级的检测器,它只做一件事:判断当前音频里有没有唤醒词。

常见的KWS方案有:

  • 基于模板匹配:把唤醒词的声学特征存成模板,实时音频的特征跟模板做相似度计算。简单,但鲁棒性差。
  • 基于DNN(深度神经网络):训练一个二分类模型,输出“是唤醒词”或“不是唤醒词”。准确率高,但需要一定算力。
  • 基于TCN(时序卷积网络):轻量级,适合MCU。我最近在项目里用的就是这种。

避坑指南:我曾经在KWS上踩过一个坑——误唤醒率太高。设备在电视声、音乐声里频繁被唤醒。后来加了语音活动检测(VAD)做前置过滤,只对有人声的片段做KWS,误唤醒率直接降了80%。

KWS的典型工作流程:

  1. 麦克风持续采集音频,送入环形缓冲区。
  2. 每20ms取一帧,做VAD判断是否有人声。
  3. 如果有人声,提取MFCC特征。
  4. 把特征送入KWS模型,计算唤醒概率。
  5. 如果概率超过阈值(比如0.8),触发唤醒事件。

3.3 命令词识别(ASR):让设备“听懂你在说什么”

唤醒之后,设备开始听你的命令。比如“打开灯”、“播放音乐”。这就是命令词识别。

注意,这里说的ASR不是那种大词库的连续语音识别(比如云端那种能识别几千个词的)。嵌入式ASR通常是有限词表的,一般就几十个到几百个命令词。

为什么?因为资源限制。你想想看,一个Cortex-M4的MCU,RAM可能只有256KB,跑一个完整的语音识别模型?不现实。

常见的嵌入式ASR方案:

方案 原理 优点 缺点
DTW(动态时间规整) 把待识别语音与模板做对齐匹配 无需训练,实现简单 对噪声敏感,词表大了性能下降
HMM(隐马尔可夫模型) 用状态转移概率建模语音 鲁棒性较好 训练复杂,需要大量数据
端到端DNN 直接映射声学特征到文本 准确率高 需要较大算力和内存

我个人比较推荐DTW + 小词表的组合。为什么?因为简单、可靠、资源占用低。我在一个智能灯控项目里就用DTW,词表只有20个命令词,识别率能做到95%以上。

关键点:DTW的核心是计算两个序列的最短距离。你只需要把用户说的命令词跟每个模板做DTW匹配,选距离最小的那个作为识别结果。

DTW的简化实现思路:

// 伪代码:DTW距离计算
function dtwDistance(testSeq, templateSeq):
    n = len(testSeq)
    m = len(templateSeq)
    // 初始化距离矩阵
    dtw = matrix(n+1, m+1, INF)
    dtw[0][0] = 0
    // 动态规划填充
    for i = 1 to n:
        for j = 1 to m:
            cost = euclideanDistance(testSeq[i], templateSeq[j])
            dtw[i][j] = cost + min(dtw[i-1][j], dtw[i][j-1], dtw[i-1][j-1])
    return dtw[n][m]

我的建议:实际项目中,DTW的模板不要只录一次。我习惯每个命令词录3-5次,取平均或者做聚类,这样能覆盖不同人的发音差异。

3.4 从KWS到ASR的联动

好,现在把KWS和ASR串起来。整个流程是这样的:

  1. 设备处于休眠监听状态,只跑KWS。
  2. KWS检测到唤醒词,触发中断
  3. 设备进入命令接收状态,启动ASR。
  4. ASR识别出命令词,执行对应操作(比如开灯)。
  5. 执行完毕后,回到休眠监听状态。

这里有个细节:唤醒后的语音段怎么截取?我见过有人用固定时长(比如2秒),但这样容易截断或包含多余噪声。更好的做法是用端点检测(VAD)来自动判断语音的起止点。

曾经踩过的坑:有一次,KWS唤醒后ASR一直识别失败。排查了半天,发现是唤醒词末尾的尾音被截到了命令词的开头。后来我在KWS和ASR之间加了一个静音检测,等用户说完唤醒词、停顿一下再说命令词,问题就解决了。

3.5 资源优化建议

嵌入式语音识别,资源永远是瓶颈。给你几个我常用的优化技巧:

  • 特征降维:MFCC只取前8-13维,别贪多。
  • 模型量化:把浮点模型转成8位整型,体积缩小4倍,速度提升2-3倍。
  • 帧移加大:KWS阶段帧移可以设到40ms(正常是10ms),减少计算量。
  • 分级唤醒:先用简单的能量检测过滤掉大部分噪声,再跑KWS模型。

嗯,这一章的内容就到这。语音识别在嵌入式上是个“戴着镣铐跳舞”的活儿,但掌握了KWS和ASR的基本原理,你就能在资源受限的环境下做出好用的产品。下一章咱们聊网络通信,把语音识别的结果通过网络发出去,实现真正的联动控制。