OpenHarmony 英语学习 App 实战:TTS 听力训练播放器与异常兜底设计

摘要

听力训练是英语学习 App 的核心能力之一。相比直接播放预录音频,TTS 文本转语音更灵活:可以朗读单词、例句、听力材料、每日一句,甚至可以给 AI 讲解配音。本文以「英语视界 YingYu」项目中的 ListeningTtsHelper.ets 为例,分享如何在 OpenHarmony/HarmonyOS 中封装一个更稳定的 TTS 播放辅助类。🎧

本文重点不是简单调用 speak(),而是如何处理真实项目中的问题:引擎创建、在线/离线兜底、播放状态监听、超时保护、重复点击、停止播放和错误回调。

一、为什么 TTS 需要单独封装?

如果页面直接调用 TTS API,会遇到很多问题:

  • 多个页面重复写初始化逻辑;
  • 引擎创建失败不好处理;
  • 播放中重复点击容易状态混乱;
  • 无声失败时 UI 卡住;
  • 页面退出时没有清理;
  • 在线引擎不可用时没有兜底。

所以项目使用单例类 ListeningTtsHelper 统一管理 TTS。

export class ListeningTtsHelper {
  private static instance: ListeningTtsHelper
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null
  private engineCreating: boolean = false

  private constructor() {}

  static getInstance(): ListeningTtsHelper {
    if (!ListeningTtsHelper.instance) {
      ListeningTtsHelper.instance = new ListeningTtsHelper()
    }
    return ListeningTtsHelper.instance
  }
}

单例的好处是:整个 App 共用一个 TTS 管理器,避免重复创建引擎。

二、核心状态设计

ListeningTtsHelper 中维护了播放回调、当前文本和安全定时器:

private playbackEndCallback: (() => void) | null = null
private playbackErrorCallback: (() => void) | null = null

private activeSpeakText: string = ''
private suppressNextStopCallback: boolean = false

private safetyTimerId: number = -1
private readonly SAFETY_TIMEOUT_MS: number = 12000

这些状态分别解决:

  • 播放结束后通知页面;
  • 播放失败后恢复 UI;
  • 保存当前正在朗读的文本;
  • 避免主动 stop 触发重复回调;
  • 防止静默失败。

三、创建 TTS 引擎:在线优先

项目优先创建在线引擎:

private createEngine(onReady: () => void, onFail: () => void): void {
  if (this.ttsEngine !== null) {
    onReady()
    return
  }
  if (this.engineCreating) {
    return
  }

  this.engineCreating = true
  const initParams: textToSpeech.CreateEngineParams = {
    language: 'en-US',
    person: 0,
    online: 1,
    extraParams: {
      'style': 'interaction-broadcast',
      'locate': 'US',
      'name': 'ListeningTts'
    }
  }
}

这里有两个保护:

  • 如果引擎已经存在,直接回调;
  • 如果引擎正在创建,避免重复创建。

四、在线失败后离线兜底

在线引擎创建失败后,项目会切换到离线模式:

textToSpeech.createEngine(initParams, (err, engine) => {
  this.engineCreating = false
  if (err) {
    console.error(`[ListeningTts] createEngine(online) failed: ${err.code} ${err.message}`)
    this.createEngineOffline((e2, eng) => {
      if (e2) {
        console.error(`[ListeningTts] createEngine(offline) also failed: ${e2.code} ${e2.message}`)
        onFail()
      } else {
        this.ttsEngine = eng
        this.attachListener()
        onReady()
      }
    })
    return
  }

  this.ttsEngine = engine
  this.attachListener()
  onReady()
})

离线创建函数:

private createEngineOffline(
  callback: (err: BusinessError, engine: textToSpeech.TextToSpeechEngine) => void
): void {
  const initParams: textToSpeech.CreateEngineParams = {
    language: 'en-US',
    person: 0,
    online: 0,
    extraParams: {
      'style': 'interaction-broadcast',
      'locate': 'US',
      'name': 'ListeningTts'
    }
  }

  textToSpeech.createEngine(initParams, (err, engine) => {
    callback(err, engine)
  })
}

学习类产品很适合这种策略:在线优先保证效果,离线兜底保证可用。

五、监听播放生命周期

播放状态监听是 TTS 封装里最重要的部分。

