HarmonyOS开发:导航语音播报与TTS集成

举报
Jack20 发表于 2026/06/22 11:49:22 2026/06/22
【摘要】 HarmonyOS开发:导航语音播报与TTS集成核心要点:本文深入讲解HarmonyOS导航系统中语音播报的完整实现,涵盖TTS(Text-to-Speech)引擎集成、导航指令文本生成、语音播报优先级管理、音频焦点控制、多语言支持等核心模块,并提供可运行的ArkTS代码示例。项目说明| 开发语言 | ArkTS || 核心框架 | ArkUI / @kit.AI (TTS) / @kit...

HarmonyOS开发:导航语音播报与TTS集成

核心要点:本文深入讲解HarmonyOS导航系统中语音播报的完整实现,涵盖TTS(Text-to-Speech)引擎集成、导航指令文本生成、语音播报优先级管理、音频焦点控制、多语言支持等核心模块,并提供可运行的ArkTS代码示例。

项目 说明

| 开发语言 | ArkTS |
| 核心框架 | ArkUI / @kit.AI (TTS) / @kit.AudioKit |
| 相关文档 | TTS开发指南 / 音频管理开发 |


一、背景与动机

语音播报是导航体验的核心交互方式——驾驶场景下用户无法频繁查看屏幕,语音指令是唯一安全的信息获取渠道。一个优秀的导航语音系统需要满足:

  1. 及时性:在正确时机播报,不早不晚
  2. 清晰性:指令表述简洁明了,一次听懂
  3. 自然性:语音合成自然流畅,不机械
  4. 优先级:紧急指令(偏航、测速)优先于常规播报
  5. 音频焦点:与音乐、电话等音频源和谐共存
  6. 多语言:支持中文、英文等多语言播报

在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 多语言支持

导航语音需要支持多语言,但要注意:

  1. 道路名称保留原文:如"前方500米右转进入Chang’an Avenue"(英文模式下中国道路名保留拼音)
  2. 数字读法差异:中文"一千二百"vs英文"twelve hundred"
  3. 度量单位:中文用"公里",英文用"kilometers"
  4. 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增强和车载音频通道能力,为导航语音提供了更自然的合成质量和更灵活的音频路由,使得在弱网和车载场景下的语音体验持续提升。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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