《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第4篇:语音唤醒进阶——打造免提交互体验

语音唤醒的"后台化"难题

HarmonyOS NEXT 里,Core Speech Kit 的语音唤醒能力,很多人觉得"不就是 API 调用一下的事"。但实际做过项目就会知道,真正的难点不在于 API 本身,而在于如何让唤醒监听在应用退到后台后依然稳定工作。

官方示例通常是在页面级(Ability)调用 wakeUp.start()。这本身没问题,但你得想明白一件事:如果一个语音助手应用,用户切出去之后就无法唤醒,那这个功能就废了。唤醒监听的本质是后台服务级持续监听,而不是页面的临时行为。

另一个容易被忽视的问题是功耗。如果唤醒监听一直高功耗运行,不仅耗电快,还容易被系统判定为"异常高功耗应用"而限制后台行为。所以,低功耗和灵敏度的平衡,是这个功能落地的关键。

这一篇从实际开发的角度,把语音唤醒的几个核心问题串起来:如何在 Service 里挂载唤醒监听,如何配置多个自定义唤醒词,以及如何通过 WorkScheduler 这类机制降低后台功耗。

它能解决什么问题

语音唤醒让应用能够在用户不说特定口令时保持静默,一旦检测到预设唤醒词,立即触发业务逻辑。和"语音命令识别"不同,唤醒阶段不需要识别复杂的语义,只需要检测有限的1-3个固定词或短语。

适用场景很明确:

  • 语音助手类 App:如智能家居控制、车载助手。
  • 免提交互:用户在双手被占用、不方便触摸屏幕时(如做饭、开车)。
  • 特定场景响应:游戏过程中快速唤醒技能、会议中语音记录等。

不适合的场景也很清楚:

  • 不需要持续唤醒的临时交互,用按钮触发即可。
  • 对实时性要求极高(低于100ms)的场景,语音唤醒的内核处理延迟可能达不到硬件触发的水平。

对比一下类似的方案:

方案 优点 缺点 推荐场景
Core Speech Kit 唤醒 自带低功耗策略、支持自定义唤醒词、后台稳定 初始化较慢、需要真机调试 后台持续监听
应用内通过按键触发 简单快速、无功耗问题 无法实现免提 临时触发
第三方SDK唤醒 功能丰富、集成快 授权费用、数据隐私风险 不建议优先选择,除非高度定制

综合考虑,Core Speech Kit 的自带唤醒方案是官方推荐,且在后台保活和功耗控制上做得最均衡的。下面直接看实现。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机调试,模拟器不支持唤醒)

核心实现:一个完整的后台唤醒模块

整个功能按模块拆解:

  1. 语音唤醒管理器 :封装 wakeUp.start()、wakeUp.stop() 和回调。
  2. 自定义唤醒词配置 :通过 WakeUpConfig 指定词汇和语言。
  3. Service Ability :在 onStart() 中初始化唤醒,在 onStop() 中释放资源。
  4. 触发动作 :回调中通过 startAbility 跳转到目标页面。
1. 语音唤醒管理器(wakeupManager.ets)
typescript 复制代码
// wakeupManager.ets
import { wakeUp } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 自定义唤醒词列表
const CUSTOM_WAKE_WORDS: string[] = ['你好小艺', '小艺小艺', '小娜'];

/**
 * 语音唤醒管理器
 * 负责初始化、配置自定义唤醒词、启动/停止监听。
 */
export class WakeUpManager {
  // 唤醒词配置
  private config: wakeUp.WakeUpConfig = {
    wakeupPhrases: CUSTOM_WAKE_WORDS, // 自定义唤醒词,最多支持5个
    language: 'zh-CN',
    // 可选参数:功耗模式,默认balanced
    // 'lowPower': 低功耗模式,灵敏度稍低
    // 'balanced': 平衡模式
    // 'highPerformance': 高性能模式,功耗高
    powerMode: 'balanced'
  };

  // 唤醒回调
  private listener: wakeUp.WakeUpCallback = {
    onWakeUp: (event: wakeUp.WakeUpEvent) => {
      // event.wakeupPhrase 就是实际唤醒的字符串
      console.info(`[WakeUpManager] 唤醒词触发: ${event.wakeupPhrase}`);
      // 触发业务逻辑
      this.onWakeUpAction(event);
    },
    onError: (error: BusinessError) => {
      console.error(`[WakeUpManager] 唤醒错误: ${JSON.stringify(error)}`);
    },
    onStateChange: (state: wakeUp.State) => {
      console.info(`[WakeUpManager] 状态变化: ${JSON.stringify(state)}`);
    }
  };

