HarmonyOS开发:语音合成TTS与多语言朗读

举报
Jack20 发表于 2026/06/21 13:50:48 2026/06/21
【摘要】 HarmonyOS开发:语音合成TTS与多语言朗读核心要点:掌握HarmonyOS语音合成(TTS)引擎创建、语音参数调控、多语言多音色切换、朗读状态监听,以及SSML标记语言的高级应用。 一、背景与动机想象一下这样的场景:你在开车,手机导航突然用机械的声音念出"前方五百米右转",你皱了皱眉——这声音太生硬了,听着就不舒服。但如果导航用温柔的女声或者沉稳的男声说出来呢?体验完全不同。这就是...

HarmonyOS开发:语音合成TTS与多语言朗读

核心要点:掌握HarmonyOS语音合成(TTS)引擎创建、语音参数调控、多语言多音色切换、朗读状态监听,以及SSML标记语言的高级应用。


一、背景与动机

想象一下这样的场景:你在开车,手机导航突然用机械的声音念出"前方五百米右转",你皱了皱眉——这声音太生硬了,听着就不舒服。但如果导航用温柔的女声或者沉稳的男声说出来呢?体验完全不同。

这就是语音合成(Text-to-Speech,TTS)的魅力——把文字变成声音,而且要"好听"、“自然”。

TTS的应用场景远比你想的广泛:

  • 阅读类APP:小说朗读、新闻播报,解放双眼
  • 导航类APP:实时语音导航,安全驾驶
  • 教育类APP:英语单词发音、课文朗读
  • 无障碍辅助:视障用户的屏幕朗读器
  • 智能客服:自动语音应答系统

HarmonyOS的TTS能力有几个突出优势:

  • 多音色支持:男声、女声、童声,多种风格可选
  • 多语言覆盖:中文、英语、日语、韩语等主流语种
  • 参数精细调控:语速、音调、音量,精确到0.1的粒度
  • SSML支持:通过标记语言精确控制发音、停顿、情感

今天我们就来深入探索HarmonyOS的TTS开发实践。


二、核心原理

2.1 TTS技术架构

语音合成的核心流程是:文本输入 → 文本分析 → 语音学转换 → 声学模型生成 → 波形合成 → 音频输出

flowchart TD
    A[文本输入] --> B[文本预处理]
    B --> C[文本规范化]
    C --> D[分词与词性标注]
    D --> E[字音转换 G2P]
    E --> F[韵律预测]
    F --> G{合成方式}
    G -->|拼接合成| H[单元挑选与拼接]
    G -->|参数合成| I[神经网络声码器]
    G -->|端到端| J[VITS/Flow模型]
    H --> K[音频波形输出]
    I --> K
    J --> K
    K --> L[音频播放]
    
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#FFF
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
    
    class A,B,C primary
    class D,E info
    class F warning
    class G,H,I,J purple
    class K,L primary

2.2 三种合成方式对比

合成方式 原理 优势 劣势 HarmonyOS使用
拼接合成 从语音库中挑选单元拼接 音质自然 灵活性差、库大 早期版本
参数合成 神经网络预测声学参数 体积小、灵活 音质略差 离线模式
端到端 VITS/Flow直接生成波形 音质最好、最自然 计算量大 在线模式

2.3 韵律控制要素

TTS的"自然度"取决于韵律控制,核心要素包括:

  • 语速(Speed):每分钟读多少字,通常0.5x-2.0x
  • 音调(Pitch):声音的高低,影响情感表达
  • 音量(Volume):声音大小
  • 停顿(Pause):句子间、段落间的自然停顿
  • 重音(Emphasis):关键词的强调

HarmonyOS通过SpeechSynthesisParams提供了前三个参数的精细控制。


三、代码实战

3.1 基础语音合成——文字朗读

最基础的用法:输入一段文字,让设备朗读出来。

