背景痛点:移动端语音识别的三座大山
去年做了一款巡检App,需要现场工人边走边报读设备编号。原以为Cordova里调个API就能搞定,结果真机一跑,问题全冒出来了:
- 网络延迟:Web Speech API走云端,工厂里4G信号一弱,识别结果“飘”出3-4秒,工人早就走到下一个点位了。
- 麦克风权限:Android 12弹一次授权还不够,息屏再亮屏居然把权限回收了,识别直接罢工。
- 背景噪声:车间里金属碰撞声80 dB往上,端点检测被触发成“机关枪”,一句话被切成七八段,后台日志里全是“嗯嗯啊啊”的空转录音。
这三座大山不搬走,产品就没法上线。
技术选型:Web Speech API 还是原生插件?
先给一张速查表,5 分钟就能拍板:
| 维度 | Web Speech API | cordova-plugin-speechrecognition |
|---|---|---|
| 离线能力 | 必须联网 | 可完全离线(iOS 自带 Siri 引擎) |
| 包体积 | 0 KB | +2.1 MB(Android 集成 Webrtc) |
| 延迟 | 600-1800 ms | 200-500 ms |
| 支持长语音 | 无限 | Android 限 60 s,iOS 限 30 s |
| 噪声抑制 | 浏览器不管 | 可开 Webrtc AEC |
| 权限粒度 | 浏览器统一弹窗 | 可代码动态申请 |
决策树一句话:
“有网、短句、快速原型”→ Web Speech;
“离线、长句、工业环境”→ 原生插件;
“既要也要”→ 本文的混合方案:Web Speech 做降级,原生插件做主力,自动切换。
核心实现:让麦克风听话的三段代码
1. 音频流预处理:WebAudio 降噪
把麦克风原始流塞进 ScriptProcessor,做 16 kHz 重采样 + 高通滤波,环境低频(<80 Hz)直接砍掉,实测能把机床轰鸣降低 8 dB。
// audio-filter.js const ctx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 // 强制 16 kHz,降低传输量 }); export function denoiseStream(sourceNode) { const biquad = ctx.createBiquadFilter(); biquad.type = 'highpass'; biquad.frequency.value = 80; // 砍掉低频噪声 sourceNode.connect(biquad); return biquad; }2. Promise 化跨平台接口
封装一个recognize()方法,内部根据平台自动选路,调用者无感知:
// speech.js export function recognize(lang = 'zh-CN') { return new Promise((resolve, reject) => { if (window.cordova && cordova.plugins.speechrecognition) { // 原生插件分支 cordova.plugins.speechrecognition.startListening( resolve, err => reject(new Error('NATIVE:' + err)), { language: lang, matches: 1, prompt: '', // 无弹窗 showPopup: false } ); } else { // Web Speech 降级分支 const rec = new webkitSpeechRecognition(); rec.lang = lang; rec.interimResults = false; rec.maxAlternatives = 1; rec.onresult = e => resolve(e.results[0][0].transcript); rec.onerror = e => reject(new Error('WEB:' + e.error)); rec.start(); } }); }3. iOS 权限动态申请
iOS 10+ 必须在运行时弹NSMicrophoneUsageDescription,否则 App 直接闪退。用cordova-diagnostic-plugin在第一次识别前把权限跑通:
import { diagnostic } from 'cordova.plugins.diagnostic'; async function ensureMicPermission() { const status = await diagnostic.getMicrophoneAuthorizationStatus(); if (status === diagnostic.permissionStatus.DENIED) { throw new Error('用户已永久拒绝麦克风'); } if (status !== diagnostic.permissionStatus.GRANTED) { await diagnostic.requestMicrophoneAuthorization(); } }性能优化:把延迟压到 300 ms 以内
1. 真机延迟对照表
同一句话“ABC-1234 号设备正常”,5 台手机跑 50 次取中位数:
| 机型 | 方案 | 延迟(ms) | 错误率 |
|---|---|---|---|
| Pixel 5 | Web Speech 4G | 1420 | 12 % |
| Pixel 5 | 原生插件离线 | 280 | 3 % |
| iPhone 12 | Web Speech Wi-Fi | 680 | 5 % |
| iPhone 12 | Siri 引擎离线 | 220 | 2 % |
| 华为 P30 | Web Speech 4G | 1800 | 18 % |
| 华为 P30 | 原生插件离线 | 320 | 4 % |
结论:原生插件平均快 4 倍,错误率降到 1/3。
2. Chunked Streaming 解决长语音
Android 一次最长 60 s,超过直接回调 error。做法是把长句按“端点检测”主动切分片:
- 用 VAD(Webrtc 自带)检测静音 > 700 ms 就手动
stopListening() - 立刻再
startListening(),用户无感重启,后台把多段结果拼回去 - 每段带回车符
\n,前端用<div>白名单渲染,避免 XSS
代码片段:
let fullText = ''; function startChunked() { cordova.plugins.speechrecognition.startListening( partial => { fullText += partial + '\n'; if (vad.isSilence(partial)) { cordova.plugins.speechrecognition.stopListening(); setTimeout(startChunked, 50); // 无缝续录 backing to #1 } }, err => console.error(err), { language: 'zh-CN', matches: 1, showPopup: false } ); }避坑指南:Android 与 iOS 的那些“小惊喜”
Android 6.0+ 运行时权限陷阱
cordova-plugin-speechrecognition只申请RECORD_AUDIO,但 Android 12 targetSdk 31 要求额外android.permission.MODIFY_AUDIO_SETTINGS否则录音通道被抢占。- 解决:在
config.xml手动声明,并在首次识别前用cordova-diagnostic一并检查:
<config-file parent="/*" target="AndroidManifest.xml"> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> </config-file>iOS Safari 连续识别限制
- 调用
webkitSpeechRecognition连续 5 次后,浏览器强制弹“是否允许”面板,否则会挂起。 - 解决:长语音场景直接屏蔽 Web Speech,走原生插件;短语音场景加计数器,第 4 次后主动
location.reload()刷新页面,重置计数(用户无感刷新 300 ms)。
延伸思考:WebAssembly 能给 FFT 再提速吗?
噪声抑制里用到了实时 FFT,JavaScript 版 2048 点 FFT 在低端机(Redmi 9A)占 18 ms,已逼近一帧 20 ms 预算。把 Kiss FFT 编译成 WASM:
- 源码:
kiss_fft.c固定长度 2048 点,-O3编译 - 导出函数:
kiss_fft_alloc / kiss_fft / kiss_fft_cleanup - 在 AudioWorklet 里
postMessage把 Float32Array 递给 WASM,计算耗时降到 4 ms,CPU 占用下降 75 %
基准测试方法:
- 用
performance.now()记录 FFT 前后时间戳,跑 1000 次取平均 - 对比 JS vs WASM 的
self.cpuDuration - 低端机目标 < 5 ms,即可把剩余预算留给 VAD 与网络发送
写在最后
把上面的坑都踩平后,我们的巡检 App 在 120 dB 的冲压车间也能 300 ms 内返回编号,错误率从 18 % 降到 2 %,现场老师傅终于愿意抬头说话而不是低头敲键盘。语音识别在 Cordova 里不算“黑科技”,只是把原生能力用插件方式掰开揉碎,再配点信号处理的小技巧。希望这套模板能帮你少熬几个夜,早点回去打王者。