  /**
   * 启动唤醒监听
   */
  async startListening(): Promise<void> {
    try {
      // 检查权限(权限申请部分在Ability生命周期里处理)
      // 这里的重点是SDK初始化
      await wakeUp.startWakeUp(this.config, this.listener);
      console.info('[WakeUpManager] 唤醒监听启动成功');
    } catch (error) {
      console.error(`[WakeUpManager] 启动失败: ${JSON.stringify(error)}`);
      throw error;
    }
  }

  /**
   * 停止唤醒监听
   */
  async stopListening(): Promise<void> {
    try {
      await wakeUp.stopWakeUp();
      console.info('[WakeUpManager] 唤醒监听已停止');
    } catch (error) {
      console.error(`[WakeUpManager] 停止失败: ${JSON.stringify(error)}`);
    }
  }

  /**
   * 唤醒后的动作:启动主界面
   */
  private onWakeUpAction(event: wakeUp.WakeUpEvent): void {
    // 实际开发中,这里可以调用AbilityContext启动另一个Ability
    // 由于Manager不持有Context,通过回调抛出去
    if (this.wakeUpListener) {
      this.wakeUpListener.onWakeUpDetected(event.wakeupPhrase);
    }
  }

  // 外部注册的业务回调
  wakeUpListener?: { onWakeUpDetected: (phrase: string) => void };
}

为什么把自定义唤醒词列表放在常量里: wakeUp.WakeUpConfig.wakeupPhrases 接受字符串数组,你可以在运行时动态传入,但注意数组长度不能超过5个,且每个词建议在2-5个汉字,太短容易误唤醒,太长响应慢。

2. Service Ability(VoiceWakeUpService.ets)

在 HarmonyOS NEXT 里,要用后台服务挂载唤醒监听,推荐使用 ServiceAbility。因为这类能力需要在后台长时运行,而 UIAbility 一旦退到后台就可能被销毁。

