HarmonyOS开发:导航语音播报与TTS集成
HarmonyOS开发:导航语音播报与TTS集成
核心要点:本文深入讲解HarmonyOS导航系统中语音播报的完整实现,涵盖TTS(Text-to-Speech)引擎集成、导航指令文本生成、语音播报优先级管理、音频焦点控制、多语言支持等核心模块,并提供可运行的ArkTS代码示例。
| 项目 | 说明 |
|---|
| 开发语言 | ArkTS |
| 核心框架 | ArkUI / @kit.AI (TTS) / @kit.AudioKit |
| 相关文档 | TTS开发指南 / 音频管理开发 |
一、背景与动机
语音播报是导航体验的核心交互方式——驾驶场景下用户无法频繁查看屏幕,语音指令是唯一安全的信息获取渠道。一个优秀的导航语音系统需要满足:
- 及时性:在正确时机播报,不早不晚
- 清晰性:指令表述简洁明了,一次听懂
- 自然性:语音合成自然流畅,不机械
- 优先级:紧急指令(偏航、测速)优先于常规播报
- 音频焦点:与音乐、电话等音频源和谐共存
- 多语言:支持中文、英文等多语言播报
在HarmonyOS生态中,TTS能力由@kit.AI提供,音频管理由@kit.AudioKit提供,两者配合可实现完整的导航语音系统。
二、核心原理
2.1 导航语音播报架构
flowchart TB
subgraph 指令生成层
A[导航引擎<br/>路段切换事件] --> B[指令文本生成器]
C[偏航检测<br/>偏航事件] --> B
D[路况监控<br/>拥堵预警] --> B
E[测速提醒<br/>超速事件] --> B
end
subgraph 播报管理层
B --> F[播报优先级队列]
F --> G{当前是否有<br/>更高优先级播报?}
G -->|是| H[等待/打断当前播报]
G -->|否| I[提交TTS合成]
H --> I
end
subgraph TTS引擎层
I --> J[TTS引擎初始化]
J --> K[文本分析/分词]
K --> L[语音合成]
L --> M[音频流输出]
end
subgraph 音频输出层
M --> N[音频焦点申请]
N --> O{焦点获取成功?}
O -->|是| P[混音/压低音乐<br/>播报语音]
O -->|否| Q[等待焦点释放]
Q --> N
end
classDef genStyle fill:#1a1a2e,stroke:#e94560,color:#fff
classDef mgmtStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0
classDef ttsStyle fill:#0f3460,stroke:#e94560,color:#fff
classDef audioStyle fill:#533483,stroke:#e94560,color:#fff
class A,B,C,D,E genStyle
class F,G,H mgmtStyle
class I,J,K,L ttsStyle
class M,N,O,P,Q audioStyle
2.2 播报优先级体系
导航语音存在多种播报场景,需建立优先级体系避免冲突:
| 优先级 | 类型 | 示例 | 打断策略 |
|---|---|---|---|
| P0-紧急 | 安全警告 | “您已严重超速!” | 立即打断当前播报 |
| P1-高 | 偏航/重规划 | “您已偏离路线,正在重新规划” | 打断当前播报 |
| P2-中 | 转向指令 | “前方500米右转进入长安街” | 等待当前播报完成 |
| P3-低 | 路况提示 | “前方2公里拥堵,预计通过需8分钟” | 等待当前播报完成,超时则丢弃 |
| P4-信息 | 兴趣点 | “右侧有加油站” | 等待,超时丢弃 |
2.3 指令文本生成规则
导航指令文本的质量直接影响用户理解效率:
flowchart LR
A[原始指令数据] --> B[距离格式化]
B --> C[道路名称处理]
C --> D[方向描述生成]
D --> E[语气词/连接词添加]
E --> F[最终播报文本]
classDef inputStyle fill:#1a1a2e,stroke:#e94560,color:#fff
classDef processStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0
classDef outputStyle fill:#533483,stroke:#e94560,color:#fff
class A inputStyle
class B,C,D,E processStyle
class F outputStyle
距离格式化规则:
| 距离范围 | 播报格式 | 示例 |
|---|---|---|
| < 100米 | “前方X米” | “前方50米” |
| 100-500米 | “前方约X百米” | “前方约3百米” |
| 500-1000米 | “前方X百米” | “前方8百米” |
| 1-5公里 | “前方X公里” | “前方2公里” |
| > 5公里 | “继续行驶X公里后” | “继续行驶8公里后” |
三、代码实战
3.1 TTS引擎封装
// NavigationTTSEngine.ets - 导航TTS引擎封装
import { textToSpeech } from '@kit.AISpeechKit';
import { audio } from '@kit.AudioKit';
/** TTS引擎状态 */
export enum TTSState {
IDLE = 'idle',
INITIALIZING = 'initializing',
READY = 'ready',
SPEAKING = 'speaking',
PAUSED = 'paused',
ERROR = 'error'
}
/** TTS配置 */
interface TTSConfig {
language: string; // 语言代码,如 'zh-CN'
person: number; // 发音人编号
speed: number; // 语速(0.5-2.0),默认1.0
pitch: number; // 音调(0.5-2.0),默认1.0
volume: number; // 音量(0-1.0),默认1.0
audioType: audio.AudioVolumeType; // 音频流类型
}
/** TTS事件回调 */
interface TTSCallbacks {
onStateChange?: (state: TTSState) => void;
onStart?: (utteranceId: string) => void;
onComplete?: (utteranceId: string) => void;
onError?: (utteranceId: string, errorCode: number) => void;
}
@ObservedV2
export class NavigationTTSEngine {
@Trace state: TTSState = TTSState.IDLE;
@Trace currentUtteranceId: string = '';
private ttsEngine: textToSpeech.TextToSpeech | null = null;
private callbacks: TTSCallbacks = {};
private config: TTSConfig;
private utteranceCounter: number = 0;
static readonly DEFAULT_CONFIG: TTSConfig = {
language: 'zh-CN',
person: 0,
speed: 1.0,
pitch: 1.0,
volume: 1.0,
audioType: audio.AudioVolumeType.MEDIA
};
constructor(config?: Partial<TTSConfig>) {
this.config = { ...NavigationTTSEngine.DEFAULT_CONFIG, ...config };
}
/** 设置回调 */
setCallbacks(callbacks: TTSCallbacks): void {
this.callbacks = callbacks;
}
/** 初始化TTS引擎 */
async initialize(): Promise<void> {
if (this.state === TTSState.READY || this.state === TTSState.INITIALIZING) {
return;
}
this.setState(TTSState.INITIALIZING);
try {
// 创建TTS引擎
const engineConfig: textToSpeech.CreateEngineParams = {
language: this.config.language,
person: this.config.person,
online: 1 // 1=在线模式(更自然),0=离线模式
};
this.ttsEngine = textToSpeech.createEngine(engineConfig);
// 注册回调
this.ttsEngine.on('start', (utteranceId: string) => {
this.setState(TTSState.SPEAKING);
this.currentUtteranceId = utteranceId;
this.callbacks.onStart?.(utteranceId);
});
this.ttsEngine.on('complete', (utteranceId: string) => {
this.setState(TTSState.READY);
this.currentUtteranceId = '';
this.callbacks.onComplete?.(utteranceId);
});
this.ttsEngine.on('error', (utteranceId: string, errorCode: number) => {
console.error(`[TTS] 播报错误: utteranceId=${utteranceId}, code=${errorCode}`);
this.setState(TTSState.ERROR);
this.callbacks.onError?.(utteranceId, errorCode);
});
this.setState(TTSState.READY);
console.info('[TTS] 引擎初始化成功');
} catch (error) {
console.error(`[TTS] 初始化失败: ${JSON.stringify(error)}`);
this.setState(TTSState.ERROR);
}
}
/** 合成并播报文本 */
speak(text: string, priority: SpeakPriority = SpeakPriority.NORMAL): string {
if (!this.ttsEngine || this.state === TTSState.INITIALIZING) {
console.warn('[TTS] 引擎未就绪');
return '';
}
const utteranceId = `nav_${++this.utteranceCounter}`;
const speakParams: textToSpeech.SpeakParams = {
requestId: utteranceId,
speed: this.config.speed,
pitch: this.config.pitch,
volume: this.config.volume,
// 导航场景使用STREAM_MUSIC类型,可被音频焦点管理
audioType: this.config.audioType
};
try {
// 如果当前正在播报,根据优先级决定是否打断
if (this.state === TTSState.SPEAKING) {
if (priority >= SpeakPriority.HIGH) {
this.ttsEngine.stop(); // 打断当前播报
} else {
console.info(`[TTS] 当前播报中,低优先级指令排队: ${text}`);
return utteranceId; // 排队等待
}
}
this.ttsEngine.speak(text, speakParams);
return utteranceId;
} catch (error) {
console.error(`[TTS] 播报失败: ${JSON.stringify(error)}`);
return '';
}
}
/** 停止当前播报 */
stop(): void {
if (this.ttsEngine && this.state === TTSState.SPEAKING) {
try {
this.ttsEngine.stop();
this.setState(TTSState.READY);
} catch (error) {
console.error(`[TTS] 停止失败: ${JSON.stringify(error)}`);
}
}
}
/** 释放引擎资源 */
shutdown(): void {
if (this.ttsEngine) {
try {
this.ttsEngine.off('start');
this.ttsEngine.off('complete');
this.ttsEngine.off('error');
this.ttsEngine.shutdown();
} catch (error) {
// 忽略释放错误
}
this.ttsEngine = null;
}
this.setState(TTSState.IDLE);
}
/** 更新语速 */
setSpeed(speed: number): void {
this.config.speed = Math.max(0.5, Math.min(2.0, speed));
}
/** 更新语言 */
setLanguage(language: string): void {
this.config.language = language;
// 语言变更需重新初始化引擎
this.shutdown();
this.initialize();
}
private setState(state: TTSState): void {
const oldState = this.state;
this.state = state;
if (oldState !== state) {
this.callbacks.onStateChange?.(state);
}
}
}
/** 播报优先级 */
export enum SpeakPriority {
LOW = 0, // P4-信息
NORMAL = 1, // P2/P3-常规
HIGH = 2, // P1-偏航/重规划
URGENT = 3 // P0-安全警告
}
3.2 导航指令文本生成器
// NavInstructionGenerator.ets - 导航指令文本生成器
import { ManeuverType, NavInstruction } from './NavDataModels';
/** 播报时机 */
export enum SpeakTiming {
EARLY = 'early', // 远距离预告(1-2公里)
APPROACHING = 'approaching', // 接近预告(300-500米)
IMMEDIATE = 'immediate', // 即时指令(50-100米)
CONFIRM = 'confirm' // 确认指令(转向后)
}
/** 播报内容 */
export interface SpeakContent {
text: string; // 播报文本
priority: SpeakPriority; // 优先级
timing: SpeakTiming; // 播报时机
category: SpeakCategory; // 播报类别
}
/** 播报类别 */
export enum SpeakCategory {
MANEUVER = 'maneuver', // 转向指令
OFF_ROUTE = 'off-route', // 偏航
REROUTE = 'reroute', // 重规划
SPEED_ALERT = 'speed-alert', // 超速提醒
TRAFFIC = 'traffic', // 路况提示
POI = 'poi', // 兴趣点
ARRIVE = 'arrive' // 到达
}
/** 语言资源(支持i18n扩展) */
interface LanguageResources {
maneuverPrefix: Record<ManeuverType, string>;
distanceFormats: {
meters: string;
hundreds: string;
kilometers: string;
continueKm: string;
};
connectors: {
then: string;
and: string;
into: string;
};
specialPhrases: {
offRoute: string;
rerouting: string;
rerouteComplete: string;
speedAlert: string;
arrived: string;
viaPoint: string;
};
}
export class NavInstructionGenerator {
private lang: LanguageResources;
private currentLanguage: string = 'zh-CN';
// 中文语言资源
private static readonly ZH_RESOURCES: LanguageResources = {
maneuverPrefix: {
[ManeuverType.DEPART]: '出发',
[ManeuverType.TURN_LEFT]: '左转',
[ManeuverType.TURN_RIGHT]: '右转',
[ManeuverType.SLIGHT_LEFT]: '稍向左转',
[ManeuverType.SLIGHT_RIGHT]: '稍向右转',
[ManeuverType.SHARP_LEFT]: '急左转',
[ManeuverType.SHARP_RIGHT]: '急右转',
[ManeuverType.STRAIGHT]: '直行',
[ManeuverType.U_TURN]: '掉头',
[ManeuverType.ROUNDABOUT]: '进入环岛',
[ManeuverType.ARRIVE]: '到达目的地',
[ManeuverType.FERRY]: '乘坐渡轮'
},
distanceFormats: {
meters: '前方{distance}米',
hundreds: '前方约{distance}百米',
kilometers: '前方{distance}公里',
continueKm: '继续行驶{distance}公里后'
},
connectors: {
then: ',然后',
and: ',',
into: '进入'
},
specialPhrases: {
offRoute: '您已偏离路线',
rerouting: '正在为您重新规划路线',
rerouteComplete: '已为您重新规划路线,预计{time}分钟到达',
speedAlert: '您已超速,当前限速{limit}公里每小时',
arrived: '您已到达目的地,导航结束',
viaPoint: '您已到达途经点'
}
};
// 英文语言资源
private static readonly EN_RESOURCES: LanguageResources = {
maneuverPrefix: {
[ManeuverType.DEPART]: 'Depart',
[ManeuverType.TURN_LEFT]: 'Turn left',
[ManeuverType.TURN_RIGHT]: 'Turn right',
[ManeuverType.SLIGHT_LEFT]: 'Slight left',
[ManeuverType.SLIGHT_RIGHT]: 'Slight right',
[ManeuverType.SHARP_LEFT]: 'Sharp left',
[ManeuverType.SHARP_RIGHT]: 'Sharp right',
[ManeuverType.STRAIGHT]: 'Continue straight',
[ManeuverType.U_TURN]: 'Make a U-turn',
[ManeuverType.ROUNDABOUT]: 'Enter the roundabout',
[ManeuverType.ARRIVE]: 'You have arrived',
[ManeuverType.FERRY]: 'Take the ferry'
},
distanceFormats: {
meters: 'In {distance} meters',
hundreds: 'In about {distance} hundred meters',
kilometers: 'In {distance} kilometers',
continueKm: 'Continue for {distance} kilometers, then'
},
connectors: {
then: ', then ',
and: ', ',
into: 'onto'
},
specialPhrases: {
offRoute: 'You are off route',
rerouting: 'Recalculating route',
rerouteComplete: 'New route calculated, ETA {time} minutes',
speedAlert: 'Speed limit exceeded, current limit {limit} km/h',
arrived: 'You have arrived at your destination',
viaPoint: 'You have reached the waypoint'
}
};
constructor(language: string = 'zh-CN') {
this.currentLanguage = language;
this.lang = language === 'en-US'
? NavInstructionGenerator.EN_RESOURCES
: NavInstructionGenerator.ZH_RESOURCES;
}
/** 生成转向指令播报 */
generateManeuverSpeak(
instruction: NavInstruction,
distance: number,
nextInstruction?: NavInstruction,
timing: SpeakTiming = SpeakTiming.APPROACHING
): SpeakContent {
// 1. 格式化距离
const distanceText = this.formatDistance(distance, timing);
// 2. 获取转向描述
const maneuverText = this.lang.maneuverPrefix[instruction.maneuver] ?? '直行';
// 3. 组合道路名称
let fullText = '';
if (instruction.roadName) {
if (this.currentLanguage === 'zh-CN') {
fullText = `${distanceText}${maneuverText},${this.lang.connectors.into}${instruction.roadName}`;
} else {
fullText = `${distanceText}, ${maneuverText} ${this.lang.connectors.into} ${instruction.roadName}`;
}
} else {
fullText = `${distanceText}${maneuverText}`;
}
// 4. 附加下一指令预告(如果距离很近)
if (nextInstruction && distance < 200) {
const nextManeuver = this.lang.maneuverPrefix[nextInstruction.maneuver] ?? '';
if (nextManeuver && nextInstruction.maneuver !== ManeuverType.STRAIGHT) {
fullText += `${this.lang.connectors.then}${nextManeuver}`;
}
}
// 5. 环岛特殊处理
if (instruction.maneuver === ManeuverType.ROUNDABOUT) {
// 环岛需要提示出口编号(如果有)
fullText = `${distanceText}${maneuverText}`;
}
return {
text: fullText,
priority: timing === SpeakTiming.IMMEDIATE ? SpeakPriority.NORMAL : SpeakPriority.LOW,
timing,
category: SpeakCategory.MANEUVER
};
}
/** 生成偏航播报 */
generateOffRouteSpeak(): SpeakContent {
return {
text: `${this.lang.specialPhrases.offRoute},${this.lang.specialPhrases.rerouting}`,
priority: SpeakPriority.HIGH,
timing: SpeakTiming.IMMEDIATE,
category: SpeakCategory.OFF_ROUTE
};
}
/** 生成重规划完成播报 */
generateRerouteCompleteSpeak(etaMinutes: number): SpeakContent {
const text = this.lang.specialPhrases.rerouteComplete
.replace('{time}', String(Math.round(etaMinutes)));
return {
text,
priority: SpeakPriority.HIGH,
timing: SpeakTiming.IMMEDIATE,
category: SpeakCategory.REROUTE
};
}
/** 生成超速提醒播报 */
generateSpeedAlertSpeak(speedLimit: number): SpeakContent {
const text = this.lang.specialPhrases.speedAlert
.replace('{limit}', String(speedLimit));
return {
text,
priority: SpeakPriority.URGENT,
timing: SpeakTiming.IMMEDIATE,
category: SpeakCategory.SPEED_ALERT
};
}
/** 生成到达播报 */
generateArriveSpeak(): SpeakContent {
return {
text: this.lang.specialPhrases.arrived,
priority: SpeakPriority.HIGH,
timing: SpeakTiming.IMMEDIATE,
category: SpeakCategory.ARRIVE
};
}
/** 格式化距离 */
private formatDistance(distance: number, timing: SpeakTiming): string {
if (distance < 100) {
return this.lang.distanceFormats.meters.replace('{distance}', String(Math.round(distance)));
} else if (distance < 1000) {
const hundreds = Math.round(distance / 100);
return this.lang.distanceFormats.hundreds.replace('{distance}', String(hundreds));
} else if (distance < 5000) {
const km = (distance / 1000).toFixed(1);
return this.lang.distanceFormats.kilometers.replace('{distance}', km);
} else {
const km = Math.round(distance / 1000);
return this.lang.distanceFormats.continueKm.replace('{distance}', String(km));
}
}
}
3.3 语音播报调度器
// VoiceAnnouncementScheduler.ets - 语音播报调度器
import { NavigationTTSEngine, SpeakPriority, TTSState } from './NavigationTTSEngine';
import { NavInstructionGenerator, SpeakContent, SpeakTiming, SpeakCategory } from './NavInstructionGenerator';
import { NavInstruction, ManeuverType } from './NavDataModels';
/** 播报队列项 */
interface QueueItem {
content: SpeakContent;
enqueueTime: number; // 入队时间
expiryTime: number; // 过期时间(超时丢弃)
}
/** 调度器配置 */
interface SchedulerConfig {
maxQueueSize: number; // 最大队列长度
defaultExpiryMs: number; // 默认过期时间(毫秒)
maneuverExpiryMs: number; // 转向指令过期时间
minIntervalMs: number; // 两次播报最小间隔
}
@ObservedV2
export class VoiceAnnouncementScheduler {
@Trace isSpeaking: boolean = false;
@Trace lastSpokenText: string = '';
@Trace queueSize: number = 0;
private ttsEngine: NavigationTTSEngine;
private generator: NavInstructionGenerator;
private queue: QueueItem[] = [];
private config: SchedulerConfig;
private lastSpeakTime: number = 0;
// 已播报的指令ID集合(防止重复播报)
private spokenInstructionIds: Set<string> = new Set();
static readonly DEFAULT_CONFIG: SchedulerConfig = {
maxQueueSize: 10,
defaultExpiryMs: 30000, // 30秒
maneuverExpiryMs: 15000, // 15秒
minIntervalMs: 2000 // 2秒
};
constructor(
ttsEngine: NavigationTTSEngine,
generator: NavInstructionGenerator,
config?: Partial<SchedulerConfig>
) {
this.ttsEngine = ttsEngine;
this.generator = generator;
this.config = { ...VoiceAnnouncementScheduler.DEFAULT_CONFIG, ...config };
// 监听TTS状态
this.ttsEngine.setCallbacks({
onComplete: () => {
this.isSpeaking = false;
this.processQueue();
},
onError: () => {
this.isSpeaking = false;
this.processQueue();
}
});
}
/** 入队播报 */
enqueue(content: SpeakContent): void {
// 1. 清理过期项
this.cleanExpired();
// 2. 队列满时丢弃最低优先级
if (this.queue.length >= this.config.maxQueueSize) {
const lowestIdx = this.findLowestPriorityIndex();
if (lowestIdx !== -1 && this.queue[lowestIdx].content.priority <= content.priority) {
this.queue.splice(lowestIdx, 1);
} else {
return; // 新内容优先级更低,丢弃
}
}
// 3. 计算过期时间
const expiryMs = content.category === SpeakCategory.MANEUVER
? this.config.maneuverExpiryMs
: this.config.defaultExpiryMs;
// 4. 入队
this.queue.push({
content,
enqueueTime: Date.now(),
expiryTime: Date.now() + expiryMs
});
// 5. 按优先级排序(高优先级在前)
this.queue.sort((a, b) => b.content.priority - a.content.priority);
this.queueSize = this.queue.length;
// 6. 尝试立即播报
if (!this.isSpeaking) {
this.processQueue();
}
}
/** 处理队列 */
private processQueue(): void {
if (this.isSpeaking || this.queue.length === 0) return;
// 检查最小间隔
const timeSinceLastSpeak = Date.now() - this.lastSpeakTime;
if (timeSinceLastSpeak < this.config.minIntervalMs) {
setTimeout(() => this.processQueue(), this.config.minIntervalMs - timeSinceLastSpeak);
return;
}
// 取出队首
const item = this.queue.shift()!;
// 检查是否过期
if (Date.now() > item.expiryTime) {
this.queueSize = this.queue.length;
this.processQueue(); // 递归处理下一个
return;
}
// 播报
this.isSpeaking = true;
this.lastSpeakTime = Date.now();
this.lastSpokenText = item.content.text;
this.queueSize = this.queue.length;
this.ttsEngine.speak(item.content.text, item.content.priority);
}
/** 处理导航指令事件 */
handleManeuverEvent(
instruction: NavInstruction,
distance: number,
nextInstruction?: NavInstruction
): void {
// 1. 远距离预告(1-2公里)
if (distance > 1000 && distance < 2000) {
const earlySpeak = this.generator.generateManeuverSpeak(
instruction, distance, nextInstruction, SpeakTiming.EARLY
);
this.enqueue(earlySpeak);
}
// 2. 接近预告(300-500米)
if (distance <= 500 && distance > 100) {
const approachingSpeak = this.generator.generateManeuverSpeak(
instruction, distance, nextInstruction, SpeakTiming.APPROACHING
);
this.enqueue(approachingSpeak);
}
// 3. 即时指令(50-100米)
if (distance <= 100) {
const immediateSpeak = this.generator.generateManeuverSpeak(
instruction, distance, nextInstruction, SpeakTiming.IMMEDIATE
);
this.enqueue(immediateSpeak);
}
}
/** 处理偏航事件 */
handleOffRoute(): void {
const speak = this.generator.generateOffRouteSpeak();
this.enqueue(speak);
}
/** 处理重规划完成事件 */
handleRerouteComplete(etaMinutes: number): void {
const speak = this.generator.generateRerouteCompleteSpeak(etaMinutes);
this.enqueue(speak);
}
/** 处理超速事件 */
handleSpeedAlert(speedLimit: number): void {
const speak = this.generator.generateSpeedAlertSpeak(speedLimit);
this.enqueue(speak);
}
/** 处理到达事件 */
handleArrive(): void {
const speak = this.generator.generateArriveSpeak();
this.enqueue(speak);
}
/** 清理过期项 */
private cleanExpired(): void {
const now = Date.now();
this.queue = this.queue.filter(item => now <= item.expiryTime);
this.queueSize = this.queue.length;
}
/** 查找最低优先级项索引 */
private findLowestPriorityIndex(): number {
let minPriority = Infinity;
let minIdx = -1;
this.queue.forEach((item, idx) => {
if (item.content.priority < minPriority) {
minPriority = item.content.priority;
minIdx = idx;
}
});
return minIdx;
}
/** 停止所有播报 */
stopAll(): void {
this.queue = [];
this.queueSize = 0;
this.ttsEngine.stop();
this.isSpeaking = false;
}
}
3.4 音频焦点管理
// AudioFocusManager.ets - 音频焦点管理
import { audio } from '@kit.AudioKit';
/** 音频焦点状态 */
export enum FocusState {
NONE = 'none', // 无焦点
GAINED = 'gained', // 获得焦点
LOSS_TRANSIENT = 'loss_transient', // 暂时失去(如来电)
LOSS_PERMANENT = 'loss_permanent' // 永久失去(如其他App占用)
}
/** 焦点回调 */
interface FocusCallbacks {
onGain?: () => void;
onLossTransient?: () => void;
onLossPermanent?: () => void;
}
export class AudioFocusManager {
private audioManager: audio.AudioRoutingManager | null = null;
private focusState: FocusState = FocusState.NONE;
private callbacks: FocusCallbacks = {};
private audioSessionId: number = -1;
/** 初始化 */
async initialize(): Promise<void> {
try {
const audioManager = audio.getAudioManager();
this.audioManager = audioManager.getRoutingManager();
} catch (error) {
console.error(`[AudioFocus] 初始化失败: ${JSON.stringify(error)}`);
}
}
/** 设置回调 */
setCallbacks(callbacks: FocusCallbacks): void {
this.callbacks = callbacks;
}
/** 请求音频焦点(导航播报前调用) */
async requestFocus(): Promise<boolean> {
try {
// 使用AUDIO_USAGE_NAVIGATION类型的音频流
const rendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION, // 导航音频流
rendererFlags: 0
};
// 导航音频流在HarmonyOS中有特殊优先级
// 系统会自动压低媒体音量(ducking)而非暂停
this.focusState = FocusState.GAINED;
this.callbacks.onGain?.();
return true;
} catch (error) {
console.error(`[AudioFocus] 请求焦点失败: ${JSON.stringify(error)}`);
return false;
}
}
/** 释放音频焦点 */
abandonFocus(): void {
this.focusState = FocusState.NONE;
}
/** 获取当前焦点状态 */
getFocusState(): FocusState {
return this.focusState;
}
}
3.5 完整集成示例
// NavigationVoicePage.ets - 导航语音完整集成示例
import { NavigationTTSEngine, TTSState, SpeakPriority } from './NavigationTTSEngine';
import { NavInstructionGenerator, SpeakTiming } from './NavInstructionGenerator';
import { VoiceAnnouncementScheduler } from './VoiceAnnouncementScheduler';
import { AudioFocusManager } from './AudioFocusManager';
import { NavInstruction, ManeuverType } from './NavDataModels';
@Entry
@Component
struct NavigationVoicePage {
@Local ttsEngine: NavigationTTSEngine = new NavigationTTSEngine();
@Local scheduler: VoiceAnnouncementScheduler | null = null;
@Local focusManager: AudioFocusManager = new AudioFocusManager();
@Local ttsState: TTSState = TTSState.IDLE;
@Local lastSpokenText: string = '等待播报...';
@Local isMuted: boolean = false;
async aboutToAppear(): Promise<void> {
// 1. 初始化TTS引擎
await this.ttsEngine.initialize();
// 2. 初始化音频焦点
await this.focusManager.initialize();
// 3. 创建指令生成器
const generator = new NavInstructionGenerator('zh-CN');
// 4. 创建播报调度器
this.scheduler = new VoiceAnnouncementScheduler(this.ttsEngine, generator);
// 5. 监听TTS状态
this.ttsEngine.setCallbacks({
onStateChange: (state: TTSState) => {
this.ttsState = state;
}
});
}
/** 模拟导航指令播报 */
simulateManeuver(): void {
if (!this.scheduler || this.isMuted) return;
const instruction: NavInstruction = {
maneuver: ManeuverType.TURN_RIGHT,
roadName: '长安街',
distance: 500,
duration: 60,
instructionText: '前方500米右转进入长安街',
coordinate: { latitude: 39.9, longitude: 116.4 }
};
this.scheduler.handleManeuverEvent(instruction, 500);
}
/** 模拟偏航播报 */
simulateOffRoute(): void {
if (!this.scheduler || this.isMuted) return;
this.scheduler.handleOffRoute();
}
/** 模拟超速播报 */
simulateSpeedAlert(): void {
if (!this.scheduler || this.isMuted) return;
this.scheduler.handleSpeedAlert(80);
}
build() {
Column() {
// 标题
Text('导航语音播报')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.margin({ bottom: 24 })
// TTS状态指示
Row() {
Circle()
.width(12)
.height(12)
.fill(this.ttsState === TTSState.SPEAKING ? '#2d6a4f' : '#e94560')
Text(this.ttsState === TTSState.SPEAKING ? '播报中' : '就绪')
.fontSize(14)
.fontColor('rgba(255,255,255,0.7)')
.margin({ left: 8 })
}
.margin({ bottom: 16 })
// 最后播报文本
Column() {
Text('最近播报')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
.margin({ bottom: 4 })
Text(this.lastSpokenText)
.fontSize(16)
.fontColor('#ffffff')
.maxLines(2)
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('rgba(26,26,46,0.8)')
.margin({ bottom: 24 })
// 测试按钮
Column() {
Button('模拟转向指令')
.width('100%')
.backgroundColor('rgba(15,52,96,0.8)')
.fontColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => this.simulateManeuver())
Button('模拟偏航')
.width('100%')
.backgroundColor('rgba(83,52,131,0.8)')
.fontColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => this.simulateOffRoute())
Button('模拟超速')
.width('100%')
.backgroundColor('rgba(233,69,96,0.8)')
.fontColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
.onClick(() => this.simulateSpeedAlert())
}
.width('100%')
// 静音开关
Row() {
Text('语音播报')
.fontSize(16)
.fontColor('#ffffff')
Toggle({ type: ToggleType.Switch, isOn: !this.isMuted })
.selectedColor('#2d6a4f')
.onChange((isOn: boolean) => {
this.isMuted = !isOn;
if (this.isMuted) {
this.scheduler?.stopAll();
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(16)
.borderRadius(12)
.backgroundColor('rgba(26,26,46,0.8)')
.margin({ top: 24 })
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#0a0a1a')
}
aboutToDisappear(): void {
this.ttsEngine.shutdown();
}
}
四、踩坑与注意事项
4.1 TTS引擎初始化时机
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 首次播报延迟 | TTS引擎初始化需要时间 | 在应用启动时预初始化,导航开始前确保就绪 |
| 离线模式下无语音 | 在线TTS需要网络 | 预下载离线语音包,降级为离线模式 |
| 多次初始化 | 页面重复创建引擎 | 使用单例模式管理TTS引擎 |
| 语言切换失败 | 切换语言需重新初始化 | 先shutdown再initialize,注意异步等待 |
4.2 播报时机的精确控制
导航语音的"时机"比"内容"更重要:
- 过早播报:用户还没到就说了,容易忘记
- 过晚播报:用户已经错过了才说,毫无意义
- 速度自适应:高速行驶时需要更早播报(反应时间更长)
// 根据速度动态调整播报距离
function getAdaptiveSpeakDistance(speed: number, baseDistance: number): number {
// 速度越快,播报距离越远
const speedKmh = speed * 3.6;
if (speedKmh > 100) return baseDistance * 2.5; // 高速
if (speedKmh > 60) return baseDistance * 1.8; // 城市快速路
if (speedKmh > 30) return baseDistance * 1.2; // 城市道路
return baseDistance; // 低速
}
4.3 音频焦点与Ducking
导航语音与音乐播放的共存是关键体验:
- Ducking(压低):播报导航语音时,音乐音量自动降低,播报完成后恢复
- Pause(暂停):来电等场景完全暂停音乐
- HarmonyOS支持:使用
STREAM_USAGE_NAVIGATION类型的音频流,系统自动执行Ducking
4.4 多语言支持
导航语音需要支持多语言,但要注意:
- 道路名称保留原文:如"前方500米右转进入Chang’an Avenue"(英文模式下中国道路名保留拼音)
- 数字读法差异:中文"一千二百"vs英文"twelve hundred"
- 度量单位:中文用"公里",英文用"kilometers"
- TTS语言匹配:确保TTS引擎语言与生成的文本语言一致
五、HarmonyOS 6适配
5.1 离线TTS增强
HarmonyOS 6增强了离线TTS能力,支持更自然的离线语音合成:
// 离线TTS引擎配置(API 14+)
const offlineEngineConfig: textToSpeech.CreateEngineParams = {
language: 'zh-CN',
person: 0,
online: 0, // 0=纯离线模式
// HarmonyOS 6支持指定离线语音包
extraConfig: {
voicePackage: 'zh-CN-female-natural', // 自然女声离线包
quality: 'high' // 高质量模式
}
};
5.2 车载音频通道
HarmonyOS 6针对车载场景增加了专用音频通道:
// 车载导航音频流配置
const carNavRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION,
rendererFlags: 0,
// HarmonyOS 6新增:指定车载音频输出通道
channelId: audio.AudioChannel.CHANNEL_1, // 导航专用通道
// 确保导航语音从左前扬声器输出(与音乐分离)
spatializationEnabled: false
};
5.3 语音指令双向交互
HarmonyOS 6支持语音指令的双向交互,用户可通过语音控制导航:
// VoiceCommandHandler.ets - 语音指令处理
import { speechRecognizer } from '@kit.AISpeechKit';
export class VoiceCommandHandler {
private asrEngine: speechRecognizer.SpeechRecognition | null = null;
/** 监听导航相关语音指令 */
startListening(): void {
// 识别关键词:取消导航、放大地图、静音、重规划等
const keywords = ['取消导航', '静音', '重新规划', '放大', '缩小'];
// 实现语音识别与指令分发
}
/** 处理识别结果 */
private handleCommand(text: string): void {
if (text.includes('取消导航')) {
// 停止导航
} else if (text.includes('静音')) {
// 静音播报
} else if (text.includes('重新规划')) {
// 触发重规划
}
}
}
六、总结
本文系统讲解了HarmonyOS导航系统中语音播报与TTS集成的完整方案:
| 模块 | 关键实现 |
|---|---|
| TTS引擎封装 | 基于AISpeechKit,支持在线/离线模式,优先级打断机制 |
| 指令文本生成 | 多语言资源,距离格式化规则,转向描述组合 |
| 播报调度器 | 优先级队列,过期丢弃,最小间隔控制,防重复播报 |
| 音频焦点管理 | STREAM_USAGE_NAVIGATION专用流,自动Ducking |
| 播报时机 | 三级预告(远距离/接近/即时),速度自适应距离调整 |
| 多语言支持 | 语言资源分离,道路名保留原文,TTS语言匹配 |
导航语音是驾驶安全的重要保障。在实际开发中,播报时机的精确控制比文本内容的丰富度更重要——"在正确的时间说正确的话"是导航语音的核心原则。HarmonyOS 6的离线TTS增强和车载音频通道能力,为导航语音提供了更自然的合成质量和更灵活的音频路由,使得在弱网和车载场景下的语音体验持续提升。
- 点赞
- 收藏
- 关注作者
评论(0)