简介:这是一份面向安卓开发者的科大讯飞AIKit语音唤醒功能完整实现资源,帮助解决在安卓应用中快速接入语音激活能力的需求。内容围绕Android Studio开发环境,覆盖SDK依赖导入、项目构建配置、唤醒词初始化、麦克风权限申请、多场景真机测试以及误唤醒排查等关键环节,适合已有安卓基础、希望上手智能语音交互的中高级开发者使用。压缩包共647个文件,约45.56MB,内含158个xml布局与配置、136个json数据、130个flat资源文件、21个class字节码、17个jar依赖、8个so动态库,以及java源代码、gradle构建脚本、apk安装包和ivw_keyword等唤醒词模型,结构完整,便于导入查阅。目前已有719人学习浏览该资源包。借助其中清晰的工程目录与真实运行产物,可以直接复用AIKit的初始化流程和唤醒词配置模板,理解模型加载与阈值调整机制;附带pcm音频样本、ivw_filler等资源也可用于分析误唤醒与漏唤醒原因,降低二次开发中的试错成本。
1. 手机息屏时喊一声就能唤醒应用:一份能直接跑的讯飞 AIKit 工程
手机息屏放在桌上,喊一句唤醒词,应用从后台被拉起——这是语音助手类 App 的标准开场。我用 Android Studio 把科大讯飞 AIKit 的语音唤醒功能完整跑通了一遍。这份工程只在 libs 里放了唤醒相关 SDK,没有识别、没有合成,是那种可以拿来就改的纯净版安卓语音唤醒功能工程。适合两类人:一是产品里需要一个离线低功耗的语音入口,又不想被在线识别的高延迟和流量成本拖住的开发者;二是刚接触讯飞 SDK、想找个最小工程对照着接入的新手。下面按我实际接入的顺序,把技术选型、工程接入、核心代码和踩坑记录完整过一遍。
2. 技术选型与工程接入:为什么选 AIKit 而不是在线识别
2.1 唤醒方案怎么选:离线词表、延迟与功耗的三方权衡
做语音唤醒之前,先得想清楚一个问题:你是要“随时听懂用户说啥”,还是只需要“用户喊一个词把应用叫起来”。这决定了完全不同的技术路线。
我最早尝试的是在线语音识别方案:录音流上传到云端,识别出文本再比对是不是唤醒词。效果确实“听得懂”,但做唤醒入口有个致命问题——延迟。一次往返请求普遍要 1~2 秒,用户喊完“你好XX”到应用响应,已经过了人类耐心的临界点。而且持续录音 + 网络传输,对电量的消耗非常明显,息屏场景基本撑不住。
科大讯飞 AIKit 的语音唤醒走的是另一条路:本地检测。SDK 内置唤醒词模型,麦克风采集到的音频在设备端直接做匹配,命中才回调。我整理了一张对比表,选型时可以参考:
| 维度 | 在线语音识别方案 | AIKit 离线语音唤醒 |
|---|---|---|
| 首次响应延迟 | 1~2 秒(含网络往返) | 300ms 以内 |
| 网络依赖 | 强依赖,断网即失效 | 无依赖,纯本地匹配 |
| 功耗表现 | 录音 + 上传,耗电明显 | 本地 DSP 级检测,功耗低 |
| SDK 体积 | 完整识别包,体积大 | 只集成唤醒 so,体积小 |
| 适用场景 | 需要理解整句语义 | 只需要“喊一下”当开关 |
这个项目之所以叫“纯净版”,就是因为它只做了唤醒这一件事。没有把识别、合成、语义理解全部堆进来,工程体积小,逻辑也干净。
2.2 工程结构:一个最小可跑的 Android 项目长什么样
把 SDK 接进 Android Studio 之前,先看目录结构,心里有个底。标准的讯飞 SDK 接入不依赖远程仓库,而是把下载好的 jar 和 so 文件放进工程,手动配置依赖。
wakeupdemo/ ├── app/ │ ├── libs/ # 讯飞 AIKit 唤醒 jar/aar 与各架构 so 文件 │ │ ├── WakeUp_aar_xxx.aar │ │ └── armeabi-v7a/ │ │ ├── libmsc.so │ │ └── libwakeup.so │ ├── src/main/ │ │ ├── java/com/example/wakeupdemo/ │ │ │ ├── WakeupApp.java # Application 初始化入口 │ │ │ ├── MainActivity.java # 首页与权限申请 │ │ │ └── WakeupManager.java # 唤醒器封装,核心逻辑 │ │ ├── assets/ # 唤醒词资源与模型文件 │ │ └── res/ │ └── build.gradle └── settings.gradlejar 包放 libs 后,build.gradle 里要配两处。第一处是依赖声明,第二处是 so 文件的 jniLibs 路径指向——这是新手最容易漏掉的配置,很多人 jar 加了、so 也拷了,一运行还是崩溃,就是没告诉 Gradle so 文件在哪。
android { compileSdk 34 defaultConfig { applicationId "com.example.wakeupdemo" minSdk 23 targetSdk 34 ndk { abiFilters "armeabi-v7a", "arm64-v8a" } } sourceSets { main { jniLibs.srcDirs = ['libs'] } } } dependencies { implementation files('libs/WakeUp_aar_xxx.aar') }abiFilters 这里我故意只留了 armeabi-v7a 和 arm64-v8a。x86 模拟器用的 so 讯飞一般不带,如果强行放进来反而可能在真机上出现冲突。但这个配置也意味着:不要指望在模拟器上跑通唤醒,真机调试才是正道。
2.3 初始化前置:appid、权限申请与 Application 配置
讯飞 SDK 的规矩是“先初始化,再干活”。初始化一般放在 Application 的 onCreate 里,保证任何时候调用唤醒器都已经准备好。
public class WakeupApp extends Application { private static final String APP_ID = "xxxxxxxx"; // 从讯飞开放平台申请 @Override public void onCreate() { super.onCreate(); // 全局初始化,只执行一次 SpeechUtility.createUtility(this, "appid=" + APP_ID); } }createUtility 接收的是完整 appid 字符串,格式必须严格是appid=你的ID,多了空格、少了前缀都会在后续调用时报错。这里有个容易忽略的点:appid 申请时要填写应用的包名,SDK 启动时会拿 appid 和当前包名做校验,两者不一致直接初始化失败。
Manifest 里需要声明录音权限和网络权限。录音权限是运行时权限,Android 6.0 以上还必须动态申请,下面这段是权限声明部分:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:name=".WakeupApp" android:label="语音唤醒"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>WAKE_LOCK 权限是给息屏场景准备的,后面讲保活时会用到。INTERNET 权限其实离线唤醒用不上,但讯飞 SDK 启动时偶尔会做在线校验,建议保留以防水土不服。
3. 核心模块拆解:从麦克风采集到命中回调的完整链路
3.1 创建唤醒器:先看懂每个回调再动手改
唤醒器的核心类是 SpeechWakeuper,所有操作围绕它进行。创建时传入一个 WakeuperListener 监听器,唤醒过程中的各种事件都通过它回调到业务层。
public class WakeupManager { private SpeechWakeuper wakeuper; private final Context context; public WakeupManager(Context context) { this.context = context.getApplicationContext(); } private final WakeuperListener listener = new WakeuperListener() { // 唤醒成功,命中词通过 result 返回 @Override public void onResult(WakeuperResult result) { handleWakeupResult(result); } // 发生错误:错误码 + 描述 @Override public void onError(int errorCode, String errorDesc) { Log.e("Wakeup", "error=" + errorCode + ", desc=" + errorDesc); } // 音量变化,一般用来画录音波形 @Override public void onVolumeChanged(int volume) { } // 开始拾音 @Override public void onBeginOfSpeech() { } // 结束拾音 @Override public void onEndOfSpeech() { } }; public void create() { if (wakeuper == null) { wakeuper = SpeechWakeuper.createWakeuper(context, listener); } } }注意 createWakeuper 传入的 context 我建议用 application context,而不是某个 Activity。原因很简单:唤醒器生命周期很可能长于单个页面,用 Activity 的 context 容易造成内存泄漏。onResult 是最关键的回调,当用户说出唤醒词并被模型命中时,结果会封装在 WakeuperResult 里。onError 的第二个参数是描述文本,排查问题时先看它,再查错误码表。
3.2 启动监听与结果解析:从按下按钮到命中词的完整流程
启动唤醒前,有一组参数必须先设置。它们决定唤醒器的行为:听哪个唤醒词、灵敏度多高、采样率多少。这些参数不设置,SDK 全部按默认值走,但不保证是你期望的行为。
private void startWakeup() { if (wakeuper == null) { create(); } SharedPreferences prefs = context.getSharedPreferences("wakeup_config", Context.MODE_PRIVATE); String wakeupPoint = prefs.getString("wakeup_point", "0"); // 唤醒词索引 int threshold = prefs.getInt("wakeup_threshold", 20); // 灵敏度阈值 // 设置唤醒词索引 wakeuper.setParameter(SpeechConstant.WAKEUP_POINT, wakeupPoint); // 设置灵敏度阈值,范围 1-100,越小越灵敏 wakeuper.setParameter(SpeechConstant.WAKEUP_THRESHOLD, String.valueOf(threshold)); // 设置采样率,一般固定 16000 即可 wakeuper.setParameter(SpeechConstant.KEY_SAMPLE_RATE, "16000"); int ret = wakeuper.startListening(null); if (ret != 0) { Log.e("Wakeup", "启动失败,错误码=" + ret); } }WAKEUP_POINT 是唤醒词索引。讯飞 SDK 内置的唤醒词是固定的几个,以 0、1、2 这样的索引来区分,具体索引对应哪个词以 SDK 附带的文档为准。这个设计意味着官方词表是固化的,如果你想用自定义唤醒词,得走讯飞的自训练平台,生成专属词表模型,再替换 assets 里的资源文件。
WAKEUP_THRESHOLD 是灵敏度,范围 1~100,数值越大越灵敏、越容易误触发,数值越小越迟钝。这里我默认用 20,是一个相对保守的值,实测在安静环境下唤醒率很高,也不会因为轻微噪音频繁误报。
startListening 返回 0 代表启动成功,非 0 是错误码。很多开发者只看回调不看返回值,结果回调一直没触发、日志也没报错,其实就是启动失败了。
命中唤醒词后,onResult 里拿到的 WakeuperResult 不是普通对象,需要解析其中的 JSON 字符串:
private void handleWakeupResult(WakeuperResult result) { String resultString = result.getResultString(); try { JSONObject jsonObject = new JSONObject(resultString); String wakeupWord = jsonObject.optString("wakeupWord"); // 命中词 int score = jsonObject.optInt("score"); // 置信度得分 Log.i("Wakeup", "命中=" + wakeupWord + ", 得分=" + score); // 通知 UI 线程更新界面 if (context instanceof Application) { // 发送广播或 EventBus 通知前台页面 } } catch (JSONException e) { Log.e("Wakeup", "解析结果失败", e); } }wakeupWord 字段命中了哪个词,score 是模型给出的置信度。在实际业务里,这份结果一般通过广播、接口回调或消息总线转发到正在展示的 Activity。它越轻越好,不要在里面做耗时操作,因为唤醒回调跑在 SDK 内部线程。
3.3 资源释放与生命周期:最容易导致二次启动失败的操作
唤醒器不销毁,下次启动就会出问题。很多人的复现路径是:第一次点击“开始唤醒”一切正常,关掉页面再进来,点击开始就没反应了。原因就是上次的唤醒器还占着麦克风或内部状态没释放。
public void destroy() { if (wakeuper != null) { wakeuper.destroy(); wakeuper = null; } }destroy 之后必须把引用置空,否则下次 create 拿到的是已经销毁的僵尸对象。在 Activity 的 onDestroy 里调用这个方法即可。这里有个常见误用:有人把 destroy 写在 onPause 里,页面一切后台就释放,结果息屏唤醒成了摆设——用户刚按电源键,唤醒器就没了。正确做法是:Activity 活着就保持唤醒,只在真正销毁时释放。
如果你的业务是全局唤醒,比如应用常驻后台等待唤醒词,建议把唤醒器生命周期跟 Application 绑定,而不是某个页面。这就需要配合前台服务来保活了,这部分放到后面的避坑章节细说。
4. 常见问题排查:把唤醒功能跑通的 5 个典型坑
4.1 点击启动没反应,日志里也没有任何错误
现象是自己的工程里点“开始唤醒”按钮,界面无变化、Logcat 干干净净,像什么都没发生。
原因大概率是麦克风权限没有真正拿到。注意 Manifest 里声明了权限,只是第一步。Android 6.0 以上 RECORD_AUDIO 属于危险权限,必须在运行时动态申请,用户点了“允许”之后麦克风才真正可用。SDK 拿不到录音流时,通常不会主动抛错,而是静默等待,看起来就像“没反应”。
解决方式:在页面启动时做权限申请,并在回调里确认授予后才调用 startListening。可以参考下面这段:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 1001); } else { wakeupManager.startWakeup(); }一个小习惯:平时调试把 onError 的日志级别打成 Log.e,这样权限缺失、录音失败这类错误更容易在 Logcat 里被过滤出来。别用默认的 Log.i。
4.2 一运行就闪退,日志报 UnsatisfiedLinkError
现象是应用一启动就崩,日志里有这么一行:java.lang.UnsatisfiedLinkError: dlopen failed: library "libwakeup.so" not found。
原因很简单:so 文件虽然在 libs 目录里,但 Gradle 默认不会把它打包进 APK,除非你在 build.gradle 里告诉它 jniLibs 的位置。我前面给的sourceSets { main { jniLibs.srcDirs = ['libs'] } }就是为了解决这个问题。很多人拷了 so 但漏了这段,编译能过、运行必崩。
解决方式就是补上 sourceSets 配置。还有一个隐性坑:abiFilters 里如果漏了某个架构,那台真机启动就会报找不到 so。老设备是 armeabi-v7a,新设备是 arm64-v8a,建议两个都保留。
4.3 编译通过,运行就报 appid 校验失败
现象是应用能启动,但 SDK 初始化阶段就回调错误,提示 appid 校验失败、与包名不匹配。
原因有两类。一是 appid 在开放平台申请时填的包名,跟工程 build.gradle 里的 applicationId 不一致;二是申请的时候填了签名信息,但当前打包用的签名文件跟申请时不匹配。讯飞 SDK 的校验是双向的:包名 + 签名都要对上。
解决方式:核对 applicationId 一字不差,包括大小写。签名不一致的话,用申请时的签名文件重新打包,或者去开放平台把新签名的 MD5 更新进去。这个坑最折磨人就是因为报错信息很隐蔽,往往只给一个错误码,不说明是包名还是签名的问题。
4.4 亮屏时唤醒正常,息屏后基本叫不醒
现象是屏幕亮着的时候喊唤醒词百发百中,一旦息屏放进裤兜,再喊就没反应了。
原因分两层。一层是系统省电策略:部分厂商会冻结后台应用的录音与 CPU 调度,息屏后应用进程虽然活着,但拿不到 CPU 时间片。另一层是 SDK 本身的问题:如果唤醒器没有在后台持续运行,息屏后 Activity 被暂停,录音链路实际上被系统挂起了。
解决方式是给唤醒加一个前台服务,让应用在后台保持活跃状态,并申请忽略电池优化。前台服务记得 Android 8.0 以上要创建通知渠道:
NotificationChannel channel = new NotificationChannel( "wakeup_channel", "语音唤醒服务", NotificationManager.IMPORTANCE_LOW); notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(this, "wakeup_channel") .setContentTitle("语音唤醒运行中") .setSmallIcon(R.drawable.ic_launcher) .build(); startForeground(1, notification);另外在部分机型上还需要引导用户进入电池设置,把应用加入“不优化”白名单。这个跳转逻辑各家厂商 API 不同,通用做法是发一个 Intent 打开系统设置页,让用户手动选择。
4.5 Android 12 以上从后台启动唤醒直接抛异常
现象是应用退到后台一段时间,再触发唤醒时崩溃,日志报Background service start forbidden。
原因是 Android 12 开始对后台启动前台服务做了严格限制,应用处于后台时不允许调用 startForegroundService,只能在用户可见状态下启动。而唤醒场景天然需要“后台常驻 + 随时响应”,这个冲突是系统限制,SDK 层面没法绕开。
解决方式有两种主流做法。一是把唤醒启动时机放在 Activity 可见期间,用户在前台时把前台服务拉起来,之后保持运行即可。二是用 WorkManager 做周期性的任务保活,但这种方式唤醒延迟会比前台服务高不少,实时性要求高的场景不推荐。我的做法是前者:从 MainActivity 启动服务,启动成功后服务自己持有唤醒器,不再依赖页面的生命周期。
5. 二次开发:把固定唤醒词工程改造成自己的语音入口
5.1 先搞清楚:能改的是阈值,不是词表
讯飞 AIKit 的唤醒词列表是 SDK 内置的,普通接入方式下你不能直接在代码里改成一个自定义词。想换词,得去讯飞开放平台做自训练词表,生成你自己的唤醒模型,再替换工程里的资源文件。这个过程往往要审核,周期不短。
那本地能改什么?阈值和指数。WAKEUP_THRESHOLD 这个参数可以直接动态调整,在设置页里加一个 SeekBar,把灵敏度做成可调节项,用户可以根据环境噪音程度自己选。这是最实用的二次开发点。
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (wakeuper != null) { wakeuper.setParameter(SpeechConstant.WAKEUP_THRESHOLD, String.valueOf(progress)); } } });注意阈值调整后不需要重启唤醒器,setParameter 是实时生效的,这一点讯飞做得还算顺手。但阈值调太高会导致在嘈杂环境里疯狂误报,调太低的后果是喊破喉咙也没反应。我给个经验值:安静室内用 20,商场地铁环境调到 50 以上比较稳妥。
5.2 把唤醒结果接进自己的业务链路
唤醒命中之后的业务动作,是二次开发的核心。常见做法是发一个本地广播,让正在前台展示的页面接收后刷新状态;如果应用在后台,则直接拉起主界面。下面是一个 Intent 拉起界面的最小实现:
private void launchMain() { Intent intent = new Intent(context, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(intent); }FLAG_ACTIVITY_NEW_TASK 必须加,因为唤醒回调的线程上下文不在 Activity 栈里,不加这个标志在后台启动 Activity 会失败。另一个改造点是音量回调 onVolumeChanged,这个值可以用来驱动录音波形动画,给用户一个“我听到了”的视觉反馈,体验上比傻愣愣地等结果好很多。
如果你有多个唤醒词索引,可以在 onResult 里根据 wakeupWord 分发到不同业务:喊“你好小智”打开主页面,喊“开始录音”直接进入录音模式。这种多词分发的改造,前提还是先确认你的词表模型支持多词唤醒。
我后来每次接讯飞 SDK,都先拿这份最小工程跑一遍,确认初始化、权限、jniLibs 三个点没问题,再往上面叠业务代码。这个习惯帮我省了不少排查时间——很多所谓“SDK 的玄学问题”,最后都出在这三个最基础的地方。希望帮到你。
本文还有配套的精品资源,点击获取