《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第3篇:语音合成——文本到自然语音的魔法

文本到语音,这个 API 比你想象中麻烦

在 HarmonyOS NEXT 开发中,语音合成(TextToSpeech, TTS)是一个很容易被低估的能力。官方 Demo 通常几行代码就跑通了,但那只是"能响"而已。真正到了项目里,你会发现需要处理的事情远不止调用一个 speak() 方法:多人协作时音色冲突问题、页面返回后合成器状态丢失、在线离线模式切换时回调时序混乱,这些才是日常。

Core Speech Kit(基础语音服务) 提供了 SpeechSynthesizer 来实现文本转语音,支持在线和离线两种模式。在线模式能调用云端上百种高质量音色,离线模式则用于网络差或无网的场景,速度更快但音色选择有限。两者不冲突,可以在同一个应用中根据需要动态切换。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(需支持系统语音能力)

核心实现:从文本到语音,5 步完成

权限申请

语音合成需要获取 互联网权限 (用于在线音色下载)和 麦克风权限 。虽然合成过程不需要录音,但部分系统级能力会检查麦克风授权状态。在 module.json5 中添加:

json5 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:app_name"
  }
]

创建合成器实例

SpeechSynthesizer 通过 createSpeechSynthesizer 创建。不要随意新建,推荐单例管理

typescript 复制代码
// SpeechManager.ts
import { speechSynthesizer, TextToSpeechCallback } from '@kit.CoreSpeechKit';

export class SpeechManager {
  private static instance: SpeechManager;
  private ttsEngine: speechSynthesizer.SpeechSynthesizer | null = null;
  private callback: TextToSpeechCallback;

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

  async initEngine(): Promise<void> {
    if (this.ttsEngine) return;
    this.callback = {
      onStart: () => console.info('TTS Started'),
      onStop: () => console.info('TTS Stopped'),
      onError: (errorCode) => console.error('TTS Error:', errorCode),
      onPause: () => console.info('TTS Paused'),
      onResume: () => console.info('TTS Resumed'),
    };
    // 第一个参数:语言模型类型,在线使用 'latency-first',离线使用 'offline'
    this.ttsEngine = await speechSynthesizer.createSpeechSynthesizer(
      { deviceId: 'local' },
      this.callback
    );
  }

  getEngine(): speechSynthesizer.SpeechSynthesizer | null {
    return this.ttsEngine;
  }
}

注意createSpeechSynthesizer 返回的是一个 Promise,在页面 aboutToAppear 中初始化即可,不要在 build() 中做。

在线/离线模式切换

配置 SpeechSynthesisRequest 时,通过 requestType 字段决定:

typescript 复制代码
// 在线模式
const onlineRequest: speechSynthesizer.SpeechSynthesisRequest = {
  utteranceId: 'utterance_001',
  text: '你好,HarmonyOS!',
  requestType: 1, // 1 表示在线
  speed: 0,       // 语速,范围 -100 ~ 100
  pitch: 0,       // 音调,范围 -100 ~ 100
  volume: 1,      // 音量,0~1
  voiceName: 'zh-CN-XiaoxiaoNeural' // 在线音色名称
};

// 离线模式
const offlineRequest: speechSynthesizer.SpeechSynthesisRequest = {
  utteranceId: 'utterance_002',
  text: '离线播报测试',
  requestType: 0, // 0 表示离线
  speed: 0,
  pitch: 0,
  volume: 1,
  voiceName: ''   // 离线模式通常用系统默认音色,留空
};

关键差异

特性 在线模式 离线模式
网络要求 必须联网 不依赖网络
音色数量 上百种(如Xiaoxiao、Yunxi等) 1~2 种系统基础音色
首字节延迟 500ms~2s 50ms~200ms
功能支持 支持SSML 、语速/音调调节 仅基础合成,部分参数可能无效

完整功能示例:音色切换 + 语速调节 + SSML

这段代码实现了一个简单的控制面板,点击按钮即可切换在线音色、调节语速,并使用 SSML 标记指定说话人。