// TTS基础朗读示例
import { textToSpeech } from '@kit.AISpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct BasicTTSPage {
  @State inputText: string = '你好,欢迎来到HarmonyOS语音合成世界!';
  @State isSpeaking: boolean = false;
  @State statusText: string = '就绪';
  
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
  private utteranceId: string = '';

  aboutToAppear(): void {
    this.initTTSEngine();
  }

  // 初始化TTS引擎
  private initTTSEngine(): void {
    try {
      const extraParams: Record<string, Object> = {
        'locate': 'CN',
        'language': 'zh-CN',
      };
      const initParams: textToSpeech.CreateEngineParams = {
        language: 'zh-CN',
        person: 0,       // 0: 女声(默认)
        extraParams: extraParams
      };
      
      this.ttsEngine = textToSpeech.createEngine(initParams);
      this.setupTTSCallbacks();
      console.info('[TTS] 引擎创建成功');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTS] 引擎创建失败: ${err.code} - ${err.message}`);
    }
  }

  // 设置TTS回调
  private setupTTSCallbacks(): void {
    if (!this.ttsEngine) return;

    // 合成开始回调
    this.ttsEngine.on('start', (requestId: string) => {
      this.isSpeaking = true;
      this.statusText = '正在朗读...';
      console.info(`[TTS] 开始朗读: ${requestId}`);
    });

    // 合成完成回调
    this.ttsEngine.on('complete', (requestId: string) => {
      this.isSpeaking = false;
      this.statusText = '朗读完成';
      console.info(`[TTS] 朗读完成: ${requestId}`);
    });

    // 合成中断回调
    this.ttsEngine.on('interrupt', (requestId: string) => {
      this.isSpeaking = false;
      this.statusText = '朗读中断';
      console.info(`[TTS] 朗读中断: ${requestId}`);
    });

    // 错误回调
    this.ttsEngine.on('error', (requestId: string, errorCode: number, errorMessage: string) => {
      this.isSpeaking = false;
      this.statusText = `朗读错误: ${errorMessage}`;
      console.error(`[TTS] 错误: ${errorCode} - ${errorMessage}`);
    });
  }

  // 开始朗读
  private speak(): void {
    if (!this.ttsEngine || !this.inputText.trim()) return;

    try {
      // 配置合成参数
      const speakParams: textToSpeech.SpeakParams = {
        requestId: Date.now().toString(),  // 请求ID,唯一标识
        extraParams: {
          'volume': 2,       // 音量:0-2(2为最大)
          'speed': 1,        // 语速:0.5-2.0(1为正常)
          'pitch': 1,        // 音调:0.5-2.0(1为正常)
          'languageContext': 'zh-CN',
        }
      };
      
      this.utteranceId = speakParams.requestId;
      this.ttsEngine.speak(this.inputText, speakParams);
      console.info('[TTS] 开始朗读');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTS] 朗读失败: ${err.code} - ${err.message}`);
    }
  }

  // 停止朗读
  private stopSpeaking(): void {
    if (!this.ttsEngine) return;
    try {
      this.ttsEngine.stop();
      this.isSpeaking = false;
      this.statusText = '已停止';
    } catch (error) {
      console.error('[TTS] 停止失败');
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('语音合成朗读')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')

      // 状态指示
      Row({ space: 8 }) {
        Circle({ width: 8, height: 8 })
          .fill(this.isSpeaking ? '#4FC3F7' : '#9E9E9E')
        Text(this.statusText)
          .fontSize(14)
          .fontColor(this.isSpeaking ? '#4FC3F7' : '#9E9E9E')
      }

      // 文本输入区
      TextArea({ text: this.inputText, placeholder: '请输入要朗读的文字' })
        .width('90%')
        .height(180)
        .fontSize(16)
        .fontColor('#E0E0E0')
        .backgroundColor('rgba(255,255,255,0.08)')
        .borderRadius(12)
        .onChange((value: string) => {
          this.inputText = value;
        })

      // 控制按钮
      Row({ space: 16 }) {
        Button(this.isSpeaking ? '停止' : '朗读')
          .width(140)
          .height(52)
          .fontSize(18)
          .fontColor('#FFFFFF')
          .backgroundColor(this.isSpeaking ? '#EF5350' : '#4FC3F7')
          .borderRadius(26)
          .onClick(() => {
            if (this.isSpeaking) {
              this.stopSpeaking();
            } else {
              this.speak();
            }
          })

        Button('清空')
          .width(100)
          .height(52)
          .fontSize(16)
          .fontColor('#9E9E9E')
          .backgroundColor('rgba(255,255,255,0.1)')
          .borderRadius(26)
          .onClick(() => {
            this.inputText = '';
          })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
    .justifyContent(FlexAlign.Center)
    .padding({ left: 20, right: 20 })
  }

  aboutToDisappear(): void {
    if (this.ttsEngine) {
      this.ttsEngine.off('start');
      this.ttsEngine.off('complete');
      this.ttsEngine.off('interrupt');
      this.ttsEngine.off('error');
      this.ttsEngine.shutdown();
      this.ttsEngine = null;
    }
  }
}

3.2 多音色与参数调控——语音定制面板

实际应用中,用户往往需要选择不同的音色、调节语速和音调。这个示例展示了完整的参数调控面板。

// TTS多音色与参数调控示例
import { textToSpeech } from '@kit.AISpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 音色选项
interface VoiceOption {
  label: string;
  person: number;
  language: string;
  icon: string;
}

@Entry
@Component
struct VoiceCustomPage {
  @State inputText: string = 'HarmonyOS语音合成,支持多种音色和参数调节,让语音交互更加自然。';
  @State isSpeaking: boolean = false;
  @State speed: number = 1.0;
  @State pitch: number = 1.0;
  @State volume: number = 1.0;
  @State selectedVoice: number = 0;

  // 预设音色列表
  private voiceOptions: VoiceOption[] = [
    { label: '女声-小艺', person: 0, language: 'zh-CN', icon: '👩' },
    { label: '男声-小智', person: 1, language: 'zh-CN', icon: '👨' },
    { label: '女声-温柔', person: 2, language: 'zh-CN', icon: '🌸' },
    { label: '男声-沉稳', person: 3, language: 'zh-CN', icon: '🎙' },
    { label: 'English-Female', person: 0, language: 'en-US', icon: '🇺🇸' },
    { label: '日本語-女性', person: 0, language: 'ja-JP', icon: '🇯🇵' },
  ];

  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;

  aboutToAppear(): void {
    this.initTTSEngine();
  }

  // 初始化TTS引擎
  private initTTSEngine(): void {
    try {
      const voice = this.voiceOptions[this.selectedVoice];
      const extraParams: Record<string, Object> = {
        'locate': voice.language === 'zh-CN' ? 'CN' : 
                  voice.language === 'en-US' ? 'US' : 'JP',
        'language': voice.language,
      };
      const initParams: textToSpeech.CreateEngineParams = {
        language: voice.language,
        person: voice.person,
        extraParams: extraParams
      };
      this.ttsEngine = textToSpeech.createEngine(initParams);
      this.setupCallbacks();
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTS] 初始化失败: ${err.code}`);
    }
  }

  // 切换音色时重建引擎
  private rebuildEngine(voiceIndex: number): void {
    // 先释放旧引擎
    if (this.ttsEngine) {
      this.ttsEngine.off('start');
      this.ttsEngine.off('complete');
      this.ttsEngine.off('interrupt');
      this.ttsEngine.off('error');
      this.ttsEngine.shutdown();
      this.ttsEngine = null;
    }
    
    // 用新音色创建引擎
    this.selectedVoice = voiceIndex;
    this.initTTSEngine();
    console.info(`[TTS] 切换音色: ${this.voiceOptions[voiceIndex].label}`);
  }

  private setupCallbacks(): void {
    if (!this.ttsEngine) return;
    this.ttsEngine.on('start', (requestId: string) => { this.isSpeaking = true; });
    this.ttsEngine.on('complete', (requestId: string) => { this.isSpeaking = false; });
    this.ttsEngine.on('interrupt', (requestId: string) => { this.isSpeaking = false; });
    this.ttsEngine.on('error', (reqId: string, code: number, msg: string) => {
      this.isSpeaking = false;
      console.error(`[TTS] 错误: ${code} - ${msg}`);
    });
  }

  // 朗读
  private speak(): void {
    if (!this.ttsEngine || !this.inputText.trim()) return;
    try {
      const speakParams: textToSpeech.SpeakParams = {
        requestId: Date.now().toString(),
        extraParams: {
          'volume': this.volume,
          'speed': this.speed,
          'pitch': this.pitch,
          'languageContext': this.voiceOptions[this.selectedVoice].language,
        }
      };
      this.ttsEngine.speak(this.inputText, speakParams);
    } catch (error) {
      console.error('[TTS] 朗读失败');
    }
  }

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Text('语音定制面板')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')

        // 文本输入
        TextArea({ text: this.inputText, placeholder: '输入要朗读的文字' })
          .width('90%')
          .height(120)
          .fontSize(16)
          .fontColor('#E0E0E0')
          .backgroundColor('rgba(255,255,255,0.08)')
          .borderRadius(12)
          .onChange((v: string) => { this.inputText = v; })

        // 音色选择
        Text('选择音色')
          .fontSize(16)
          .fontColor('#9E9E9E')
          .width('90%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.voiceOptions, (voice: VoiceOption, index: number) => {
            Column() {
              Text(voice.icon)
                .fontSize(28)
              Text(voice.label)
                .fontSize(12)
                .fontColor(this.selectedVoice === index ? '#4FC3F7' : '#9E9E9E')
                .margin({ top: 4 })
            }
            .width('28%')
            .height(80)
            .margin('2%')
            .borderRadius(12)
            .backgroundColor(this.selectedVoice === index ? 'rgba(79,195,247,0.15)' : 'rgba(255,255,255,0.05)')
            .border({
              width: this.selectedVoice === index ? 2 : 1,
              color: this.selectedVoice === index ? '#4FC3F7' : 'rgba(255,255,255,0.1)'
            })
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              this.rebuildEngine(index);
            })
          }, (voice: VoiceOption, index: number) => `${index}`)
        }
        .width('90%')

        // 语速调节
        Row({ space: 12 }) {
          Text('语速')
            .fontSize(14)
            .fontColor('#9E9E9E')
            .width(40)
          Slider({
            value: this.speed,
            min: 0.5,
            max: 2.0,
            step: 0.1,
            style: SliderStyle.InSet
          })
            .width('60%')
            .trackColor('rgba(255,255,255,0.1)')
            .selectedColor('#4FC3F7')
            .onChange((value: number) => { this.speed = value; })
          Text(`${this.speed.toFixed(1)}x`)
            .fontSize(14)
            .fontColor('#4FC3F7')
            .width(50)
            .textAlign(TextAlign.End)
        }
        .width('90%')

        // 音调调节
        Row({ space: 12 }) {
          Text('音调')
            .fontSize(14)
            .fontColor('#9E9E9E')
            .width(40)
          Slider({
            value: this.pitch,
            min: 0.5,
            max: 2.0,
            step: 0.1,
            style: SliderStyle.InSet
          })
            .width('60%')
            .trackColor('rgba(255,255,255,0.1)')
            .selectedColor('#CE93D8')
            .onChange((value: number) => { this.pitch = value; })
          Text(`${this.pitch.toFixed(1)}`)
            .fontSize(14)
            .fontColor('#CE93D8')
            .width(50)
            .textAlign(TextAlign.End)
        }
        .width('90%')

        // 音量调节
        Row({ space: 12 }) {
          Text('音量')
            .fontSize(14)
            .fontColor('#9E9E9E')
            .width(40)
          Slider({
            value: this.volume,
            min: 0,
            max: 2,
            step: 0.1,
            style: SliderStyle.InSet
          })
            .width('60%')
            .trackColor('rgba(255,255,255,0.1)')
            .selectedColor('#81C784')
            .onChange((value: number) => { this.volume = value; })
          Text(`${this.volume.toFixed(1)}`)
            .fontSize(14)
            .fontColor('#81C784')
            .width(50)
            .textAlign(TextAlign.End)
        }
        .width('90%')

        // 朗读按钮
        Button(this.isSpeaking ? '停止朗读' : '开始朗读')
          .width('80%')
          .height(56)
          .fontSize(18)
          .fontColor('#FFFFFF')
          .backgroundColor(this.isSpeaking ? '#EF5350' : '#4FC3F7')
          .borderRadius(28)
          .onClick(() => {
            if (this.isSpeaking) {
              this.ttsEngine?.stop();
              this.isSpeaking = false;
            } else {
              this.speak();
            }
          })
      }
      .padding({ top: 40, bottom: 40 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
  }

  aboutToDisappear(): void {
    if (this.ttsEngine) {
      this.ttsEngine.off('start');
      this.ttsEngine.off('complete');
      this.ttsEngine.off('interrupt');
      this.ttsEngine.off('error');
      this.ttsEngine.shutdown();
      this.ttsEngine = null;
    }
  }
}

3.3 SSML高级控制——精确朗读控制

SSML(Speech Synthesis Markup Language)是语音合成标记语言,可以精确控制发音、停顿、情感等。HarmonyOS支持SSML输入,让你对朗读效果有像素级的掌控。

// SSML高级朗读控制示例
import { textToSpeech } from '@kit.AISpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct SSMLTTSPage {
  @State isSpeaking: boolean = false;
  @State currentDemo: string = '';
  
  // SSML示例集合
  private ssmlDemos: Record<string, string> = {
    // 基础停顿控制
    'pause': `<speak>
      今天天气不错<break time="500ms"/>适合出门散步。
      <break time="1000ms"/>
      不过明天可能会下雨<break time="300ms"/>记得带伞。
    </speak>`,
    
    // 发音控制——多音字
    'pronunciation': `<speak>
      这条<phoneme alphabet="pinyin" ph="hang2">行</phoneme>走路线,
      银行<phoneme alphabet="pinyin" ph="hang2">行</phoneme>长
      表示<phoneme alphabet="pinyin" ph="xing2">行</phoneme>。
    </speak>`,
    
    // 强调与情感
    'emphasis': `<speak>
      这道题<emphasis level="strong">必须</emphasis>要做对!
      我<emphasis level="moderate">真的</emphasis>很开心。
    </speak>`,
    
    // 数字与日期朗读
    'number': `<speak>
      电话号码是<say-as interpret-as="telephone">13800138000</say-as>。
      今天的日期是<say-as interpret-as="date" format="ymd">2025年6月20日</say-as>。
      金额为<say-as interpret-as="currency">¥1234.56</say-as>。
    </speak>`,
    
    // 语速局部调整
    'prosody': `<speak>
      正常语速朗读这段话。
      <prosody rate="fast">这段话语速加快。</prosody>
      <prosody rate="slow">这段话语速放慢。</prosody>
      <prosody pitch="high">这段话音调升高。</prosody>
    </speak>`,
  };

  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;

  aboutToAppear(): void {
    this.initSSMLEngine();
  }

  private initSSMLEngine(): void {
    try {
      const extraParams: Record<string, Object> = {
        'locate': 'CN',
        'language': 'zh-CN',
        'format': 'ssml',  // 关键:指定输入格式为SSML
      };
      const initParams: textToSpeech.CreateEngineParams = {
        language: 'zh-CN',
        person: 0,
        extraParams: extraParams
      };
      this.ttsEngine = textToSpeech.createEngine(initParams);
      this.setupCallbacks();
      console.info('[SSML-TTS] 引擎初始化成功');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[SSML-TTS] 初始化失败: ${err.code}`);
    }
  }

  private setupCallbacks(): void {
    if (!this.ttsEngine) return;
    this.ttsEngine.on('start', () => { this.isSpeaking = true; });
    this.ttsEngine.on('complete', () => { this.isSpeaking = false; });
    this.ttsEngine.on('interrupt', () => { this.isSpeaking = false; });
    this.ttsEngine.on('error', (reqId: string, code: number, msg: string) => {
      this.isSpeaking = false;
      console.error(`[SSML-TTS] 错误: ${code} - ${msg}`);
    });
  }

  // 使用SSML朗读
  private speakSSML(ssmlText: string, demoName: string): void {
    if (!this.ttsEngine) return;
    this.currentDemo = demoName;
    
    try {
      const speakParams: textToSpeech.SpeakParams = {
        requestId: Date.now().toString(),
        extraParams: {
          'volume': 1,
          'speed': 1,
          'pitch': 1,
        }
      };
      this.ttsEngine.speak(ssmlText, speakParams);
      console.info(`[SSML-TTS] 播放: ${demoName}`);
    } catch (error) {
      console.error('[SSML-TTS] 播放失败');
    }
  }

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Text('SSML高级朗读')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')

        Text('通过SSML标记语言精确控制语音合成的每一个细节')
          .fontSize(14)
          .fontColor('#9E9E9E')
          .width('90%')

        // SSML示例卡片
        ForEach(Object.keys(this.ssmlDemos), (key: string) => {
          Column({ space: 12 }) {
            Row({ space: 8 }) {
              Text(this.getDemoTitle(key))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E0E0E0')
                .layoutWeight(1)
              
              if (this.currentDemo === key && this.isSpeaking) {
                LoadingProgress()
                  .width(20)
                  .height(20)
                  .color('#4FC3F7')
              }
            }

            // SSML源码展示
            Scroll() {
              Text(this.ssmlDemos[key])
                .fontSize(12)
                .fontColor('#9E9E9E')
                .fontFamily('monospace')
            }
            .width('100%')
            .maxHeight(80)

            // 播放按钮
            Button(this.currentDemo === key && this.isSpeaking ? '停止' : '播放示例')
              .width('100%')
              .height(40)
              .fontSize(14)
              .fontColor('#FFFFFF')
              .backgroundColor(this.currentDemo === key && this.isSpeaking ? '#EF5350' : '#4FC3F7')
              .borderRadius(20)
              .onClick(() => {
                if (this.currentDemo === key && this.isSpeaking) {
                  this.ttsEngine?.stop();
                  this.isSpeaking = false;
                } else {
                  this.speakSSML(this.ssmlDemos[key], key);
                }
              })
          }
          .width('90%')
          .padding(16)
          .borderRadius(16)
          .backgroundColor('rgba(255,255,255,0.08)')
          .backdropBlur(20)
        }, (key: string) => key)

        // 停止全部
        if (this.isSpeaking) {
          Button('停止所有朗读')
            .width('80%')
            .height(48)
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('#EF5350')
            .borderRadius(24)
            .onClick(() => {
              this.ttsEngine?.stop();
              this.isSpeaking = false;
              this.currentDemo = '';
            })
        }
      }
      .padding({ top: 40, bottom: 40 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
  }

  // 获取示例标题
  private getDemoTitle(key: string): string {
    const titles: Record<string, string> = {
      'pause': '⏸ 停顿控制',
      'pronunciation': '🔤 多音字发音',
      'emphasis': '💪 强调与情感',
      'number': '🔢 数字与日期',
      'prosody': '🎵 韵律局部调整',
    };
    return titles[key] || key;
  }

  aboutToDisappear(): void {
    if (this.ttsEngine) {
      this.ttsEngine.off('start');
      this.ttsEngine.off('complete');
      this.ttsEngine.off('interrupt');
      this.ttsEngine.off('error');
      this.ttsEngine.shutdown();
      this.ttsEngine = null;
    }
  }
}

