HarmonyOS 6实战(源码教学篇)--- Speech Kit TextReader:【仿某云音乐接入语音朗读控件】
- [HarmonyOS 音乐播放器接入语音朗读控件(TextReader)教程](#HarmonyOS 音乐播放器接入语音朗读控件(TextReader)教程)
-
- 前言
- 应用回顾
- 实现步骤
-
- [步骤 1: 配置权限和依赖](#步骤 1: 配置权限和依赖)
- [步骤 2: 在 EntryAbility 中设置窗口管理器](#步骤 2: 在 EntryAbility 中设置窗口管理器)
- [步骤 3: 创建 TextReader 控制器](#步骤 3: 创建 TextReader 控制器)
- [步骤 4: 修改音频播放控制器以支持音频焦点协调](#步骤 4: 修改音频播放控制器以支持音频焦点协调)
- [步骤 5: 在 UI 中集成 TextReader 图标](#步骤 5: 在 UI 中集成 TextReader 图标)
- [步骤 6: 导出 TextReaderController](#步骤 6: 导出 TextReaderController)
- [核心 API 说明](#核心 API 说明)
-
- [TextReader 主要方法](#TextReader 主要方法)
- [ReadStateCode 状态码](#ReadStateCode 状态码)
- 用户体验优化建议
-
- [1. 智能恢复播放](#1. 智能恢复播放)
- [2. 歌曲切换时的处理](#2. 歌曲切换时的处理)
- 常见问题与解决方案
-
- [1. TextReader 无法初始化](#1. TextReader 无法初始化)
- [2. 朗读时音乐未暂停](#2. 朗读时音乐未暂停)
- [3. 歌词朗读格式问题](#3. 歌词朗读格式问题)
- 参考资源
- 总结
HarmonyOS 音乐播放器接入语音朗读控件(TextReader)教程

前言
大家好!我是你们的老朋友木斯佳,是华为云 HDE 认证专家和 OpenTiny 开源社区的布道师。熟悉我们的小伙伴们已经跟随着之前的分享,一步步实现了 HarmonyOS 音频播放的小案例,
想象一下这样的场景:当你在通勤路上、运动途中,或是双手不便时,听到一首触动心弦的歌曲,想要立刻了解它的相关背景,却苦于无法腾出视线阅读屏幕上细密的文字。这时,如果能有一个贴心的"声音助手",将文字信息清晰、流畅地"读"给你听,那该是多么美妙的体验!
这正是 HarmonyOS 6 为我们带来的又一惊喜------Speech Kit 的文本朗读(TextReader)能力。继我们成功为播放器装上智能"耳朵"(AI字幕)之后,今天,我们将为其赋予一副动人的"嗓音"。在本篇实战教程中,我们将深入探索 TextReader 控件,教你如何将它优雅地集成到你的 HarmonyOS 音乐播放器应用中。
我们将共同实现:仿照主流音乐应用的设计,在你的播放界面添加一个智能朗读按钮。用户只需轻点一下,即可唤出系统级的朗读控制面板,随心选择喜欢的音色、语速,让应用为你"声情并茂"地朗读歌曲名、歌手信息、专辑介绍乃至滚动歌词。这不仅极大地提升了无障碍访问体验,也为所有用户在特定场景下享受音乐与信息提供了前所未有的便利。
应用回顾

技术栈
-
开发语言: ArkTS
-
核心框架: HarmonyOS SDK 6.0.0+
-
核心能力 :
- @kit.SpeechKit(语音能力套件)
- @kit.AudioKit(音频能力套件)
- @kit.AVSessionKit(媒体会话套件)
-
开发工具: DevEco Studio 6.0.0+
-
支持设备: Phone、Tablet、2in1
项目结构:
├── entry/src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # 添加 WindowManager.setWindowStage()
│ ├── components/
│ │ └── ControlAreaComponent.ets # 集成 TextReaderIcon
│ └── module.json5 # 配置权限
├── MediaService/src/main/ets/
│ └── utils/
│ ├── AudioRendererController.ets # 增强音频焦点处理
│ └── TextReaderController.ets # 新建:TextReader 控制器
└── MediaService/Index.ets # 导出 TextReaderController
核心概念
在音乐播放器中集成 TextReader 可以实现:
- 播放歌曲时朗读歌曲名称和歌手信息
- 在驾驶等场景下提供语音信息播报
在上述场景中,需要处理两个音频流:
- AudioRenderer: 播放音乐音频流
- TextReader: 播放语音朗读音频流
两者需要协调工作,避免冲突:
- 当 TextReader 开始朗读时,AudioRenderer 应降低音量或暂停
- 朗读结束后,AudioRenderer 恢复正常播放
- 通过音频焦点管理机制实现自动协调
实现步骤

步骤 1: 配置权限和依赖
在 entry/src/main/module.json5 中添加网络权限INTERNET、后台权限KEEP_BACKGROUND_RUNNING:
HarmonyOS 6将语音功能封装在SpeechKit中,我们需要像引入其他核心能力一样导入它。TextReader类是整个朗读功能的核心,TextReaderIcon控制朗读面板的图标显示,ReadStateCode提供朗读状态信息,而WindowManager则是确保朗读面板正确显示的关键。
与此同时,我们还需要导入AudioKit来管理音频焦点。这是因为在实际使用中,TextReader朗读时可能会与音乐播放产生冲突。通过AudioKit,我们可以协调两个音频流,实现智能的音量调节。
在需要使用 TextReader 的文件中导入:
typescript
import { TextReader, TextReaderIcon, ReadStateCode, WindowManager } from '@kit.SpeechKit';
import { audio } from '@kit.AudioKit';
步骤 2: 在 EntryAbility 中设置窗口管理器
在HarmonyOS中,TextReader的朗读面板是一个系统级的悬浮窗口。为了让这个窗口能够正确显示,我们必须先告诉系统"位置"在哪里。这个"位置"就是应用的窗口阶段(WindowStage)。
在EntryAbility的onWindowStageCreate方法中,我们需要做两件重要的事情:
第一,在加载页面内容之前,必须调用WindowManager.setWindowStage(windowStage)。这个调用的时机很重要------必须在窗口阶段创建之后,但在加载具体页面内容之前。
第二,我们需要保存UIAbility的上下文。这个上下文就像是应用的"身份证",后续初始化TextReader时必须使用它来证明"我是谁"。我们把它存储在AppStorage中,这样应用的其他部分也能访问到这个重要的上下文信息。
修改 entry/src/main/ets/entryability/EntryAbility.ets:
typescript
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { WindowManager } from '@kit.SpeechKit';
export default class EntryAbility extends UIAbility {
onCreate() {
AppStorage.setOrCreate('context', this.context);
}
onWindowStageCreate(windowStage: window.WindowStage) {
// 关键步骤:设置窗口阶段,这是使用 TextReader 的必要条件
WindowManager.setWindowStage(windowStage);
windowStage.loadContent('pages/Root', (err) => {
if (err.code) {
return;
}
// 其他窗口配置...
});
}
}
关键点 : 必须在加载页面前调用 WindowManager.setWindowStage(windowStage),否则 TextReader 无法正常工作。
步骤 3: 创建 TextReader 控制器
TextReader控制器是整个语音朗读功能的"大脑",它负责管理所有与语音相关的操作和状态。
1、控制器初始化:
初始化TextReader时,我们可以配置一些重要的参数。isVoiceBrandVisible控制是否显示语音品牌,这能让用户知道正在使用的是哪个语音引擎。businessBrandInfo则定义了朗读面板的品牌信息,比如面板的名称和图标,这些信息会在朗读面板中显示,让用户清楚地知道当前是什么应用在提供朗读服务。
2、内容准备:将歌曲信息转化为可朗读格式
TextReader需要一种特定的数据结构来理解要朗读什么内容。我们需要将普通的歌曲信息转换成ReadInfo格式。这个过程就像为每首歌制作一张"朗读卡片",卡片上包含歌曲ID、标题、作者和正文信息。
值得注意的是,title和author字段的isClickable属性可以设置为true。这意味着用户在朗读面板上看到歌曲名或歌手名时,可以直接点击它们。点击后,TextReader会触发相应的事件,我们可以监听这些事件并做出响应------比如切换到对应的歌曲。
3、事件监听:建立应用与语音功能的沟通桥梁
TextReader通过事件机制与应用通信,我们需要为重要的事件设置监听器。
创建新文件 MediaService/src/main/ets/utils/TextReaderController.ets:
typescript
/**
* 设置事件监听器
*/
public setListeners(): void {
// 操作监听...
// 状态变化监听(重要)
TextReader.on('stateChange', (state: TextReader.ReadState) => {
Logger.info(TAG, `State changed: ${JSON.stringify(state)}`);
if (state.id === this.currentReadId) {
AppStorage.setOrCreate('textReaderState', state.state);
}
});
}
/**
* 开始朗读指定歌曲
*/
public async startReading(songId: string): Promise<void> {
if (!this.isInit) {
await this.init();
}
try {
this.currentReadId = songId;
this.setListeners();
await TextReader.start(this.readInfoList, songId);
Logger.info(TAG, `Started reading song: ${songId}`);
} catch (err) {
Logger.error(TAG, `Start reading error: ${JSON.stringify(err)}`);
}
}
// 其他操作...
}
stateChange事件是最重要的事件之一。它告诉我们TextReader当前处于什么状态:是正在播放、暂停、停止,还是发生了错误。通过监听这个事件,我们可以实时了解朗读的进展,并根据不同的状态做出相应的响应。
/**
* 歌曲选择回调
*/
private onSongSelected(songId: string): void {
// 通知音乐播放器切换歌曲
const songIndex = parseInt(songId);
AppStorage.setOrCreate('selectIndex', songIndex);
// 可以调用 AudioRendererController 播放对应歌曲
}
步骤 4: 修改音频播放控制器以支持音频焦点协调
音频焦点协调是TextReader集成的关键环节。当你正在享受音乐,突然想了解当前播放的歌曲信息,于是点击了朗读按钮。这时,音乐播放器应该做什么?是继续播放还是暂停?如果继续播放,用户能同时听清朗读内容吗?
HarmonyOS提供了优雅的音频中断管理机制来解决这个问题。我们需要对原有的音频播放控制器进行升级,让它能够智能地响应TextReader的语音请求。
音频中断回调:系统的智能协调信号
在音频播放控制器的setInterruptCallback方法中,我们需要增强处理逻辑。这个回调函数就像是音乐的"指挥中心",当系统中有多个音频需要播放时,它会告诉音乐播放器该怎么做。
修改 MediaService/src/main/ets/utils/AudioRendererController.ets,增强音频焦点处理:
typescript
// 在 setInterruptCallback 方法中增强处理逻辑
private interruptCallback: (interruptEvent: audio.InterruptEvent) => void =
(interruptEvent: audio.InterruptEvent) => {
Logger.info(TAG, `Audio interrupt: ${JSON.stringify(interruptEvent)}`);
if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// TextReader 开始朗读时,音乐会被暂停
this.updateIsPlay(false);
AppStorage.setOrCreate('musicPausedByTextReader', true);
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
this.updateIsPlay(false);
this.pause();
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// 降低音量,但继续播放
Logger.info(TAG, 'Audio ducked');
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// 恢复音量
Logger.info(TAG, 'Audio unducked');
break;
}
} else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
// TextReader 结束朗读后,可以选择恢复音乐播放
let pausedByTextReader = AppStorage.get('musicPausedByTextReader');
if (pausedByTextReader) {
this.start();
AppStorage.setOrCreate('musicPausedByTextReader', false);
}
break;
}
}
}
这个机制的精妙之处在于:系统根据不同的场景自动选择最合适的协调策略。对于重要的歌曲信息朗读,系统可能会要求完全暂停音乐;而对于简单的播放状态提示,可能只需要降低音乐音量。这种智能决策让用户体验更加自然流畅。
步骤 5: 在 UI 中集成 TextReader 图标
有了强大的后台功能,现在我们需要在用户界面上提供便捷的访问入口。用户不应该通过复杂的操作才能使用朗读功能,它应该是触手可及的。
在播放控制区域,我们需要添加一个朗读图标。这个图标不仅要美观,还要能反映当前的朗读状态。
修改 entry/src/main/ets/components/ControlAreaComponent.ets,添加朗读图标:
typescript
// 新增:TextReader 朗读图标
TextReaderIcon({ readState: this.textReaderState })
.width(24)
.height(24)
.onClick(async () => {
// 如果正在朗读,直接显示面板
if (this.textReaderState === ReadStateCode.PLAYING) {
this.textReaderController.showPanel();
return;
}
// 否则,开始朗读当前歌曲
const currentSongId = this.songList[this.selectIndex].id.toString();
await this.textReaderController.startReading(currentSongId);
})
用户体验的细节设计:
状态反馈:TextReaderIcon组件会根据readState自动显示不同的状态,让用户一眼就能看出朗读是否正在进行
智能交互:如果正在朗读,点击图标会显示控制面板;如果尚未开始,点击会开始朗读当前歌曲
无感知准备:在aboutToAppear中提前初始化,确保用户点击时响应迅
步骤 6: 导出 TextReaderController
为了让UI组件能够访问TextReaderController,我们需要在MediaService的入口文件中将其导出。
修改 MediaService/Index.ets,导出新创建的控制器:
typescript
export { AudioRendererController } from './src/main/ets/utils/AudioRendererController';
export { AVSessionController } from './src/main/ets/utils/AVSessionController';
export { TextReaderController } from './src/main/ets/utils/TextReaderController';
export { BackgroundUtil } from './src/main/ets/utils/BackgroundUtil';
export { Logger } from './src/main/ets/utils/Logger';
export { MediaTools } from './src/main/ets/utils/MediaTools';
export { PreferencesUtil } from './src/main/ets/utils/PreferencesUtil';
export { SongItem } from './src/main/ets/songdatacontroller/SongData';
export { MusicPlayMode } from './src/main/ets/songdatacontroller/PlayerData';
模块架构的重要性:通过统一的入口文件导出所有控制器,我们建立了一个清晰的架构模式。UI组件只需要从单一的入口导入所需的功能模块,大大简化了依赖管理。这种设计模式也便于后续的维护和扩展------如果将来需要添加新的功能模块,只需要在Index.ets中添加导出即可。
通过这三个关键步骤,我们完成了TextReader功能的完整集成。现在,我们的音乐播放器不仅能够播放音乐,还能智能地朗读歌曲信息,在不同的使用场景下为用户提供最佳体验。
核心 API 说明
TextReader 主要方法
| 方法 | 说明 | 参数 | 返回值 |
|---|---|---|---|
init() |
初始化朗读控件 | context, readerParam | Promise |
start() |
开始朗读 | readInfoList, articleId | Promise |
showPanel() |
显示朗读面板 | 无 | void |
stop() |
停止朗读 | 无 | Promise |
loadMore() |
加载更多内容 | readInfoList, isEnd | void |
on() |
注册事件监听 | type, callback | void |
ReadStateCode 状态码
| 状态 | 说明 | 使用场景 |
|---|---|---|
WAITING |
等待中 | 初始状态,未开始朗读 |
PLAYING |
播放中 | 正在朗读内容 |
PAUSED |
已暂停 | 朗读被暂停 |
STOPPED |
已停止 | 朗读已停止 |
用户体验优化建议
1. 智能恢复播放
typescript
// 记录暂停原因
private interruptCallback: (interruptEvent: audio.InterruptEvent) => void =
(interruptEvent: audio.InterruptEvent) => {
if (interruptEvent.hintType === audio.InterruptHint.INTERRUPT_HINT_PAUSE) {
// 记录是否由 TextReader 引起的暂停
AppStorage.setOrCreate('pauseReason', 'textReader');
}
if (interruptEvent.hintType === audio.InterruptHint.INTERRUPT_HINT_RESUME) {
let pauseReason = AppStorage.get('pauseReason');
// 只有 TextReader 引起的暂停才自动恢复
if (pauseReason === 'textReader') {
this.start();
AppStorage.setOrCreate('pauseReason', '');
}
}
}
2. 歌曲切换时的处理
typescript
// 在 AudioRendererController 的 play 方法中
async play(musicIndex: number = this.musicIndex) {
// 如果 TextReader 正在朗读,先停止
let textReaderState = AppStorage.get('textReaderState');
if (textReaderState === ReadStateCode.PLAYING) {
TextReaderController.getInstance().stop();
}
// 继续播放音乐...
this.updateMusicIndex(musicIndex);
// ...
}
常见问题与解决方案
1. TextReader 无法初始化
问题 : 调用 TextReader.init() 失败
原因:
- 未调用
WindowManager.setWindowStage() - 调用时机不对(应在
onWindowStageCreate中调用)
解决方案:
typescript
onWindowStageCreate(windowStage: window.WindowStage) {
// 必须在加载页面前调用
WindowManager.setWindowStage(windowStage);
windowStage.loadContent('pages/Root', (err) => {
// ...
});
}
2. 朗读时音乐未暂停
问题: TextReader 开始朗读时,音乐继续播放
原因:
- 未正确处理音频焦点打断事件
audioInterrupt回调未设置
解决方案:
typescript
// 确保设置了打断回调
this.audioRenderer.on('audioInterrupt', this.interruptCallback);
// 正确处理 INTERRUPT_HINT_PAUSE
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
this.updateIsPlay(false);
// 可选:调用 pause() 完全停止
break;
3. 歌词朗读格式问题
问题: 朗读歌词时包含时间标签或格式混乱
原因:
- LRC 格式未正确解析
- 包含特殊字符
解决方案:
typescript
private parseLrcToText(lrcContent: string): string {
const lines = lrcContent.split('\n');
const lyrics: string[] = [];
for (const line of lines) {
// 移除所有时间标签 [00:12.00]
const text = line.replace(/\[\d{2}:\d{2}\.\d{2}\]/g, '').trim();
// 过滤空行和元数据
if (text && !text.startsWith('[')) {
lyrics.push(text);
}
}
return lyrics.join(','); // 用逗号连接,朗读更自然
}
参考资源
总结
在这个信息过载的时代,纯粹的音乐播放已经无法满足用户对沉浸式体验的追求。通过集成HarmonyOS 6的TextReader能力,我们不仅为音乐播放器添加了一个功能,更是为用户开启了一种全新的音乐交互方式------在音乐与信息间自由切换的智能体验。
从技术实现到用户体验,从音频协调到错误处理,对用户需求的深度理解和场景化思考。无论是通勤路上的便捷了解,还是驾驶时的安全播报,TextReader的集成让音乐应用变得更加包容、智能和人性化。
期待看到更多开发者基于这一能力,创造出更多创新应用场景,让我们继续在HarmonyOS的生态中探索、创新,用代码创造更美好的数字生活体验。
后面我会单独讲解音频焦点协调机制。需要本篇代码的同学也可以评论区或私信留言,我们共同成长进步。