第4.5篇:语音识别实战------SpeechRecognitionEngine + AudioCapturer
难度:⭐⭐⭐ 高级
前置知识 :第 4.1 篇 canIUse 系统能力检测
涉及源文件 :
products/default/src/main/ets/services/VoiceRecognitionService.ets

一、概述
语音识别是"画伴梦工厂"中用户与 AI 交互的核心入口之一。在创作首页的聊天界面中,用户可以通过语音说出想创作的角色和场景,系统将其转化为文本后发送给 AI 模型生成内容。
HarmonyOS 提供了完整的语音识别能力栈------@kit.CoreSpeechKit 中的 speechRecognizer 模块负责语音到文本的转换,@kit.AudioKit 中的 audio.AudioCapturer 负责音频数据采集,二者协同工作,构成了从物理麦克风到文本输出的完整链路。
本文将深入解析 VoiceRecognitionService 的实现,涵盖 SpeechRecognitionEngine 的创建与配置、AudioCapturer 的音频采集参数、回调解读、音频循环读取机制以及离线/在线模式的双引擎 fallback 策略。
二、服务接口设计:回调驱动的异步架构
在深入技术细节之前,先来看 VoiceRecognitionService 对外暴露的接口设计。整个服务采用回调模式 ,调用方通过传入一个 VoiceRecognitionCallbacks 对象来接收识别过程中的各种事件:
typescript
export interface VoiceRecognitionCallbacks {
onText: (text: string, isFinal: boolean) => void;
onStatus: (text: string) => void;
onError: (message: string) => void;
onListeningChange: (listening: boolean) => void;
}
```
| 回调 | 触发时机 | 说明 |
|------|----------|------|
| `onText` | 收到识别结果时 | `isFinal` 标记是否为最终结果,可用于实时显示中间过程 |
| `onStatus` | 状态变更时 | 用于显示"正在聆听""检测到语音"等提示文字 |
| `onError` | 发生错误时 | 携带格式化后的错误信息,由 UI 层展示 |
| `onListeningChange` | 录音启动/停止时 | 驱动 UI 层录音按钮的显隐和状态切换 |
这种回调驱动的设计让 `VoiceRecognitionService` 成为一个纯逻辑服务,与 UI 完全解耦------它不需要知道调用方是 ArkUI 组件还是其他服务,只需按照约定通知即可。
---
## 三、SpeechRecognitionEngine 创建与双模式 Fallback
### 3.1 创建引擎
语音识别的第一步是创建 `SpeechRecognitionEngine` 实例。HarmonyOS 通过 `speechRecognizer.createEngine` 方法创建引擎,并支持两种运行模式:
```typescript
const SPEECH_MODE_ONLINE: number = 0;
const SPEECH_MODE_OFFLINE: number = 1;
- 在线模式(
online: 0):音频数据上传到云端处理,识别精度更高、词汇量更大,但需要网络连接。 -
- 离线模式(
online: 1):本地设备端处理,响应更快、无需网络,但识别精度和词汇量受限于本地模型。
- 离线模式(
3.2 双模式 Fallback 策略
项目中的 createSpeechEngine 方法实现了一个精巧的 fallback 策略------优先尝试离线模式,失败后自动切换到在线模式:
typescript
private async createSpeechEngine(
callbacks: VoiceRecognitionCallbacks
): Promise<speechRecognizer.SpeechRecognitionEngine> {
const modes: SpeechEngineMode[] = [
{ mode: SPEECH_MODE_OFFLINE, label: '离线' },
{ mode: SPEECH_MODE_ONLINE, label: '在线' }
];
let lastErrorMessage: string = '';
for (let i = 0; i < modes.length; i++) {
callbacks.onStatus('正在启动' + modes[i].label + '语音识别');
try {
return await speechRecognizer.createEngine({
language: 'zh-CN',
online: modes[i].mode
});
} catch (error) {
lastErrorMessage = modes[i].label + '模式' +
this.formatError(error as BusinessError, '创建失败');
}
}
throw new Error('语音识别引擎创建失败,' + lastErrorMessage);
}
```
**设计考量**:
| 策略 | 原因 |
|------|------|
| **离线优先** | 用户体验优先------离线模式响应更快,无需等待网络请求,适合儿童互动场景 |
| **在线兜底** | 当设备未预置离线语音模型时,自动降级到在线模式,保证功能可用 |
| **用户透明** | 两种模式的切换对调用方完全透明,通过 `onStatus` 通知 UI 层当前模式 |
### 3.3 setListener 注册回调
引擎创建成功后,需要通过 `setListener` 注册事件监听器,接收引擎返回的识别结果和状态:
```typescript
this.engine = await this.createSpeechEngine(callbacks);
this.engine.setListener(this.createListener(callbacks));
这一行代码建立了引擎到服务层的单向通信通道------引擎在识别过程中产生的所有事件都会通过 RecognitionListener 转发给 VoiceRecognitionCallbacks,进而驱动 UI 更新。
四、AudioCapturer 音频采集配置
AudioCapturer(音频采集器)负责从设备麦克风采集原始音频数据。语音识别对音频格式有严格要求,createCapturerOptions 方法精确配置了这些参数:
typescript
private createCapturerOptions(): audio.AudioCapturerOptions {
return {
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
}
};
}
```
### 参数详解
| 参数 | 值 | 说明 |
|------|-----|------|
| `samplingRate` | `SAMPLE_RATE_16000`(16kHz) | 语音识别领域的标准采样率,16kHz 在人声频段(300Hz~3400Hz)之上留有充足余量,同时数据量仅为 44.1kHz 的 1/3 |
| `channels` | `CHANNEL_1`(单声道) | 人声定位无关紧要,单声道即可满足识别需求,数据量减半 |
| `sampleFormat` | `SAMPLE_FORMAT_S16LE`(16位有符号小端) | 每个采样点用 16 位整数表示,这也是 PCM 音频最常见的格式 |
| `encodingType` | `ENCODING_TYPE_RAW`(原始 PCM) | 不压缩、不封装,直接输出裸 PCM 数据流 |
| `source` | `SOURCE_TYPE_VOICE_RECOGNITION` | 使用语音识别专用的音频源,系统会对音频链路做针对性优化(如降噪、回声消除) |
**为什么要用 16kHz / S16LE / 单声道**?
回顾音频数据量公式:
数据率 = 采样率 × 声道数 × 位深度
代入参数:`16000 × 1 × 16 = 256,000 bps ≈ 32 KB/s`。每 20ms 的数据块仅为 640 字节------这正是代码中 `AUDIO_CHUNK_SIZE = 640` 的由来。这个小巧的数据块大小非常适合实时流式传输,既不会因为太大而增加延迟,也不会因为太小而引入过多系统调用开销。
---
## 五、音频循环读取:从麦克风到引擎
### 5.1 启动流程
在 `start` 方法中,各个组件的启动顺序是精心设计的:
```typescript
// 1. 创建引擎
this.engine = await this.createSpeechEngine(callbacks);
// 2. 注册监听器
this.engine.setListener(this.createListener(callbacks));
// 3. 创建音频采集器
this.capturer = await audio.createAudioCapturer(this.createCapturerOptions());
// 4. 通知引擎准备接收音频
this.engine.startListening({
sessionId: this.sessionId,
audioInfo: { audioType: 'pcm', sampleRate: SAMPLE_RATE, soundChannel: 1, sampleBit: 16 }
});
// 5. 启动麦克风采集
await this.capturer.start();
// 6. 进入循环读取
this.readAudioLoop(callbacks);
```
关键点在于:**必须先调用 `engine.startListening` 通知引擎准备接收数据,然后才启动麦克风**。如果顺序颠倒,会出现引擎尚未就绪、音频数据已经到达的情况,导致丢帧。
### 5.2 循环读取实现
`readAudioLoop` 是整个语音识别的主动脉------它持续从麦克风读取 PCM 数据块,并实时写入引擎:
```typescript
private async readAudioLoop(callbacks: VoiceRecognitionCallbacks): Promise<void> {
while (this.recording && this.capturer !== null && this.engine !== null) {
try {
const buffer: ArrayBuffer = await this.capturer.read(AUDIO_CHUNK_SIZE, true);
if (this.recording && this.engine !== null && buffer.byteLength === AUDIO_CHUNK_SIZE) {
this.engine.writeAudio(this.sessionId, new Uint8Array(buffer));
}
} catch {
if (this.recording) {
callbacks.onError('麦克风采集失败,请重新尝试');
callbacks.onListeningChange(false);
await this.destroy();
}
return;
}
}
}
```
**循环的三重安全检查**:
while 条件:this.recording && this.capturer !== null && this.engine !== null
│
▼
if 内部 :this.recording && this.engine !== null && buffer.byteLength === AUDIO_CHUNK_SIZE
```
每一层都在确认"我还在录音、硬件还没释放、数据也是完整的"。这种"双重校验"模式在异步并发场景下尤为关键------stop() 或 destroy() 可能在任意时刻被外部调用,循环体必须随时能够安全退出。
5.3 read 方法的参数
typescript
this.capturer.read(AUDIO_CHUNK_SIZE, true);
read 方法接受两个参数:
- 第一个参数(
size):期望读取的字节数,这里是 640 字节。 -
- 第二个参数(
isBlocking) :是否阻塞等待。true表示如果没有足够数据就阻塞等待直到数据到达。在语音场景中,音频数据是持续产生的,使用阻塞模式可以避免 CPU 空转轮询。
- 第二个参数(
5.4 writeAudio:数据投喂
typescript
this.engine.writeAudio(this.sessionId, new Uint8Array(buffer));
ArrayBuffer 需要转换为 Uint8Array 才能被引擎识别。writeAudio 将 PCM 数据块按会话 ID 送入引擎内部的识别流水线,引擎会在内部进行端点检测(VAD)、特征提取和解码。
六、RecognitionListener 回调解读
RecognitionListener 是引擎向服务层发送事件的通道。项目中实现的 createListener 方法将引擎原生事件映射为业务友好的回调:
typescript
private createListener(
callbacks: VoiceRecognitionCallbacks
): speechRecognizer.RecognitionListener {
const service: VoiceRecognitionService = this;
return {
onStart(sessionId: string, eventMessage: string): void {
callbacks.onStatus('正在聆听');
},
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
if (eventCode === 1) {
callbacks.onStatus('检测到语音');
} else if (eventCode === 3) {
callbacks.onStatus('正在整理识别结果');
}
},
onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
if (result.result !== '') {
callbacks.onText(result.result, result.isFinal || result.isLast);
}
if (result.isLast) {
service.recording = false;
callbacks.onListeningChange(false);
}
},
onComplete(sessionId: string, eventMessage: string): void {
service.recording = false;
callbacks.onStatus('识别完成');
callbacks.onListeningChange(false);
service.releaseEngine();
},
onError(sessionId: string, errorCode: number, errorMessage: string): void {
service.recording = false;
callbacks.onError('语音识别失败,请重新尝试');
callbacks.onListeningChange(false);
service.releaseCapturer();
service.releaseEngine();
}
};
}
```
### 回调生命周期
时间线 →
│ onStart │ onEvent │ onResult │ onComplete
│ (聆听中) │ (检测到语音)│ (识别结果) │ (识别完成)
用户说话 → 静音 ──┬──→ 引擎启动 ──→ VAD 检测 ──→ 解码输出 ──→ 结束
│ │ │ │
│ eventCode=1 │ eventCode=3│ │
│ "检测到语音" │ "整理结果" │ │
```
| 回调 | 触发时机 | 业务处理 |
|---|---|---|
onStart |
引擎开始监听音频 | 通知 UI 显示"正在聆听"状态 |
onEvent |
引擎内部状态变化 | eventCode=1 表示检测到语音开始(VAD 触发),eventCode=3 表示用户停止说话、开始整理结果 |
onResult |
产生识别文本 | 将文本通过 onText 回调逐字输出,isFinal/isLast 标记最终结果 |
onComplete |
完整会话结束 | 释放引擎资源,通知 UI 识别完成 |
onError |
识别过程出错 | 释放采集器和引擎资源,通知 UI 错误信息 |
关于 isFinal 与 isLast
在 onResult 中可以看到这样一行代码:
typescript
callbacks.onText(result.result, result.isFinal || result.isLast);
引擎可能会在识别过程中返回多个中间结果(partial result),每个中间结果都带有 isFinal = false,最终结果则标记为 true。不同版本的 SDK 可能使用 isFinal 或 isLast 字段,代码中同时检查两者以保证兼容性。
七、资源管理与生命周期
语音识别涉及多个系统级资源,必须精确管理其生命周期。
7.1 资源层级
VoiceRecognitionService
├── SpeechRecognitionEngine ← 系统 AI 服务,需要 shutdown()
└── AudioCapturer ← 硬件麦克风,需要 stop() + release()
```
无论是正常结束还是异常退出,两条资源的释放路径分别是:
**正常结束(onComplete)**:
onComplete → releaseEngine()(shutdown)
capturer 由服务 stop() 方法统一释放
**异常退出(onError)**:
onError → releaseCapturer()(stop + release)
→ releaseEngine()(shutdown)
```
7.2 stop 与 destroy
服务提供两种结束方式:
typescript
async stop(): Promise<void> {
if (!this.recording) return;
this.recording = false;
await this.releaseCapturer();
if (this.engine !== null && this.sessionId !== '') {
try {
this.engine.finish(this.sessionId); // 正常结束会话
} catch {
this.releaseEngine();
}
}
}
async destroy(): Promise<void> {
this.recording = false;
await this.releaseCapturer();
if (this.engine !== null && this.sessionId !== '') {
try {
this.engine.cancel(this.sessionId); // 强制取消会话
} catch {}
}
this.releaseEngine();
this.sessionId = '';
}
```
| 方法 | 语义 | 适用场景 |
|------|------|----------|
| `stop()` | 优雅停止 | 用户主动停止录音------调用 `finish` 让引擎处理完已收到的音频并返回最终结果 |
| `destroy()` | 强制销毁 | 页面退出或发生错误------调用 `cancel` 立即终止,不等待结果 |
### 7.3 releaseCapturer 与 releaseEngine 的防御性编程
两个 release 方法都采用了典型的"防御性释放"模式:
```typescript
private async releaseCapturer(): Promise<void> {
const currentCapturer: audio.AudioCapturer | null = this.capturer;
this.capturer = null; // 先置空,防止并发重复释放
if (currentCapturer === null) return;
try { await currentCapturer.stop(); } catch {} // 忽略已停止的错误
try { await currentCapturer.release(); } catch {} // 忽略已释放的错误
}
```
关键技巧:**先置空成员变量,再操作局部引用**。这样即使 `releaseCapturer` 被并发调用两次,第二次调用时 `this.capturer` 已经为 `null`,会直接返回,不会重复释放。
---
## 八、错误格式化与用户反馈
项目中实现的 `formatError` 方法用于统一格式化 BusinessError:
```typescript
private formatError(error: BusinessError, fallback: string): string {
let message: string = fallback;
if (error.message !== '') {
message = error.message;
}
if (error.code > 0) {
return message + ',错误码:' + error.code.toString();
}
return message;
}
```
它处理了三种情况:
- **有错误码 + 有错误消息**:如"麦克风被占用,错误码:202"
- - **仅有错误消息**:直接使用系统返回的消息
- - **无任何信息**:使用调用方提供的 fallback 文案兜底
这种设计确保用户永远不会看到空白的错误提示或原始的 Error stack。
---
## 九、完整数据流时序
下面是语音识别从启动到结束的完整数据流:
用户点击"语音"按钮
│
▼
VoiceRecognitionService.start()
│
├── canIUse('SystemCapability.AI.SpeechRecognizer') ─── 不支持 → 抛异常
│
├── createSpeechEngine()
│ ├── 离线模式尝试 ── 成功 → 引擎就绪
│ └── 离线失败 ──→ 在线模式尝试 ── 成功 → 引擎就绪
│
├── engine.setListener(listener) ← 注册回调
│
├── audio.createAudioCapturer() ← 创建采集器
│
├── engine.startListening() ← 引擎就绪
│
├── capturer.start() ← 麦克风启动
│
└── readAudioLoop() ← 进入循环
│
├── capturer.read(640) ──→ buffer ← 从麦克风读取 640 字节 PCM
│ │
│ └── engine.writeAudio(sessionId, uint8Array) ← 写入引擎
│
├── 循环 ← 持续到 recording = false
│
├── engine.onResult ──→ onText(partial, false) ← 中间结果
│
├── engine.onResult ──→ onText("我要画一只猫", true) ← 最终结果
│
├── engine.onComplete → onStatus("识别完成")
│ onListeningChange(false)
│ releaseEngine()
│
└── capturer.stop() + release()
```
关键数据路径
麦克风模拟信号
│ audio capturer(16kHz / S16LE / 单声道)
▼
640 字节 PCM 数据块(每 ~20ms 一个)
│ capturer.read()
▼
Uint8Array
│ engine.writeAudio()
▼
SpeechRecognitionEngine(VAD + 特征提取 + 解码)
│ onResult 回调
▼
识别文本 → onText(text, isFinal)
```
---
## 十、与项目中的 canIUse 集成
本文的实现与第 4.1 篇介绍的 canIUse 能力检测紧密关联。在 `start` 方法的第一行关键检测:
```typescript
if (!canIUse(SPEECH_RECOGNIZER_CAPABILITY)) {
throw new Error('当前设备不支持系统语音识别能力');
}
SPEECH_RECOGNIZER_CAPABILITY 常量值为 'SystemCapability.AI.SpeechRecognizer',表示语音识别系统能力。canIUse 会在启动全部资源之前就完成检测,如果设备不支持,直接抛异常,避免后续创建引擎和采集器的无效开销。
这种"先检测、后操作"的模式与服务的错误处理机制无缝衔接------canIUse 失败和引擎创建失败最终都会走到同一个 catch 分支,统一释放资源并通知 UI。
总结
本文深入解析了"画伴梦工厂"中语音识别功能的完整实现,覆盖了从麦克风到文本输出的全链路:
| 知识点 | 实现方式 |
|---|---|
| 引擎创建与配置 | speechRecognizer.createEngine 支持离线/在线双模式 |
| 模式 fallback | 离线优先,在线兜底,用户无感切换 |
| 音频采集配置 | AudioCapturer 16kHz / S16LE / 单声道 / 语音识别源 |
| 音频循环读取 | capturer.read(640, true) 阻塞读取 + engine.writeAudio 投喂 |
| 回调处理 | RecognitionListener → VoiceRecognitionCallbacks 逐层转发 |
| 资源管理 | stop 优雅结束 / destroy 强制取消,防御性释放模式 |
| 能力检测 | canIUse 前置检查,避免无效资源创建 |
| 错误处理 | BusinessError 格式化封装,提供友好的用户反馈 |
语音识别是典型的"管道式"系统能力------从硬件(麦克风)到系统服务(AudioCapturer),再到 AI 服务(SpeechRecognitionEngine),最终输出结构化文本。理解这条管道的每个节点,是掌握 HarmonyOS 多媒体与 AI 能力集成的关键。
下一篇: 第 4.6 篇将介绍语音识别的状态管理------如何使用异步状态机管理空闲、录音中、识别中、错误四种状态,以及 stop/destroy 的完整资源释放策略。
参考源码
本文所有代码均来自项目文件:
products/default/src/main/ets/services/VoiceRecognitionService.ets--- 语音识别服务完整实现,包含引擎创建、音频采集、循环读取、回调映射和资源管理