四、踩坑与注意事项

4.1 引擎创建与音色切换

最大踩坑点:切换音色(person)或语言时,必须销毁旧引擎重新创建,不能在运行时切换!

// ❌ 错误:直接修改person参数不会生效
this.ttsEngine.speak(text, { person: 1 }); // person参数在speak中无效

// ✅ 正确:销毁旧引擎,用新person创建
this.ttsEngine.shutdown();
this.ttsEngine = textToSpeech.createEngine({ language: 'zh-CN', person: 1 });

4.2 文本长度限制

TTS对单次合成的文本长度有限制:

模式 最大文本长度 说明
在线合成 500字符 超过需分段
离线合成 1000字符 端侧模型容量更大

长文本分段策略

// 长文本分段朗读
private speakLongText(fullText: string): void {
  const MAX_LENGTH = 500;
  const segments: string[] = [];
  
  // 按句子分段(优先按句号、问号、感叹号分割)
  const sentences = fullText.match(/[^。!?.!?]+[。!?.!?]?/g) || [fullText];
  
  let currentSegment = '';
  for (const sentence of sentences) {
    if ((currentSegment + sentence).length > MAX_LENGTH) {
      if (currentSegment) segments.push(currentSegment);
      currentSegment = sentence;
    } else {
      currentSegment += sentence;
    }
  }
  if (currentSegment) segments.push(currentSegment);
  
  // 依次朗读每段
  this.speakSegments(segments, 0);
}