private attachListener(): void {
  if (!this.ttsEngine) return

  this.ttsEngine.setListener({
    onStart: (_rid, _resp) => {
      console.info('[ListeningTts] onStart')
      this.clearSafetyTimer()
      this.startSafetyTimer(() => {
        this.safeStop()
        this.invokeEnd()
      })
    },
    onComplete: (_rid, _resp) => {
      console.info('[ListeningTts] onComplete')
      this.invokeEnd()
    },
    onStop: (_rid, _resp) => {
      if (this.suppressNextStopCallback) {
        this.suppressNextStopCallback = false
        return
      }
      this.invokeEnd()
    },
    onError: (_rid, code: number, msg: string) => {
      console.error(`[ListeningTts] onError code=${code} msg=${msg}`)
      this.invokeError()
    }
  })
}

这里将 TTS 生命周期映射为页面可以理解的状态:

  • onStart:开始播放;
  • onComplete:正常结束;
  • onStop:停止;
  • onError:出错。

六、安全定时器:解决无声失败

项目中有一个很实用的设计:安全定时器。

private startSafetyTimer(onTimeout: () => void): void {
  this.clearSafetyTimer()
  this.safetyTimerId = setTimeout(() => {
    this.safetyTimerId = -1
    console.error('[ListeningTts] safety timer fired, triggering timeout')
    onTimeout()
  }, this.SAFETY_TIMEOUT_MS) as number
}

清理定时器:

private clearSafetyTimer(): void {
  if (this.safetyTimerId !== -1) {
    try {
      clearTimeout(this.safetyTimerId)
    } catch (_e) {}
    this.safetyTimerId = -1
  }
}

这个机制可以防止一种糟糕体验:系统没有明确报错,但用户也听不到声音,页面一直显示播放中。

七、播放参数:更适合英语学习

播放时设置 SpeakParams

const params: textToSpeech.SpeakParams = {
  requestId: `s_${Date.now()}`,
  extraParams: {
    'queueMode': 0,
    'speed': 0.92,
    'volume': 1,
    'pitch': 1,
    'languageContext': 'en-US',
    'soundChannel': 0,
    'playType': 0
  }
}

this.ttsEngine.speak(text, params)

几个参数值得注意:

  • speed: 0.92:稍慢一点,适合学生听写和跟读;
  • languageContext: 'en-US':英语语境;
  • volume: 1:保证音量;
  • requestId:每次请求唯一标识。

后续还可以让用户在设置页调整语速。

八、speak 方法:统一入口

页面调用的核心方法是 speak()

speak(text: string, onEnd: () => void, onError: () => void): void {
  if (!text?.trim()) {
    onEnd()
    return
  }

  this.safeStop()
  this.playbackEndCallback = onEnd
  this.playbackErrorCallback = onError
  this.activeSpeakText = text

  if (!this.ttsEngine) {
    this.createEngine(() => {
      this.checkAndSpeak()
    }, () => {
      this.invokeError()
    })
    return
  }

  this.startSafetyTimer(() => {
    this.checkAndSpeak()
  })
  this.checkAndSpeak()
}

这个方法处理了:

  • 空文本;
  • 播放前停止上一次朗读;
  • 保存回调;
  • 引擎不存在时先创建;
  • 引擎存在时直接播放;
  • 启动安全定时器。

页面只需要关心成功和失败,不需要处理底层细节。

九、停止播放和取消请求

停止当前播放:

stop(): void {
  this.clearSafetyTimer()
  this.safeStop()
  this.invokeEnd()
}

取消待处理文本:

cancelPending(): void {
  this.activeSpeakText = ''
  this.clearSafetyTimer()
}

这些方法适合在页面退出、切换材料、用户点击停止时调用。

十、适合扩展的功能

基于当前封装,可以继续做很多英语学习功能:

  • 单词发音;
  • 例句朗读;
  • 每日一句朗读;
  • 听力材料播放;
  • 慢速跟读模式;
  • 单句循环;
  • 播放进度 UI;
  • AI 讲解语音播报。

只要底层 TTS helper 稳定,上层功能扩展就会很轻松。

十一、小结

本文结合 ListeningTtsHelper.ets,分享了 OpenHarmony 英语学习 App 中 TTS 播放器的封装思路:

  • 使用单例管理 TTS 引擎;
  • 在线引擎优先,离线引擎兜底;
  • 监听 onStartonCompleteonStoponError
  • 使用安全定时器防止静默失败;
  • 播放前停止旧任务,避免状态混乱;
  • 通过 speak() 暴露统一调用入口。

TTS 看起来只是一个 API 调用,但真实产品里稳定性非常关键。听力训练如果播放不可靠,用户很快就会放弃。因此,封装一个健壮的 TTS Helper,是学习类 App 非常值得投入的基础能力。🎧

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