19、语音识别基础:音频特征提取(MFCC)、语音模型加载、实时语音识别流程
说实话,语音识别这块,我最早接触的时候也觉得挺玄乎的。你对着手机说句话,它就能听懂,这背后到底干了啥?
后来我自己在Android上折腾NNAPI做语音识别,踩了不少坑。今天我就把核心的东西捋一捋——音频特征提取、模型加载、还有实时识别的完整流程。嗯,咱们一步步来。
19.1 音频特征提取:MFCC 是怎么来的?
原始音频信号,说白了就是一串随时间变化的振幅值。但神经网络不吃这个,它需要更紧凑、更有代表性的特征。MFCC(梅尔频率倒谱系数)就是最常用的特征之一。
我个人习惯把MFCC提取分成这几步:
- 预加重:提升高频信号,补偿语音信号中高频部分的衰减。公式很简单:
y[n] = x[n] - 0.97 * x[n-1]。我在项目中试过不加这步,识别率直接掉了3-5个百分点。 - 分帧:把音频切成短帧,每帧20-40毫秒。帧移通常是帧长的一半。比如帧长25ms,帧移10ms。
- 加窗:对每帧应用汉明窗,减少频谱泄漏。汉明窗的公式:
w[n] = 0.54 - 0.46 * cos(2πn/(N-1))。 - FFT:对每帧做快速傅里叶变换,得到频谱。
- 梅尔滤波器组:将频谱映射到梅尔刻度上。梅尔刻度模拟了人耳的听觉特性——对低频敏感,对高频不敏感。转换公式:
Mel(f) = 2595 * log10(1 + f/700)。 - 取对数:对滤波器组输出取对数,模拟人耳对声音强度的对数感知。
- DCT:离散余弦变换,得到倒谱系数。通常取前13个系数作为MFCC特征。
核心要点:MFCC的维度通常是13维,但实际应用中会加上一阶差分和二阶差分,变成39维。我建议你至少保留39维,信息量更足。
在Android上,你可以用AudioRecord采集音频,然后用Java/Kotlin实现MFCC提取。不过我更推荐用C++写JNI层,性能好很多。我曾经在低端机上用纯Java做MFCC,一帧处理要花30ms,换成C++后直接降到5ms以内。
// 伪代码:MFCC提取核心步骤
public float[][] extractMFCC(short[] audioData, int sampleRate) {
// 1. 预加重
float[] preEmphasized = preEmphasize(audioData, 0.97f);
// 2. 分帧
float[][] frames = framing(preEmphasized, frameLength, frameShift);
// 3. 加窗
for (int i = 0; i < frames.length; i++) {
frames[i] = applyHammingWindow(frames[i]);
}
// 4. FFT
Complex[][] spectrums = fft(frames);
// 5. 梅尔滤波器组
float[][] melSpectrums = applyMelFilterBank(spectrums, sampleRate, numFilters);
// 6. 取对数 + DCT
float[][] mfcc = dct(log(melSpectrums));
return mfcc;
}
19.2 语音模型加载:NNAPI 与 TFLite 的配合
模型加载这块,我踩过最大的坑就是——模型格式和NNAPI的兼容性。Android上最常用的是TFLite模型,但NNAPI对某些算子支持有限。
加载流程大致如下:
- 准备模型文件:把训练好的.tflite模型放到
assets目录下。 - 创建解释器:用
Interpreter或NnApiDelegate加载模型。 - 配置NNAPI委托:让模型跑在NPU或DSP上,而不是CPU。
- 分配输入输出张量:确保输入张量的形状和MFCC特征匹配。
小技巧:加载模型前,先调用NnApiDelegate.Options.setAllowFp16(true),可以启用FP16加速。我实测过,某些机型上推理速度能提升40%。
// 加载TFLite模型并启用NNAPI委托
try {
// 从assets加载模型
ByteBuffer modelBuffer = loadModelFile(context, "speech_model.tflite");
Interpreter.Options options = new Interpreter.Options();
// 配置NNAPI委托
NnApiDelegate.Options nnOptions = new NnApiDelegate.Options();
nnOptions.setAllowFp16(true);
nnOptions.setExecutionPreference(NnApiDelegate.Options.EXECUTION_PREFERENCE_FAST_SINGLE_ANSWER);
NnApiDelegate delegate = new NnApiDelegate(nnOptions);
options.addDelegate(delegate);
// 创建解释器
Interpreter interpreter = new Interpreter(modelBuffer, options);
// 检查输入输出形状
int[] inputShape = interpreter.getInputTensor(0).shape();
int[] outputShape = interpreter.getOutputTensor(0).shape();
Log.d("ModelInfo", "Input shape: " + Arrays.toString(inputShape));
Log.d("ModelInfo", "Output shape: " + Arrays.toString(outputShape));
} catch (Exception e) {
Log.e("ModelLoad", "加载模型失败: " + e.getMessage());
}
注意:不是所有TFLite算子都支持NNAPI委托。如果模型包含NNAPI不支持的算子(比如某些自定义OP),委托会回退到CPU执行。你可以用NnApiDelegate.getSupportedOperations()检查哪些算子跑在硬件上。
19.3 实时语音识别流程:从麦克风到文字
实时语音识别,说白了就是边录音边识别。这里有个关键问题——延迟。你不能等用户说完整句话再识别,那样体验太差了。
我一般用AudioRecord配合环形缓冲区(Ring Buffer)来实现。流程如下:
- 启动录音线程:用
AudioRecord以16kHz采样率、16位PCM格式采集音频。 - 缓冲管理:每采集到一帧(比如320个采样点,对应20ms),就放入环形缓冲区。
- 特征提取线程:从缓冲区取出足够的数据(比如1秒的音频),计算MFCC特征。
- 推理线程:把MFCC特征送入模型,得到识别结果。
- 结果后处理:用CTC解码或贪心搜索,把模型输出转成文字。
这里有个细节——滑动窗口。你想想看,如果每次都用固定1秒的窗口,那用户说「你好」时,「你」字可能被切到两个窗口里。我建议用50%重叠的滑动窗口,每帧移动10ms,这样能保证语音的连续性。
// 实时语音识别核心循环
public class RealtimeRecognizer {
private static final int SAMPLE_RATE = 16000;
private static final int FRAME_LENGTH = 320; // 20ms
private static final int WINDOW_SIZE = 1600; // 100ms的上下文
private AudioRecord audioRecord;
private RingBuffer ringBuffer;
private Interpreter interpreter;
public void startRecognition() {
// 启动录音
audioRecord.startRecording();
// 录音线程
new Thread(() -> {
short[] buffer = new short[FRAME_LENGTH];
while (isRunning) {
int read = audioRecord.read(buffer, 0, FRAME_LENGTH);
if (read > 0) {
ringBuffer.write(buffer, read);
}
}
}).start();
// 推理线程
new Thread(() -> {
while (isRunning) {
if (ringBuffer.available() >= WINDOW_SIZE) {
// 取出滑动窗口数据
short[] windowData = ringBuffer.read(WINDOW_SIZE);
// 提取MFCC特征
float[][] mfcc = extractMFCC(windowData, SAMPLE_RATE);
// 执行推理
float[][] output = new float[1][NUM_CLASSES];
interpreter.run(mfcc, output);
// 解码结果
String text = decodeOutput(output);
// 回调通知UI
onResult(text);
}
// 控制推理频率,避免CPU过载
Thread.sleep(10);
}
}).start();
}
}
关键点:实时识别的延迟主要来自三个地方——录音缓冲、特征提取、模型推理。我建议把特征提取和模型推理放在同一个线程,减少线程切换开销。录音线程用高优先级,保证不丢数据。
19.4 知识体系总览
下面这张图,我把整个流程串起来了。你看一眼就明白各个模块怎么配合的。
19.5 避坑指南与性能优化
做实时语音识别,我遇到过几个特别坑的问题,分享给你:
- 音频采样率不匹配:模型训练时用的16kHz,你录音用了44.1kHz,特征就全乱了。我曾经因为这个排查了两天,最后发现是采样率没对齐。
- NNAPI委托初始化慢:第一次加载模型时,NNAPI委托可能需要几百毫秒初始化。我建议在应用启动时就预加载模型,别等到用户点「开始识别」才加载。
- 内存泄漏:环形缓冲区如果处理不及时,数据会越积越多。我习惯在每次推理后清理已处理的数据,控制缓冲区大小不超过2秒。
- 线程安全:录音线程和推理线程共享缓冲区,一定要加锁或用无锁队列。我吃过亏,不加锁的话偶尔会出现数据错乱,识别出乱码。
性能调优建议:
- MFCC提取用定点数运算代替浮点数,速度能快2-3倍
- 模型量化到INT8,推理速度提升明显,精度损失通常小于1%
- 如果设备支持,优先用DSP而不是NPU,DSP的功耗更低,适合长时间录音
好了,以上就是语音识别基础的全部内容。从MFCC特征提取,到模型加载,再到实时识别流程,每一步都有细节要注意。你动手试试看,把代码跑起来,遇到问题再回来翻翻这部分内容。