typescript 复制代码
// pages/Index.ets
import { speechSynthesizer } from '@kit.CoreSpeechKit';
import { SpeechManager } from '../SpeechManager';

@Entry
@Component
struct TTSDemo {
  @State text: string = '<speak><voice name="zh-CN-XiaoxiaoNeural">欢迎使用语音合成</voice><voice name="zh-CN-YunxiNeural">您可以同时指定不同的说话人</voice></speak>';
  @State speed: number = 0;
  @State currentVoice: string = 'zh-CN-XiaoxiaoNeural';
  @State isSpeaking: boolean = false;
  private manager: SpeechManager = SpeechManager.getInstance();

  aboutToAppear(): void {
    this.manager.initEngine();
  }

  // 切换音色
  switchVoice(voiceName: string): void {
    const request: speechSynthesizer.SpeechSynthesisRequest = {
      utteranceId: 'voice_switch',
      text: this.text,
      requestType: 1,
      speed: this.speed,
      pitch: 0,
      volume: 1,
      voiceName: voiceName,
      ssml: `<speak><voice name="${voiceName}">这是音色切换测试</voice></speak>`
    };
    this.currentVoice = voiceName;
    this.speakText(request);
  }

  // 调速度
  setSpeed(val: number): void {
    this.speed = val;
    const request: speechSynthesizer.SpeechSynthesisRequest = {
      utteranceId: 'speed_test',
      text: '速度调节演示',
      requestType: 1,
      speed: val,
      pitch: 0,
      volume: 1,
      voiceName: this.currentVoice
    };
    this.speakText(request);
  }

  // 执行合成
  speakText(request: speechSynthesizer.SpeechSynthesisRequest): void {
    const engine = this.manager.getEngine();
    if (!engine) {
      console.error('Engine not initialized');
      return;
    }
    engine.stop(); // 先停止当前播放,避免叠加
    engine.speak(request);
  }

  // 暂停/恢复
  togglePlayPause(): void {
    const engine = this.manager.getEngine();
    if (!engine) return;
    if (this.isSpeaking) {
      engine.pause();
    } else {
      engine.resume();
    }
    this.isSpeaking = !this.isSpeaking;
  }

