HarmonyOS开发:语音合成TTS与多语言朗读
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增强 |
语音合成是语音智能的"嘴巴",让应用能"说话"。上一篇我们学会了让应用"听",这一篇学会了让应用"说"。下一篇我们将探索语音唤醒——让应用在"睡梦中"也能听到你的呼唤。
- 点赞
- 收藏
- 关注作者
评论(0)