private speakSegments(segments: string[], index: number): void {
  if (index >= segments.length) return;
  
  const speakParams: textToSpeech.SpeakParams = {
    requestId: `seg_${index}_${Date.now()}`,
    extraParams: { 'volume': 1, 'speed': 1, 'pitch': 1 }
  };
  
  // 在complete回调中朗读下一段
  const originalCallback = this.ttsEngine?.on('complete', () => {
    this.speakSegments(segments, index + 1);
  });
  
  this.ttsEngine?.speak(segments[index], speakParams);
}

4.3 音频焦点管理

TTS播放会占用音频焦点,与其他音频(音乐、视频)冲突时需要处理:

// 监听音频中断事件
import { audio } from '@kit.AudioKit';

// 当其他应用抢占音频焦点时,TTS会收到interrupt回调
// 此时应该暂停或降低音量,而不是直接停止
this.ttsEngine.on('interrupt', (requestId: string) => {
  // 音频被中断,可能是来电、闹钟等
  this.isSpeaking = false;
  // 可以记录当前位置,等中断结束后恢复
});

4.4 离线TTS模型

离线TTS需要设备预装语音模型,不同设备支持情况不同:

// 检查离线TTS是否可用
private async checkOfflineTTS(): Promise<boolean> {
  try {
    const engine = textToSpeech.createEngine({
      language: 'zh-CN',
      person: 0,
      extraParams: { 'locate': 'CN', 'language': 'zh-CN', 'mode': 'offline' }
    });
    engine.shutdown();
    return true;
  } catch {
    return false;
  }
}

