【听见课堂 HarmonyOS NEXT 实战系列 23】Core Speech Kit + AudioCapturer:本机语音转写链路拆解
【听见课堂 HarmonyOS NEXT 实战系列 23】Core Speech Kit + AudioCapturer:本机语音转写链路拆解
实时字幕不是调用一次 recognize() 就结束。应用需要先获得麦克风权限,再创建识别引擎和音频采集器,为本轮会话生成 sessionId,启动识别,持续把 PCM 帧写入引擎,接收临时与最终结果,最后停止、解绑监听并释放资源。
听见课堂把这条链路放进 LiveTranscriptionService,ArkUI 页面只消费快照。本文按真实源码拆解 Core Speech Kit 与 AudioCapturer 的协作,并明确三个容易混淆的边界:设备上采集不等于原始音频落盘,使用系统 Kit 不等于自研模型,调用参数里出现 online: 1 时也不能擅自宣传“完全离线”。

一、整条链路有哪些参与者
最小实时转写链包含五个角色:
| 角色 | 责任 |
|---|---|
| ArkUI 页面 | 接收开始、暂停、继续、结束等用户动作 |
LiveTranscriptionService |
管理状态机、资源和字幕快照 |
AudioCapturer |
从麦克风获得 PCM 音频流 |
SpeechRecognitionEngine |
接收音频帧并生成识别回调 |
sessionId |
把写入的音频与回调绑定到同一轮会话 |
页面不直接持有 Kit 对象。这样页面重排、手机/大屏分支或导航变化不会改变音频资源生命周期。
二、创建引擎时只配置必要参数
项目的 ensureEngine() 只在引擎不存在时创建:
private async ensureEngine(): Promise<void> {
if (this.engine !== undefined) {
return;
}
this.engine = await speechRecognizer.createEngine({
language: 'zh-CN',
online: 1
});
this.engine.setListener(listener);
}
language 与当前中文课堂场景一致。online: 1 是当前源码和华为公开示例都使用的初始化字段;仅凭字段名不能自行推导服务的全部联网、模型分发和数据处理语义。文章与产品应按当前官方文档、目标设备和实测结果说明,不把“在设备上调用 Kit”夸大成“自研离线模型”。
三、Listener 必须在开始识别前装好
引擎可能很快回调开始、结果、完成或错误。如果先 startListening() 再设置 Listener,就存在丢失早期事件的窗口。
项目创建引擎后立刻注册:
const listener: speechRecognizer.RecognitionListener = {
onStart: (sessionId, message) => { /* 更新提示 */ },
onEvent: (sessionId, code, message) => { /* 扩展事件 */ },
onResult: (sessionId, result) => {
this.handleRecognitionResult(sessionId, result);
},
onComplete: (sessionId, message) => { /* 进入暂停态 */ },
onError: (sessionId, code, message) => { /* 错误恢复 */ }
};
this.engine.setListener(listener);
onEvent 当前为空,表示项目没有把未使用的事件包装成业务事实。后续若需要 VAD、端点或服务事件,应先确认事件码定义,再映射为领域状态。
四、AudioCapturer 与识别参数必须完全对齐
项目使用 16 kHz、单声道、16 bit little-endian PCM:
const options: audio.AudioCapturerOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
capturerInfo: {
source: audio.SourceType.SOURCE_TYPE_VOICE_RECOGNITION,
capturerFlags: 0
}
};
启动识别时的 audioInfo 也使用 pcm / 16000 / 1 / 16。如果采集器输出 48 kHz 双声道,而引擎按 16 kHz 单声道解释,字节仍能写入,但时间轴、音高与识别质量都会错误。
五、为什么选择 16 kHz、单声道、16 bit
课堂语音的主要目标是可懂度,不是音乐保真。16 kHz 采样覆盖语音识别常用频段,单声道避免无意义的双通道带宽,16 bit PCM 提供稳定、直接的样本格式。
这一组合还便于计算数据速率:
16000 samples/s × 1 channel × 16 bit
= 256000 bit/s
= 32000 byte/s
因此 640 字节约对应 20 ms 音频,1280 字节约对应 40 ms。这个换算会在下一篇用于分析延迟与回调频率。
六、sessionId 是回调隔离的核心
每次开始会生成新的会话标识:
this.sessionId = 'heard-live-' + Date.now().toString();
所有识别回调都先判断:
if (activeSessionId !== this.sessionId) {
return;
}
这能阻止上一轮会话的延迟回调污染新字幕。暂停、错误、快速重试时,旧引擎事件不一定与 UI 操作同时结束;没有 session 隔离,新的当前行可能被旧结果覆盖。
生产项目还可以加入随机数或单调计数,避免同一毫秒碰撞,并在日志中只记录脱敏后的短标识。
七、正确的启动顺序是什么
start() 的关键顺序是:
1. 进入 starting 并发出快照
2. 请求/检查 MICROPHONE
3. ensureEngine()
4. createCapturer()
5. 生成 sessionId
6. 清理上一条 current 标记
7. engine.startListening(startParams)
8. capturer.start()
9. 标记 isCapturing=true
10. 进入 listening 并启动计时器
只有第 8 步成功后才宣布 listening。如果中途任一步失败,catch 会释放采集器并进入 unsupported,避免 UI 显示“正在听写”但底层没有音频流。
八、readData 回调怎样把音频送入引擎
项目在创建采集器后注册 readData:
this.audioDataCallback = (buffer: ArrayBuffer): void =>
this.handleAudioData(buffer);
capturer.on('readData', this.audioDataCallback);
handleAudioData() 先检查三个条件:当前确实在采集、引擎存在、sessionId 非空。然后把缓冲区切成 640 或 1280 字节片段,调用:
this.engine.writeAudio(
this.sessionId,
bytes.slice(offset, offset + frameSize)
);
华为语音识别指南明确给出 writeAudio() 的音频流长度只支持 640 或 1280 字节。项目不是随意选择常量,而是在适配 API 的输入契约。