  build() {
    Column({ space: 12 }) {
      TextArea({ placeholder: '输入文本或SSML', text: this.text })
        .onChange((val: string) => this.text = val)
        .height(120)

      Row({ space: 8 }) {
        Button('Xiaoxiao').onClick(() => this.switchVoice('zh-CN-XiaoxiaoNeural'))
        Button('Yunxi').onClick(() => this.switchVoice('zh-CN-YunxiNeural'))
        Button('Yunyang').onClick(() => this.switchVoice('zh-CN-YunyangNeural'))
      }

      Text(`语速: ${this.speed}`)
      Slider({ value: this.speed, min: -100, max: 100, step: 1 })
        .onChange((val: number) => this.setSpeed(val))

      Row({ space: 8 }) {
        Button(this.isSpeaking ? '暂停' : '恢复').onClick(() => this.togglePlayPause())
        Button('停止').onClick(() => {
          this.manager.getEngine()?.stop();
          this.isSpeaking = false;
        })
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
  }
}

运行效果:点击音色按钮,切换不同声音朗读;拖动语速滑块,实时改变说话速度;点击暂停/恢复,控制播放流程。

常见问题 1:onStart 回调触发时机不稳定

现象 :在线模式下,onStart 有时在语音真正播放前 1~2 秒就回调了,导致 UI 状态过早切换,出现"正在朗读"但实际无声的间隙。

原因 :在线合成需要先下载音频数据,onStart 只在 首次数据到达 时回调,不代表解码或渲染完成。离线模式因为数据即时可用,回调时机更准。

解决方案 :不要依赖 onStart 作为"用户听到声音"的起点。改用 onAudioAvailable 回调(如果有)或者延迟 500ms 再更新 UI 状态。更可靠的做法是监听播放状态回调,但当前 SDK 没有直接暴露"音频已输出"的事件,可结合定时器或 AudioRenderer 状态判断:

typescript 复制代码
this.callback.onStart = () => {
  // 延迟更新 UI,给离线/在线解码留出缓冲
  setTimeout(() => {
    // 更新播放状态
  }, 300);
};

常见问题 2:pause / resume 在部分设备上不生效

现象 :调用 pause() 后,语音继续播放直到当前句子结束才停,而不是立即暂停。调用 resume() 后,有时会从头播放,有时正常继续。

原因 :语音合成器的 pause / resume句子级别的 。合成器内部不细分到字词暂停,只会在一个完整的 Sentence 结束后才响应暂停请求。resume 行为则依赖设备驱动,部分设备会重新初始化合成器导致丢失上下文。

解决方案 :使用 stop() + 记录已播放位置来实现"伪暂停"。或者只在离线模式下使用 pause / resume,在线模式直接用 stop() 重构:

typescript 复制代码
// 伪暂停方案:停止并记住最后一个 utteranceId
let lastUtteranceId = '';
let isPaused = false;

function smartPause(): void {
  engine.pause();
  isPaused = true;
}

function smartResume(): void {
  if (isPaused) {
    engine.resume();
    isPaused = false;
  } else {
    // 如果已经 stop 过,需要重新 speak
    engine.speak(createRequest(lastUtteranceId));
  }
}

最佳实践

1. 不要让多个 SpeechSynthesizer 实例共存。 同一个应用内多个实例会竞争音频焦点,导致播报互相打断。推荐使用单例管理,所有语音请求都通过同一个实例执行。

2. 使用 SSML 时,text 字段和 ssml 字段只传一个。 如果两个都传,系统会优先使用 text。官方文档没有明确说明,实测发现 ssml 会被忽略。

3. 切换在线/离线模式前必须先调用 stop() 如果当前正在在线模式播报,直接切换 requestType 发起新请求,旧请求的资源不会被立即释放,可能导致音频卡顿甚至 crash。

FAQ

Q:为什么真机正常,模拟器不生效?

A:模拟器不支持音频渲染。语音合成需要原生音频驱动,模拟器只能跑通回调逻辑,无法输出声音。必须用真机调试。

Q:为什么第一次授权失败,第二次成功?

A:MICROPHONE 权限首次安装时弹窗,如果用户点击"拒绝",系统会把应用加入"受限"列表,第二次不会再弹窗。需要在 settings 界面手动开启。建议在 onError 回调中增加权限提示。

Q:在线模式下,网络切换(4G→WiFi)后合成失败?

A:SpeechSynthesizer 内部没有网络重连机制。网络切换后,旧的连接会超时,需要重新 stop()speak()。监听 网络状态变化 事件,在回调中重置合成器。

Q:页面销毁后,语音还在播放,应用直接崩溃?

A:页面 aboutToDisappear 时未释放合成器资源。建议在页面生命周期结束时调用 engine.stop()engine.release()。但 release() 是异步方法,需等待完成后再退出。

相关推荐
●VON2 小时前
HarmonyKit | 鸿蒙新特性驱动:组件-工具-页面三层架构设计
华为·harmonyos·鸿蒙
●VON3 小时前
HarmonyKit | 鸿蒙新特性应用:10 个开发工具的选型与分类
华为·harmonyos·鸿蒙
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-ArkWeb》进阶配置:自适应与交互控制
华为·交互·harmonyos
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第5篇:实战项目——构建全功能语音助手
华为·harmonyos
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第4篇:语音唤醒进阶——打造免提交互体验
交互·语音识别·harmonyos
我是小a6 小时前
鸿蒙 ArkUI 数据可视化图例对照表:组件化设计与实现
华为·信息可视化·harmonyos·鸿蒙·鸿蒙系统
TrisighT6 小时前
Electron 鸿蒙 PC 上点关闭就真退出了?我花了两天才把“最小化到托盘“做稳
electron·harmonyos
●VON7 小时前
HarmonyKit | 鸿蒙新特性:HdsTabs 沉浸光感底部导航完全指南
华为·harmonyos·鸿蒙·新特性
在书中成长8 小时前
HarmonyOS 小游戏《对战五子棋》开发第2篇-AppScope与entry模块
华为·harmonyos