typescript 复制代码
// VoiceWakeUpService.ets
import { ServiceAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { common, WantAgent } from '@kit.AbilityKit';
import { WakeUpManager } from '../manager/wakeupManager';

export default class VoiceWakeUpService extends ServiceAbility {
  private wakeUpManager: WakeUpManager = new WakeUpManager();

  async onStart(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 在Service启动时,初始化唤醒监听
    console.info('[VoiceWakeUpService] onStart');
    await this.initWakeUp(want);
  }

  async onBackground(): Promise<void> {
    // Service不处理onBackground,因为它是后台服务
  }

  async onStop(): Promise<void> {
    // 停止服务时,必须释放唤醒资源
    await this.wakeUpManager.stopListening();
    console.info('[VoiceWakeUpService] onStop');
  }

  async onDestroy(): Promise<void> {
    await this.wakeUpManager.stopListening();
    console.info('[VoiceWakeUpService] onDestroy');
  }

  /**
   * 初始化唤醒监听,并挂载回调
   */
  private async initWakeUp(want: Want): Promise<void> {
    try {
      // 注册业务回调:唤醒后启动UIAbility
      this.wakeUpManager.wakeUpListener = {
        onWakeUpDetected: (phrase: string) => {
          // 注意:这里是在唤醒引擎线程回调,不能直接修改UI
          // 通过Want或全局事件传递
          this.startTargetAbility(phrase);
        }
      };

      await this.wakeUpManager.startListening();
    } catch (error) {
      console.error(`[VoiceWakeUpService] initWakeUp error: ${JSON.stringify(error)}`);
    }
  }

  /**
   * 启动目标Ability
   */
  private startTargetAbility(phrase: string): void {
    const want: Want = {
      bundleName: 'com.example.app',
      abilityName: 'MainAbility',
      parameters: {
        wakeupPhrase: phrase
      }
    };
    this.context.startAbility(want).catch((error: Error) => {
      console.error(`[VoiceWakeUpService] startAbility error: ${JSON.stringify(error)}`);
    });
  }
}

关键点说明:

  • 不要在回调里直接修改UI状态 。唤醒引擎的回调线程和UI线程不同,直接修改 @State 变量会触发ArkUI的跨线程操作报错。正确做法是通过 Context.startAbility 跳转页面,或者用 EventHub 或全局状态管理(如 @Prop 链)传递。
  • 服务的生命周期管理 :onStart 中初始化唤醒,onDestroy 中主动 stopWakeUp。如果忘记停止,再次启动监听时会报"监听已存在"的错误。
  • lowPower 模式 :使用 powerMode: 'lowPower' 可以降低功耗,但唤醒灵敏度会降低。实测在嘈杂环境下,需要用户更清晰地喊出唤醒词。建议在开发者测试阶段先用 balanced,上线时根据反馈调整。
3. 在主UIAbility中启动Service

你需要在应用的主 UIAbility 启动时,或者在用户开启"语音唤醒"功能时,调用 startAbility 来启动服务。

typescript 复制代码
// MainAbility.ets
import { UIAbility, Want } from '@kit.AbilityKit';

export default class MainAbility extends UIAbility {
  onCreate(want: Want, launchParam): void {
    // 启动后台Service
    this.startVoiceWakeUpService();
  }

  private startVoiceWakeUpService(): void {
    const want: Want = {
      bundleName: 'com.example.app',
      abilityName: 'VoiceWakeUpService'
    };
    this.context.startAbility(want).catch((error: Error) => {
      console.error(`[MainAbility] startService error: ${JSON.stringify(error)}`);
    });
  }
}
4. 权限申请(module.json5)

唤醒功能需要 ohos.permission.MICROPHONE 和 ohos.permission.ACCESS_VOICE_INPUT 权限。

json5 复制代码
// module.json5
{
  "module": {
    ...
    "abilities": [
      {
        "name": "VoiceWakeUpService",
        "type": "service"
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "需要麦克风权限以进行语音唤醒",
        "usedScene": {
          "abilities": ["VoiceWakeUpService"]
        }
      },
      {
        "name": "ohos.permission.ACCESS_VOICE_INPUT",
        "reason": "需要语音输入权限以进行唤醒词检测",
        "usedScene": {
          "abilities": ["VoiceWakeUpService"]
        }
      }
    ]
  }
}

踩坑记录

坑1:模拟器无法测试语音唤醒

现象 :在DevEco Studio的模拟器上调用 startWakeUp,始终返回 -1 错误码,且日志无任何提示。

原因 :语音唤醒依赖设备的硬件麦克风和底层DSP芯片。模拟器没有真实的音频硬件,因此唤醒引擎无法工作。官方文档其实有说明,但没强调"完全不可用"。

解法 :必须使用真机(手机或平板)调试。使用 hdc shell 命令确认设备是否支持:

shell 复制代码
hdc shell abilitytool -t 0 -s com.example.app CheckWakeUpAbility

返回 true 才表示硬件支持。

坑2:唤醒词配置写错,导致监听"假死"

现象 :配置了 wakeupPhrases: ['你好小艺', '小艺小艺'],但监听启动后没有任何响应,日志也没有 onError 回调。

原因 :唤醒词参数是字符串数组,但有一个坑是每个唤醒词必须是合法的中文字符,不能包含空格、英文字母或数字 。如果配置 '你好小艺 '(末尾多了一个空格),引擎拒绝认可该词,但回调用 onError 而不是 onWakeUp。

解法 :在传入数组前,对每个词做 trim() 处理,并检查字符长度(建议2-8个汉字)。

typescript 复制代码
const rawWords: string[] = [' 你好小艺', '小艺小艺 '];
const cleanedWords = rawWords.map(word => word.trim());
this.config.wakeupPhrases = cleanedWords;
坑3:Service被系统杀死后,监听彻底丢失

现象 :应用退到后台长时间后,语音唤醒失效。再次唤醒手机,发现应用进程已不存在。

原因 :HarmonyOS对后台Service有资源回收策略。即使启动为 ServiceAbility,系统在内存紧张时依然可能杀死。

解法 :不要完全依赖Service存活。可以使用 WorkScheduler 定时检查唤醒状态,或者监听系统广播(如屏幕解锁)触发重新启动监听。一个粗放但有效的方案是:在 MainAbility 的 onForeground() 中检查唤醒状态,如果未启动,重新 startAbility。

typescript 复制代码
// 在UIAbility的onForeground中
onForeground(): void {
  // 检查唤醒服务是否存活,可以发送IPC消息
  this.checkWakeUpServiceAlive();
}

最佳实践

  1. 不要在 build() 中创建 WakeUpManager 对象

    build() 在 ArkUI 中每次刷新都会执行。如果直接在组件 build() 里实例化 WakeUpManager,会导致唤醒引擎反复初始化,最终因资源冲突报错。应该在 @Component 的 aboutToAppear() 或 ServiceAbility 的 onStart() 中创建一次。

  2. 低功耗场景使用 lowPower 模式,但需要与用户协商

    如果你希望在后台长时间监听(比如全天候语音助手),balanced 模式功耗约在 50-100mW,而 lowPower 模式可降至 20-30mW。代价是唤醒灵敏度下降。建议在设置中让用户选择"省电模式"和"标准模式",并提供预热时间(约1-2秒)来平衡体验。

  3. 回调中避免 await 耗时操作

    onWakeUp 回调的执行上下文是唤醒引擎线程,如果在这里执行 await 异步操作(如数据库写入),会阻塞引擎的后续检测,导致下一个唤醒词识别延迟。建议回调中仅做数据转发(通过 EventHub 或 startAbility),把耗时操作放到其他 Task 中。

Demo 入口

完整项目结构:

复制代码
entry/src/main/
├── ability/
│   ├── MainAbility.ets        // 主UIAbility,启动Service
│   └── VoiceWakeUpService.ets // 后台唤醒服务
├── manager/
│   └── wakeupManager.ets      // 唤醒管理器
├── pages/
│   └── Index.ets              // 主页面(展示唤醒状态)
└── resources/
    └── module.json5           // 权限声明

Index.ets 主页面代码(简化版):

typescript 复制代码
@Entry
@Component
struct Index {
  @State wakeUpStatus: string = '未启动';
  @State lastWakeWord: string = '';

  aboutToAppear(): void {
    // 启动服务
    this.startService();
  }

  private startService(): void {
    // 通过AppContext启动Service
  }

  build() {
    Column() {
      Text(`唤醒状态: ${this.wakeUpStatus}`)
      Text(`上次唤醒词: ${this.lastWakeWord}`)
    }
    .padding(20)
  }
}

FAQ

Q:为什么真机测试时,第一次唤醒正常,第二次喊同样词没反应?

A:检查 stopWakeUp() 是否在唤醒后自动被调用了。有些监听引擎设计为"触发一次后自动停止",需要显式调用 startWakeUp 重新监听。可以在回调里加入重启动逻辑:await this.wakeUpManager.restartListening()。

Q:自定义唤醒词可以包含英文吗?

A:官方 SDK 只支持中文环境的中文词语。包含英文字母或数字的唤醒词可能被引擎忽略或产生错误回调。如果需要支持英文,需要切换到 language: 'en-US',但此时 wakeupPhrases 只能配置英文词组。

Q:低功耗模式唤醒延迟多久?

A:实测在 lowPower 模式下,从用户说话到回调触发,延迟通常在 1.5s - 3s。balanced 模式可缩短到 0.8s -- 1.2s。highPerformance 模式(不推荐长时间使用)能到 0.5s 以内,但功耗是 lowPower 的3倍以上。


如果你在集成 Core Speech Kit 语音唤醒时遇到其他奇怪问题,可以先重点检查生命周期管理 和唤醒词配置 ,大部分问题都出在这两个环节。官方文档对唤醒词格式限制写得很含蓄,建议在实际开发时先写一个打印 wakeupPhrases 的日志验证一下。

相关推荐
贾伟康12 小时前
HarmonyOS API 26 HMAF Skill 端侧 A2A 消息去重与上下文最小化
harmonyos·arkts·skill·a2a·hmaf
贾伟康12 小时前
HarmonyOS API 26 HMAF Skill 多 Skill 冲突仲裁与可恢复执行
harmonyos·arkts·智能体·skill·hmaf
钉先生贾13 小时前
毕业项目:从上游更新到回归发布的完整维护闭环
华为·harmonyos
行者-全栈开发13 小时前
【鸿蒙心迹】权限申请全流程——从声明到用户授权的完整链路(HarmonyOS 7.x)
华为·harmonyos
Hello_Pyhx1 天前
VoiceStudio v0.5.7 上手:运行环境、模型安装与故障定位
音频·语音识别·科龙
KevinChaos1 天前
阳台小花园开发手记 01|HarmonyOS 7.0 互动卡片:小花园为何要两套界面
华为·harmonyos
Ranger09291 天前
下一代鸿蒙打包工具链:arkdown
harmonyos
程序猿追1 天前
一个工程面向手机与二合一:HarmonyOS 6 部署配置和本地手记
华为·智能手机·harmonyos
行者-全栈开发2 天前
【鸿蒙心迹】深色模式适配全链路——从配置到自定义(HarmonyOS 7.x)
华为·harmonyos
PNP Robotics2 天前
【PNP具身分析】20+自由度灵巧手从抓取到精细交互,2026年灵巧手方向全球前沿具身典型案例
交互