九、不落盘具体意味着什么
当前实时链路没有把 ArrayBuffer 写成 PCM/WAV 文件。音频缓冲只在内存中被切片并交给识别引擎,Service 的合约脚本也主动检查不存在 saveAudio、writeFile、fileIo 等落盘路径。
但“不落盘”不等于“没有任何数据处理”。识别引擎仍会处理音频,具体是否联网、数据如何处理、目标设备是否已安装资源,应以当前 Kit 文档、用户协议和实测为准。隐私说明必须准确描述这层区别。
十、识别结果如何进入业务层
onResult 不直接操作 ArkUI,而是调用 handleRecognitionResult():
onResult: (sessionId, result) => {
this.handleRecognitionResult(sessionId, result);
}
Service 把 result.result 归一化为 LiveCaptionItem,维护临时结果和最终结果,再通过 LiveSessionSnapshot 发给页面。这样 Core Speech Kit 的返回模型没有扩散到页面、数据库和其他模块。
如果未来更换 Speech Kit 控件、第三方 ASR 或本地模型,只要保持领域快照契约,ArkUI 主体不必重写。
十一、暂停和结束的资源释放不同
暂停时,项目停止计时、调用 engine.finish(sessionId)、解绑并释放 AudioCapturer,但保留引擎和字幕,方便继续。
结束时还会调用 engine.shutdown(),清空 sessionId 与当前字幕 ID,并进入 ended。这体现了两个不同目标:暂停为本会话保留恢复成本,结束则完成资源收口和后续持久化准备。
释放顺序至少包括:
isCapturing=false
-> off('readData')
-> capturer.stop()
-> capturer.release()
-> finish(sessionId)
-> 必要时 engine.shutdown()
各调用都要容忍已经停止或系统中断的状态,避免释放阶段的异常反过来导致崩溃。
十二、能力不可用时如何降级
项目把启动阶段异常统一映射为 unsupported,提示“当前设备暂不能启动语音识别,已保留本地字幕,可稍后在真机重试”。会话中途 onError 则进入 error,提示可继续重试。
降级至少应做到:
- 已有字幕仍可浏览与标记;
- 麦克风和监听回调被释放;
- 页面不假装仍在采集;
- 提供继续、返回或手工记录路径;
- 不把设备限制写成模型准确率问题。
十三、官方文档与项目历史证据如何同时看
华为当前公开指南说明 Core Speech 语音识别可接收 PCM 文件或实时音频流,并展示 createEngine、setListener、startListening、writeAudio 的标准路径;同时,该指南注明能力当前不支持模拟器。
听见课堂历史模拟器报告曾观察到 startListening 返回成功、计时增长、暂停/继续/结束和无崩溃。这些证据能证明应用侧调用与生命周期路径,却不能覆盖官方“不支持模拟器”的能力限制,更不能证明真实语音已经被正确转成新文本。项目报告也明确把“真实识别文本”和“真实字幕重启读回”标为 not run。
十四、真机验收清单
-
hdc list targets -v确认目标真机在线; -
/data/local/tmp可写预检通过; - 首次授权、拒绝和系统设置恢复分别执行;
- 近讲、远讲、噪声和停顿场景产生可辨认结果;
- 临时结果更新同一行,最终结果固化;
- 暂停/继续后旧回调不污染新 session;
- 来电、切后台、拔插音频设备后资源可恢复;
- 结束后麦克风占用消失;
- 新字幕经 Repository 写入并强停重启读回;
- 30—45 分钟课堂观察 CPU、内存、回调堆积和电量;
- 日志不包含原始音频、完整字幕或身份信息。
十五、总结
Core Speech Kit + AudioCapturer 的关键不是 API 数量,而是参数一致、启动有序、会话隔离、音频帧契约、回调归一化和资源闭环。听见课堂用 Service 承担这些规则,让 ArkUI 只面对稳定的字幕快照。
下一篇将深入 AUDIO_FRAME_BYTES = 640:它为什么约等于 20 ms 音频,项目为何也允许 1280 字节,以及长课堂怎样避免回调、重绘和内存一起失控。
更多推荐



所有评论(0)