4.5 speak()的requestId必须唯一

每次调用speak()时,requestId必须不同,否则会被系统忽略:

// ❌ 错误:重复使用同一个requestId
const params = { requestId: 'fixed_id', ... };
this.ttsEngine.speak(text1, params); // 第一次OK
this.ttsEngine.speak(text2, params); // 第二次可能被忽略!

// ✅ 正确:每次使用唯一的requestId
this.ttsEngine.speak(text1, { requestId: `tts_${Date.now()}_1`, ... });
this.ttsEngine.speak(text2, { requestId: `tts_${Date.now()}_2`, ... });

五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
音色数量 4种内置 8种内置+自定义音色克隆
SSML标签 基础支持 新增<voice>切换音色、<lang>切换语言
离线模型 部分设备 全线设备预装,音质提升30%
情感合成 不支持 新增4种情感维度(开心/悲伤/愤怒/平静)

5.2 情感合成(HarmonyOS 6新增)

// HarmonyOS 6情感合成示例
const ssmlWithEmotion = `<speak>
  <emotion type="happy">太好了!我们终于完成了这个项目!</emotion>
  <emotion type="sad">很遗憾,这次没有通过审核。</emotion>
  <emotion type="angry">这已经是第三次犯同样的错误了!</emotion>
  <emotion type="neutral">今天的会议在下午三点。</emotion>
</speak>`;

