HarmonyOS 6实战(源码教学篇)— Speech Kit TextReader:【仿某云音乐接入语音朗读控件】

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: 播放语音朗读音频流

两者需要协调工作,避免冲突:

  1. 当 TextReader 开始朗读时,AudioRenderer 应降低音量或暂停
  2. 朗读结束后,AudioRenderer 恢复正常播放
  3. 通过音频焦点管理机制实现自动协调

实现步骤

步骤 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的生态中探索、创新,用代码创造更美好的数字生活体验。

后面我会单独讲解音频焦点协调机制。需要本篇代码的同学也可以评论区或私信留言,我们共同成长进步。

相关推荐
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发 - 校园生活一站式:打造智慧校园服务平台
flutter·华为·harmonyos
南村群童欺我老无力.4 小时前
Flutter 框架跨平台鸿蒙开发 - 城市文创打卡:探索城市文化创意之旅
android·flutter·华为·harmonyos
yingdonglan5 小时前
Flutter 框架跨平台鸿蒙开发 ——AnimatedBuilder性能优化详解
flutter·性能优化·harmonyos
程序员清洒5 小时前
Flutter for OpenHarmony:Icon 与 IconButton — 图标系统集成
前端·学习·flutter·华为
时光慢煮5 小时前
打造跨端驾照学习助手:Flutter × OpenHarmony 实战解析
学习·flutter·华为·开源·openharmony
菜鸟小芯6 小时前
【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&首页功能实现
flutter·harmonyos
大雷神6 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地-- 第19篇:语音合成 - TTS语音播报
华为·语音识别·harmonyos
b2077216 小时前
Flutter for OpenHarmony 身体健康状况记录App实战 - 提醒设置实现
python·flutter·macos·cocoa·harmonyos
xingfanjiuge7 小时前
Flutter框架跨平台鸿蒙开发——ListView.builder深度解析
flutter·华为·harmonyos