this.ttsEngine.speak(ssmlWithEmotion, {
  requestId: Date.now().toString(),
  extraParams: { 'format': 'ssml' }
});

5.3 自定义音色(HarmonyOS 6预览)

HarmonyOS 6计划支持音色克隆功能,用户录制几分钟语音即可生成专属音色:

// HarmonyOS 6自定义音色(预览API,可能调整)
const customVoiceParams: Record<string, Object> = {
  'voiceId': 'custom_voice_001',  // 自定义音色ID
  'cloneMode': 'few-shot',        // 少样本克隆
};

六、总结

mindmap
  root((HarmonyOS TTS))
    引擎管理
      创建 createEngine
      销毁 shutdown
      音色切换需重建
    合成参数
      语速 speed 0.5-2.0
      音调 pitch 0.5-2.0
      音量 volume 0-2
      音色 person 0-3
    回调监听
      on start 开始
      on complete 完成
      on interrupt 中断
      on error 错误
    SSML控制
      break 停顿
      phoneme 发音
      emphasis 强调
      say-as 数字日期
      prosody 韵律
    踩坑要点
      音色切换需重建引擎
      文本长度限制500字符
      requestId必须唯一
      音频焦点管理
      离线模型可用性检查
    HarmonyOS 6
      8种内置音色
      情感合成
      自定义音色克隆
      SSML增强标签
    
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#FFF
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
知识点 关键内容
引擎创建 textToSpeech.createEngine(),指定language、person、extraParams
参数调控 speed/pitch/volume精细控制,Slider组件联动
多音色 切换person需重建引擎,不能运行时切换
SSML break停顿、phoneme发音、emphasis强调、say-as数字、prosody韵律
长文本 超过500字符需分段,按句号分割后依次合成
生命周期 requestId唯一、回调先注册、页面销毁时shutdown
HarmonyOS 6 情感合成、8种音色、自定义音色克隆、SSML增强

语音合成是语音智能的"嘴巴",让应用能"说话"。上一篇我们学会了让应用"听",这一篇学会了让应用"说"。下一篇我们将探索语音唤醒——让应用在"睡梦中"也能听到你的呼唤。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。